Merge remote-tracking branch 'origin/develop' into t3chguy/clear_notifications
This commit is contained in:
commit
96ffe94876
733 changed files with 50889 additions and 9584 deletions
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,15 +15,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'CompatibilityPage',
|
||||
propTypes: {
|
||||
onAccept: React.PropTypes.func,
|
||||
onAccept: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 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,11 +16,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import {focusCapturedRef} from "../../utils/Accessibility";
|
||||
import {KeyCode} from "../../Keyboard";
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
|
@ -48,7 +50,6 @@ export default class ContextualMenu extends React.Component {
|
|||
menuWidth: PropTypes.number,
|
||||
menuHeight: PropTypes.number,
|
||||
chevronOffset: PropTypes.number,
|
||||
menuColour: PropTypes.string,
|
||||
chevronFace: PropTypes.string, // top, bottom, left, right or none
|
||||
// Function to be called on menu close
|
||||
onFinished: PropTypes.func,
|
||||
|
@ -67,7 +68,7 @@ export default class ContextualMenu extends React.Component {
|
|||
// on resize callback
|
||||
windowResize: PropTypes.func,
|
||||
// method to close menu
|
||||
closeMenu: PropTypes.func,
|
||||
closeMenu: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
@ -84,6 +85,9 @@ export default class ContextualMenu extends React.Component {
|
|||
// We don't need to clean up when unmounting, so ignore
|
||||
if (!element) return;
|
||||
|
||||
// For screen readers to find the thing
|
||||
focusCapturedRef(element);
|
||||
|
||||
this.setState({
|
||||
contextMenuRect: element.getBoundingClientRect(),
|
||||
});
|
||||
|
@ -111,6 +115,14 @@ export default class ContextualMenu extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_onKeyDown = (ev) => {
|
||||
if (ev.keyCode === KeyCode.ESCAPE) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.props.closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const position = {};
|
||||
let chevronFace = null;
|
||||
|
@ -157,25 +169,6 @@ export default class ContextualMenu extends React.Component {
|
|||
chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted);
|
||||
}
|
||||
|
||||
// To override the default chevron colour, if it's been set
|
||||
let chevronCSS = "";
|
||||
if (props.menuColour) {
|
||||
chevronCSS = `
|
||||
.mx_ContextualMenu_chevron_left:after {
|
||||
border-right-color: ${props.menuColour};
|
||||
}
|
||||
.mx_ContextualMenu_chevron_right:after {
|
||||
border-left-color: ${props.menuColour};
|
||||
}
|
||||
.mx_ContextualMenu_chevron_top:after {
|
||||
border-left-color: ${props.menuColour};
|
||||
}
|
||||
.mx_ContextualMenu_chevron_bottom:after {
|
||||
border-left-color: ${props.menuColour};
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
const chevron = hasChevron ?
|
||||
<div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace} /> :
|
||||
undefined;
|
||||
|
@ -183,11 +176,14 @@ export default class ContextualMenu extends React.Component {
|
|||
|
||||
const menuClasses = classNames({
|
||||
'mx_ContextualMenu': true,
|
||||
'mx_ContextualMenu_noChevron': chevronFace === 'none',
|
||||
'mx_ContextualMenu_left': chevronFace === 'left',
|
||||
'mx_ContextualMenu_right': chevronFace === 'right',
|
||||
'mx_ContextualMenu_top': chevronFace === 'top',
|
||||
'mx_ContextualMenu_bottom': chevronFace === 'bottom',
|
||||
'mx_ContextualMenu_left': !hasChevron && position.left,
|
||||
'mx_ContextualMenu_right': !hasChevron && position.right,
|
||||
'mx_ContextualMenu_top': !hasChevron && position.top,
|
||||
'mx_ContextualMenu_bottom': !hasChevron && position.bottom,
|
||||
'mx_ContextualMenu_withChevron_left': chevronFace === 'left',
|
||||
'mx_ContextualMenu_withChevron_right': chevronFace === 'right',
|
||||
'mx_ContextualMenu_withChevron_top': chevronFace === 'top',
|
||||
'mx_ContextualMenu_withChevron_bottom': chevronFace === 'bottom',
|
||||
});
|
||||
|
||||
const menuStyle = {};
|
||||
|
@ -199,10 +195,6 @@ export default class ContextualMenu extends React.Component {
|
|||
menuStyle.height = props.menuHeight;
|
||||
}
|
||||
|
||||
if (props.menuColour) {
|
||||
menuStyle["backgroundColor"] = props.menuColour;
|
||||
}
|
||||
|
||||
if (!isNaN(Number(props.menuPaddingTop))) {
|
||||
menuStyle["paddingTop"] = props.menuPaddingTop;
|
||||
}
|
||||
|
@ -226,14 +218,13 @@ export default class ContextualMenu extends React.Component {
|
|||
|
||||
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
|
||||
// property set here so you can't close the menu from a button click!
|
||||
return <div className={className} style={{...position, ...wrapperStyle}}>
|
||||
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect}>
|
||||
return <div className={className} style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown}>
|
||||
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} tabIndex={0}>
|
||||
{ chevron }
|
||||
<ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} />
|
||||
</div>
|
||||
{ props.hasBackground && <div className="mx_ContextualMenu_background" style={wrapperStyle}
|
||||
onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
|
||||
<style>{ chevronCSS }</style>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,8 @@ import request from 'browser-request';
|
|||
import { _t } from '../../languageHandler';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import classnames from 'classnames';
|
||||
|
||||
|
@ -44,6 +46,8 @@ export default class EmbeddedPage extends React.PureComponent {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._dispatcherRef = null;
|
||||
|
||||
this.state = {
|
||||
page: '',
|
||||
};
|
||||
|
@ -82,19 +86,31 @@ export default class EmbeddedPage extends React.PureComponent {
|
|||
this.setState({ page: body });
|
||||
},
|
||||
);
|
||||
|
||||
this._dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
if (this._dispatcherRef !== null) dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
// HACK: Workaround for the context's MatrixClient not being set up at render time.
|
||||
if (payload.action === 'client_started') {
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const client = this.context.matrixClient;
|
||||
// HACK: Workaround for the context's MatrixClient not updating.
|
||||
const client = this.context.matrixClient || MatrixClientPeg.get();
|
||||
const isGuest = client ? client.isGuest() : true;
|
||||
const className = this.props.className;
|
||||
const classes = classnames({
|
||||
[className]: true,
|
||||
[`${className}_guest`]: isGuest,
|
||||
[`${className}_loggedIn`]: !!client,
|
||||
});
|
||||
|
||||
const content = <div className={`${className}_body`}
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
|
@ -25,7 +26,7 @@ import { _t } from '../../languageHandler';
|
|||
/*
|
||||
* Component which shows the filtered file using a TimelinePanel
|
||||
*/
|
||||
const FilePanel = React.createClass({
|
||||
const FilePanel = createReactClass({
|
||||
displayName: 'FilePanel',
|
||||
|
||||
propTypes: {
|
||||
|
@ -38,23 +39,10 @@ const FilePanel = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this.updateTimelineSet(this.props.roomId);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if (nextProps.roomId !== this.props.roomId) {
|
||||
// otherwise we race between re-rendering the TimelinePanel and setting the new timelineSet.
|
||||
//
|
||||
// FIXME: this race only happens because of the promise returned by getOrCreateFilter().
|
||||
// We should only need to create the containsUrl filter once per login session, so in practice
|
||||
// it shouldn't be being done here at all. Then we could just update the timelineSet directly
|
||||
// without resetting it first, and speed up room-change.
|
||||
this.setState({ timelineSet: null });
|
||||
this.updateTimelineSet(nextProps.roomId);
|
||||
}
|
||||
},
|
||||
|
||||
updateTimelineSet: function(roomId) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(roomId);
|
||||
|
@ -116,20 +104,21 @@ const FilePanel = React.createClass({
|
|||
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
|
||||
// "(" + this.state.timelineSet._timelines.join(", ") + ")" + " with key " + this.props.roomId);
|
||||
return (
|
||||
<TimelinePanel key={"filepanel_" + this.props.roomId}
|
||||
className="mx_FilePanel"
|
||||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={this.state.timelineSet}
|
||||
showUrlPreview = {false}
|
||||
tileShape="file_grid"
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
empty={_t('There are no visible files in this room')}
|
||||
/>
|
||||
<div className="mx_FilePanel" role="tabpanel">
|
||||
<TimelinePanel key={"filepanel_" + this.props.roomId}
|
||||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={this.state.timelineSet}
|
||||
showUrlPreview = {false}
|
||||
tileShape="file_grid"
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
empty={_t('There are no visible files in this room')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="mx_FilePanel">
|
||||
<div className="mx_FilePanel" role="tabpanel">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -16,22 +16,18 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../languageHandler";
|
||||
|
||||
export default class GenericErrorPage extends React.PureComponent {
|
||||
static propTypes = {
|
||||
message: PropTypes.string.isRequired,
|
||||
title: PropTypes.object.isRequired, // jsx for title
|
||||
message: PropTypes.object.isRequired, // jsx to display
|
||||
};
|
||||
|
||||
render() {
|
||||
return <div className='mx_GenericErrorPage'>
|
||||
<div className='mx_GenericErrorPage_box'>
|
||||
<h1>{_t("Error loading Riot")}</h1>
|
||||
<h1>{this.props.title}</h1>
|
||||
<p>{this.props.message}</p>
|
||||
<p>{_t(
|
||||
"If this is unexpected, please contact your system administrator " +
|
||||
"or technical support representative.",
|
||||
)}</p>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd.
|
||||
Copyright 2017, 2018 New Vector Ltd.
|
||||
Copyright 2019 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,8 +17,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import Promise from 'bluebird';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
|
@ -34,8 +35,9 @@ import classnames from 'classnames';
|
|||
import GroupStore from '../../stores/GroupStore';
|
||||
import FlairStore from '../../stores/FlairStore';
|
||||
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to";
|
||||
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
|
||||
import {Group} from "matrix-js-sdk";
|
||||
import {allSettled, sleep} from "../../utils/promise";
|
||||
|
||||
const LONG_DESC_PLACEHOLDER = _td(
|
||||
`<h1>HTML for your community's page</h1>
|
||||
|
@ -66,7 +68,7 @@ const UserSummaryType = PropTypes.shape({
|
|||
}).isRequired,
|
||||
});
|
||||
|
||||
const CategoryRoomList = React.createClass({
|
||||
const CategoryRoomList = createReactClass({
|
||||
displayName: 'CategoryRoomList',
|
||||
|
||||
props: {
|
||||
|
@ -96,11 +98,10 @@ const CategoryRoomList = React.createClass({
|
|||
onFinished: (success, addrs) => {
|
||||
if (!success) return;
|
||||
const errorList = [];
|
||||
Promise.all(addrs.map((addr) => {
|
||||
allSettled(addrs.map((addr) => {
|
||||
return GroupStore
|
||||
.addRoomToGroupSummary(this.props.groupId, addr.address)
|
||||
.catch(() => { errorList.push(addr.address); })
|
||||
.reflect();
|
||||
.catch(() => { errorList.push(addr.address); });
|
||||
})).then(() => {
|
||||
if (errorList.length === 0) {
|
||||
return;
|
||||
|
@ -118,7 +119,7 @@ const CategoryRoomList = React.createClass({
|
|||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -155,7 +156,7 @@ const CategoryRoomList = React.createClass({
|
|||
},
|
||||
});
|
||||
|
||||
const FeaturedRoom = React.createClass({
|
||||
const FeaturedRoom = createReactClass({
|
||||
displayName: 'FeaturedRoom',
|
||||
|
||||
props: {
|
||||
|
@ -243,7 +244,7 @@ const FeaturedRoom = React.createClass({
|
|||
},
|
||||
});
|
||||
|
||||
const RoleUserList = React.createClass({
|
||||
const RoleUserList = createReactClass({
|
||||
displayName: 'RoleUserList',
|
||||
|
||||
props: {
|
||||
|
@ -265,7 +266,7 @@ const RoleUserList = React.createClass({
|
|||
Modal.createTrackedDialog('Add Users to Group Summary', '', AddressPickerDialog, {
|
||||
title: _t('Add users to the community summary'),
|
||||
description: _t("Who would you like to add to this summary?"),
|
||||
placeholder: _t("Name or matrix ID"),
|
||||
placeholder: _t("Name or Matrix ID"),
|
||||
button: _t("Add to summary"),
|
||||
validAddressTypes: ['mx-user-id'],
|
||||
groupId: this.props.groupId,
|
||||
|
@ -273,11 +274,10 @@ const RoleUserList = React.createClass({
|
|||
onFinished: (success, addrs) => {
|
||||
if (!success) return;
|
||||
const errorList = [];
|
||||
Promise.all(addrs.map((addr) => {
|
||||
allSettled(addrs.map((addr) => {
|
||||
return GroupStore
|
||||
.addUserToGroupSummary(addr.address)
|
||||
.catch(() => { errorList.push(addr.address); })
|
||||
.reflect();
|
||||
.catch(() => { errorList.push(addr.address); });
|
||||
})).then(() => {
|
||||
if (errorList.length === 0) {
|
||||
return;
|
||||
|
@ -295,7 +295,7 @@ const RoleUserList = React.createClass({
|
|||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -326,7 +326,7 @@ const RoleUserList = React.createClass({
|
|||
},
|
||||
});
|
||||
|
||||
const FeaturedUser = React.createClass({
|
||||
const FeaturedUser = createReactClass({
|
||||
displayName: 'FeaturedUser',
|
||||
|
||||
props: {
|
||||
|
@ -342,7 +342,6 @@ const FeaturedUser = React.createClass({
|
|||
dis.dispatch({
|
||||
action: 'view_start_chat_or_reuse',
|
||||
user_id: this.props.summaryInfo.user_id,
|
||||
go_home_on_cancel: false,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -399,7 +398,7 @@ const FeaturedUser = React.createClass({
|
|||
const GROUP_JOINPOLICY_OPEN = "open";
|
||||
const GROUP_JOINPOLICY_INVITE = "invite";
|
||||
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'GroupView',
|
||||
|
||||
propTypes: {
|
||||
|
@ -408,10 +407,6 @@ export default React.createClass({
|
|||
groupIsNew: PropTypes.bool,
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
groupStore: PropTypes.instanceOf(GroupStore),
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
summary: null,
|
||||
|
@ -641,7 +636,7 @@ export default React.createClass({
|
|||
title: _t('Error'),
|
||||
description: _t('Failed to upload image'),
|
||||
});
|
||||
}).done();
|
||||
});
|
||||
},
|
||||
|
||||
_onJoinableChange: function(ev) {
|
||||
|
@ -680,7 +675,7 @@ export default React.createClass({
|
|||
this.setState({
|
||||
avatarChanged: false,
|
||||
});
|
||||
}).done();
|
||||
});
|
||||
},
|
||||
|
||||
_saveGroup: async function() {
|
||||
|
@ -695,7 +690,7 @@ export default React.createClass({
|
|||
|
||||
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
|
||||
// spinner disappearing after we have fetched new group data.
|
||||
await Promise.delay(500);
|
||||
await sleep(500);
|
||||
|
||||
GroupStore.acceptGroupInvite(this.props.groupId).then(() => {
|
||||
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||
|
@ -714,7 +709,7 @@ export default React.createClass({
|
|||
|
||||
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
|
||||
// spinner disappearing after we have fetched new group data.
|
||||
await Promise.delay(500);
|
||||
await sleep(500);
|
||||
|
||||
GroupStore.leaveGroup(this.props.groupId).then(() => {
|
||||
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||
|
@ -738,7 +733,7 @@ export default React.createClass({
|
|||
|
||||
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
|
||||
// spinner disappearing after we have fetched new group data.
|
||||
await Promise.delay(500);
|
||||
await sleep(500);
|
||||
|
||||
GroupStore.joinGroup(this.props.groupId).then(() => {
|
||||
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||
|
@ -790,7 +785,7 @@ export default React.createClass({
|
|||
|
||||
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
|
||||
// spinner disappearing after we have fetched new group data.
|
||||
await Promise.delay(500);
|
||||
await sleep(500);
|
||||
|
||||
GroupStore.leaveGroup(this.props.groupId).then(() => {
|
||||
// don't reset membershipBusy here: wait for the membership change to come down the sync
|
||||
|
@ -861,9 +856,9 @@ export default React.createClass({
|
|||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const ToolTipButton = sdk.getComponent('elements.ToolTipButton');
|
||||
const TooltipButton = sdk.getComponent('elements.TooltipButton');
|
||||
|
||||
const roomsHelpNode = this.state.editing ? <ToolTipButton helpText={
|
||||
const roomsHelpNode = this.state.editing ? <TooltipButton helpText={
|
||||
_t(
|
||||
'These rooms are displayed to community members on the community page. '+
|
||||
'Community members can join the rooms by clicking on them.',
|
||||
|
@ -1226,7 +1221,7 @@ export default React.createClass({
|
|||
blurToCancel={false}
|
||||
initialValue={this.state.profileForm.name}
|
||||
onValueChanged={this._onNameChange}
|
||||
tabIndex="1"
|
||||
tabIndex="0"
|
||||
dir="auto" />;
|
||||
|
||||
shortDescNode = <EditableText ref="descriptionEditor"
|
||||
|
@ -1236,7 +1231,7 @@ export default React.createClass({
|
|||
blurToCancel={false}
|
||||
initialValue={this.state.profileForm.short_description}
|
||||
onValueChanged={this._onShortDescChange}
|
||||
tabIndex="2"
|
||||
tabIndex="0"
|
||||
dir="auto" />;
|
||||
} else {
|
||||
const onGroupHeaderItemClick = this.state.isUserMember ? this._onEditClick : null;
|
||||
|
|
|
@ -19,7 +19,7 @@ import PropTypes from "prop-types";
|
|||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
||||
export default class IndicatorScrollbar extends React.Component {
|
||||
static PropTypes = {
|
||||
static propTypes = {
|
||||
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
|
||||
// and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
|
||||
// by the parent element.
|
||||
|
@ -38,6 +38,8 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
this.checkOverflow = this.checkOverflow.bind(this);
|
||||
this._scrollElement = null;
|
||||
this._autoHideScrollbar = null;
|
||||
this._likelyTrackpadUser = null;
|
||||
this._checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
|
||||
|
||||
this.state = {
|
||||
leftIndicatorOffset: 0,
|
||||
|
@ -129,9 +131,39 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
// the harshness of the scroll behaviour. Should be a value between 0 and 1.
|
||||
const yRetention = 1.0;
|
||||
|
||||
if (Math.abs(e.deltaX) < xyThreshold) {
|
||||
// whenever we see horizontal scrolling, assume the user is on a trackpad
|
||||
// for at least the next 1 minute.
|
||||
const now = new Date().getTime();
|
||||
if (Math.abs(e.deltaX) > 0) {
|
||||
this._likelyTrackpadUser = true;
|
||||
this._checkAgainForTrackpad = now + (1 * 60 * 1000);
|
||||
} else {
|
||||
// if we haven't seen any horizontal scrolling for a while, assume
|
||||
// the user might have plugged in a mousewheel
|
||||
if (this._likelyTrackpadUser && now >= this._checkAgainForTrackpad) {
|
||||
this._likelyTrackpadUser = false;
|
||||
}
|
||||
}
|
||||
|
||||
// don't mess with the horizontal scroll for trackpad users
|
||||
// See https://github.com/vector-im/riot-web/issues/10005
|
||||
if (this._likelyTrackpadUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(e.deltaX) <= xyThreshold) { // we are vertically scrolling.
|
||||
// HACK: We increase the amount of scroll to counteract smooth scrolling browsers.
|
||||
// Smooth scrolling browsers (Firefox) use the relative area to determine the scroll
|
||||
// amount, which means the likely small area of content results in a small amount of
|
||||
// movement - not what people expect. We pick arbitrary values for when to apply more
|
||||
// scroll, and how much to apply. On Windows 10, Chrome scrolls 100 units whereas
|
||||
// Firefox scrolls just 3 due to smooth scrolling.
|
||||
|
||||
const additionalScroll = e.deltaY < 0 ? -50 : 50;
|
||||
|
||||
// noinspection JSSuspiciousNameCombination
|
||||
this._scrollElement.scrollLeft += e.deltaY * yRetention;
|
||||
const val = Math.abs(e.deltaY) < 25 ? (e.deltaY + additionalScroll) : e.deltaY;
|
||||
this._scrollElement.scrollLeft += val * yRetention;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd.
|
||||
Copyright 2019 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,11 +19,14 @@ import Matrix from 'matrix-js-sdk';
|
|||
const InteractiveAuth = Matrix.InteractiveAuth;
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {getEntryComponentForLoginType} from '../views/auth/InteractiveAuthEntryComponents';
|
||||
|
||||
export default React.createClass({
|
||||
import sdk from '../../index';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'InteractiveAuth',
|
||||
|
||||
propTypes: {
|
||||
|
@ -60,7 +64,7 @@ export default React.createClass({
|
|||
inputs: PropTypes.object,
|
||||
|
||||
// As js-sdk interactive-auth
|
||||
makeRegistrationUrl: PropTypes.func,
|
||||
requestEmailToken: PropTypes.func,
|
||||
sessionId: PropTypes.string,
|
||||
clientSecret: PropTypes.string,
|
||||
emailSid: PropTypes.string,
|
||||
|
@ -90,12 +94,14 @@ export default React.createClass({
|
|||
this._authLogic = new InteractiveAuth({
|
||||
authData: this.props.authData,
|
||||
doRequest: this._requestCallback,
|
||||
busyChanged: this._onBusyChanged,
|
||||
inputs: this.props.inputs,
|
||||
stateUpdated: this._authStateUpdated,
|
||||
matrixClient: this.props.matrixClient,
|
||||
sessionId: this.props.sessionId,
|
||||
clientSecret: this.props.clientSecret,
|
||||
emailSid: this.props.emailSid,
|
||||
requestEmailToken: this._requestEmailToken,
|
||||
});
|
||||
|
||||
this._authLogic.attemptAuth().then((result) => {
|
||||
|
@ -115,7 +121,7 @@ export default React.createClass({
|
|||
this.setState({
|
||||
errorText: msg,
|
||||
});
|
||||
}).done();
|
||||
});
|
||||
|
||||
this._intervalId = null;
|
||||
if (this.props.poll) {
|
||||
|
@ -133,6 +139,19 @@ export default React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_requestEmailToken: async function(...args) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
});
|
||||
try {
|
||||
return await this.props.requestEmailToken(...args);
|
||||
} finally {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
tryContinue: function() {
|
||||
if (this.refs.stageComponent && this.refs.stageComponent.tryContinue) {
|
||||
this.refs.stageComponent.tryContinue();
|
||||
|
@ -150,27 +169,26 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_requestCallback: function(auth, background) {
|
||||
const makeRequestPromise = this.props.makeRequest(auth);
|
||||
_requestCallback: function(auth) {
|
||||
// This wrapper just exists because the js-sdk passes a second
|
||||
// 'busy' param for backwards compat. This throws the tests off
|
||||
// so discard it here.
|
||||
return this.props.makeRequest(auth);
|
||||
},
|
||||
|
||||
// if it's a background request, just do it: we don't want
|
||||
// it to affect the state of our UI.
|
||||
if (background) return makeRequestPromise;
|
||||
|
||||
// otherwise, manage the state of the spinner and error messages
|
||||
this.setState({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
});
|
||||
return makeRequestPromise.finally(() => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
_onBusyChanged: function(busy) {
|
||||
// if we've started doing stuff, reset the error messages
|
||||
if (busy) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_setFocus: function() {
|
||||
|
@ -185,7 +203,14 @@ export default React.createClass({
|
|||
|
||||
_renderCurrentStage: function() {
|
||||
const stage = this.state.authStage;
|
||||
if (!stage) return null;
|
||||
if (!stage) {
|
||||
if (this.state.busy) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return <Loader />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const StageComponent = getEntryComponentForLoginType(stage);
|
||||
return (
|
||||
|
@ -202,7 +227,6 @@ export default React.createClass({
|
|||
stageState={this.state.stageState}
|
||||
fail={this._onAuthStageFailed}
|
||||
setEmailSid={this._setEmailSid}
|
||||
makeRegistrationUrl={this.props.makeRegistrationUrl}
|
||||
showContinue={!this.props.continueIsManaged}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 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.
|
||||
|
@ -14,13 +15,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import { Key } from '../../Keyboard';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import VectorConferenceHandler from '../../VectorConferenceHandler';
|
||||
|
@ -30,7 +30,7 @@ import {_t} from "../../languageHandler";
|
|||
import Analytics from "../../Analytics";
|
||||
|
||||
|
||||
const LeftPanel = React.createClass({
|
||||
const LeftPanel = createReactClass({
|
||||
displayName: 'LeftPanel',
|
||||
|
||||
// NB. If you add props, don't forget to update
|
||||
|
@ -53,16 +53,19 @@ const LeftPanel = React.createClass({
|
|||
componentWillMount: function() {
|
||||
this.focusedElement = null;
|
||||
|
||||
this._settingWatchRef = SettingsStore.watchSetting(
|
||||
"feature_room_breadcrumbs", null, this._onBreadcrumbsChanged);
|
||||
this._breadcrumbsWatcherRef = SettingsStore.watchSetting(
|
||||
"breadcrumbs", null, this._onBreadcrumbsChanged);
|
||||
this._tagPanelWatcherRef = SettingsStore.watchSetting(
|
||||
"TagPanel.enableTagPanel", null, () => this.forceUpdate());
|
||||
|
||||
const useBreadcrumbs = SettingsStore.isFeatureEnabled("feature_room_breadcrumbs");
|
||||
const useBreadcrumbs = !!SettingsStore.getValue("breadcrumbs");
|
||||
Analytics.setBreadcrumbs(useBreadcrumbs);
|
||||
this.setState({breadcrumbs: useBreadcrumbs});
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
SettingsStore.unwatchSetting(this._settingWatchRef);
|
||||
SettingsStore.unwatchSetting(this._breadcrumbsWatcherRef);
|
||||
SettingsStore.unwatchSetting(this._tagPanelWatcherRef);
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
|
@ -82,6 +85,9 @@ const LeftPanel = React.createClass({
|
|||
if (this.state.searchFilter !== nextState.searchFilter) {
|
||||
return true;
|
||||
}
|
||||
if (this.state.searchExpanded !== nextState.searchExpanded) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
@ -110,37 +116,38 @@ const LeftPanel = React.createClass({
|
|||
this.focusedElement = null;
|
||||
},
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
_onFilterKeyDown: function(ev) {
|
||||
if (!this.focusedElement) return;
|
||||
let handled = true;
|
||||
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.TAB:
|
||||
this._onMoveFocus(ev.shiftKey);
|
||||
break;
|
||||
case KeyCode.UP:
|
||||
this._onMoveFocus(true);
|
||||
break;
|
||||
case KeyCode.DOWN:
|
||||
this._onMoveFocus(false);
|
||||
break;
|
||||
case KeyCode.ENTER:
|
||||
this._onMoveFocus(false);
|
||||
if (this.focusedElement) {
|
||||
this.focusedElement.click();
|
||||
switch (ev.key) {
|
||||
// On enter of rooms filter select and activate first room if such one exists
|
||||
case Key.ENTER: {
|
||||
const firstRoom = ev.target.closest(".mx_LeftPanel").querySelector(".mx_RoomTile");
|
||||
if (firstRoom) {
|
||||
firstRoom.click();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onMoveFocus: function(up) {
|
||||
_onKeyDown: function(ev) {
|
||||
if (!this.focusedElement) return;
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.TAB:
|
||||
this._onMoveFocus(ev, ev.shiftKey);
|
||||
break;
|
||||
case Key.ARROW_UP:
|
||||
this._onMoveFocus(ev, true, true);
|
||||
break;
|
||||
case Key.ARROW_DOWN:
|
||||
this._onMoveFocus(ev, false, true);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_onMoveFocus: function(ev, up, trap) {
|
||||
let element = this.focusedElement;
|
||||
|
||||
// unclear why this isn't needed
|
||||
|
@ -174,28 +181,24 @@ const LeftPanel = React.createClass({
|
|||
|
||||
if (element) {
|
||||
classes = element.classList;
|
||||
if (classes.contains("mx_LeftPanel")) { // we hit the top
|
||||
element = up ? element.lastElementChild : element.firstElementChild;
|
||||
descending = true;
|
||||
}
|
||||
}
|
||||
} while (element && !(
|
||||
classes.contains("mx_RoomTile") ||
|
||||
classes.contains("mx_textinput_search")));
|
||||
classes.contains("mx_RoomSubList_label") ||
|
||||
classes.contains("mx_LeftPanel_filterRooms")));
|
||||
|
||||
if (element) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
element.focus();
|
||||
this.focusedElement = element;
|
||||
this.focusedDescending = descending;
|
||||
} else if (trap) {
|
||||
// if navigation is via up/down arrow-keys, trap in the widget so it doesn't send to composer
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
onHideClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'hide_left_panel',
|
||||
});
|
||||
},
|
||||
|
||||
onSearch: function(term) {
|
||||
this.setState({ searchFilter: term });
|
||||
},
|
||||
|
@ -204,12 +207,23 @@ const LeftPanel = React.createClass({
|
|||
if (source === "keyboard") {
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
}
|
||||
this.setState({searchExpanded: false});
|
||||
},
|
||||
|
||||
collectRoomList: function(ref) {
|
||||
this._roomList = ref;
|
||||
},
|
||||
|
||||
_onSearchFocus: function() {
|
||||
this.setState({searchExpanded: true});
|
||||
},
|
||||
|
||||
_onSearchBlur: function(event) {
|
||||
if (event.target.value.length === 0) {
|
||||
this.setState({searchExpanded: false});
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const RoomList = sdk.getComponent('rooms.RoomList');
|
||||
const RoomBreadcrumbs = sdk.getComponent('rooms.RoomBreadcrumbs');
|
||||
|
@ -218,6 +232,7 @@ const LeftPanel = React.createClass({
|
|||
const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton');
|
||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
||||
const CallPreview = sdk.getComponent('voip.CallPreview');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const tagPanelEnabled = SettingsStore.getValue("TagPanel.enableTagPanel");
|
||||
let tagPanelContainer;
|
||||
|
@ -241,11 +256,25 @@ const LeftPanel = React.createClass({
|
|||
},
|
||||
);
|
||||
|
||||
let exploreButton;
|
||||
if (!this.props.collapsed) {
|
||||
exploreButton = (
|
||||
<div className={classNames("mx_LeftPanel_explore", {"mx_LeftPanel_explore_hidden": this.state.searchExpanded})}>
|
||||
<AccessibleButton onClick={() => dis.dispatch({action: 'view_room_directory'})}>{_t("Explore")}</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const searchBox = (<SearchBox
|
||||
className="mx_LeftPanel_filterRooms"
|
||||
enableRoomSearchFocus={true}
|
||||
placeholder={ _t('Filter room names') }
|
||||
blurredPlaceholder={ _t('Filter') }
|
||||
placeholder={ _t('Filter rooms…') }
|
||||
onKeyDown={this._onFilterKeyDown}
|
||||
onSearch={ this.onSearch }
|
||||
onCleared={ this.onSearchCleared }
|
||||
onFocus={this._onSearchFocus}
|
||||
onBlur={this._onSearchBlur}
|
||||
collapsed={this.props.collapsed} />);
|
||||
|
||||
let breadcrumbs;
|
||||
|
@ -256,12 +285,18 @@ const LeftPanel = React.createClass({
|
|||
return (
|
||||
<div className={containerClasses}>
|
||||
{ tagPanelContainer }
|
||||
<aside className={"mx_LeftPanel dark-panel"} onKeyDown={ this._onKeyDown } onFocus={ this._onFocus } onBlur={ this._onBlur }>
|
||||
<TopLeftMenuButton collapsed={ this.props.collapsed } />
|
||||
<aside className="mx_LeftPanel dark-panel">
|
||||
<TopLeftMenuButton collapsed={this.props.collapsed} />
|
||||
{ breadcrumbs }
|
||||
{ searchBox }
|
||||
<CallPreview ConferenceHandler={VectorConferenceHandler} />
|
||||
<div className="mx_LeftPanel_exploreAndFilterRow" onKeyDown={this._onKeyDown} onFocus={this._onFocus} onBlur={this._onBlur}>
|
||||
{ exploreButton }
|
||||
{ searchBox }
|
||||
</div>
|
||||
<RoomList
|
||||
onKeyDown={this._onKeyDown}
|
||||
onFocus={this._onFocus}
|
||||
onBlur={this._onBlur}
|
||||
ref={this.collectRoomList}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapsed}
|
||||
|
|
|
@ -16,14 +16,16 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
|
||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||
import { Key, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import CallMediaHandler from '../../CallMediaHandler';
|
||||
import { fixupColorFonts } from '../../utils/FontManager';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import sessionStore from '../../stores/SessionStore';
|
||||
|
@ -41,6 +43,13 @@ import {Resizer, CollapseDistributor} from '../../resizer';
|
|||
// NB. this is just for server notices rather than pinned messages in general.
|
||||
const MAX_PINNED_NOTICES_PER_ROOM = 2;
|
||||
|
||||
function canElementReceiveInput(el) {
|
||||
return el.tagName === "INPUT" ||
|
||||
el.tagName === "TEXTAREA" ||
|
||||
el.tagName === "SELECT" ||
|
||||
!!el.getAttribute("contenteditable");
|
||||
}
|
||||
|
||||
/**
|
||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||
* determined by the page_type property.
|
||||
|
@ -50,11 +59,11 @@ const MAX_PINNED_NOTICES_PER_ROOM = 2;
|
|||
*
|
||||
* Components mounted below us can access the matrix client via the react context.
|
||||
*/
|
||||
const LoggedInView = React.createClass({
|
||||
const LoggedInView = createReactClass({
|
||||
displayName: 'LoggedInView',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
page_type: PropTypes.string.isRequired,
|
||||
onRoomCreated: PropTypes.func,
|
||||
|
||||
|
@ -70,7 +79,7 @@ const LoggedInView = React.createClass({
|
|||
},
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient),
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
authCache: PropTypes.object,
|
||||
},
|
||||
|
||||
|
@ -105,7 +114,7 @@ const LoggedInView = React.createClass({
|
|||
|
||||
CallMediaHandler.loadDevices();
|
||||
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
document.addEventListener('keydown', this._onNativeKeyDown, false);
|
||||
|
||||
this._sessionStore = sessionStore;
|
||||
this._sessionStoreToken = this._sessionStore.addListener(
|
||||
|
@ -118,6 +127,8 @@ const LoggedInView = React.createClass({
|
|||
this._matrixClient.on("accountData", this.onAccountData);
|
||||
this._matrixClient.on("sync", this.onSync);
|
||||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||
|
||||
fixupColorFonts();
|
||||
},
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
@ -133,7 +144,7 @@ const LoggedInView = React.createClass({
|
|||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
document.removeEventListener('keydown', this._onNativeKeyDown, false);
|
||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||
this._matrixClient.removeListener("sync", this.onSync);
|
||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
|
@ -269,6 +280,58 @@ const LoggedInView = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onPaste: function(ev) {
|
||||
let canReceiveInput = false;
|
||||
let element = ev.target;
|
||||
// test for all parents because the target can be a child of a contenteditable element
|
||||
while (!canReceiveInput && element) {
|
||||
canReceiveInput = canElementReceiveInput(element);
|
||||
element = element.parentElement;
|
||||
}
|
||||
if (!canReceiveInput) {
|
||||
// refocusing during a paste event will make the
|
||||
// paste end up in the newly focused element,
|
||||
// so dispatch synchronously before paste happens
|
||||
dis.dispatch({action: 'focus_composer'}, true);
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
SOME HACKERY BELOW:
|
||||
React optimizes event handlers, by always attaching only 1 handler to the document for a given type.
|
||||
It then internally determines the order in which React event handlers should be called,
|
||||
emulating the capture and bubbling phases the DOM also has.
|
||||
|
||||
But, as the native handler for React is always attached on the document,
|
||||
it will always run last for bubbling (first for capturing) handlers,
|
||||
and thus React basically has its own event phases, and will always run
|
||||
after (before for capturing) any native other event handlers (as they tend to be attached last).
|
||||
|
||||
So ideally one wouldn't mix React and native event handlers to have bubbling working as expected,
|
||||
but we do need a native event handler here on the document,
|
||||
to get keydown events when there is no focused element (target=body).
|
||||
|
||||
We also do need bubbling here to give child components a chance to call `stopPropagation()`,
|
||||
for keydown events it can handle itself, and shouldn't be redirected to the composer.
|
||||
|
||||
So we listen with React on this component to get any events on focused elements, and get bubbling working as expected.
|
||||
We also listen with a native listener on the document to get keydown events when no element is focused.
|
||||
Bubbling is irrelevant here as the target is the body element.
|
||||
*/
|
||||
_onReactKeyDown: function(ev) {
|
||||
// events caught while bubbling up on the root element
|
||||
// of this component, so something must be focused.
|
||||
this._onKeyDown(ev);
|
||||
},
|
||||
|
||||
_onNativeKeyDown: function(ev) {
|
||||
// only pass this if there is no focused element.
|
||||
// if there is, _onKeyDown will be called by the
|
||||
// react keydown handler that respects the react bubbling order.
|
||||
if (ev.target === document.body) {
|
||||
this._onKeyDown(ev);
|
||||
}
|
||||
},
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
/*
|
||||
|
@ -287,34 +350,26 @@ const LoggedInView = React.createClass({
|
|||
|
||||
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;
|
||||
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.UP:
|
||||
case KeyCode.DOWN:
|
||||
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
const action = ev.keyCode == KeyCode.UP ?
|
||||
'view_prev_room' : 'view_next_room';
|
||||
dis.dispatch({action: action});
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.PAGE_UP:
|
||||
case KeyCode.PAGE_DOWN:
|
||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
switch (ev.key) {
|
||||
case Key.PAGE_UP:
|
||||
case Key.PAGE_DOWN:
|
||||
if (!hasModifier) {
|
||||
this._onScrollKeyPressed(ev);
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.HOME:
|
||||
case KeyCode.END:
|
||||
case Key.HOME:
|
||||
case Key.END:
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this._onScrollKeyPressed(ev);
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
case KeyCode.KEY_K:
|
||||
case Key.K:
|
||||
if (ctrlCmdOnly) {
|
||||
dis.dispatch({
|
||||
action: 'focus_room_filter',
|
||||
|
@ -322,11 +377,44 @@ const LoggedInView = React.createClass({
|
|||
handled = true;
|
||||
}
|
||||
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
|
||||
// composer, so CTRL+` it is
|
||||
|
||||
if (ctrlCmdOnly) {
|
||||
dis.dispatch({
|
||||
action: 'toggle_top_left_menu',
|
||||
});
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
} else if (!hasModifier) {
|
||||
const isClickShortcut = ev.target !== document.body &&
|
||||
(ev.key === Key.SPACE || ev.key === Key.ENTER);
|
||||
|
||||
// XXX: Remove after CIDER replaces Slate completely: https://github.com/vector-im/riot-web/issues/11036
|
||||
// If using Slate, consume the Backspace without first focusing as it causes an implosion
|
||||
if (ev.key === Key.BACKSPACE && !SettingsStore.getValue("useCiderComposer")) {
|
||||
ev.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) {
|
||||
// synchronous dispatch so we focus before key generates input
|
||||
dis.dispatch({action: 'focus_composer'}, true);
|
||||
ev.stopPropagation();
|
||||
// we should *not* preventDefault() here as
|
||||
// that would prevent typing in the now-focussed composer
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -437,6 +525,7 @@ const LoggedInView = React.createClass({
|
|||
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
|
||||
const GroupView = sdk.getComponent('structures.GroupView');
|
||||
const MyGroups = sdk.getComponent('structures.MyGroups');
|
||||
const ToastContainer = sdk.getComponent('structures.ToastContainer');
|
||||
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||
const CookieBar = sdk.getComponent('globals.CookieBar');
|
||||
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||
|
@ -538,8 +627,9 @@ const LoggedInView = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp}>
|
||||
<div onPaste={this._onPaste} onKeyDown={this._onReactKeyDown} className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp}>
|
||||
{ topBar }
|
||||
<ToastContainer />
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div ref={this._setResizeContainerRef} className={bodyClasses}>
|
||||
<LeftPanel
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 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.
|
||||
|
@ -79,9 +80,12 @@ export default class MainSplit extends React.Component {
|
|||
const wasPanelSet = this.props.panel && !prevProps.panel;
|
||||
const wasPanelCleared = !this.props.panel && prevProps.panel;
|
||||
|
||||
if (wasExpanded || wasPanelSet) {
|
||||
if (this.resizeContainer && (wasExpanded || wasPanelSet)) {
|
||||
// The resizer can only be created when **both** expanded and the panel is
|
||||
// set. Once both are true, the container ref will mount, which is required
|
||||
// for the resizer to work.
|
||||
this._createResizer();
|
||||
} else if (wasCollapsed || wasPanelCleared) {
|
||||
} else if (this.resizer && (wasCollapsed || wasPanelCleared)) {
|
||||
this.resizer.detach();
|
||||
this.resizer = null;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 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,16 +25,18 @@ import {wantsDateSeparator} from '../../DateUtils';
|
|||
import sdk from '../../index';
|
||||
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import {_t} from "../../languageHandler";
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
|
||||
const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() === 'm.room.third_party_invite';
|
||||
|
||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MessagePanel',
|
||||
|
||||
propTypes: {
|
||||
export default class MessagePanel extends React.Component {
|
||||
static propTypes = {
|
||||
// true to give the component a 'display: none' style.
|
||||
hidden: PropTypes.bool,
|
||||
|
||||
|
@ -51,6 +54,10 @@ module.exports = React.createClass({
|
|||
// ID of an event to highlight. If undefined, no event will be highlighted.
|
||||
highlightedEventId: PropTypes.string,
|
||||
|
||||
// The room these events are all in together, if any.
|
||||
// (The notification panel won't have a room here, for example.)
|
||||
room: PropTypes.object,
|
||||
|
||||
// Should we show URL Previews
|
||||
showUrlPreview: PropTypes.bool,
|
||||
|
||||
|
@ -92,58 +99,111 @@ module.exports = React.createClass({
|
|||
|
||||
// show timestamps always
|
||||
alwaysShowTimestamps: PropTypes.bool,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// the event after which we put a visible unread marker on the last
|
||||
// render cycle; null if readMarkerVisible was false or the RM was
|
||||
// suppressed (eg because it was at the end of the timeline)
|
||||
this.currentReadMarkerEventId = null;
|
||||
// helper function to access relations for an event
|
||||
getRelationsForEvent: PropTypes.func,
|
||||
|
||||
// the event after which we are showing a disappearing read marker
|
||||
// animation
|
||||
this.currentGhostEventId = null;
|
||||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
// previous positions the read marker has been in, so we can
|
||||
// display 'ghost' read markers that are animating away
|
||||
ghostReadMarkers: [],
|
||||
};
|
||||
|
||||
// opaque readreceipt info for each userId; used by ReadReceiptMarker
|
||||
// to manage its animations
|
||||
this._readReceiptMap = {};
|
||||
|
||||
// Remember the read marker ghost node so we can do the cleanup that
|
||||
// Velocity requires
|
||||
this._readMarkerGhostNode = null;
|
||||
// Track read receipts by event ID. For each _shown_ event ID, we store
|
||||
// the list of read receipts to display:
|
||||
// [
|
||||
// {
|
||||
// userId: string,
|
||||
// member: RoomMember,
|
||||
// ts: number,
|
||||
// },
|
||||
// ]
|
||||
// This is recomputed on each render. It's only stored on the component
|
||||
// for ease of passing the data around since it's computed in one pass
|
||||
// over all events.
|
||||
this._readReceiptsByEvent = {};
|
||||
|
||||
this._isMounted = true;
|
||||
},
|
||||
// Track read receipts by user ID. For each user ID we've ever shown a
|
||||
// a read receipt for, we store an object:
|
||||
// {
|
||||
// lastShownEventId: string,
|
||||
// receipt: {
|
||||
// userId: string,
|
||||
// member: RoomMember,
|
||||
// ts: number,
|
||||
// },
|
||||
// }
|
||||
// so that we can always keep receipts displayed by reverting back to
|
||||
// the last shown event for that user ID when needed. This may feel like
|
||||
// it duplicates the receipt storage in the room, but at this layer, we
|
||||
// are tracking _shown_ event IDs, which the JS SDK knows nothing about.
|
||||
// This is recomputed on each render, using the data from the previous
|
||||
// render as our fallback for any user IDs we can't match a receipt to a
|
||||
// displayed event in the current render cycle.
|
||||
this._readReceiptsByUserId = {};
|
||||
|
||||
// Cache hidden events setting on mount since Settings is expensive to
|
||||
// query, and we check this in a hot code path.
|
||||
this._showHiddenEventsInTimeline =
|
||||
SettingsStore.getValue("showHiddenEventsInTimeline");
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.readMarkerVisible && this.props.readMarkerEventId !== prevProps.readMarkerEventId) {
|
||||
const ghostReadMarkers = this.state.ghostReadMarkers;
|
||||
ghostReadMarkers.push(prevProps.readMarkerEventId);
|
||||
this.setState({
|
||||
ghostReadMarkers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* get the DOM node representing the given event */
|
||||
getNodeForEventId: function(eventId) {
|
||||
getNodeForEventId(eventId) {
|
||||
if (!this.eventNodes) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.eventNodes[eventId];
|
||||
},
|
||||
}
|
||||
|
||||
/* return true if the content is fully scrolled down right now; else false.
|
||||
*/
|
||||
isAtBottom: function() {
|
||||
isAtBottom() {
|
||||
return this.refs.scrollPanel
|
||||
&& this.refs.scrollPanel.isAtBottom();
|
||||
},
|
||||
}
|
||||
|
||||
/* get the current scroll state. See ScrollPanel.getScrollState for
|
||||
* details.
|
||||
*
|
||||
* returns null if we are not mounted.
|
||||
*/
|
||||
getScrollState: function() {
|
||||
getScrollState() {
|
||||
if (!this.refs.scrollPanel) { return null; }
|
||||
return this.refs.scrollPanel.getScrollState();
|
||||
},
|
||||
}
|
||||
|
||||
// returns one of:
|
||||
//
|
||||
|
@ -151,7 +211,7 @@ module.exports = React.createClass({
|
|||
// -1: read marker is above the window
|
||||
// 0: read marker is within the window
|
||||
// +1: read marker is below the window
|
||||
getReadMarkerPosition: function() {
|
||||
getReadMarkerPosition() {
|
||||
const readMarker = this.refs.readMarkerNode;
|
||||
const messageWrapper = this.refs.scrollPanel;
|
||||
|
||||
|
@ -171,45 +231,45 @@ module.exports = React.createClass({
|
|||
} else {
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/* jump to the top of the content.
|
||||
*/
|
||||
scrollToTop: function() {
|
||||
scrollToTop() {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.scrollToTop();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/* jump to the bottom of the content.
|
||||
*/
|
||||
scrollToBottom: function() {
|
||||
scrollToBottom() {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.scrollToBottom();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Page up/down.
|
||||
*
|
||||
* @param {number} mult: -1 to page up, +1 to page down
|
||||
*/
|
||||
scrollRelative: function(mult) {
|
||||
scrollRelative(mult) {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.scrollRelative(mult);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll up/down in response to a scroll key
|
||||
*
|
||||
* @param {KeyboardEvent} ev: the keyboard event to handle
|
||||
*/
|
||||
handleScrollKey: function(ev) {
|
||||
handleScrollKey(ev) {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.handleScrollKey(ev);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/* jump to the given event id.
|
||||
*
|
||||
|
@ -221,30 +281,41 @@ module.exports = React.createClass({
|
|||
* node (specifically, the bottom of it) will be positioned. If omitted, it
|
||||
* defaults to 0.
|
||||
*/
|
||||
scrollToEvent: function(eventId, pixelOffset, offsetBase) {
|
||||
scrollToEvent(eventId, pixelOffset, offsetBase) {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.scrollToToken(eventId, pixelOffset, offsetBase);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
scrollToEventIfNeeded(eventId) {
|
||||
const node = this.eventNodes[eventId];
|
||||
if (node) {
|
||||
node.scrollIntoView({block: "nearest", behavior: "instant"});
|
||||
}
|
||||
}
|
||||
|
||||
/* check the scroll state and send out pagination requests if necessary.
|
||||
*/
|
||||
checkFillState: function() {
|
||||
checkFillState() {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.checkFillState();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_isUnmounting: function() {
|
||||
_isUnmounting() {
|
||||
return !this._isMounted;
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: Implement granular (per-room) hide options
|
||||
_shouldShowEvent: function(mxEv) {
|
||||
_shouldShowEvent(mxEv) {
|
||||
if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) {
|
||||
return false; // ignored = no show (only happens if the ignore happens after an event was received)
|
||||
}
|
||||
|
||||
if (this._showHiddenEventsInTimeline) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||
return false; // no tile = no show
|
||||
|
@ -254,15 +325,87 @@ module.exports = React.createClass({
|
|||
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
||||
|
||||
return !shouldHideEvent(mxEv);
|
||||
},
|
||||
}
|
||||
|
||||
_getEventTiles: function() {
|
||||
_readMarkerForEvent(eventId, isLastEvent) {
|
||||
const visible = !isLastEvent && this.props.readMarkerVisible;
|
||||
|
||||
if (this.props.readMarkerEventId === eventId) {
|
||||
let hr;
|
||||
// if the read marker comes at the end of the timeline (except
|
||||
// for local echoes, which are excluded from RMs, because they
|
||||
// don't have useful event ids), we don't want to show it, but
|
||||
// we still want to create the <li/> for it so that the
|
||||
// algorithms which depend on its position on the screen aren't
|
||||
// confused.
|
||||
if (visible) {
|
||||
hr = <hr className="mx_RoomView_myReadMarker"
|
||||
style={{opacity: 1, width: '99%'}}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={"readMarker_"+eventId} ref="readMarkerNode"
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
{ hr }
|
||||
</li>
|
||||
);
|
||||
} else if (this.state.ghostReadMarkers.includes(eventId)) {
|
||||
// We render 'ghost' read markers in the DOM while they
|
||||
// transition away. This allows the actual read marker
|
||||
// to be in the right place straight away without having
|
||||
// to wait for the transition to finish.
|
||||
// There are probably much simpler ways to do this transition,
|
||||
// possibly using react-transition-group which handles keeping
|
||||
// elements in the DOM whilst they transition out, although our
|
||||
// case is a little more complex because only some of the items
|
||||
// transition (ie. the read markers do but the event tiles do not)
|
||||
// and TransitionGroup requires that all its children are Transitions.
|
||||
const hr = <hr className="mx_RoomView_myReadMarker"
|
||||
ref={this._collectGhostReadMarker}
|
||||
onTransitionEnd={this._onGhostTransitionEnd}
|
||||
data-eventid={eventId}
|
||||
/>;
|
||||
|
||||
// give it a key which depends on the event id. That will ensure that
|
||||
// we get a new DOM node (restarting the animation) when the ghost
|
||||
// moves to a different event.
|
||||
return (
|
||||
<li key={"_readuptoghost_"+eventId}
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
{ hr }
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_collectGhostReadMarker = (node) => {
|
||||
if (node) {
|
||||
// now the element has appeared, change the style which will trigger the CSS transition
|
||||
requestAnimationFrame(() => {
|
||||
node.style.width = '10%';
|
||||
node.style.opacity = '0';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onGhostTransitionEnd = (ev) => {
|
||||
// we can now clean up the ghost element
|
||||
const finishedEventId = ev.target.dataset.eventid;
|
||||
this.setState({
|
||||
ghostReadMarkers: this.state.ghostReadMarkers.filter(eid => eid !== finishedEventId),
|
||||
});
|
||||
};
|
||||
|
||||
_getEventTiles() {
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
|
||||
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
||||
|
||||
this.eventNodes = {};
|
||||
|
||||
let visible = false;
|
||||
let i;
|
||||
|
||||
// first figure out which is the last event in the list which we're
|
||||
|
@ -297,28 +440,104 @@ module.exports = React.createClass({
|
|||
|
||||
let prevEvent = null; // the last event we showed
|
||||
|
||||
// assume there is no read marker until proven otherwise
|
||||
let readMarkerVisible = false;
|
||||
|
||||
// if the readmarker has moved, cancel any active ghost.
|
||||
if (this.currentReadMarkerEventId && this.props.readMarkerEventId &&
|
||||
this.props.readMarkerVisible &&
|
||||
this.currentReadMarkerEventId !== this.props.readMarkerEventId) {
|
||||
this.currentGhostEventId = null;
|
||||
this._readReceiptsByEvent = {};
|
||||
if (this.props.showReadReceipts) {
|
||||
this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();
|
||||
}
|
||||
|
||||
const isMembershipChange = (e) => e.getType() === 'm.room.member';
|
||||
|
||||
for (i = 0; i < this.props.events.length; i++) {
|
||||
const mxEv = this.props.events[i];
|
||||
const eventId = mxEv.getId();
|
||||
const last = (mxEv === lastShownEvent);
|
||||
|
||||
// Wrap initial room creation events into an EventListSummary
|
||||
// Grouping only events sent by the same user that sent the `m.room.create` and only until
|
||||
// the first non-state event or membership event which is not regarding the sender of the `m.room.create` event
|
||||
const shouldGroup = (ev) => {
|
||||
if (ev.getType() === "m.room.member"
|
||||
&& (ev.getStateKey() !== mxEv.getSender() || ev.getContent()["membership"] !== "join")) {
|
||||
return false;
|
||||
}
|
||||
if (ev.isState() && ev.getSender() === mxEv.getSender()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (mxEv.getType() === "m.room.create") {
|
||||
let summaryReadMarker = null;
|
||||
const ts1 = mxEv.getTs();
|
||||
|
||||
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
|
||||
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} /></li>;
|
||||
ret.push(dateSeparator);
|
||||
}
|
||||
|
||||
// If RM event is the first in the summary, append the RM after the summary
|
||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId());
|
||||
|
||||
// If this m.room.create event should be shown (room upgrade) then show it before the summary
|
||||
if (this._shouldShowEvent(mxEv)) {
|
||||
// pass in the mxEv as prevEvent as well so no extra DateSeparator is rendered
|
||||
ret.push(...this._getTilesForEvent(mxEv, mxEv, false));
|
||||
}
|
||||
|
||||
const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary
|
||||
for (;i + 1 < this.props.events.length; i++) {
|
||||
const collapsedMxEv = this.props.events[i + 1];
|
||||
|
||||
// Ignore redacted/hidden member events
|
||||
if (!this._shouldShowEvent(collapsedMxEv)) {
|
||||
// If this hidden event is the RM and in or at end of a summary put RM after the summary.
|
||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!shouldGroup(collapsedMxEv) || this._wantsDateSeparator(mxEv, collapsedMxEv.getDate())) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If RM event is in the summary, mark it as such and the RM will be appended after the summary.
|
||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
|
||||
|
||||
summarisedEvents.push(collapsedMxEv);
|
||||
}
|
||||
|
||||
// At this point, i = the index of the last event in the summary sequence
|
||||
const eventTiles = summarisedEvents.map((e) => {
|
||||
// In order to prevent DateSeparators from appearing in the expanded form
|
||||
// of EventListSummary, render each member event as if the previous
|
||||
// one was itself. This way, the timestamp of the previous event === the
|
||||
// timestamp of the current event, and no DateSeparator is inserted.
|
||||
return this._getTilesForEvent(e, e, e === lastShownEvent);
|
||||
}).reduce((a, b) => a.concat(b), []);
|
||||
|
||||
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
|
||||
const ev = this.props.events[i];
|
||||
ret.push(<EventListSummary
|
||||
key="roomcreationsummary"
|
||||
events={summarisedEvents}
|
||||
onToggle={this._onHeightChanged} // Update scroll state
|
||||
summaryMembers={[ev.sender]}
|
||||
summaryText={_t("%(creator)s created and configured the room.", {
|
||||
creator: ev.sender ? ev.sender.name : ev.getSender(),
|
||||
})}
|
||||
>
|
||||
{ eventTiles }
|
||||
</EventListSummary>);
|
||||
|
||||
if (summaryReadMarker) {
|
||||
ret.push(summaryReadMarker);
|
||||
}
|
||||
|
||||
prevEvent = mxEv;
|
||||
continue;
|
||||
}
|
||||
|
||||
const wantTile = this._shouldShowEvent(mxEv);
|
||||
|
||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||
if (isMembershipChange(mxEv) && wantTile) {
|
||||
let readMarkerInMels = false;
|
||||
let summaryReadMarker = null;
|
||||
const ts1 = mxEv.getTs();
|
||||
// Ensure that the key of the MemberEventListSummary does not change with new
|
||||
// member events. This will prevent it from being re-created unnecessarily, and
|
||||
|
@ -336,9 +555,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
// If RM event is the first in the MELS, append the RM after MELS
|
||||
if (mxEv.getId() === this.props.readMarkerEventId) {
|
||||
readMarkerInMels = true;
|
||||
}
|
||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId());
|
||||
|
||||
const summarisedEvents = [mxEv];
|
||||
for (;i + 1 < this.props.events.length; i++) {
|
||||
|
@ -347,9 +564,7 @@ module.exports = React.createClass({
|
|||
// Ignore redacted/hidden member events
|
||||
if (!this._shouldShowEvent(collapsedMxEv)) {
|
||||
// If this hidden event is the RM and in or at end of a MELS put RM after MELS.
|
||||
if (collapsedMxEv.getId() === this.props.readMarkerEventId) {
|
||||
readMarkerInMels = true;
|
||||
}
|
||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -359,9 +574,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
// If RM event is in MELS mark it as such and the RM will be appended after MELS.
|
||||
if (collapsedMxEv.getId() === this.props.readMarkerEventId) {
|
||||
readMarkerInMels = true;
|
||||
}
|
||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
|
||||
|
||||
summarisedEvents.push(collapsedMxEv);
|
||||
}
|
||||
|
@ -376,9 +589,9 @@ module.exports = React.createClass({
|
|||
// In order to prevent DateSeparators from appearing in the expanded form
|
||||
// of MemberEventListSummary, render each member event as if the previous
|
||||
// one was itself. This way, the timestamp of the previous event === the
|
||||
// timestamp of the current event, and no DateSeperator is inserted.
|
||||
// timestamp of the current event, and no DateSeparator is inserted.
|
||||
return this._getTilesForEvent(e, e, e === lastShownEvent);
|
||||
}).reduce((a, b) => a.concat(b));
|
||||
}).reduce((a, b) => a.concat(b), []);
|
||||
|
||||
if (eventTiles.length === 0) {
|
||||
eventTiles = null;
|
||||
|
@ -392,8 +605,8 @@ module.exports = React.createClass({
|
|||
{ eventTiles }
|
||||
</MemberEventListSummary>);
|
||||
|
||||
if (readMarkerInMels) {
|
||||
ret.push(this._getReadMarkerTile(visible));
|
||||
if (summaryReadMarker) {
|
||||
ret.push(summaryReadMarker);
|
||||
}
|
||||
|
||||
prevEvent = mxEv;
|
||||
|
@ -408,48 +621,20 @@ module.exports = React.createClass({
|
|||
prevEvent = mxEv;
|
||||
}
|
||||
|
||||
let isVisibleReadMarker = false;
|
||||
|
||||
if (eventId === this.props.readMarkerEventId) {
|
||||
visible = this.props.readMarkerVisible;
|
||||
|
||||
// if the read marker comes at the end of the timeline (except
|
||||
// for local echoes, which are excluded from RMs, because they
|
||||
// don't have useful event ids), we don't want to show it, but
|
||||
// we still want to create the <li/> for it so that the
|
||||
// algorithms which depend on its position on the screen aren't
|
||||
// confused.
|
||||
if (i >= lastShownNonLocalEchoIndex) {
|
||||
visible = false;
|
||||
}
|
||||
ret.push(this._getReadMarkerTile(visible));
|
||||
readMarkerVisible = visible;
|
||||
isVisibleReadMarker = visible;
|
||||
}
|
||||
|
||||
// XXX: there should be no need for a ghost tile - we should just use a
|
||||
// a dispatch (user_activity_end) to start the RM animation.
|
||||
if (eventId === this.currentGhostEventId) {
|
||||
// if we're showing an animation, continue to show it.
|
||||
ret.push(this._getReadMarkerGhostTile());
|
||||
} else if (!isVisibleReadMarker &&
|
||||
eventId === this.currentReadMarkerEventId) {
|
||||
// there is currently a read-up-to marker at this point, but no
|
||||
// more. Show an animation of it disappearing.
|
||||
ret.push(this._getReadMarkerGhostTile());
|
||||
this.currentGhostEventId = eventId;
|
||||
}
|
||||
const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
|
||||
if (readMarker) ret.push(readMarker);
|
||||
}
|
||||
|
||||
this.currentReadMarkerEventId = readMarkerVisible ? this.props.readMarkerEventId : null;
|
||||
return ret;
|
||||
},
|
||||
}
|
||||
|
||||
_getTilesForEvent: function(prevEvent, mxEv, last) {
|
||||
_getTilesForEvent(prevEvent, mxEv, last) {
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const ret = [];
|
||||
|
||||
const isEditing = this.props.editState &&
|
||||
this.props.editState.getEvent().getId() === mxEv.getId();
|
||||
// is this a continuation of the previous message?
|
||||
let continuation = false;
|
||||
|
||||
|
@ -506,48 +691,54 @@ module.exports = React.createClass({
|
|||
// Local echos have a send "status".
|
||||
const scrollToken = mxEv.status ? undefined : eventId;
|
||||
|
||||
let readReceipts;
|
||||
if (this.props.showReadReceipts) {
|
||||
readReceipts = this._getReadReceiptsForEvent(mxEv);
|
||||
}
|
||||
const readReceipts = this._readReceiptsByEvent[eventId];
|
||||
|
||||
ret.push(
|
||||
<li key={eventId}
|
||||
ref={this._collectEventNode.bind(this, eventId)}
|
||||
data-scroll-tokens={scrollToken}>
|
||||
<EventTile mxEvent={mxEv} continuation={continuation}
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
onHeightChanged={this._onHeightChanged}
|
||||
readReceipts={readReceipts}
|
||||
readReceiptMap={this._readReceiptMap}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
checkUnmounting={this._isUnmounting}
|
||||
eventSendStatus={mxEv.status}
|
||||
tileShape={this.props.tileShape}
|
||||
isTwelveHour={this.props.isTwelveHour}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
last={last} isSelectedEvent={highlight} />
|
||||
</li>,
|
||||
<li key={eventId}
|
||||
ref={this._collectEventNode.bind(this, eventId)}
|
||||
data-scroll-tokens={scrollToken}
|
||||
>
|
||||
<EventTile mxEvent={mxEv}
|
||||
continuation={continuation}
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
replacingEventId={mxEv.replacingEventId()}
|
||||
editState={isEditing && this.props.editState}
|
||||
onHeightChanged={this._onHeightChanged}
|
||||
readReceipts={readReceipts}
|
||||
readReceiptMap={this._readReceiptMap}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
checkUnmounting={this._isUnmounting}
|
||||
eventSendStatus={mxEv.getAssociatedStatus()}
|
||||
tileShape={this.props.tileShape}
|
||||
isTwelveHour={this.props.isTwelveHour}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
last={last}
|
||||
isSelectedEvent={highlight}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
showReactions={this.props.showReactions}
|
||||
/>
|
||||
</li>,
|
||||
);
|
||||
|
||||
return ret;
|
||||
},
|
||||
}
|
||||
|
||||
_wantsDateSeparator: function(prevEvent, nextEventDate) {
|
||||
_wantsDateSeparator(prevEvent, nextEventDate) {
|
||||
if (prevEvent == null) {
|
||||
// first event in the panel: depends if we could back-paginate from
|
||||
// here.
|
||||
return !this.props.suppressFirstDateSeparator;
|
||||
}
|
||||
return wantsDateSeparator(prevEvent.getDate(), nextEventDate);
|
||||
},
|
||||
}
|
||||
|
||||
// get a list of read receipts that should be shown next to this event
|
||||
// Get a list of read receipts that should be shown next to this event
|
||||
// Receipts are objects which have a 'userId', 'roomMember' and 'ts'.
|
||||
_getReadReceiptsForEvent: function(event) {
|
||||
_getReadReceiptsForEvent(event) {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
// get list of read receipts, sorted most recent first
|
||||
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
||||
const { room } = this.props;
|
||||
if (!room) {
|
||||
return null;
|
||||
}
|
||||
|
@ -566,81 +757,90 @@ module.exports = React.createClass({
|
|||
ts: r.data ? r.data.ts : 0,
|
||||
});
|
||||
});
|
||||
return receipts;
|
||||
}
|
||||
|
||||
return receipts.sort((r1, r2) => {
|
||||
return r2.ts - r1.ts;
|
||||
});
|
||||
},
|
||||
// Get an object that maps from event ID to a list of read receipts that
|
||||
// should be shown next to that event. If a hidden event has read receipts,
|
||||
// they are folded into the receipts of the last shown event.
|
||||
_getReadReceiptsByShownEvent() {
|
||||
const receiptsByEvent = {};
|
||||
const receiptsByUserId = {};
|
||||
|
||||
_getReadMarkerTile: function(visible) {
|
||||
let hr;
|
||||
if (visible) {
|
||||
hr = <hr className="mx_RoomView_myReadMarker"
|
||||
style={{opacity: 1, width: '99%'}}
|
||||
/>;
|
||||
let lastShownEventId;
|
||||
for (const event of this.props.events) {
|
||||
if (this._shouldShowEvent(event)) {
|
||||
lastShownEventId = event.getId();
|
||||
}
|
||||
if (!lastShownEventId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingReceipts = receiptsByEvent[lastShownEventId] || [];
|
||||
const newReceipts = this._getReadReceiptsForEvent(event);
|
||||
receiptsByEvent[lastShownEventId] = existingReceipts.concat(newReceipts);
|
||||
|
||||
// Record these receipts along with their last shown event ID for
|
||||
// each associated user ID.
|
||||
for (const receipt of newReceipts) {
|
||||
receiptsByUserId[receipt.userId] = {
|
||||
lastShownEventId,
|
||||
receipt,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<li key="_readupto" ref="readMarkerNode"
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
{ hr }
|
||||
</li>
|
||||
);
|
||||
},
|
||||
|
||||
_startAnimation: function(ghostNode) {
|
||||
if (this._readMarkerGhostNode) {
|
||||
Velocity.Utilities.removeData(this._readMarkerGhostNode);
|
||||
// It's possible in some cases (for example, when a read receipt
|
||||
// advances before we have paginated in the new event that it's marking
|
||||
// received) that we can temporarily not have a matching event for
|
||||
// someone which had one in the last. By looking through our previous
|
||||
// mapping of receipts by user ID, we can cover recover any receipts
|
||||
// that would have been lost by using the same event ID from last time.
|
||||
for (const userId in this._readReceiptsByUserId) {
|
||||
if (receiptsByUserId[userId]) {
|
||||
continue;
|
||||
}
|
||||
const { lastShownEventId, receipt } = this._readReceiptsByUserId[userId];
|
||||
const existingReceipts = receiptsByEvent[lastShownEventId] || [];
|
||||
receiptsByEvent[lastShownEventId] = existingReceipts.concat(receipt);
|
||||
receiptsByUserId[userId] = { lastShownEventId, receipt };
|
||||
}
|
||||
this._readMarkerGhostNode = ghostNode;
|
||||
this._readReceiptsByUserId = receiptsByUserId;
|
||||
|
||||
if (ghostNode) {
|
||||
Velocity(ghostNode, {opacity: '0', width: '10%'},
|
||||
{duration: 400, easing: 'easeInSine',
|
||||
delay: 1000});
|
||||
// After grouping receipts by shown events, do another pass to sort each
|
||||
// receipt list.
|
||||
for (const eventId in receiptsByEvent) {
|
||||
receiptsByEvent[eventId].sort((r1, r2) => {
|
||||
return r2.ts - r1.ts;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_getReadMarkerGhostTile: function() {
|
||||
const hr = <hr className="mx_RoomView_myReadMarker"
|
||||
style={{opacity: 1, width: '99%'}}
|
||||
ref={this._startAnimation}
|
||||
/>;
|
||||
return receiptsByEvent;
|
||||
}
|
||||
|
||||
// give it a key which depends on the event id. That will ensure that
|
||||
// we get a new DOM node (restarting the animation) when the ghost
|
||||
// moves to a different event.
|
||||
return (
|
||||
<li key={"_readuptoghost_"+this.currentGhostEventId}
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
{ hr }
|
||||
</li>
|
||||
);
|
||||
},
|
||||
|
||||
_collectEventNode: function(eventId, node) {
|
||||
_collectEventNode = (eventId, node) => {
|
||||
this.eventNodes[eventId] = node;
|
||||
},
|
||||
}
|
||||
|
||||
// once dynamic content in the events load, make the scrollPanel check the
|
||||
// scroll offsets.
|
||||
_onHeightChanged: function() {
|
||||
_onHeightChanged = () => {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
if (scrollPanel) {
|
||||
scrollPanel.checkScroll();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
_onTypingShown: function() {
|
||||
_onTypingShown = () => {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
// this will make the timeline grow, so checkScroll
|
||||
scrollPanel.checkScroll();
|
||||
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
|
||||
scrollPanel.preventShrinking();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
_onTypingHidden: function() {
|
||||
_onTypingHidden = () => {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
if (scrollPanel) {
|
||||
// as hiding the typing notifications doesn't
|
||||
|
@ -651,9 +851,9 @@ module.exports = React.createClass({
|
|||
// reveal added padding to balance the notifs disappearing.
|
||||
scrollPanel.checkScroll();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
updateTimelineMinHeight: function() {
|
||||
updateTimelineMinHeight() {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
|
||||
if (scrollPanel) {
|
||||
|
@ -668,16 +868,16 @@ module.exports = React.createClass({
|
|||
scrollPanel.preventShrinking();
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onTimelineReset: function() {
|
||||
onTimelineReset() {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
if (scrollPanel) {
|
||||
scrollPanel.clearPreventShrinking();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
@ -700,7 +900,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
|
||||
let whoIsTyping;
|
||||
if (this.props.room) {
|
||||
if (this.props.room && !this.props.tileShape) {
|
||||
whoIsTyping = (<WhoIsTypingTile
|
||||
room={this.props.room}
|
||||
onShown={this._onTypingShown}
|
||||
|
@ -724,5 +924,5 @@ module.exports = React.createClass({
|
|||
{ bottomSpinner }
|
||||
</ScrollPanel>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,20 +16,17 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import sdk from '../../index';
|
||||
import { _t } from '../../languageHandler';
|
||||
import dis from '../../dispatcher';
|
||||
import withMatrixClient from '../../wrappers/withMatrixClient';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
|
||||
export default withMatrixClient(React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'MyGroups',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
groups: null,
|
||||
|
@ -36,6 +34,10 @@ export default withMatrixClient(React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._fetch();
|
||||
},
|
||||
|
@ -45,7 +47,7 @@ export default withMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
_fetch: function() {
|
||||
this.props.matrixClient.getJoinedGroups().done((result) => {
|
||||
this.context.matrixClient.getJoinedGroups().then((result) => {
|
||||
this.setState({groups: result.groups, error: null});
|
||||
}, (err) => {
|
||||
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
|
@ -146,4 +148,4 @@ export default withMatrixClient(React.createClass({
|
|||
</div>
|
||||
</div>;
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 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.
|
||||
|
@ -14,18 +15,16 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { _t } from '../../languageHandler';
|
||||
const Matrix = require("matrix-js-sdk");
|
||||
const sdk = require('../../index');
|
||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const dis = require("../../dispatcher");
|
||||
|
||||
/*
|
||||
* Component which shows the global notification list using a TimelinePanel
|
||||
*/
|
||||
const NotificationPanel = React.createClass({
|
||||
const NotificationPanel = createReactClass({
|
||||
displayName: 'NotificationPanel',
|
||||
|
||||
propTypes: {
|
||||
|
@ -39,20 +38,21 @@ const NotificationPanel = React.createClass({
|
|||
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
||||
if (timelineSet) {
|
||||
return (
|
||||
<TimelinePanel key={"NotificationPanel_" + this.props.roomId}
|
||||
className="mx_NotificationPanel"
|
||||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={timelineSet}
|
||||
showUrlPreview = {false}
|
||||
tileShape="notif"
|
||||
empty={_t('You have no visible notifications')}
|
||||
/>
|
||||
<div className="mx_NotificationPanel" role="tabpanel">
|
||||
<TimelinePanel key={"NotificationPanel_" + this.props.roomId}
|
||||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={timelineSet}
|
||||
showUrlPreview={false}
|
||||
tileShape="notif"
|
||||
empty={_t('You have no visible notifications')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
console.error("No notifTimelineSet available!");
|
||||
return (
|
||||
<div className="mx_NotificationPanel">
|
||||
<div className="mx_NotificationPanel" role="tabpanel">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -3,6 +3,7 @@ Copyright 2015, 2016 OpenMarket Ltd
|
|||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -26,13 +27,14 @@ import { MatrixClient } from 'matrix-js-sdk';
|
|||
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import GroupStore from '../../stores/GroupStore';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
export default class RightPanel extends React.Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
roomId: React.PropTypes.string, // if showing panels for a given room, this is set
|
||||
groupId: React.PropTypes.string, // if showing panels for a given group, this is set
|
||||
user: React.PropTypes.object,
|
||||
roomId: PropTypes.string, // if showing panels for a given room, this is set
|
||||
groupId: PropTypes.string, // if showing panels for a given group, this is set
|
||||
user: PropTypes.object,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -164,6 +166,7 @@ export default class RightPanel extends React.Component {
|
|||
render() {
|
||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||
const UserInfo = sdk.getComponent('right_panel.UserInfo');
|
||||
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
|
||||
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
||||
const FilePanel = sdk.getComponent('structures.FilePanel');
|
||||
|
@ -182,14 +185,46 @@ export default class RightPanel extends React.Component {
|
|||
} else if (this.state.phase === RightPanel.Phase.GroupRoomList) {
|
||||
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) {
|
||||
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
|
||||
if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
|
||||
const onClose = () => {
|
||||
dis.dispatch({
|
||||
action: "view_user",
|
||||
member: null,
|
||||
});
|
||||
};
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
roomId={this.props.roomId}
|
||||
key={this.props.roomId || this.state.member.userId}
|
||||
onClose={onClose}
|
||||
/>;
|
||||
} else {
|
||||
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
|
||||
}
|
||||
} else if (this.state.phase === RightPanel.Phase.Room3pidMemberInfo) {
|
||||
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
|
||||
} else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) {
|
||||
panel = <GroupMemberInfo
|
||||
groupMember={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.user_id} />;
|
||||
if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
|
||||
const onClose = () => {
|
||||
dis.dispatch({
|
||||
action: "view_user",
|
||||
member: null,
|
||||
});
|
||||
};
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.userId}
|
||||
onClose={onClose} />;
|
||||
} else {
|
||||
panel = (
|
||||
<GroupMemberInfo
|
||||
groupMember={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.user_id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (this.state.phase === RightPanel.Phase.GroupRoomInfo) {
|
||||
panel = <GroupRoomInfo
|
||||
groupRoomId={this.state.groupRoomId}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 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.
|
||||
|
@ -14,9 +16,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
||||
const MatrixClientPeg = require('../../MatrixClientPeg');
|
||||
const ContentRepo = require("matrix-js-sdk").ContentRepo;
|
||||
|
@ -25,7 +26,7 @@ const sdk = require('../../index');
|
|||
const dis = require('../../dispatcher');
|
||||
|
||||
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
|
||||
import Promise from 'bluebird';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../languageHandler';
|
||||
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
||||
import Analytics from '../../Analytics';
|
||||
|
@ -37,12 +38,12 @@ function track(action) {
|
|||
Analytics.trackEvent('RoomDirectory', action);
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'RoomDirectory',
|
||||
|
||||
propTypes: {
|
||||
config: React.PropTypes.object,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
config: PropTypes.object,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -65,7 +66,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: React.PropTypes.object,
|
||||
matrixClient: PropTypes.object,
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
|
@ -87,7 +88,7 @@ module.exports = React.createClass({
|
|||
this.setState({protocolsLoading: false});
|
||||
return;
|
||||
}
|
||||
MatrixClientPeg.get().getThirdpartyProtocols().done((response) => {
|
||||
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
|
||||
this.protocols = response;
|
||||
this.setState({protocolsLoading: false});
|
||||
}, (err) => {
|
||||
|
@ -133,19 +134,23 @@ module.exports = React.createClass({
|
|||
publicRooms: [],
|
||||
loading: true,
|
||||
});
|
||||
this.getMoreRooms().done();
|
||||
this.getMoreRooms();
|
||||
},
|
||||
|
||||
getMoreRooms: function() {
|
||||
if (!MatrixClientPeg.get()) return Promise.resolve();
|
||||
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
|
||||
const my_filter_string = this.state.filterString;
|
||||
const my_server = this.state.roomServer;
|
||||
// remember the next batch token when we sent the request
|
||||
// too. If it's changed, appending to the list will corrupt it.
|
||||
const my_next_batch = this.nextBatch;
|
||||
const opts = {limit: 20};
|
||||
if (my_server != MatrixClientPeg.getHomeServerName()) {
|
||||
if (my_server != MatrixClientPeg.getHomeserverName()) {
|
||||
opts.server = my_server;
|
||||
}
|
||||
if (this.state.instanceId) {
|
||||
|
@ -240,7 +245,7 @@ module.exports = React.createClass({
|
|||
if (!alias) return;
|
||||
step = _t('delete the alias.');
|
||||
return MatrixClientPeg.get().deleteAlias(alias);
|
||||
}).done(() => {
|
||||
}).then(() => {
|
||||
modal.close();
|
||||
this.refreshRoomList();
|
||||
}, (err) => {
|
||||
|
@ -320,12 +325,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onCreateRoomClicked: function() {
|
||||
this.props.onFinished();
|
||||
dis.dispatch({action: 'view_create_room'});
|
||||
},
|
||||
|
||||
onJoinClick: function(alias) {
|
||||
onJoinFromSearchClick: function(alias) {
|
||||
// If we don't have a particular instance id selected, just show that rooms alias
|
||||
if (!this.state.instanceId) {
|
||||
// If the user specified an alias without a domain, add on whichever server is selected
|
||||
|
@ -333,7 +333,7 @@ module.exports = React.createClass({
|
|||
if (alias.indexOf(':') == -1) {
|
||||
alias = alias + ':' + this.state.roomServer;
|
||||
}
|
||||
this.showRoomAlias(alias);
|
||||
this.showRoomAlias(alias, true);
|
||||
} else {
|
||||
// This is a 3rd party protocol. Let's see if we can join it
|
||||
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
|
||||
|
@ -347,9 +347,9 @@ module.exports = React.createClass({
|
|||
});
|
||||
return;
|
||||
}
|
||||
MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => {
|
||||
MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).then((resp) => {
|
||||
if (resp.length > 0 && resp[0].alias) {
|
||||
this.showRoomAlias(resp[0].alias);
|
||||
this.showRoomAlias(resp[0].alias, true);
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Room not found', '', ErrorDialog, {
|
||||
|
@ -367,13 +367,46 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
showRoomAlias: function(alias) {
|
||||
this.showRoom(null, alias);
|
||||
onPreviewClick: function(ev, room) {
|
||||
this.props.onFinished();
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: room.room_id,
|
||||
should_peek: true,
|
||||
});
|
||||
ev.stopPropagation();
|
||||
},
|
||||
|
||||
showRoom: function(room, room_alias) {
|
||||
onViewClick: function(ev, room) {
|
||||
this.props.onFinished();
|
||||
const payload = {action: 'view_room'};
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: room.room_id,
|
||||
should_peek: false,
|
||||
});
|
||||
ev.stopPropagation();
|
||||
},
|
||||
|
||||
onJoinClick: function(ev, room) {
|
||||
this.showRoom(room, null, true);
|
||||
ev.stopPropagation();
|
||||
},
|
||||
|
||||
onCreateRoomClick: function(room) {
|
||||
this.props.onFinished();
|
||||
dis.dispatch({action: 'view_create_room'});
|
||||
},
|
||||
|
||||
showRoomAlias: function(alias, autoJoin=false) {
|
||||
this.showRoom(null, alias, autoJoin);
|
||||
},
|
||||
|
||||
showRoom: function(room, room_alias, autoJoin=false) {
|
||||
this.props.onFinished();
|
||||
const payload = {
|
||||
action: 'view_room',
|
||||
auto_join: autoJoin,
|
||||
};
|
||||
if (room) {
|
||||
// Don't let the user view a room they won't be able to either
|
||||
// peek or join: fail earlier so they don't have to click back
|
||||
|
@ -408,74 +441,70 @@ module.exports = React.createClass({
|
|||
dis.dispatch(payload);
|
||||
},
|
||||
|
||||
getRows: function() {
|
||||
getRow(room) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const clientRoom = client.getRoom(room.room_id);
|
||||
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
|
||||
const isGuest = client.isGuest();
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
let previewButton;
|
||||
let joinOrViewButton;
|
||||
|
||||
if (!this.state.publicRooms) return [];
|
||||
|
||||
const rooms = this.state.publicRooms;
|
||||
const rows = [];
|
||||
const self = this;
|
||||
let guestRead; let guestJoin; let perms;
|
||||
for (let i = 0; i < rooms.length; i++) {
|
||||
guestRead = null;
|
||||
guestJoin = null;
|
||||
|
||||
if (rooms[i].world_readable) {
|
||||
guestRead = (
|
||||
<div className="mx_RoomDirectory_perm">{ _t('World readable') }</div>
|
||||
);
|
||||
}
|
||||
if (rooms[i].guest_can_join) {
|
||||
guestJoin = (
|
||||
<div className="mx_RoomDirectory_perm">{ _t('Guests can join') }</div>
|
||||
);
|
||||
}
|
||||
|
||||
perms = null;
|
||||
if (guestRead || guestJoin) {
|
||||
perms = <div className="mx_RoomDirectory_perms">{guestRead}{guestJoin}</div>;
|
||||
}
|
||||
|
||||
let name = rooms[i].name || get_display_alias_for_room(rooms[i]) || _t('Unnamed room');
|
||||
if (name.length > MAX_NAME_LENGTH) {
|
||||
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
|
||||
}
|
||||
|
||||
let topic = rooms[i].topic || '';
|
||||
if (topic.length > MAX_TOPIC_LENGTH) {
|
||||
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
|
||||
}
|
||||
topic = linkifyAndSanitizeHtml(topic);
|
||||
|
||||
rows.push(
|
||||
<tr key={ rooms[i].room_id }
|
||||
onClick={self.onRoomClicked.bind(self, rooms[i])}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
>
|
||||
<td className="mx_RoomDirectory_roomAvatar">
|
||||
<BaseAvatar width={24} height={24} resizeMethod='crop'
|
||||
name={ name } idName={ name }
|
||||
url={ ContentRepo.getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
rooms[i].avatar_url, 24, 24, "crop") } />
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_roomDescription">
|
||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
||||
{ perms }
|
||||
<div className="mx_RoomDirectory_topic"
|
||||
onClick={ function(e) { e.stopPropagation(); } }
|
||||
dangerouslySetInnerHTML={{ __html: topic }} />
|
||||
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(rooms[i]) }</div>
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_roomMemberCount">
|
||||
{ rooms[i].num_joined_members }
|
||||
</td>
|
||||
</tr>,
|
||||
if (room.world_readable && !hasJoinedRoom) {
|
||||
previewButton = (
|
||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onPreviewClick(ev, room)}>{_t("Preview")}</AccessibleButton>
|
||||
);
|
||||
}
|
||||
return rows;
|
||||
if (hasJoinedRoom) {
|
||||
joinOrViewButton = (
|
||||
<AccessibleButton kind="secondary" onClick={(ev) => this.onViewClick(ev, room)}>{_t("View")}</AccessibleButton>
|
||||
);
|
||||
} else if (!isGuest || room.guest_can_join) {
|
||||
joinOrViewButton = (
|
||||
<AccessibleButton kind="primary" onClick={(ev) => this.onJoinClick(ev, room)}>{_t("Join")}</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let name = room.name || get_display_alias_for_room(room) || _t('Unnamed room');
|
||||
if (name.length > MAX_NAME_LENGTH) {
|
||||
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
|
||||
}
|
||||
|
||||
let topic = room.topic || '';
|
||||
if (topic.length > MAX_TOPIC_LENGTH) {
|
||||
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
|
||||
}
|
||||
topic = linkifyAndSanitizeHtml(topic);
|
||||
const avatarUrl = ContentRepo.getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
room.avatar_url, 32, 32, "crop",
|
||||
);
|
||||
return (
|
||||
<tr key={ room.room_id }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
>
|
||||
<td className="mx_RoomDirectory_roomAvatar">
|
||||
<BaseAvatar width={32} height={32} resizeMethod='crop'
|
||||
name={ name } idName={ name }
|
||||
url={ avatarUrl } />
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_roomDescription">
|
||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
||||
<div className="mx_RoomDirectory_topic"
|
||||
onClick={ (ev) => { ev.stopPropagation(); } }
|
||||
dangerouslySetInnerHTML={{ __html: topic }} />
|
||||
<div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(room) }</div>
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_roomMemberCount">
|
||||
{ room.num_joined_members }
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_preview">{previewButton}</td>
|
||||
<td className="mx_RoomDirectory_join">{joinOrViewButton}</td>
|
||||
</tr>
|
||||
);
|
||||
},
|
||||
|
||||
collectScrollPanel: function(element) {
|
||||
|
@ -526,20 +555,26 @@ module.exports = React.createClass({
|
|||
let content;
|
||||
if (this.state.error) {
|
||||
content = this.state.error;
|
||||
} else if (this.state.protocolsLoading || this.state.loading) {
|
||||
} else if (this.state.protocolsLoading) {
|
||||
content = <Loader />;
|
||||
} else {
|
||||
const rows = this.getRows();
|
||||
const rows = (this.state.publicRooms || []).map(room => this.getRow(room));
|
||||
// we still show the scrollpanel, at least for now, because
|
||||
// otherwise we don't fetch more because we don't get a fill
|
||||
// request from the scrollpanel because there isn't one
|
||||
|
||||
let spinner;
|
||||
if (this.state.loading) {
|
||||
spinner = <Loader />;
|
||||
}
|
||||
|
||||
let scrollpanel_content;
|
||||
if (rows.length == 0) {
|
||||
if (rows.length === 0 && !this.state.loading) {
|
||||
scrollpanel_content = <i>{ _t('No rooms to show') }</i>;
|
||||
} else {
|
||||
scrollpanel_content = <table ref="directory_table" className="mx_RoomDirectory_table">
|
||||
<tbody>
|
||||
{ this.getRows() }
|
||||
{ rows }
|
||||
</tbody>
|
||||
</table>;
|
||||
}
|
||||
|
@ -551,6 +586,7 @@ module.exports = React.createClass({
|
|||
startAtBottom={false}
|
||||
>
|
||||
{ scrollpanel_content }
|
||||
{ spinner }
|
||||
</ScrollPanel>;
|
||||
}
|
||||
|
||||
|
@ -572,10 +608,9 @@ module.exports = React.createClass({
|
|||
instance_expected_field_type = this.protocols[protocolName].field_types[last_field];
|
||||
}
|
||||
|
||||
|
||||
let placeholder = _t('Search for a room');
|
||||
let placeholder = _t('Find a room…');
|
||||
if (!this.state.instanceId) {
|
||||
placeholder = _t('Search for a room like #example') + ':' + this.state.roomServer;
|
||||
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;
|
||||
}
|
||||
|
@ -591,27 +626,31 @@ module.exports = React.createClass({
|
|||
listHeader = <div className="mx_RoomDirectory_listheader">
|
||||
<DirectorySearchBox
|
||||
className="mx_RoomDirectory_searchbox"
|
||||
onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinClick}
|
||||
onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinFromSearchClick}
|
||||
placeholder={placeholder} showJoinButton={showJoinButton}
|
||||
/>
|
||||
<NetworkDropdown config={this.props.config} protocols={this.protocols} onOptionChange={this.onOptionChange} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
const createRoomButton = (<AccessibleButton
|
||||
onClick={this.onCreateRoomClicked}
|
||||
className="mx_RoomDirectory_createRoom"
|
||||
>{_t("Create new room")}</AccessibleButton>);
|
||||
const explanation =
|
||||
_t("If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", null,
|
||||
{a: sub => {
|
||||
return (<AccessibleButton
|
||||
kind="secondary"
|
||||
onClick={this.onCreateRoomClick}
|
||||
>{sub}</AccessibleButton>);
|
||||
}},
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className={'mx_RoomDirectory_dialog'}
|
||||
hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
headerButton={createRoomButton}
|
||||
title={_t("Room directory")}
|
||||
title={_t("Explore rooms")}
|
||||
>
|
||||
<div className="mx_RoomDirectory">
|
||||
<p>{explanation}</p>
|
||||
<div className="mx_RoomDirectory_list">
|
||||
{listHeader}
|
||||
{content}
|
||||
|
|
|
@ -16,13 +16,12 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import { _t, _td } from '../../languageHandler';
|
||||
import sdk from '../../index';
|
||||
import WhoIsTyping from '../../WhoIsTyping';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import MemberAvatar from '../views/avatars/MemberAvatar';
|
||||
import Resend from '../../Resend';
|
||||
import * as cryptodevices from '../../cryptodevices';
|
||||
import dis from '../../dispatcher';
|
||||
|
@ -39,7 +38,7 @@ function getUnsentMessages(room) {
|
|||
});
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'RoomStatusBar',
|
||||
|
||||
propTypes: {
|
||||
|
@ -290,7 +289,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
return <div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src={require("../../../res/img/e2e/warning.svg")} width="24" height="24" title={_t("Warning")} alt="" />
|
||||
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" height="24" title={_t("Warning")} alt="" />
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ title }
|
||||
|
@ -304,12 +303,10 @@ module.exports = React.createClass({
|
|||
|
||||
// return suitable content for the main (text) part of the status bar.
|
||||
_getContent: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
if (this._shouldShowConnectionError()) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src={require("../../../res/img/e2e/warning.svg")} width="24" height="24" title="/!\ " alt="/!\ " />
|
||||
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" height="24" title="/!\ " alt="/!\ " />
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ _t('Connectivity to the server has been lost.') }
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 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,7 +17,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
|
@ -24,16 +26,17 @@ import Unread from '../../Unread';
|
|||
import * as RoomNotifs from '../../RoomNotifs';
|
||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||
import IndicatorScrollbar from './IndicatorScrollbar';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import {Key, KeyCode} from '../../Keyboard';
|
||||
import { Group } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
import RoomTile from "../views/rooms/RoomTile";
|
||||
import LazyRenderList from "../views/elements/LazyRenderList";
|
||||
import {_t} from "../../languageHandler";
|
||||
|
||||
// turn this on for drop & drag console debugging galore
|
||||
const debug = false;
|
||||
|
||||
const RoomSubList = React.createClass({
|
||||
const RoomSubList = createReactClass({
|
||||
displayName: 'RoomSubList',
|
||||
|
||||
debug: debug,
|
||||
|
@ -42,6 +45,7 @@ const RoomSubList = React.createClass({
|
|||
list: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
tagName: PropTypes.string,
|
||||
addRoomLabel: PropTypes.string,
|
||||
|
||||
order: PropTypes.string.isRequired,
|
||||
|
||||
|
@ -53,9 +57,8 @@ const RoomSubList = React.createClass({
|
|||
collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed?
|
||||
onHeaderClick: PropTypes.func,
|
||||
incomingCall: PropTypes.object,
|
||||
isFiltered: PropTypes.bool,
|
||||
headerItems: PropTypes.node, // content shown in the sublist header
|
||||
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
|
||||
forceExpand: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -64,6 +67,9 @@ const RoomSubList = React.createClass({
|
|||
// some values to get LazyRenderList starting
|
||||
scrollerHeight: 800,
|
||||
scrollTop: 0,
|
||||
// React 16's getDerivedStateFromProps(props, state) doesn't give the previous props so
|
||||
// we have to store the length of the list here so we can see if it's changed or not...
|
||||
listLength: null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -76,17 +82,27 @@ const RoomSubList = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this._headerButton = createRef();
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
},
|
||||
|
||||
statics: {
|
||||
getDerivedStateFromProps: function(props, state) {
|
||||
return {
|
||||
listLength: props.list.length,
|
||||
scrollTop: props.list.length === state.listLength ? state.scrollTop : 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
// The header is collapsable if it is hidden or not stuck
|
||||
// The header is collapsible if it is hidden or not stuck
|
||||
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
|
||||
isCollapsableOnClick: function() {
|
||||
isCollapsibleOnClick: function() {
|
||||
const stuck = this.refs.header.dataset.stuck;
|
||||
if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) {
|
||||
return true;
|
||||
|
@ -111,8 +127,8 @@ const RoomSubList = React.createClass({
|
|||
},
|
||||
|
||||
onClick: function(ev) {
|
||||
if (this.isCollapsableOnClick()) {
|
||||
// The header isCollapsable, so the click is to be interpreted as collapse and truncation logic
|
||||
if (this.isCollapsibleOnClick()) {
|
||||
// The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
|
||||
const isHidden = !this.state.hidden;
|
||||
this.setState({hidden: isHidden}, () => {
|
||||
this.props.onHeaderClick(isHidden);
|
||||
|
@ -123,6 +139,49 @@ const RoomSubList = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onHeaderKeyDown: function(ev) {
|
||||
switch (ev.key) {
|
||||
case Key.TAB:
|
||||
// Prevent LeftPanel handling Tab if focus is on the sublist header itself
|
||||
ev.stopPropagation();
|
||||
break;
|
||||
case Key.ARROW_LEFT:
|
||||
// On ARROW_LEFT collapse the room sublist
|
||||
if (!this.state.hidden && !this.props.forceExpand) {
|
||||
this.onClick();
|
||||
}
|
||||
ev.stopPropagation();
|
||||
break;
|
||||
case Key.ARROW_RIGHT: {
|
||||
ev.stopPropagation();
|
||||
if (this.state.hidden && !this.props.forceExpand) {
|
||||
// sublist is collapsed, expand it
|
||||
this.onClick();
|
||||
} else if (!this.props.forceExpand) {
|
||||
// sublist is expanded, go to first room
|
||||
const element = this.refs.subList && this.refs.subList.querySelector(".mx_RoomTile");
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onKeyDown: function(ev) {
|
||||
switch (ev.key) {
|
||||
// On ARROW_LEFT go to the sublist header
|
||||
case Key.ARROW_LEFT:
|
||||
ev.stopPropagation();
|
||||
this._headerButton.current.focus();
|
||||
break;
|
||||
// Consume ARROW_RIGHT so it doesn't cause focus to get sent to composer
|
||||
case Key.ARROW_RIGHT:
|
||||
ev.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
onRoomTileClick(roomId, ev) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
|
@ -190,8 +249,14 @@ const RoomSubList = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onAddRoom: function(e) {
|
||||
e.stopPropagation();
|
||||
if (this.props.onAddRoom) this.props.onAddRoom();
|
||||
},
|
||||
|
||||
_getHeaderJsx: function(isCollapsed) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton');
|
||||
const subListNotifications = !this.props.isInvite ?
|
||||
RoomNotifs.aggregateNotificationCount(this.props.list) :
|
||||
{count: 0, highlight: true};
|
||||
|
@ -204,13 +269,24 @@ const RoomSubList = React.createClass({
|
|||
'mx_RoomSubList_badge': true,
|
||||
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
|
||||
});
|
||||
// Wrap the contents in a div and apply styles to the child div so that the browser default outline works
|
||||
if (subListNotifCount > 0) {
|
||||
badge = <div className={badgeClasses} onClick={this._onNotifBadgeClick}>
|
||||
{ FormattingUtils.formatCount(subListNotifCount) }
|
||||
</div>;
|
||||
badge = (
|
||||
<AccessibleButton className={badgeClasses} onClick={this._onNotifBadgeClick} aria-label={_t("Jump to first unread room.")}>
|
||||
<div>
|
||||
{ FormattingUtils.formatCount(subListNotifCount) }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else if (this.props.isInvite && this.props.list.length) {
|
||||
// no notifications but highlight anyway because this is an invite badge
|
||||
badge = <div className={badgeClasses} onClick={this._onInviteBadgeClick}>{this.props.list.length}</div>;
|
||||
badge = (
|
||||
<AccessibleButton className={badgeClasses} onClick={this._onInviteBadgeClick} aria-label={_t("Jump to first invite.")}>
|
||||
<div>
|
||||
{ this.props.list.length }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -232,7 +308,11 @@ const RoomSubList = React.createClass({
|
|||
let addRoomButton;
|
||||
if (this.props.onAddRoom) {
|
||||
addRoomButton = (
|
||||
<AccessibleButton onClick={ this.props.onAddRoom } className="mx_RoomSubList_addRoom" />
|
||||
<AccessibleTooltipButton
|
||||
onClick={this.onAddRoom}
|
||||
className="mx_RoomSubList_addRoom"
|
||||
title={this.props.addRoomLabel || _t("Add room")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -244,13 +324,20 @@ const RoomSubList = React.createClass({
|
|||
'mx_RoomSubList_chevronRight': isCollapsed,
|
||||
'mx_RoomSubList_chevronDown': !isCollapsed,
|
||||
});
|
||||
chevron = (<div className={chevronClasses}></div>);
|
||||
chevron = (<div className={chevronClasses} />);
|
||||
}
|
||||
|
||||
const tabindex = this.props.isFiltered ? "0" : "-1";
|
||||
return (
|
||||
<div className="mx_RoomSubList_labelContainer" title={ title } ref="header">
|
||||
<AccessibleButton onClick={ this.onClick } className="mx_RoomSubList_label" tabIndex={tabindex}>
|
||||
<div className="mx_RoomSubList_labelContainer" title={title} ref="header" onKeyDown={this.onHeaderKeyDown}>
|
||||
<AccessibleButton
|
||||
onClick={this.onClick}
|
||||
className="mx_RoomSubList_label"
|
||||
tabIndex={0}
|
||||
aria-expanded={!isCollapsed}
|
||||
inputRef={this._headerButton}
|
||||
role="treeitem"
|
||||
aria-level="1"
|
||||
>
|
||||
{ chevron }
|
||||
<span>{this.props.label}</span>
|
||||
{ incomingCall }
|
||||
|
@ -291,21 +378,20 @@ const RoomSubList = React.createClass({
|
|||
render: function() {
|
||||
const len = this.props.list.length + this.props.extraTiles.length;
|
||||
const isCollapsed = this.state.hidden && !this.props.forceExpand;
|
||||
if (len) {
|
||||
const subListClasses = classNames({
|
||||
"mx_RoomSubList": true,
|
||||
"mx_RoomSubList_hidden": isCollapsed,
|
||||
"mx_RoomSubList_nonEmpty": len && !isCollapsed,
|
||||
});
|
||||
|
||||
const subListClasses = classNames({
|
||||
"mx_RoomSubList": true,
|
||||
"mx_RoomSubList_hidden": len && isCollapsed,
|
||||
"mx_RoomSubList_nonEmpty": len && !isCollapsed,
|
||||
});
|
||||
|
||||
let content;
|
||||
if (len) {
|
||||
if (isCollapsed) {
|
||||
return <div ref="subList" className={subListClasses}>
|
||||
{this._getHeaderJsx(isCollapsed)}
|
||||
</div>;
|
||||
// no body
|
||||
} else if (this._canUseLazyListRendering()) {
|
||||
return <div ref="subList" className={subListClasses}>
|
||||
{this._getHeaderJsx(isCollapsed)}
|
||||
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={ this._onScroll }>
|
||||
content = (
|
||||
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
|
||||
<LazyRenderList
|
||||
scrollTop={this.state.scrollTop }
|
||||
height={ this.state.scrollerHeight }
|
||||
|
@ -313,31 +399,35 @@ const RoomSubList = React.createClass({
|
|||
itemHeight={34}
|
||||
items={ this.props.list } />
|
||||
</IndicatorScrollbar>
|
||||
</div>;
|
||||
);
|
||||
} else {
|
||||
const roomTiles = this.props.list.map(r => this.makeRoomTile(r));
|
||||
const tiles = roomTiles.concat(this.props.extraTiles);
|
||||
return <div ref="subList" className={subListClasses}>
|
||||
{this._getHeaderJsx(isCollapsed)}
|
||||
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={ this._onScroll }>
|
||||
content = (
|
||||
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
|
||||
{ tiles }
|
||||
</IndicatorScrollbar>
|
||||
</div>;
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
let content;
|
||||
if (this.props.showSpinner && !isCollapsed) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
content = <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref="subList" className="mx_RoomSubList">
|
||||
{ this._getHeaderJsx(isCollapsed) }
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref="subList"
|
||||
className={subListClasses}
|
||||
role="group"
|
||||
aria-label={this.props.label}
|
||||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
{ this._getHeaderJsx(isCollapsed) }
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 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,13 +24,13 @@ limitations under the License.
|
|||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import Promise from 'bluebird';
|
||||
import filesize from 'filesize';
|
||||
import classNames from 'classnames';
|
||||
import {Room} from "matrix-js-sdk";
|
||||
import { _t } from '../../languageHandler';
|
||||
import {RoomPermalinkCreator} from '../../matrix-to';
|
||||
import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks';
|
||||
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import ContentMessages from '../../ContentMessages';
|
||||
|
@ -41,6 +42,7 @@ import Tinter from '../../Tinter';
|
|||
import rate_limited_func from '../../ratelimitedfunc';
|
||||
import ObjectUtils from '../../ObjectUtils';
|
||||
import * as Rooms from '../../Rooms';
|
||||
import eventSearch from '../../Searching';
|
||||
|
||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||
|
||||
|
@ -63,7 +65,13 @@ if (DEBUG) {
|
|||
debuglog = console.log.bind(console);
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
const RoomContext = PropTypes.shape({
|
||||
canReact: PropTypes.bool.isRequired,
|
||||
canReply: PropTypes.bool.isRequired,
|
||||
room: PropTypes.instanceOf(Room),
|
||||
});
|
||||
|
||||
module.exports = createReactClass({
|
||||
displayName: 'RoomView',
|
||||
propTypes: {
|
||||
ConferenceHandler: PropTypes.any,
|
||||
|
@ -87,7 +95,7 @@ module.exports = React.createClass({
|
|||
// * name (string) The room's name
|
||||
// * avatarUrl (string) The mxc:// avatar URL for the room
|
||||
// * inviterName (string) The display name of the person who
|
||||
// * invited us tovthe room
|
||||
// * invited us to the room
|
||||
oobData: PropTypes.object,
|
||||
|
||||
// is the RightPanel collapsed?
|
||||
|
@ -155,6 +163,26 @@ module.exports = React.createClass({
|
|||
// We load this later by asking the js-sdk to suggest a version for us.
|
||||
// This object is the result of Room#getRecommendedVersion()
|
||||
upgradeRecommendation: null,
|
||||
|
||||
canReact: false,
|
||||
canReply: false,
|
||||
|
||||
useCider: false,
|
||||
};
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
room: RoomContext,
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
const {canReact, canReply, room} = this.state;
|
||||
return {
|
||||
room: {
|
||||
canReact,
|
||||
canReply,
|
||||
room,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -164,6 +192,7 @@ module.exports = React.createClass({
|
|||
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);
|
||||
|
@ -174,6 +203,14 @@ module.exports = React.createClass({
|
|||
this._onRoomViewStoreUpdate(true);
|
||||
|
||||
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
|
||||
|
||||
this._onCiderUpdated();
|
||||
this._ciderWatcherRef = SettingsStore.watchSetting(
|
||||
"useCiderComposer", null, this._onCiderUpdated);
|
||||
},
|
||||
|
||||
_onCiderUpdated: function() {
|
||||
this.setState({useCider: SettingsStore.getValue("useCiderComposer")});
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdate: function(initial) {
|
||||
|
@ -320,7 +357,7 @@ module.exports = React.createClass({
|
|||
if (this.props.autoJoin) {
|
||||
this.onJoinButtonClicked();
|
||||
} else if (!room && shouldPeek) {
|
||||
console.log("Attempting to peek into room %s", roomId);
|
||||
console.info("Attempting to peek into room %s", roomId);
|
||||
this.setState({
|
||||
peekLoading: true,
|
||||
isPeeking: true, // this will change to false if peeking fails
|
||||
|
@ -452,6 +489,8 @@ module.exports = React.createClass({
|
|||
// (We could use isMounted, but facebook have deprecated that.)
|
||||
this.unmounted = true;
|
||||
|
||||
SettingsStore.unwatchSetting(this._ciderWatcherRef);
|
||||
|
||||
// update the scroll map before we get unmounted
|
||||
if (this.state.roomId) {
|
||||
RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState());
|
||||
|
@ -556,7 +595,7 @@ module.exports = React.createClass({
|
|||
payload.data.description || payload.data.name);
|
||||
break;
|
||||
case 'picture_snapshot':
|
||||
return ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
[payload.file], this.state.room.roomId, MatrixClientPeg.get(),
|
||||
);
|
||||
break;
|
||||
|
@ -597,6 +636,11 @@ module.exports = React.createClass({
|
|||
showApps: payload.show,
|
||||
});
|
||||
break;
|
||||
case 'reply_to_event':
|
||||
if (this.state.searchResults && payload.event.getRoomId() === this.state.roomId && !this.unmounted) {
|
||||
this.onCancelSearchClick();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -671,6 +715,7 @@ module.exports = React.createClass({
|
|||
this._loadMembersIfJoined(room);
|
||||
this._calculateRecommendedVersion(room);
|
||||
this._updateE2EStatus(room);
|
||||
this._updatePermissions(room);
|
||||
},
|
||||
|
||||
_calculateRecommendedVersion: async function(room) {
|
||||
|
@ -794,6 +839,15 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev, state) {
|
||||
// ignore if we don't have a room yet
|
||||
if (!this.state.room || this.state.room.roomId !== state.roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updatePermissions(this.state.room);
|
||||
},
|
||||
|
||||
onRoomStateMember: function(ev, state, member) {
|
||||
// ignore if we don't have a room yet
|
||||
if (!this.state.room) {
|
||||
|
@ -812,6 +866,17 @@ module.exports = React.createClass({
|
|||
if (room.roomId === this.state.roomId) {
|
||||
this.forceUpdate();
|
||||
this._loadMembersIfJoined(room);
|
||||
this._updatePermissions(room);
|
||||
}
|
||||
},
|
||||
|
||||
_updatePermissions: function(room) {
|
||||
if (room) {
|
||||
const me = MatrixClientPeg.get().getUserId();
|
||||
const canReact = room.getMyMembership() === "join" && room.currentState.maySendEvent("m.reaction", me);
|
||||
const canReply = room.maySendMessage();
|
||||
|
||||
this.setState({canReact, canReply});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1036,7 +1101,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get())
|
||||
.done(undefined, (error) => {
|
||||
.then(undefined, (error) => {
|
||||
if (error.name === "UnknownDeviceError") {
|
||||
// Let the staus bar handle this
|
||||
return;
|
||||
|
@ -1064,23 +1129,12 @@ module.exports = React.createClass({
|
|||
// todo: should cancel any previous search requests.
|
||||
this.searchId = new Date().getTime();
|
||||
|
||||
let filter;
|
||||
if (scope === "Room") {
|
||||
filter = {
|
||||
// XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :(
|
||||
rooms: [
|
||||
this.state.room.roomId,
|
||||
],
|
||||
};
|
||||
}
|
||||
let roomId;
|
||||
if (scope === "Room") roomId = this.state.room.roomId;
|
||||
|
||||
debuglog("sending search request");
|
||||
|
||||
const searchPromise = MatrixClientPeg.get().searchRoomEvents({
|
||||
filter: filter,
|
||||
term: term,
|
||||
});
|
||||
this._handleSearchResult(searchPromise).done();
|
||||
const searchPromise = eventSearch(term, roomId);
|
||||
this._handleSearchResult(searchPromise);
|
||||
},
|
||||
|
||||
_handleSearchResult: function(searchPromise) {
|
||||
|
@ -1251,7 +1305,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onForgetClick: function() {
|
||||
MatrixClientPeg.get().forget(this.state.room.roomId).done(function() {
|
||||
MatrixClientPeg.get().forget(this.state.room.roomId).then(function() {
|
||||
dis.dispatch({ action: 'view_next_room' });
|
||||
}, function(err) {
|
||||
const errCode = err.errcode || _t("unknown error code");
|
||||
|
@ -1268,7 +1322,7 @@ module.exports = React.createClass({
|
|||
this.setState({
|
||||
rejecting: true,
|
||||
});
|
||||
MatrixClientPeg.get().leave(this.state.roomId).done(function() {
|
||||
MatrixClientPeg.get().leave(this.state.roomId).then(function() {
|
||||
dis.dispatch({ action: 'view_next_room' });
|
||||
self.setState({
|
||||
rejecting: false,
|
||||
|
@ -1364,7 +1418,8 @@ module.exports = React.createClass({
|
|||
|
||||
const scrollState = messagePanel.getScrollState();
|
||||
|
||||
if (scrollState.stuckAtBottom) {
|
||||
// getScrollState on TimelinePanel *may* return null, so guard against that
|
||||
if (!scrollState || scrollState.stuckAtBottom) {
|
||||
// we don't really expect to be in this state, but it will
|
||||
// occasionally happen when no scroll state has been set on the
|
||||
// messagePanel (ie, we didn't have an initial event (so it's
|
||||
|
@ -1503,7 +1558,6 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
const RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
||||
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||
const ForwardMessage = sdk.getComponent("rooms.ForwardMessage");
|
||||
const AuxPanel = sdk.getComponent("rooms.AuxPanel");
|
||||
const SearchBar = sdk.getComponent("rooms.SearchBar");
|
||||
|
@ -1514,18 +1568,23 @@ module.exports = React.createClass({
|
|||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
const RoomUpgradeWarningBar = sdk.getComponent("rooms.RoomUpgradeWarningBar");
|
||||
const RoomRecoveryReminder = sdk.getComponent("rooms.RoomRecoveryReminder");
|
||||
const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary");
|
||||
|
||||
if (!this.state.room) {
|
||||
const loading = this.state.roomLoading || this.state.peekLoading;
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<RoomPreviewBar
|
||||
canPreview={false}
|
||||
error={this.state.roomLoadError}
|
||||
loading={loading}
|
||||
joining={this.state.joining}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar
|
||||
canPreview={false}
|
||||
previewLoading={this.state.peekLoading}
|
||||
error={this.state.roomLoadError}
|
||||
loading={loading}
|
||||
joining={this.state.joining}
|
||||
oobData={this.props.oobData}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
@ -1543,16 +1602,20 @@ module.exports = React.createClass({
|
|||
const roomAlias = this.state.roomAlias;
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
canPreview={false} error={this.state.roomLoadError}
|
||||
roomAlias={roomAlias}
|
||||
joining={this.state.joining}
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
room={this.state.room}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
canPreview={false} error={this.state.roomLoadError}
|
||||
roomAlias={roomAlias}
|
||||
joining={this.state.joining}
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
oobData={this.props.oobData}
|
||||
signUrl={this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : null}
|
||||
room={this.state.room}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1562,12 +1625,14 @@ module.exports = React.createClass({
|
|||
if (myMembership == 'invite') {
|
||||
if (this.state.joining || this.state.rejecting) {
|
||||
return (
|
||||
<RoomPreviewBar
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar
|
||||
canPreview={false}
|
||||
error={this.state.roomLoadError}
|
||||
joining={this.state.joining}
|
||||
rejecting={this.state.rejecting}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
} else {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
@ -1582,14 +1647,16 @@ module.exports = React.createClass({
|
|||
// We have a regular invite for this room.
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectButtonClicked}
|
||||
inviterName={inviterName}
|
||||
canPreview={false}
|
||||
joining={this.state.joining}
|
||||
room={this.state.room}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectButtonClicked}
|
||||
inviterName={inviterName}
|
||||
canPreview={false}
|
||||
joining={this.state.joining}
|
||||
room={this.state.room}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1681,6 +1748,7 @@ module.exports = React.createClass({
|
|||
joining={this.state.joining}
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
oobData={this.props.oobData}
|
||||
canPreview={this.state.canPeek}
|
||||
room={this.state.room}
|
||||
/>
|
||||
|
@ -1726,15 +1794,29 @@ module.exports = React.createClass({
|
|||
myMembership === 'join' && !this.state.searchResults
|
||||
);
|
||||
if (canSpeak) {
|
||||
messageComposer =
|
||||
<MessageComposer
|
||||
room={this.state.room}
|
||||
callState={this.state.callState}
|
||||
disabled={this.props.disabled}
|
||||
showApps={this.state.showApps}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||
/>;
|
||||
if (this.state.useCider) {
|
||||
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||
messageComposer =
|
||||
<MessageComposer
|
||||
room={this.state.room}
|
||||
callState={this.state.callState}
|
||||
disabled={this.props.disabled}
|
||||
showApps={this.state.showApps}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||
/>;
|
||||
} else {
|
||||
const SlateMessageComposer = sdk.getComponent('rooms.SlateMessageComposer');
|
||||
messageComposer =
|
||||
<SlateMessageComposer
|
||||
room={this.state.room}
|
||||
callState={this.state.callState}
|
||||
disabled={this.props.disabled}
|
||||
showApps={this.state.showApps}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Why aren't we storing the term/scope/count in this format
|
||||
|
@ -1814,7 +1896,7 @@ module.exports = React.createClass({
|
|||
highlightedEventId = this.state.initialEventId;
|
||||
}
|
||||
|
||||
// console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
||||
// console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
||||
const messagePanel = (
|
||||
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||
|
@ -1832,6 +1914,7 @@ module.exports = React.createClass({
|
|||
membersLoaded={this.state.membersLoaded}
|
||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showReactions={true}
|
||||
/>);
|
||||
|
||||
let topUnreadMessagesBar = null;
|
||||
|
@ -1870,42 +1953,46 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
|
||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
||||
oobData={this.props.oobData}
|
||||
inRoom={myMembership === 'join'}
|
||||
collapsedRhs={collapsedRhs}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onPinnedClick={this.onPinnedClick}
|
||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
/>
|
||||
<MainSplit
|
||||
panel={rightPanel}
|
||||
collapsedRhs={collapsedRhs}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
<div className={fadableSectionClasses}>
|
||||
{ auxPanel }
|
||||
<div className="mx_RoomView_timeline">
|
||||
{ topUnreadMessagesBar }
|
||||
{ jumpToBottom }
|
||||
{ messagePanel }
|
||||
{ searchResultsPanel }
|
||||
</div>
|
||||
<div className={statusBarAreaClass}>
|
||||
<div className="mx_RoomView_statusAreaBox">
|
||||
<div className="mx_RoomView_statusAreaBox_line"></div>
|
||||
{ statusBar }
|
||||
<ErrorBoundary>
|
||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
||||
oobData={this.props.oobData}
|
||||
inRoom={myMembership === 'join'}
|
||||
collapsedRhs={collapsedRhs}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onPinnedClick={this.onPinnedClick}
|
||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
/>
|
||||
<MainSplit
|
||||
panel={rightPanel}
|
||||
collapsedRhs={collapsedRhs}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
<div className={fadableSectionClasses}>
|
||||
{auxPanel}
|
||||
<div className="mx_RoomView_timeline">
|
||||
{topUnreadMessagesBar}
|
||||
{jumpToBottom}
|
||||
{messagePanel}
|
||||
{searchResultsPanel}
|
||||
</div>
|
||||
<div className={statusBarAreaClass}>
|
||||
<div className="mx_RoomView_statusAreaBox">
|
||||
<div className="mx_RoomView_statusAreaBox_line"></div>
|
||||
{statusBar}
|
||||
</div>
|
||||
</div>
|
||||
{previewBar}
|
||||
{messageComposer}
|
||||
</div>
|
||||
{ previewBar }
|
||||
{ messageComposer }
|
||||
</div>
|
||||
</MainSplit>
|
||||
</MainSplit>
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports.RoomContext = RoomContext;
|
||||
|
|
|
@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
const React = require("react");
|
||||
import React from "react";
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import Promise from 'bluebird';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import Timer from '../../utils/Timer';
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
@ -84,7 +84,7 @@ if (DEBUG_SCROLL) {
|
|||
* offset as normal.
|
||||
*/
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'ScrollPanel',
|
||||
|
||||
propTypes: {
|
||||
|
@ -214,6 +214,9 @@ module.exports = React.createClass({
|
|||
// after an update to the contents of the panel, check that the scroll is
|
||||
// where it ought to be, and set off pagination requests if necessary.
|
||||
checkScroll: function() {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this._restoreSavedScrollState();
|
||||
this.checkFillState();
|
||||
},
|
||||
|
@ -673,6 +676,11 @@ module.exports = React.createClass({
|
|||
debuglog("updateHeight getting straight to business, no scrolling going on.");
|
||||
}
|
||||
|
||||
// We might have unmounted since the timer finished, so abort if so.
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sn = this._getScrollNode();
|
||||
const itemlist = this.refs.itemlist;
|
||||
const contentHeight = this._getMessagesHeight();
|
||||
|
@ -750,8 +758,10 @@ module.exports = React.createClass({
|
|||
_getMessagesHeight() {
|
||||
const itemlist = this.refs.itemlist;
|
||||
const lastNode = itemlist.lastElementChild;
|
||||
const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0;
|
||||
const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0;
|
||||
// 18 is itemlist padding
|
||||
return (lastNode.offsetTop + lastNode.clientHeight) - itemlist.firstElementChild.offsetTop + (18 * 2);
|
||||
return lastNodeBottom - firstNodeTop + (18 * 2);
|
||||
},
|
||||
|
||||
_topFromBottom(node) {
|
||||
|
|
|
@ -16,18 +16,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import dis from '../../dispatcher';
|
||||
import { throttle } from 'lodash';
|
||||
import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||
import classNames from 'classnames';
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'SearchBox',
|
||||
|
||||
propTypes: {
|
||||
onSearch: PropTypes.func,
|
||||
onCleared: PropTypes.func,
|
||||
onKeyDown: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
|
||||
|
@ -46,6 +49,7 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
searchTerm: "",
|
||||
blurred: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -90,10 +94,22 @@ module.exports = React.createClass({
|
|||
this._clearSearch("keyboard");
|
||||
break;
|
||||
}
|
||||
if (this.props.onKeyDown) this.props.onKeyDown(ev);
|
||||
},
|
||||
|
||||
_onFocus: function(ev) {
|
||||
this.setState({blurred: false});
|
||||
ev.target.select();
|
||||
if (this.props.onFocus) {
|
||||
this.props.onFocus(ev);
|
||||
}
|
||||
},
|
||||
|
||||
_onBlur: function(ev) {
|
||||
this.setState({blurred: true});
|
||||
if (this.props.onBlur) {
|
||||
this.props.onBlur(ev);
|
||||
}
|
||||
},
|
||||
|
||||
_clearSearch: function(source) {
|
||||
|
@ -112,15 +128,21 @@ module.exports = React.createClass({
|
|||
if (this.props.collapsed) {
|
||||
return null;
|
||||
}
|
||||
const clearButton = this.state.searchTerm.length > 0 ?
|
||||
const clearButton = (!this.state.blurred || this.state.searchTerm) ?
|
||||
(<AccessibleButton key="button"
|
||||
className="mx_SearchBox_closeButton"
|
||||
onClick={ () => {this._clearSearch("button"); } }>
|
||||
</AccessibleButton>) : undefined;
|
||||
|
||||
// show a shorter placeholder when blurred, if requested
|
||||
// this is used for the room filter field that has
|
||||
// the explore button next to it when blurred
|
||||
const placeholder = this.state.blurred ?
|
||||
(this.props.blurredPlaceholder || this.props.placeholder) :
|
||||
this.props.placeholder;
|
||||
const className = this.props.className || "";
|
||||
return (
|
||||
<div className="mx_SearchBox mx_textinput">
|
||||
<div className={classNames("mx_SearchBox", "mx_textinput", {"mx_SearchBox_blurred": this.state.blurred})}>
|
||||
<input
|
||||
key="searchfield"
|
||||
type="text"
|
||||
|
@ -130,7 +152,8 @@ module.exports = React.createClass({
|
|||
onFocus={ this._onFocus }
|
||||
onChange={ this.onChange }
|
||||
onKeyDown={ this._onKeyDown }
|
||||
placeholder={ this.props.placeholder }
|
||||
onBlur={this._onBlur}
|
||||
placeholder={ placeholder }
|
||||
/>
|
||||
{ clearButton }
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 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,6 +19,7 @@ limitations under the License.
|
|||
import * as React from "react";
|
||||
import {_t} from '../../languageHandler';
|
||||
import PropTypes from "prop-types";
|
||||
import sdk from "../../index";
|
||||
|
||||
/**
|
||||
* Represents a tab for the TabbedView.
|
||||
|
@ -70,6 +72,8 @@ export class TabbedView extends React.Component {
|
|||
}
|
||||
|
||||
_renderTabLabel(tab) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let classes = "mx_TabbedView_tabLabel ";
|
||||
|
||||
const idx = this.props.tabs.indexOf(tab);
|
||||
|
@ -82,14 +86,14 @@ export class TabbedView extends React.Component {
|
|||
|
||||
const onClickHandler = () => this._setActiveTab(tab);
|
||||
|
||||
const label = _t(tab.label);
|
||||
return (
|
||||
<span className={classes} key={"tab_label_" + tab.label}
|
||||
onClick={onClickHandler}>
|
||||
<AccessibleButton className={classes} key={"tab_label_" + tab.label} onClick={onClickHandler}>
|
||||
{tabIcon}
|
||||
<span className="mx_TabbedView_tabLabel_text">
|
||||
{_t(tab.label)}
|
||||
{ label }
|
||||
</span>
|
||||
</span>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import TagOrderStore from '../../stores/TagOrderStore';
|
||||
|
@ -28,7 +29,7 @@ import { _t } from '../../languageHandler';
|
|||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const TagPanel = React.createClass({
|
||||
const TagPanel = createReactClass({
|
||||
displayName: 'TagPanel',
|
||||
|
||||
contextTypes: {
|
||||
|
|
|
@ -15,16 +15,17 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import Modal from '../../Modal';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
const TagPanelButtons = React.createClass({
|
||||
const TagPanelButtons = createReactClass({
|
||||
displayName: 'TagPanelButtons',
|
||||
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
},
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 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,10 +19,10 @@ limitations under the License.
|
|||
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import ReactDOM from "react-dom";
|
||||
import PropTypes from 'prop-types';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
const Matrix = require("matrix-js-sdk");
|
||||
const EventTimeline = Matrix.EventTimeline;
|
||||
|
@ -35,11 +36,11 @@ const Modal = require("../../Modal");
|
|||
const UserActivity = require("../../UserActivity");
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import Timer from '../../utils/Timer';
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||
|
||||
const PAGINATE_SIZE = 20;
|
||||
const INITIAL_SIZE = 20;
|
||||
const READ_MARKER_INVIEW_THRESHOLD_MS = 1 * 1000;
|
||||
const READ_MARKER_OUTOFVIEW_THRESHOLD_MS = 30 * 1000;
|
||||
const READ_RECEIPT_INTERVAL_MS = 500;
|
||||
|
||||
const DEBUG = false;
|
||||
|
@ -55,7 +56,7 @@ if (DEBUG) {
|
|||
*
|
||||
* Also responsible for handling and sending read receipts.
|
||||
*/
|
||||
const TimelinePanel = React.createClass({
|
||||
const TimelinePanel = createReactClass({
|
||||
displayName: 'TimelinePanel',
|
||||
|
||||
propTypes: {
|
||||
|
@ -106,6 +107,9 @@ const TimelinePanel = React.createClass({
|
|||
|
||||
// placeholder text to use if the timeline is empty
|
||||
empty: PropTypes.string,
|
||||
|
||||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
},
|
||||
|
||||
statics: {
|
||||
|
@ -137,6 +141,7 @@ const TimelinePanel = React.createClass({
|
|||
|
||||
return {
|
||||
events: [],
|
||||
liveEvents: [],
|
||||
timelineLoading: true, // track whether our room timeline is loading
|
||||
|
||||
// canBackPaginate == false may mean:
|
||||
|
@ -183,6 +188,12 @@ const TimelinePanel = React.createClass({
|
|||
|
||||
// always show timestamps on event tiles?
|
||||
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
|
||||
|
||||
// how long to show the RM for when it's visible in the window
|
||||
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
|
||||
|
||||
// how long to show the RM for when it's scrolled off-screen
|
||||
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -204,10 +215,13 @@ const TimelinePanel = React.createClass({
|
|||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().on("Room.timelineReset", this.onRoomTimelineReset);
|
||||
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
|
||||
// same event handler as Room.redaction as for both we just do forceUpdate
|
||||
MatrixClientPeg.get().on("Room.redactionCancelled", this.onRoomRedaction);
|
||||
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
|
||||
MatrixClientPeg.get().on("Event.replaced", this.onEventReplaced);
|
||||
MatrixClientPeg.get().on("sync", this.onSync);
|
||||
|
||||
this._initTimeline(this.props);
|
||||
|
@ -282,10 +296,12 @@ const TimelinePanel = React.createClass({
|
|||
client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
client.removeListener("Room.timelineReset", this.onRoomTimelineReset);
|
||||
client.removeListener("Room.redaction", this.onRoomRedaction);
|
||||
client.removeListener("Room.redactionCancelled", this.onRoomRedaction);
|
||||
client.removeListener("Room.receipt", this.onRoomReceipt);
|
||||
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||
client.removeListener("Room.accountData", this.onAccountData);
|
||||
client.removeListener("Event.decrypted", this.onEventDecrypted);
|
||||
client.removeListener("Event.replaced", this.onEventReplaced);
|
||||
client.removeListener("sync", this.onSync);
|
||||
}
|
||||
},
|
||||
|
@ -313,9 +329,11 @@ const TimelinePanel = React.createClass({
|
|||
|
||||
// We can now paginate in the unpaginated direction
|
||||
const canPaginateKey = (backwards) ? 'canBackPaginate' : 'canForwardPaginate';
|
||||
const { events, liveEvents } = this._getEvents();
|
||||
this.setState({
|
||||
[canPaginateKey]: true,
|
||||
events: this._getEvents(),
|
||||
events,
|
||||
liveEvents,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -347,10 +365,12 @@ const TimelinePanel = React.createClass({
|
|||
|
||||
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
|
||||
|
||||
const { events, liveEvents } = this._getEvents();
|
||||
const newState = {
|
||||
[paginatingKey]: false,
|
||||
[canPaginateKey]: r,
|
||||
events: this._getEvents(),
|
||||
events,
|
||||
liveEvents,
|
||||
};
|
||||
|
||||
// moving the window in this direction may mean that we can now
|
||||
|
@ -402,6 +422,16 @@ const TimelinePanel = React.createClass({
|
|||
if (payload.action === 'ignore_state_changed') {
|
||||
this.forceUpdate();
|
||||
}
|
||||
if (payload.action === "edit_event") {
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({editState}, () => {
|
||||
if (payload.event && this.refs.messagePanel) {
|
||||
this.refs.messagePanel.scrollToEventIfNeeded(
|
||||
payload.event.getId(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
||||
|
@ -431,18 +461,16 @@ const TimelinePanel = React.createClass({
|
|||
// timeline window.
|
||||
//
|
||||
// see https://github.com/vector-im/vector-web/issues/1035
|
||||
this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => {
|
||||
this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => {
|
||||
if (this.unmounted) { return; }
|
||||
|
||||
const events = this._timelineWindow.getEvents();
|
||||
const lastEv = events[events.length-1];
|
||||
const { events, liveEvents } = this._getEvents();
|
||||
const lastLiveEvent = liveEvents[liveEvents.length - 1];
|
||||
|
||||
// if we're at the end of the live timeline, append the pending events
|
||||
if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||
events.push(...this.props.timelineSet.room.getPendingEvents());
|
||||
}
|
||||
|
||||
const updatedState = {events: events};
|
||||
const updatedState = {
|
||||
events,
|
||||
liveEvents,
|
||||
};
|
||||
|
||||
let callRMUpdated;
|
||||
if (this.props.manageReadMarkers) {
|
||||
|
@ -459,13 +487,13 @@ const TimelinePanel = React.createClass({
|
|||
callRMUpdated = false;
|
||||
if (sender != myUserId && !UserActivity.sharedInstance().userActiveRecently()) {
|
||||
updatedState.readMarkerVisible = true;
|
||||
} else if (lastEv && this.getReadMarkerPosition() === 0) {
|
||||
} else if (lastLiveEvent && this.getReadMarkerPosition() === 0) {
|
||||
// we know we're stuckAtBottom, so we can advance the RM
|
||||
// immediately, to save a later render cycle
|
||||
|
||||
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
|
||||
this._setReadMarker(lastLiveEvent.getId(), lastLiveEvent.getTs(), true);
|
||||
updatedState.readMarkerVisible = false;
|
||||
updatedState.readMarkerEventId = lastEv.getId();
|
||||
updatedState.readMarkerEventId = lastLiveEvent.getId();
|
||||
callRMUpdated = true;
|
||||
}
|
||||
}
|
||||
|
@ -502,6 +530,17 @@ const TimelinePanel = React.createClass({
|
|||
this.forceUpdate();
|
||||
},
|
||||
|
||||
onEventReplaced: function(replacedEvent, room) {
|
||||
if (this.unmounted) return;
|
||||
|
||||
// ignore events for other rooms
|
||||
if (room !== this.props.timelineSet.room) return;
|
||||
|
||||
// we could skip an update if the event isn't in our timeline,
|
||||
// but that's probably an early optimisation.
|
||||
this.forceUpdate();
|
||||
},
|
||||
|
||||
onRoomReceipt: function(ev, room) {
|
||||
if (this.unmounted) return;
|
||||
|
||||
|
@ -537,6 +576,9 @@ const TimelinePanel = React.createClass({
|
|||
},
|
||||
|
||||
onEventDecrypted: function(ev) {
|
||||
// Can be null for the notification timeline, etc.
|
||||
if (!this.props.timelineSet.room) return;
|
||||
|
||||
// Need to update as we don't display event tiles for events that
|
||||
// haven't yet been decrypted. The event will have just been updated
|
||||
// in place so we just need to re-render.
|
||||
|
@ -554,8 +596,8 @@ const TimelinePanel = React.createClass({
|
|||
|
||||
_readMarkerTimeout(readMarkerPosition) {
|
||||
return readMarkerPosition === 0 ?
|
||||
READ_MARKER_INVIEW_THRESHOLD_MS :
|
||||
READ_MARKER_OUTOFVIEW_THRESHOLD_MS;
|
||||
this.state.readMarkerInViewThresholdMs :
|
||||
this.state.readMarkerOutOfViewThresholdMs;
|
||||
},
|
||||
|
||||
updateReadMarkerOnUserActivity: async function() {
|
||||
|
@ -585,6 +627,8 @@ const TimelinePanel = React.createClass({
|
|||
},
|
||||
|
||||
sendReadReceipt: function() {
|
||||
if (SettingsStore.getValue("lowBandwidth")) return;
|
||||
|
||||
if (!this.refs.messagePanel) return;
|
||||
if (!this.props.manageReadReceipts) return;
|
||||
// This happens on user_activity_end which is delayed, and it's
|
||||
|
@ -644,23 +688,32 @@ const TimelinePanel = React.createClass({
|
|||
}
|
||||
this.lastRMSentEventId = this.state.readMarkerEventId;
|
||||
|
||||
const roomId = this.props.timelineSet.room.roomId;
|
||||
const hiddenRR = !SettingsStore.getValue("sendReadReceipts", roomId);
|
||||
|
||||
debuglog('TimelinePanel: Sending Read Markers for ',
|
||||
this.props.timelineSet.room.roomId,
|
||||
'rm', this.state.readMarkerEventId,
|
||||
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
|
||||
' hidden:' + hiddenRR,
|
||||
);
|
||||
MatrixClientPeg.get().setRoomReadMarkers(
|
||||
this.props.timelineSet.room.roomId,
|
||||
this.state.readMarkerEventId,
|
||||
lastReadEvent, // Could be null, in which case no RR is sent
|
||||
{hidden: hiddenRR},
|
||||
).catch((e) => {
|
||||
// /read_markers API is not implemented on this HS, fallback to just RR
|
||||
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
|
||||
return MatrixClientPeg.get().sendReadReceipt(
|
||||
lastReadEvent,
|
||||
).catch(() => {
|
||||
{hidden: hiddenRR},
|
||||
).catch((e) => {
|
||||
console.error(e);
|
||||
this.lastRRSentEventId = undefined;
|
||||
});
|
||||
} else {
|
||||
console.error(e);
|
||||
}
|
||||
// it failed, so allow retries next time the user is active
|
||||
this.lastRRSentEventId = undefined;
|
||||
|
@ -695,14 +748,8 @@ const TimelinePanel = React.createClass({
|
|||
// move the RM to *after* the message at the bottom of the screen. This
|
||||
// avoids a problem whereby we never advance the RM if there is a huge
|
||||
// message which doesn't fit on the screen.
|
||||
//
|
||||
// But ignore local echoes for this - they have a temporary event ID
|
||||
// and we'll get confused when their ID changes and we can't figure out
|
||||
// where the RM is pointing to. The read marker will be invisible for
|
||||
// now anyway, so this doesn't really matter.
|
||||
const lastDisplayedIndex = this._getLastDisplayedEventIndex({
|
||||
allowPartial: true,
|
||||
ignoreEchoes: true,
|
||||
});
|
||||
|
||||
if (lastDisplayedIndex === null) {
|
||||
|
@ -726,9 +773,9 @@ const TimelinePanel = React.createClass({
|
|||
_advanceReadMarkerPastMyEvents: function() {
|
||||
if (!this.props.manageReadMarkers) return;
|
||||
|
||||
// we call _timelineWindow.getEvents() rather than using
|
||||
// this.state.events, because react batches the update to the latter, so it
|
||||
// may not have been updated yet.
|
||||
// we call `_timelineWindow.getEvents()` rather than using
|
||||
// `this.state.liveEvents`, because React batches the update to the
|
||||
// latter, so it may not have been updated yet.
|
||||
const events = this._timelineWindow.getEvents();
|
||||
|
||||
// first find where the current RM is
|
||||
|
@ -1016,8 +1063,6 @@ const TimelinePanel = React.createClass({
|
|||
});
|
||||
};
|
||||
|
||||
let prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
|
||||
|
||||
// if we already have the event in question, TimelineWindow.load
|
||||
// returns a resolved promise.
|
||||
//
|
||||
|
@ -1026,20 +1071,23 @@ const TimelinePanel = React.createClass({
|
|||
// quite slow. So we detect that situation and shortcut straight to
|
||||
// calling _reloadEvents and updating the state.
|
||||
|
||||
if (prom.isFulfilled()) {
|
||||
const timeline = this.props.timelineSet.getTimelineForEvent(eventId);
|
||||
if (timeline) {
|
||||
// This is a hot-path optimization by skipping a promise tick
|
||||
// by repeating a no-op sync branch in TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline
|
||||
this._timelineWindow.load(eventId, INITIAL_SIZE); // in this branch this method will happen in sync time
|
||||
onLoaded();
|
||||
} else {
|
||||
const prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
|
||||
this.setState({
|
||||
events: [],
|
||||
liveEvents: [],
|
||||
canBackPaginate: false,
|
||||
canForwardPaginate: false,
|
||||
timelineLoading: true,
|
||||
});
|
||||
|
||||
prom = prom.then(onLoaded, onError);
|
||||
prom.then(onLoaded, onError);
|
||||
}
|
||||
|
||||
prom.done();
|
||||
},
|
||||
|
||||
// handle the completion of a timeline load or localEchoUpdate, by
|
||||
|
@ -1050,21 +1098,26 @@ const TimelinePanel = React.createClass({
|
|||
// the results if so.
|
||||
if (this.unmounted) return;
|
||||
|
||||
this.setState({
|
||||
events: this._getEvents(),
|
||||
});
|
||||
this.setState(this._getEvents());
|
||||
},
|
||||
|
||||
// get the list of events from the timeline window and the pending event list
|
||||
_getEvents: function() {
|
||||
const events = this._timelineWindow.getEvents();
|
||||
|
||||
// Hold onto the live events separately. The read receipt and read marker
|
||||
// should use this list, so that they don't advance into pending events.
|
||||
const liveEvents = [...events];
|
||||
|
||||
// if we're at the end of the live timeline, append the pending events
|
||||
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||
events.push(...this.props.timelineSet.getPendingEvents());
|
||||
}
|
||||
|
||||
return events;
|
||||
return {
|
||||
events,
|
||||
liveEvents,
|
||||
};
|
||||
},
|
||||
|
||||
_indexForEventId: function(evId) {
|
||||
|
@ -1079,36 +1132,76 @@ const TimelinePanel = React.createClass({
|
|||
_getLastDisplayedEventIndex: function(opts) {
|
||||
opts = opts || {};
|
||||
const ignoreOwn = opts.ignoreOwn || false;
|
||||
const ignoreEchoes = opts.ignoreEchoes || false;
|
||||
const allowPartial = opts.allowPartial || false;
|
||||
|
||||
const messagePanel = this.refs.messagePanel;
|
||||
if (messagePanel === undefined) return null;
|
||||
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
|
||||
const wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
for (let i = this.state.events.length-1; i >= 0; --i) {
|
||||
const ev = this.state.events[i];
|
||||
|
||||
if (ignoreOwn && ev.sender && ev.sender.userId == myUserId) {
|
||||
continue;
|
||||
const isNodeInView = (node) => {
|
||||
if (node) {
|
||||
const boundingRect = node.getBoundingClientRect();
|
||||
if ((allowPartial && boundingRect.top < wrapperRect.bottom) ||
|
||||
(!allowPartial && boundingRect.bottom < wrapperRect.bottom)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// local echoes have a fake event ID
|
||||
if (ignoreEchoes && ev.status) {
|
||||
continue;
|
||||
}
|
||||
// We keep track of how many of the adjacent events didn't have a tile
|
||||
// but should have the read receipt moved past them, so
|
||||
// we can include those once we find the last displayed (visible) event.
|
||||
// The counter is not started for events we don't want
|
||||
// to send a read receipt for (our own events, local echos).
|
||||
let adjacentInvisibleEventCount = 0;
|
||||
// Use `liveEvents` here because we don't want the read marker or read
|
||||
// receipt to advance into pending events.
|
||||
for (let i = this.state.liveEvents.length - 1; i >= 0; --i) {
|
||||
const ev = this.state.liveEvents[i];
|
||||
|
||||
const node = messagePanel.getNodeForEventId(ev.getId());
|
||||
if (!node) continue;
|
||||
const isInView = isNodeInView(node);
|
||||
|
||||
const boundingRect = node.getBoundingClientRect();
|
||||
if ((allowPartial && boundingRect.top < wrapperRect.bottom) ||
|
||||
(!allowPartial && boundingRect.bottom < wrapperRect.bottom)) {
|
||||
// when we've reached the first visible event, and the previous
|
||||
// events were all invisible (with the first one not being ignored),
|
||||
// return the index of the first invisible event.
|
||||
if (isInView && adjacentInvisibleEventCount !== 0) {
|
||||
return i + adjacentInvisibleEventCount;
|
||||
}
|
||||
if (node && !isInView) {
|
||||
// has node but not in view, so reset adjacent invisible events
|
||||
adjacentInvisibleEventCount = 0;
|
||||
}
|
||||
|
||||
const shouldIgnore = !!ev.status || // local echo
|
||||
(ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message
|
||||
const isWithoutTile = !EventTile.haveTileForEvent(ev) || shouldHideEvent(ev);
|
||||
|
||||
if (isWithoutTile || !node) {
|
||||
// don't start counting if the event should be ignored,
|
||||
// but continue counting if we were already so the offset
|
||||
// to the previous invisble event that didn't need to be ignored
|
||||
// doesn't get messed up
|
||||
if (!shouldIgnore || (shouldIgnore && adjacentInvisibleEventCount !== 0)) {
|
||||
++adjacentInvisibleEventCount;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shouldIgnore) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isInView) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
|
@ -1168,6 +1261,10 @@ const TimelinePanel = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
getRelationsForEvent(...args) {
|
||||
return this.props.timelineSet.getRelationsForEvent(...args);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const MessagePanel = sdk.getComponent("structures.MessagePanel");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
@ -1193,9 +1290,9 @@ const TimelinePanel = React.createClass({
|
|||
|
||||
if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
|
||||
return (
|
||||
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
|
||||
<div className="mx_RoomView_empty">{ this.props.empty }</div>
|
||||
</div>
|
||||
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
|
||||
<div className="mx_RoomView_empty">{this.props.empty}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1217,28 +1314,31 @@ const TimelinePanel = React.createClass({
|
|||
);
|
||||
return (
|
||||
<MessagePanel ref="messagePanel"
|
||||
room={this.props.timelineSet.room}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
hidden={this.props.hidden}
|
||||
backPaginating={this.state.backPaginating}
|
||||
forwardPaginating={forwardPaginating}
|
||||
events={this.state.events}
|
||||
highlightedEventId={this.props.highlightedEventId}
|
||||
readMarkerEventId={this.state.readMarkerEventId}
|
||||
readMarkerVisible={this.state.readMarkerVisible}
|
||||
suppressFirstDateSeparator={this.state.canBackPaginate}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
showReadReceipts={this.props.showReadReceipts}
|
||||
ourUserId={MatrixClientPeg.get().credentials.userId}
|
||||
stickyBottom={stickyBottom}
|
||||
onScroll={this.onMessageListScroll}
|
||||
onFillRequest={this.onMessageListFillRequest}
|
||||
onUnfillRequest={this.onMessageListUnfillRequest}
|
||||
isTwelveHour={this.state.isTwelveHour}
|
||||
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
|
||||
className={this.props.className}
|
||||
tileShape={this.props.tileShape}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
room={this.props.timelineSet.room}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
hidden={this.props.hidden}
|
||||
backPaginating={this.state.backPaginating}
|
||||
forwardPaginating={forwardPaginating}
|
||||
events={this.state.events}
|
||||
highlightedEventId={this.props.highlightedEventId}
|
||||
readMarkerEventId={this.state.readMarkerEventId}
|
||||
readMarkerVisible={this.state.readMarkerVisible}
|
||||
suppressFirstDateSeparator={this.state.canBackPaginate}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
showReadReceipts={this.props.showReadReceipts}
|
||||
ourUserId={MatrixClientPeg.get().credentials.userId}
|
||||
stickyBottom={stickyBottom}
|
||||
onScroll={this.onMessageListScroll}
|
||||
onFillRequest={this.onMessageListFillRequest}
|
||||
onUnfillRequest={this.onMessageListUnfillRequest}
|
||||
isTwelveHour={this.state.isTwelveHour}
|
||||
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
|
||||
className={this.props.className}
|
||||
tileShape={this.props.tileShape}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
getRelationsForEvent={this.getRelationsForEvent}
|
||||
editState={this.state.editState}
|
||||
showReactions={this.props.showReactions}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
84
src/components/structures/ToastContainer.js
Normal file
84
src/components/structures/ToastContainer.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
Copyright 2019 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 dis from "../../dispatcher";
|
||||
import { _t } from '../../languageHandler';
|
||||
import classNames from "classnames";
|
||||
|
||||
export default class ToastContainer extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {toasts: []};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
if (payload.action === "show_toast") {
|
||||
this._addToast(payload.toast);
|
||||
}
|
||||
};
|
||||
|
||||
_addToast(toast) {
|
||||
this.setState({toasts: this.state.toasts.concat(toast)});
|
||||
}
|
||||
|
||||
dismissTopToast = () => {
|
||||
const [, ...remaining] = this.state.toasts;
|
||||
this.setState({toasts: remaining});
|
||||
};
|
||||
|
||||
render() {
|
||||
const totalCount = this.state.toasts.length;
|
||||
const isStacked = totalCount > 1;
|
||||
let toast;
|
||||
if (totalCount !== 0) {
|
||||
const topToast = this.state.toasts[0];
|
||||
const {title, icon, key, component, props} = topToast;
|
||||
const toastClasses = classNames("mx_Toast_toast", {
|
||||
"mx_Toast_hasIcon": icon,
|
||||
[`mx_Toast_icon_${icon}`]: icon,
|
||||
});
|
||||
const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null;
|
||||
|
||||
const toastProps = Object.assign({}, props, {
|
||||
dismiss: this.dismissTopToast,
|
||||
key,
|
||||
});
|
||||
toast = (<div className={toastClasses}>
|
||||
<h2>{title}{countIndicator}</h2>
|
||||
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
const containerClasses = classNames("mx_ToastContainer", {
|
||||
"mx_ToastContainer_stacked": isStacked,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={containerClasses} role="alert">
|
||||
{toast}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 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,8 @@ import BaseAvatar from '../views/avatars/BaseAvatar';
|
|||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import Avatar from '../../Avatar';
|
||||
import { _t } from '../../languageHandler';
|
||||
import dis from "../../dispatcher";
|
||||
import {focusCapturedRef} from "../../utils/Accessibility";
|
||||
|
||||
const AVATAR_SIZE = 28;
|
||||
|
||||
|
@ -37,6 +40,7 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
super();
|
||||
this.state = {
|
||||
menuDisplayed: false,
|
||||
menuFunctions: null, // should be { close: fn }
|
||||
profileInfo: null,
|
||||
};
|
||||
|
||||
|
@ -59,6 +63,8 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this._dispatcherRef = dis.register(this.onAction);
|
||||
|
||||
try {
|
||||
const profileInfo = await this._getProfileInfo();
|
||||
this.setState({profileInfo});
|
||||
|
@ -68,6 +74,17 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
// For accessibility
|
||||
if (payload.action === "toggle_top_left_menu") {
|
||||
if (this._buttonRef) this._buttonRef.click();
|
||||
}
|
||||
};
|
||||
|
||||
_getDisplayName() {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return _t("Guest");
|
||||
|
@ -81,14 +98,23 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
render() {
|
||||
const name = this._getDisplayName();
|
||||
let nameElement;
|
||||
let chevronElement;
|
||||
if (!this.props.collapsed) {
|
||||
nameElement = <div className="mx_TopLeftMenuButton_name">
|
||||
{ name }
|
||||
</div>;
|
||||
chevronElement = <span className="mx_TopLeftMenuButton_chevron" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton className="mx_TopLeftMenuButton" onClick={this.onToggleMenu}>
|
||||
<AccessibleButton
|
||||
className="mx_TopLeftMenuButton"
|
||||
onClick={this.onToggleMenu}
|
||||
inputRef={(r) => this._buttonRef = r}
|
||||
aria-label={_t("Your profile")}
|
||||
aria-haspopup={true}
|
||||
aria-expanded={this.state.menuDisplayed}
|
||||
>
|
||||
<BaseAvatar
|
||||
idName={MatrixClientPeg.get().getUserId()}
|
||||
name={name}
|
||||
|
@ -98,7 +124,7 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
resizeMethod="crop"
|
||||
/>
|
||||
{ nameElement }
|
||||
<span className="mx_TopLeftMenuButton_chevron"></span>
|
||||
{ chevronElement }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -107,20 +133,26 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (this.state.menuDisplayed && this.state.menuFunctions) {
|
||||
this.state.menuFunctions.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const elementRect = e.currentTarget.getBoundingClientRect();
|
||||
const x = elementRect.left;
|
||||
const y = elementRect.top + elementRect.height;
|
||||
|
||||
ContextualMenu.createMenu(TopLeftMenu, {
|
||||
const menuFunctions = ContextualMenu.createMenu(TopLeftMenu, {
|
||||
chevronFace: "none",
|
||||
left: x,
|
||||
top: y,
|
||||
userId: MatrixClientPeg.get().getUserId(),
|
||||
displayName: this._getDisplayName(),
|
||||
containerRef: focusCapturedRef, // Focus the TopLeftMenu on first render
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
this.setState({ menuDisplayed: false, menuFunctions: null });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
this.setState({ menuDisplayed: true, menuFunctions });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,14 +14,16 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
const React = require('react');
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import ContentMessages from '../../ContentMessages';
|
||||
const dis = require('../../dispatcher');
|
||||
const filesize = require('filesize');
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({displayName: 'UploadBar',
|
||||
module.exports = createReactClass({
|
||||
displayName: 'UploadBar',
|
||||
propTypes: {
|
||||
room: PropTypes.object,
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Matrix from "matrix-js-sdk";
|
||||
import MatrixClientPeg from "../../MatrixClientPeg";
|
||||
import sdk from "../../index";
|
||||
|
@ -24,7 +26,7 @@ import { _t } from '../../languageHandler';
|
|||
export default class UserView extends React.Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
userId: React.PropTypes.string,
|
||||
userId: PropTypes.string,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -16,13 +16,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import SyntaxHighlight from '../views/elements/SyntaxHighlight';
|
||||
import {_t} from "../../languageHandler";
|
||||
import sdk from "../../index";
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'ViewSource',
|
||||
|
||||
propTypes: {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 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,13 +17,15 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Modal from "../../../Modal";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
|
||||
import PasswordReset from "../../../PasswordReset";
|
||||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import classNames from 'classnames';
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
|
@ -36,46 +39,73 @@ const PHASE_EMAIL_SENT = 3;
|
|||
// User has clicked the link in email and completed reset
|
||||
const PHASE_DONE = 4;
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'ForgotPassword',
|
||||
|
||||
propTypes: {
|
||||
// The default server name to use when the user hasn't specified
|
||||
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
|
||||
// via `.well-known` discovery. The server name is used instead of the
|
||||
// HS URL when talking about "your account".
|
||||
defaultServerName: PropTypes.string,
|
||||
// An error passed along from higher up explaining that something
|
||||
// went wrong when finding the defaultHsUrl.
|
||||
defaultServerDiscoveryError: PropTypes.string,
|
||||
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
onLoginClick: PropTypes.func,
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||
enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||
phase: PHASE_FORGOT,
|
||||
email: "",
|
||||
password: "",
|
||||
password2: "",
|
||||
errorText: null,
|
||||
|
||||
// We perform liveliness checks later, but for now suppress the errors.
|
||||
// We also track the server dead errors independently of the regular errors so
|
||||
// that we can render it differently, and override any other error the user may
|
||||
// be seeing.
|
||||
serverIsAlive: true,
|
||||
serverErrorIsFatal: false,
|
||||
serverDeadError: "",
|
||||
serverRequiresIdServer: null,
|
||||
};
|
||||
},
|
||||
|
||||
submitPasswordReset: function(hsUrl, identityUrl, email, password) {
|
||||
componentWillMount: function() {
|
||||
this.reset = null;
|
||||
this._checkServerLiveliness(this.props.serverConfig);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||
|
||||
// Do a liveliness check on the new URLs
|
||||
this._checkServerLiveliness(newProps.serverConfig);
|
||||
},
|
||||
|
||||
_checkServerLiveliness: async function(serverConfig) {
|
||||
try {
|
||||
await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(
|
||||
serverConfig.hsUrl,
|
||||
serverConfig.isUrl,
|
||||
);
|
||||
|
||||
const pwReset = new PasswordReset(serverConfig.hsUrl, serverConfig.isUrl);
|
||||
const serverRequiresIdServer = await pwReset.doesServerRequireIdServerParam();
|
||||
|
||||
this.setState({
|
||||
serverIsAlive: true,
|
||||
serverRequiresIdServer,
|
||||
});
|
||||
} catch (e) {
|
||||
this.setState(AutoDiscoveryUtils.authComponentStateForError(e, "forgot_password"));
|
||||
}
|
||||
},
|
||||
|
||||
submitPasswordReset: function(email, password) {
|
||||
this.setState({
|
||||
phase: PHASE_SENDING_EMAIL,
|
||||
});
|
||||
this.reset = new PasswordReset(hsUrl, identityUrl);
|
||||
this.reset.resetPassword(email, password).done(() => {
|
||||
this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl);
|
||||
this.reset.resetPassword(email, password).then(() => {
|
||||
this.setState({
|
||||
phase: PHASE_EMAIL_SENT,
|
||||
});
|
||||
|
@ -87,28 +117,25 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onVerify: function(ev) {
|
||||
onVerify: async function(ev) {
|
||||
ev.preventDefault();
|
||||
if (!this.reset) {
|
||||
console.error("onVerify called before submitPasswordReset!");
|
||||
return;
|
||||
}
|
||||
this.reset.checkEmailLinkClicked().done((res) => {
|
||||
try {
|
||||
await this.reset.checkEmailLinkClicked();
|
||||
this.setState({ phase: PHASE_DONE });
|
||||
}, (err) => {
|
||||
} catch (err) {
|
||||
this.showErrorDialog(err.message);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onSubmitForm: function(ev) {
|
||||
onSubmitForm: async function(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
// Don't allow the user to register if there's a discovery error
|
||||
// Without this, the user could end up registering on the wrong homeserver.
|
||||
if (this.props.defaultServerDiscoveryError) {
|
||||
this.setState({errorText: this.props.defaultServerDiscoveryError});
|
||||
return;
|
||||
}
|
||||
// refresh the server errors, just in case the server came back online
|
||||
await this._checkServerLiveliness(this.props.serverConfig);
|
||||
|
||||
if (!this.state.email) {
|
||||
this.showErrorDialog(_t('The email address linked to your account must be entered.'));
|
||||
|
@ -132,10 +159,7 @@ module.exports = React.createClass({
|
|||
button: _t('Continue'),
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.submitPasswordReset(
|
||||
this.state.enteredHsUrl, this.state.enteredIsUrl,
|
||||
this.state.email, this.state.password,
|
||||
);
|
||||
this.submitPasswordReset(this.state.email, this.state.password);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -148,19 +172,7 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onServerConfigChange: function(config) {
|
||||
const newState = {};
|
||||
if (config.hsUrl !== undefined) {
|
||||
newState.enteredHsUrl = config.hsUrl;
|
||||
}
|
||||
if (config.isUrl !== undefined) {
|
||||
newState.enteredIsUrl = config.isUrl;
|
||||
}
|
||||
this.setState(newState);
|
||||
},
|
||||
|
||||
onServerDetailsNextPhaseClick(ev) {
|
||||
ev.stopPropagation();
|
||||
async onServerDetailsNextPhaseClick() {
|
||||
this.setState({
|
||||
phase: PHASE_FORGOT,
|
||||
});
|
||||
|
@ -190,56 +202,61 @@ module.exports = React.createClass({
|
|||
|
||||
renderServerDetails() {
|
||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<ServerConfig
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
customHsUrl={this.state.enteredHsUrl}
|
||||
customIsUrl={this.state.enteredIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={0} />
|
||||
<AccessibleButton className="mx_Login_submit"
|
||||
onClick={this.onServerDetailsNextPhaseClick}
|
||||
>
|
||||
{_t("Next")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
return <ServerConfig
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
delayTimeMs={0}
|
||||
showIdentityServerIfRequiredByHomeserver={true}
|
||||
onAfterSubmit={this.onServerDetailsNextPhaseClick}
|
||||
submitText={_t("Next")}
|
||||
submitClass="mx_Login_submit"
|
||||
/>;
|
||||
},
|
||||
|
||||
renderForgot() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
let errorText = null;
|
||||
const err = this.state.errorText || this.props.defaultServerDiscoveryError;
|
||||
const err = this.state.errorText;
|
||||
if (err) {
|
||||
errorText = <div className="mx_Login_error">{ err }</div>;
|
||||
}
|
||||
|
||||
let yourMatrixAccountText = _t('Your Matrix account');
|
||||
if (this.state.enteredHsUrl === this.props.defaultHsUrl && this.props.defaultServerName) {
|
||||
yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.defaultServerName,
|
||||
let serverDeadSection;
|
||||
if (!this.state.serverIsAlive) {
|
||||
const classes = classNames({
|
||||
"mx_Login_error": true,
|
||||
"mx_Login_serverError": true,
|
||||
"mx_Login_serverErrorNonFatal": !this.state.serverErrorIsFatal,
|
||||
});
|
||||
serverDeadSection = (
|
||||
<div className={classes}>
|
||||
{this.state.serverDeadError}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.serverConfig.hsName,
|
||||
});
|
||||
if (this.props.serverConfig.hsNameIsDifferent) {
|
||||
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
|
||||
|
||||
yourMatrixAccountText = _t('Your Matrix account on <underlinedServerName />', {}, {
|
||||
'underlinedServerName': () => {
|
||||
return <TextWithTooltip
|
||||
class="mx_Login_underlinedServerName"
|
||||
tooltip={this.props.serverConfig.hsUrl}
|
||||
>
|
||||
{this.props.serverConfig.hsName}
|
||||
</TextWithTooltip>;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.state.enteredHsUrl);
|
||||
yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
|
||||
serverName: parsedHsUrl.hostname,
|
||||
});
|
||||
} catch (e) {
|
||||
errorText = <div className="mx_Login_error">{_t(
|
||||
"The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please " +
|
||||
"enter a valid URL including the protocol prefix.",
|
||||
{
|
||||
hsUrl: this.state.enteredHsUrl,
|
||||
})}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
// If custom URLs are allowed, wire up the server details edit link.
|
||||
|
@ -252,12 +269,29 @@ module.exports = React.createClass({
|
|||
</a>;
|
||||
}
|
||||
|
||||
if (!this.props.serverConfig.isUrl && this.state.serverRequiresIdServer) {
|
||||
return <div>
|
||||
<h3>
|
||||
{yourMatrixAccountText}
|
||||
{editLink}
|
||||
</h3>
|
||||
{_t(
|
||||
"No identity server is configured: " +
|
||||
"add one in server settings to reset your password.",
|
||||
)}
|
||||
<a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#">
|
||||
{_t('Sign in instead')}
|
||||
</a>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
{errorText}
|
||||
{serverDeadSection}
|
||||
<h3>
|
||||
{yourMatrixAccountText}
|
||||
{editLink}
|
||||
</h3>
|
||||
{errorText}
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<Field
|
||||
|
@ -292,7 +326,11 @@ module.exports = React.createClass({
|
|||
'A verification email will be sent to your inbox to confirm ' +
|
||||
'setting your new password.',
|
||||
)}</span>
|
||||
<input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} />
|
||||
<input
|
||||
className="mx_Login_submit"
|
||||
type="submit"
|
||||
value={_t('Send Reset Email')}
|
||||
/>
|
||||
</form>
|
||||
<a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#">
|
||||
{_t('Sign in instead')}
|
||||
|
|
|
@ -16,16 +16,16 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import {_t, _td} from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Login from '../../../Login';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
import { AutoDiscovery } from "matrix-js-sdk";
|
||||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import classNames from "classnames";
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
|
@ -53,25 +53,15 @@ _td("General failure");
|
|||
/**
|
||||
* A wire component which glues together login UI components and Login logic
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'Login',
|
||||
|
||||
propTypes: {
|
||||
onLoggedIn: PropTypes.func.isRequired,
|
||||
|
||||
// The default server name to use when the user hasn't specified
|
||||
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
|
||||
// via `.well-known` discovery. The server name is used instead of the
|
||||
// HS URL when talking about where to "sign in to".
|
||||
defaultServerName: PropTypes.string,
|
||||
// An error passed along from higher up explaining that something
|
||||
// went wrong when finding the defaultHsUrl.
|
||||
defaultServerDiscoveryError: PropTypes.string,
|
||||
// If true, the component will consider itself busy.
|
||||
busy: PropTypes.bool,
|
||||
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
// Secondary HS which we try to log into if the user is using
|
||||
// the default HS but login fails. Useful for migrating to a
|
||||
// different homeserver without confusing users.
|
||||
|
@ -79,12 +69,13 @@ module.exports = React.createClass({
|
|||
|
||||
defaultDeviceDisplayName: PropTypes.string,
|
||||
|
||||
// login shouldn't know or care how registration is done.
|
||||
// login shouldn't know or care how registration, password recovery,
|
||||
// etc is done.
|
||||
onRegisterClick: PropTypes.func.isRequired,
|
||||
|
||||
// login shouldn't care how password recovery is done.
|
||||
onForgotPasswordClick: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -92,9 +83,7 @@ module.exports = React.createClass({
|
|||
busy: false,
|
||||
errorText: null,
|
||||
loginIncorrect: false,
|
||||
|
||||
enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||
enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||
canTryLogin: true, // can we attempt to log in or are there validation errors?
|
||||
|
||||
// used for preserving form values when changing homeserver
|
||||
username: "",
|
||||
|
@ -104,11 +93,15 @@ module.exports = React.createClass({
|
|||
// Phase of the overall login dialog.
|
||||
phase: PHASE_LOGIN,
|
||||
// The current login flow, such as password, SSO, etc.
|
||||
currentFlow: "m.login.password",
|
||||
currentFlow: null, // we need to load the flows from the server
|
||||
|
||||
// .well-known discovery
|
||||
discoveryError: "",
|
||||
findingHomeserver: false,
|
||||
// We perform liveliness checks later, but for now suppress the errors.
|
||||
// We also track the server dead errors independently of the regular errors so
|
||||
// that we can render it differently, and override any other error the user may
|
||||
// be seeing.
|
||||
serverIsAlive: true,
|
||||
serverErrorIsFatal: false,
|
||||
serverDeadError: "",
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -132,6 +125,14 @@ module.exports = React.createClass({
|
|||
this._unmounted = true;
|
||||
},
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||
|
||||
// Ensure that we end up actually logging in to the right place
|
||||
this._initLoginLogic(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
|
||||
},
|
||||
|
||||
onPasswordLoginError: function(errorText) {
|
||||
this.setState({
|
||||
errorText,
|
||||
|
@ -139,10 +140,35 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||
// Prevent people from submitting their password when homeserver
|
||||
// discovery went wrong
|
||||
if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return;
|
||||
isBusy: function() {
|
||||
return this.state.busy || this.props.busy;
|
||||
},
|
||||
|
||||
onPasswordLogin: async function(username, phoneCountry, phoneNumber, password) {
|
||||
if (!this.state.serverIsAlive) {
|
||||
this.setState({busy: true});
|
||||
// Do a quick liveliness check on the URLs
|
||||
let aliveAgain = true;
|
||||
try {
|
||||
await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(
|
||||
this.props.serverConfig.hsUrl,
|
||||
this.props.serverConfig.isUrl,
|
||||
);
|
||||
this.setState({serverIsAlive: true, errorText: ""});
|
||||
} catch (e) {
|
||||
const componentState = AutoDiscoveryUtils.authComponentStateForError(e);
|
||||
this.setState({
|
||||
busy: false,
|
||||
...componentState,
|
||||
});
|
||||
aliveAgain = !componentState.serverErrorIsFatal;
|
||||
}
|
||||
|
||||
// Prevent people from submitting their password when something isn't right.
|
||||
if (!aliveAgain) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
busy: true,
|
||||
|
@ -153,6 +179,7 @@ module.exports = React.createClass({
|
|||
this._loginLogic.loginViaPassword(
|
||||
username, phoneCountry, phoneNumber, password,
|
||||
).then((data) => {
|
||||
this.setState({serverIsAlive: true}); // it must be, we logged in.
|
||||
this.props.onLoggedIn(data);
|
||||
}, (error) => {
|
||||
if (this._unmounted) {
|
||||
|
@ -164,7 +191,7 @@ module.exports = React.createClass({
|
|||
const usingEmail = username.indexOf("@") > 0;
|
||||
if (error.httpStatus === 400 && usingEmail) {
|
||||
errorText = _t('This homeserver does not support login using email address.');
|
||||
} else if (error.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
} else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const errorTop = messageForResourceLimitError(
|
||||
error.data.limit_type,
|
||||
error.data.admin_contact, {
|
||||
|
@ -189,16 +216,17 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
if (error.errcode === 'M_USER_DEACTIVATED') {
|
||||
errorText = _t('This account has been deactivated.');
|
||||
} else if (SdkConfig.get()['disable_custom_urls']) {
|
||||
errorText = (
|
||||
<div>
|
||||
<div>{ _t('Incorrect username and/or password.') }</div>
|
||||
<div className="mx_Login_smallError">
|
||||
{ _t('Please note you are logging into the %(hs)s server, not matrix.org.',
|
||||
{
|
||||
hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''),
|
||||
})
|
||||
}
|
||||
{_t(
|
||||
'Please note you are logging into the %(hs)s server, not matrix.org.',
|
||||
{hs: this.props.serverConfig.hsName},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -225,28 +253,56 @@ module.exports = React.createClass({
|
|||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
}).done();
|
||||
});
|
||||
},
|
||||
|
||||
onUsernameChanged: function(username) {
|
||||
this.setState({ username: username });
|
||||
},
|
||||
|
||||
onUsernameBlur: function(username) {
|
||||
onUsernameBlur: async function(username) {
|
||||
const doWellknownLookup = username[0] === "@";
|
||||
this.setState({
|
||||
username: username,
|
||||
discoveryError: null,
|
||||
busy: doWellknownLookup,
|
||||
errorText: null,
|
||||
canTryLogin: true,
|
||||
});
|
||||
if (username[0] === "@") {
|
||||
if (doWellknownLookup) {
|
||||
const serverName = username.split(':').slice(1).join(':');
|
||||
try {
|
||||
// we have to append 'https://' to make the URL constructor happy
|
||||
// otherwise we get things like 'protocol: matrix.org, pathname: 8448'
|
||||
const url = new URL("https://" + serverName);
|
||||
this._tryWellKnownDiscovery(url.hostname);
|
||||
const result = await AutoDiscoveryUtils.validateServerName(serverName);
|
||||
this.props.onServerConfigChange(result);
|
||||
// We'd like to rely on new props coming in via `onServerConfigChange`
|
||||
// so that we know the servers have definitely updated before clearing
|
||||
// the busy state. In the case of a full MXID that resolves to the same
|
||||
// HS as Riot's default HS though, there may not be any server change.
|
||||
// To avoid this trap, we clear busy here. For cases where the server
|
||||
// actually has changed, `_initLoginLogic` will be called and manages
|
||||
// busy state for its own liveness check.
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e);
|
||||
this.setState({discoveryError: _t("Failed to perform homeserver discovery")});
|
||||
|
||||
let message = _t("Failed to perform homeserver discovery");
|
||||
if (e.translatedMessage) {
|
||||
message = e.translatedMessage;
|
||||
}
|
||||
|
||||
let errorText = message;
|
||||
let discoveryState = {};
|
||||
if (AutoDiscoveryUtils.isLivelinessError(e)) {
|
||||
errorText = this.state.errorText;
|
||||
discoveryState = AutoDiscoveryUtils.authComponentStateForError(e);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText,
|
||||
...discoveryState,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -262,44 +318,27 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onPhoneNumberBlur: function(phoneNumber) {
|
||||
this.setState({
|
||||
errorText: null,
|
||||
});
|
||||
|
||||
// Validate the phone number entered
|
||||
if (!PHONE_NUMBER_REGEX.test(phoneNumber)) {
|
||||
this.setState({
|
||||
errorText: _t('The phone number entered looks invalid'),
|
||||
canTryLogin: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
errorText: null,
|
||||
canTryLogin: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onServerConfigChange: function(config) {
|
||||
const self = this;
|
||||
const newState = {
|
||||
errorText: null, // reset err messages
|
||||
};
|
||||
if (config.hsUrl !== undefined) {
|
||||
newState.enteredHsUrl = config.hsUrl;
|
||||
}
|
||||
if (config.isUrl !== undefined) {
|
||||
newState.enteredIsUrl = config.isUrl;
|
||||
}
|
||||
|
||||
this.props.onServerConfigChange(config);
|
||||
this.setState(newState, function() {
|
||||
self._initLoginLogic(config.hsUrl || null, config.isUrl);
|
||||
});
|
||||
},
|
||||
|
||||
onRegisterClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onRegisterClick();
|
||||
},
|
||||
|
||||
onServerDetailsNextPhaseClick(ev) {
|
||||
ev.stopPropagation();
|
||||
async onServerDetailsNextPhaseClick() {
|
||||
this.setState({
|
||||
phase: PHASE_LOGIN,
|
||||
});
|
||||
|
@ -313,64 +352,18 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_tryWellKnownDiscovery: async function(serverName) {
|
||||
if (!serverName.trim()) {
|
||||
// Nothing to discover
|
||||
this.setState({
|
||||
discoveryError: "",
|
||||
findingHomeserver: false,
|
||||
});
|
||||
return;
|
||||
_initLoginLogic: async function(hsUrl, isUrl) {
|
||||
hsUrl = hsUrl || this.props.serverConfig.hsUrl;
|
||||
isUrl = isUrl || this.props.serverConfig.isUrl;
|
||||
|
||||
let isDefaultServer = false;
|
||||
if (this.props.serverConfig.isDefault
|
||||
&& hsUrl === this.props.serverConfig.hsUrl
|
||||
&& isUrl === this.props.serverConfig.isUrl) {
|
||||
isDefaultServer = true;
|
||||
}
|
||||
|
||||
this.setState({findingHomeserver: true});
|
||||
try {
|
||||
const discovery = await AutoDiscovery.findClientConfig(serverName);
|
||||
|
||||
const state = discovery["m.homeserver"].state;
|
||||
if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) {
|
||||
this.setState({
|
||||
discoveryError: discovery["m.homeserver"].error,
|
||||
findingHomeserver: false,
|
||||
});
|
||||
} else if (state === AutoDiscovery.PROMPT) {
|
||||
this.setState({
|
||||
discoveryError: "",
|
||||
findingHomeserver: false,
|
||||
});
|
||||
} else if (state === AutoDiscovery.SUCCESS) {
|
||||
this.setState({
|
||||
discoveryError: "",
|
||||
findingHomeserver: false,
|
||||
});
|
||||
this.onServerConfigChange({
|
||||
hsUrl: discovery["m.homeserver"].base_url,
|
||||
isUrl: discovery["m.identity_server"].state === AutoDiscovery.SUCCESS
|
||||
? discovery["m.identity_server"].base_url
|
||||
: "",
|
||||
});
|
||||
} else {
|
||||
console.warn("Unknown state for m.homeserver in discovery response: ", discovery);
|
||||
this.setState({
|
||||
discoveryError: _t("Unknown failure discovering homeserver"),
|
||||
findingHomeserver: false,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.setState({
|
||||
findingHomeserver: false,
|
||||
discoveryError: _t("Unknown error discovering homeserver"),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_initLoginLogic: function(hsUrl, isUrl) {
|
||||
const self = this;
|
||||
hsUrl = hsUrl || this.state.enteredHsUrl;
|
||||
isUrl = isUrl || this.state.enteredIsUrl;
|
||||
|
||||
const fallbackHsUrl = hsUrl === this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
||||
const fallbackHsUrl = isDefaultServer ? this.props.fallbackHsUrl : null;
|
||||
|
||||
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||
|
@ -378,12 +371,40 @@ module.exports = React.createClass({
|
|||
this._loginLogic = loginLogic;
|
||||
|
||||
this.setState({
|
||||
enteredHsUrl: hsUrl,
|
||||
enteredIsUrl: isUrl,
|
||||
busy: true,
|
||||
currentFlow: null, // reset flow
|
||||
loginIncorrect: false,
|
||||
});
|
||||
|
||||
// Do a quick liveliness check on the URLs
|
||||
try {
|
||||
const { warning } =
|
||||
await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
|
||||
if (warning) {
|
||||
this.setState({
|
||||
...AutoDiscoveryUtils.authComponentStateForError(warning),
|
||||
errorText: "",
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
serverIsAlive: true,
|
||||
errorText: "",
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
...AutoDiscoveryUtils.authComponentStateForError(e),
|
||||
});
|
||||
if (this.state.serverErrorIsFatal) {
|
||||
// Server is dead: show server details prompt instead
|
||||
this.setState({
|
||||
phase: PHASE_SERVER_DETAILS,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
loginLogic.getFlows().then((flows) => {
|
||||
// look for a flow where we understand all of the steps.
|
||||
for (let i = 0; i < flows.length; i++ ) {
|
||||
|
@ -408,16 +429,17 @@ module.exports = React.createClass({
|
|||
"supported by this client.",
|
||||
),
|
||||
});
|
||||
}, function(err) {
|
||||
self.setState({
|
||||
errorText: self._errorTextFromError(err),
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
errorText: this._errorTextFromError(err),
|
||||
loginIncorrect: false,
|
||||
canTryLogin: false,
|
||||
});
|
||||
}).finally(function() {
|
||||
self.setState({
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
}).done();
|
||||
});
|
||||
},
|
||||
|
||||
_isSupportedFlow: function(flow) {
|
||||
|
@ -445,8 +467,8 @@ module.exports = React.createClass({
|
|||
|
||||
if (err.cors === 'rejected') {
|
||||
if (window.location.protocol === 'https:' &&
|
||||
(this.state.enteredHsUrl.startsWith("http:") ||
|
||||
!this.state.enteredHsUrl.startsWith("http"))
|
||||
(this.props.serverConfig.hsUrl.startsWith("http:") ||
|
||||
!this.props.serverConfig.hsUrl.startsWith("http"))
|
||||
) {
|
||||
errorText = <span>
|
||||
{ _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
||||
|
@ -469,9 +491,9 @@ module.exports = React.createClass({
|
|||
"is not blocking requests.", {},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a target="_blank" rel="noopener"
|
||||
href={this.state.enteredHsUrl}
|
||||
>{ sub }</a>;
|
||||
return <a target="_blank" rel="noopener" href={this.props.serverConfig.hsUrl}>
|
||||
{ sub }
|
||||
</a>;
|
||||
},
|
||||
},
|
||||
) }
|
||||
|
@ -484,7 +506,6 @@ module.exports = React.createClass({
|
|||
|
||||
renderServerComponent() {
|
||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
return null;
|
||||
|
@ -494,28 +515,19 @@ module.exports = React.createClass({
|
|||
return null;
|
||||
}
|
||||
|
||||
const serverDetails = <ServerConfig
|
||||
customHsUrl={this.state.enteredHsUrl}
|
||||
customIsUrl={this.state.enteredIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={250}
|
||||
/>;
|
||||
|
||||
let nextButton = null;
|
||||
const serverDetailsProps = {};
|
||||
if (PHASES_ENABLED) {
|
||||
nextButton = <AccessibleButton className="mx_Login_submit"
|
||||
onClick={this.onServerDetailsNextPhaseClick}
|
||||
>
|
||||
{_t("Next")}
|
||||
</AccessibleButton>;
|
||||
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
|
||||
serverDetailsProps.submitText = _t("Next");
|
||||
serverDetailsProps.submitClass = "mx_Login_submit";
|
||||
}
|
||||
|
||||
return <div>
|
||||
{serverDetails}
|
||||
{nextButton}
|
||||
</div>;
|
||||
return <ServerConfig
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
delayTimeMs={250}
|
||||
{...serverDetailsProps}
|
||||
/>;
|
||||
},
|
||||
|
||||
renderLoginComponentForStep() {
|
||||
|
@ -547,13 +559,6 @@ module.exports = React.createClass({
|
|||
onEditServerDetailsClick = this.onEditServerDetailsClick;
|
||||
}
|
||||
|
||||
// If the current HS URL is the default HS URL, then we can label it
|
||||
// with the default HS name (if it exists).
|
||||
let hsName;
|
||||
if (this.state.enteredHsUrl === this.props.defaultHsUrl) {
|
||||
hsName = this.props.defaultServerName;
|
||||
}
|
||||
|
||||
return (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
|
@ -569,14 +574,20 @@ module.exports = React.createClass({
|
|||
onPhoneNumberBlur={this.onPhoneNumberBlur}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
loginIncorrect={this.state.loginIncorrect}
|
||||
hsName={hsName}
|
||||
hsUrl={this.state.enteredHsUrl}
|
||||
disableSubmit={this.state.findingHomeserver}
|
||||
/>
|
||||
serverConfig={this.props.serverConfig}
|
||||
disableSubmit={this.isBusy()}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
_renderSsoStep: function(url) {
|
||||
const SignInToText = sdk.getComponent('views.auth.SignInToText');
|
||||
|
||||
let onEditServerDetailsClick = null;
|
||||
// If custom URLs are allowed, wire up the server details edit link.
|
||||
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
|
||||
onEditServerDetailsClick = this.onEditServerDetailsClick;
|
||||
}
|
||||
// XXX: This link does *not* have a target="_blank" because single sign-on relies on
|
||||
// redirecting the user back to a URI once they're logged in. On the web, this means
|
||||
// we use the same window and redirect back to riot. On electron, this actually
|
||||
|
@ -586,7 +597,12 @@ module.exports = React.createClass({
|
|||
// user's browser, let them log into their SSO provider, then redirect their browser
|
||||
// to vector://vector which, of course, will not work.
|
||||
return (
|
||||
<a href={url} className="mx_Login_sso_link mx_Login_submit">{ _t('Sign in with single sign-on') }</a>
|
||||
<div>
|
||||
<SignInToText serverConfig={this.props.serverConfig}
|
||||
onEditServerDetailsClick={onEditServerDetailsClick} />
|
||||
|
||||
<a href={url} className="mx_Login_sso_link mx_Login_submit">{ _t('Sign in with single sign-on') }</a>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -595,9 +611,9 @@ module.exports = React.createClass({
|
|||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
const loader = this.isBusy() ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
|
||||
const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText;
|
||||
const errorText = this.state.errorText;
|
||||
|
||||
let errorTextSection;
|
||||
if (errorText) {
|
||||
|
@ -608,6 +624,20 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
let serverDeadSection;
|
||||
if (!this.state.serverIsAlive) {
|
||||
const classes = classNames({
|
||||
"mx_Login_error": true,
|
||||
"mx_Login_serverError": true,
|
||||
"mx_Login_serverErrorNonFatal": !this.state.serverErrorIsFatal,
|
||||
});
|
||||
serverDeadSection = (
|
||||
<div className={classes}>
|
||||
{this.state.serverDeadError}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<AuthHeader />
|
||||
|
@ -617,6 +647,7 @@ module.exports = React.createClass({
|
|||
{loader}
|
||||
</h2>
|
||||
{ errorTextSection }
|
||||
{ serverDeadSection }
|
||||
{ this.renderServerComponent() }
|
||||
{ this.renderLoginComponentForStep() }
|
||||
<a className="mx_AuthBody_changeFlow" onClick={this.onRegisterClick} href="#">
|
||||
|
|
|
@ -14,15 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'PostRegistration',
|
||||
|
||||
propTypes: {
|
||||
|
@ -44,7 +43,7 @@ module.exports = React.createClass({
|
|||
const cli = MatrixClientPeg.get();
|
||||
this.setState({busy: true});
|
||||
const self = this;
|
||||
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
|
||||
cli.getProfileInfo(cli.credentials.userId).then(function(result) {
|
||||
self.setState({
|
||||
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url),
|
||||
busy: false,
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 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.
|
||||
|
@ -17,16 +18,18 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
import * as ServerType from '../../views/auth/ServerTypeSelector';
|
||||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import classNames from "classnames";
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
|
@ -37,7 +40,7 @@ const PHASE_REGISTRATION = 1;
|
|||
// Enable phases for registration
|
||||
const PHASES_ENABLED = true;
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'Registration',
|
||||
|
||||
propTypes: {
|
||||
|
@ -46,18 +49,7 @@ module.exports = React.createClass({
|
|||
sessionId: PropTypes.string,
|
||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||
idSid: PropTypes.string,
|
||||
// The default server name to use when the user hasn't specified
|
||||
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
|
||||
// via `.well-known` discovery. The server name is used instead of the
|
||||
// HS URL when talking about "your account".
|
||||
defaultServerName: PropTypes.string,
|
||||
// An error passed along from higher up explaining that something
|
||||
// went wrong when finding the defaultHsUrl.
|
||||
defaultServerDiscoveryError: PropTypes.string,
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
brand: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
// registration shouldn't know or care how login is done.
|
||||
|
@ -66,7 +58,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getInitialState: function() {
|
||||
const serverType = ServerType.getTypeFromHsUrl(this.props.customHsUrl);
|
||||
const serverType = ServerType.getTypeFromServerConfig(this.props.serverConfig);
|
||||
|
||||
return {
|
||||
busy: false,
|
||||
|
@ -87,11 +79,34 @@ module.exports = React.createClass({
|
|||
// straight back into UI auth
|
||||
doingUIAuth: Boolean(this.props.sessionId),
|
||||
serverType,
|
||||
hsUrl: this.props.customHsUrl,
|
||||
isUrl: this.props.customIsUrl,
|
||||
// Phase of the overall registration dialog.
|
||||
phase: PHASE_REGISTRATION,
|
||||
flows: null,
|
||||
// If set, we've registered but are not going to log
|
||||
// the user in to their new account automatically.
|
||||
completedNoSignin: false,
|
||||
|
||||
// We perform liveliness checks later, but for now suppress the errors.
|
||||
// We also track the server dead errors independently of the regular errors so
|
||||
// that we can render it differently, and override any other error the user may
|
||||
// be seeing.
|
||||
serverIsAlive: true,
|
||||
serverErrorIsFatal: false,
|
||||
serverDeadError: "",
|
||||
|
||||
// Our matrix client - part of state because we can't render the UI auth
|
||||
// component without it.
|
||||
matrixClient: null,
|
||||
|
||||
// whether the HS requires an ID server to register with a threepid
|
||||
serverRequiresIdServer: null,
|
||||
|
||||
// The user ID we've just registered
|
||||
registeredUsername: null,
|
||||
|
||||
// if a different user ID to the one we just registered is logged in,
|
||||
// this is the user ID that's logged in.
|
||||
differentLoggedInUserId: null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -100,18 +115,22 @@ module.exports = React.createClass({
|
|||
this._replaceClient();
|
||||
},
|
||||
|
||||
onServerConfigChange: function(config) {
|
||||
const newState = {};
|
||||
if (config.hsUrl !== undefined) {
|
||||
newState.hsUrl = config.hsUrl;
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||
|
||||
this._replaceClient(newProps.serverConfig);
|
||||
|
||||
// Handle cases where the user enters "https://matrix.org" for their server
|
||||
// from the advanced option - we should default to FREE at that point.
|
||||
const serverType = ServerType.getTypeFromServerConfig(newProps.serverConfig);
|
||||
if (serverType !== this.state.serverType) {
|
||||
// Reset the phase to default phase for the server type.
|
||||
this.setState({
|
||||
serverType,
|
||||
phase: this.getDefaultPhaseForServerType(serverType),
|
||||
});
|
||||
}
|
||||
if (config.isUrl !== undefined) {
|
||||
newState.isUrl = config.isUrl;
|
||||
}
|
||||
this.props.onServerConfigChange(config);
|
||||
this.setState(newState, () => {
|
||||
this._replaceClient();
|
||||
});
|
||||
},
|
||||
|
||||
getDefaultPhaseForServerType(type) {
|
||||
|
@ -136,19 +155,17 @@ module.exports = React.createClass({
|
|||
// the new type.
|
||||
switch (type) {
|
||||
case ServerType.FREE: {
|
||||
const { hsUrl, isUrl } = ServerType.TYPES.FREE;
|
||||
this.onServerConfigChange({
|
||||
hsUrl,
|
||||
isUrl,
|
||||
});
|
||||
const { serverConfig } = ServerType.TYPES.FREE;
|
||||
this.props.onServerConfigChange(serverConfig);
|
||||
break;
|
||||
}
|
||||
case ServerType.PREMIUM:
|
||||
// We can accept whatever server config was the default here as this essentially
|
||||
// acts as a slightly different "custom server"/ADVANCED option.
|
||||
break;
|
||||
case ServerType.ADVANCED:
|
||||
this.onServerConfigChange({
|
||||
hsUrl: this.props.defaultHsUrl,
|
||||
isUrl: this.props.defaultIsUrl,
|
||||
});
|
||||
// Use the default config from the config
|
||||
this.props.onServerConfigChange(SdkConfig.get()["validated_server_config"]);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -158,13 +175,54 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_replaceClient: async function() {
|
||||
_replaceClient: async function(serverConfig) {
|
||||
this.setState({
|
||||
errorText: null,
|
||||
serverDeadError: null,
|
||||
serverErrorIsFatal: false,
|
||||
// busy while we do liveness check (we need to avoid trying to render
|
||||
// the UI auth component while we don't have a matrix client)
|
||||
busy: true,
|
||||
});
|
||||
this._matrixClient = Matrix.createClient({
|
||||
baseUrl: this.state.hsUrl,
|
||||
idBaseUrl: this.state.isUrl,
|
||||
if (!serverConfig) serverConfig = this.props.serverConfig;
|
||||
|
||||
// Do a liveliness check on the URLs
|
||||
try {
|
||||
await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(
|
||||
serverConfig.hsUrl,
|
||||
serverConfig.isUrl,
|
||||
);
|
||||
this.setState({
|
||||
serverIsAlive: true,
|
||||
serverErrorIsFatal: false,
|
||||
});
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
...AutoDiscoveryUtils.authComponentStateForError(e, "register"),
|
||||
});
|
||||
if (this.state.serverErrorIsFatal) {
|
||||
return; // Server is dead - do not continue.
|
||||
}
|
||||
}
|
||||
|
||||
const {hsUrl, isUrl} = serverConfig;
|
||||
const cli = Matrix.createClient({
|
||||
baseUrl: hsUrl,
|
||||
idBaseUrl: isUrl,
|
||||
});
|
||||
|
||||
let serverRequiresIdServer = true;
|
||||
try {
|
||||
serverRequiresIdServer = await cli.doesServerRequireIdServerParam();
|
||||
} catch (e) {
|
||||
console.log("Unable to determine is server needs id_server param", e);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
matrixClient: cli,
|
||||
serverRequiresIdServer,
|
||||
busy: false,
|
||||
});
|
||||
try {
|
||||
await this._makeRegisterRequest({});
|
||||
|
@ -179,22 +237,21 @@ module.exports = React.createClass({
|
|||
} else if (e.httpStatus === 403 && e.errcode === "M_UNKNOWN") {
|
||||
this.setState({
|
||||
errorText: _t("Registration has been disabled on this homeserver."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
} else {
|
||||
console.log("Unable to query for supported registration methods.", e);
|
||||
this.setState({
|
||||
errorText: _t("Unable to query for supported registration methods."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onFormSubmit: function(formVals) {
|
||||
// Don't allow the user to register if there's a discovery error
|
||||
// Without this, the user could end up registering on the wrong homeserver.
|
||||
if (this.props.defaultServerDiscoveryError) {
|
||||
this.setState({errorText: this.props.defaultServerDiscoveryError});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
errorText: "",
|
||||
busy: true,
|
||||
|
@ -203,11 +260,25 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_requestEmailToken: function(emailAddress, clientSecret, sendAttempt, sessionId) {
|
||||
return this.state.matrixClient.requestRegisterEmailToken(
|
||||
emailAddress,
|
||||
clientSecret,
|
||||
sendAttempt,
|
||||
this.props.makeRegistrationUrl({
|
||||
client_secret: clientSecret,
|
||||
hs_url: this.state.matrixClient.getHomeserverUrl(),
|
||||
is_url: this.state.matrixClient.getIdentityServerUrl(),
|
||||
session_id: sessionId,
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
_onUIAuthFinished: async function(success, response, extra) {
|
||||
if (!success) {
|
||||
let msg = response.message || response.toString();
|
||||
// can we give a better error message?
|
||||
if (response.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
if (response.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const errorTop = messageForResourceLimitError(
|
||||
response.data.limit_type,
|
||||
response.data.admin_contact, {
|
||||
|
@ -246,21 +317,47 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
// we're still busy until we get unmounted: don't show the registration form again
|
||||
busy: true,
|
||||
MatrixClientPeg.setJustRegisteredUserId(response.user_id);
|
||||
|
||||
const newState = {
|
||||
doingUIAuth: false,
|
||||
});
|
||||
registeredUsername: response.user_id,
|
||||
};
|
||||
|
||||
const cli = await this.props.onLoggedIn({
|
||||
userId: response.user_id,
|
||||
deviceId: response.device_id,
|
||||
homeserverUrl: this._matrixClient.getHomeserverUrl(),
|
||||
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
|
||||
accessToken: response.access_token,
|
||||
});
|
||||
// The user came in through an email validation link. To avoid overwriting
|
||||
// their session, check to make sure the session isn't someone else, and
|
||||
// isn't a guest user since we'll usually have set a guest user session before
|
||||
// starting the registration process. This isn't perfect since it's possible
|
||||
// the user had a separate guest session they didn't actually mean to replace.
|
||||
const sessionOwner = Lifecycle.getStoredSessionOwner();
|
||||
const sessionIsGuest = Lifecycle.getStoredSessionIsGuest();
|
||||
if (sessionOwner && !sessionIsGuest && sessionOwner !== response.userId) {
|
||||
console.log(
|
||||
`Found a session for ${sessionOwner} but ${response.userId} has just registered.`,
|
||||
);
|
||||
newState.differentLoggedInUserId = sessionOwner;
|
||||
} else {
|
||||
newState.differentLoggedInUserId = null;
|
||||
}
|
||||
|
||||
this._setupPushers(cli);
|
||||
if (response.access_token) {
|
||||
const cli = await this.props.onLoggedIn({
|
||||
userId: response.user_id,
|
||||
deviceId: response.device_id,
|
||||
homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
|
||||
identityServerUrl: this.state.matrixClient.getIdentityServerUrl(),
|
||||
accessToken: response.access_token,
|
||||
});
|
||||
|
||||
this._setupPushers(cli);
|
||||
// we're still busy until we get unmounted: don't show the registration form again
|
||||
newState.busy = true;
|
||||
} else {
|
||||
newState.busy = false;
|
||||
newState.completedNoSignin = true;
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
},
|
||||
|
||||
_setupPushers: function(matrixClient) {
|
||||
|
@ -273,7 +370,7 @@ module.exports = React.createClass({
|
|||
if (pushers[i].kind === 'email') {
|
||||
const emailPusher = pushers[i];
|
||||
emailPusher.data = { brand: this.props.brand };
|
||||
matrixClient.setPusher(emailPusher).done(() => {
|
||||
matrixClient.setPusher(emailPusher).then(() => {
|
||||
console.log("Set email branding to " + this.props.brand);
|
||||
}, (error) => {
|
||||
console.error("Couldn't set email branding: " + error);
|
||||
|
@ -302,8 +399,7 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onServerDetailsNextPhaseClick(ev) {
|
||||
ev.stopPropagation();
|
||||
async onServerDetailsNextPhaseClick() {
|
||||
this.setState({
|
||||
phase: PHASE_REGISTRATION,
|
||||
});
|
||||
|
@ -318,21 +414,25 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_makeRegisterRequest: function(auth) {
|
||||
// Only send the bind params if we're sending username / pw params
|
||||
// We inhibit login if we're trying to register with an email address: this
|
||||
// avoids a lot of complex race conditions that can occur if we try to log
|
||||
// the user in one one or both of the tabs they might end up with after
|
||||
// clicking the email link.
|
||||
let inhibitLogin = Boolean(this.state.formVals.email);
|
||||
|
||||
// Only send inhibitLogin if we're sending username / pw params
|
||||
// (Since we need to send no params at all to use the ones saved in the
|
||||
// session).
|
||||
const bindThreepids = this.state.formVals.password ? {
|
||||
email: true,
|
||||
msisdn: true,
|
||||
} : {};
|
||||
if (!this.state.formVals.password) inhibitLogin = null;
|
||||
|
||||
return this._matrixClient.register(
|
||||
return this.state.matrixClient.register(
|
||||
this.state.formVals.username,
|
||||
this.state.formVals.password,
|
||||
undefined, // session id: included in the auth dict already
|
||||
auth,
|
||||
bindThreepids,
|
||||
null,
|
||||
null,
|
||||
inhibitLogin,
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -344,11 +444,23 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
// Links to the login page shown after registration is completed are routed through this
|
||||
// which checks the user hasn't already logged in somewhere else (perhaps we should do
|
||||
// this more generally?)
|
||||
_onLoginClickWithCheck: async function(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
const sessionLoaded = await Lifecycle.loadSession({ignoreGuest: true});
|
||||
if (!sessionLoaded) {
|
||||
// ok fine, there's still no session: really go to the login page
|
||||
this.props.onLoginClick();
|
||||
}
|
||||
},
|
||||
|
||||
renderServerComponent() {
|
||||
const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
|
||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||
const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
return null;
|
||||
|
@ -356,7 +468,9 @@ module.exports = React.createClass({
|
|||
|
||||
// If we're on a different phase, we only show the server type selector,
|
||||
// which is always shown if we allow custom URLs at all.
|
||||
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS) {
|
||||
// (if there's a fatal server error, we need to show the full server
|
||||
// config as the user may need to change servers to resolve the error).
|
||||
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS && !this.state.serverErrorIsFatal) {
|
||||
return <div>
|
||||
<ServerTypeSelector
|
||||
selected={this.state.serverType}
|
||||
|
@ -365,47 +479,42 @@ module.exports = React.createClass({
|
|||
</div>;
|
||||
}
|
||||
|
||||
const serverDetailsProps = {};
|
||||
if (PHASES_ENABLED) {
|
||||
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
|
||||
serverDetailsProps.submitText = _t("Next");
|
||||
serverDetailsProps.submitClass = "mx_Login_submit";
|
||||
}
|
||||
|
||||
let serverDetails = null;
|
||||
switch (this.state.serverType) {
|
||||
case ServerType.FREE:
|
||||
break;
|
||||
case ServerType.PREMIUM:
|
||||
serverDetails = <ModularServerConfig
|
||||
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
delayTimeMs={250}
|
||||
{...serverDetailsProps}
|
||||
/>;
|
||||
break;
|
||||
case ServerType.ADVANCED:
|
||||
serverDetails = <ServerConfig
|
||||
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl}
|
||||
customIsUrl={this.state.discoveredIsUrl || this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
serverConfig={this.props.serverConfig}
|
||||
onServerConfigChange={this.props.onServerConfigChange}
|
||||
delayTimeMs={250}
|
||||
showIdentityServerIfRequiredByHomeserver={true}
|
||||
{...serverDetailsProps}
|
||||
/>;
|
||||
break;
|
||||
}
|
||||
|
||||
let nextButton = null;
|
||||
if (PHASES_ENABLED) {
|
||||
nextButton = <AccessibleButton className="mx_Login_submit"
|
||||
onClick={this.onServerDetailsNextPhaseClick}
|
||||
>
|
||||
{_t("Next")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<ServerTypeSelector
|
||||
selected={this.state.serverType}
|
||||
onChange={this.onServerTypeChange}
|
||||
/>
|
||||
{serverDetails}
|
||||
{nextButton}
|
||||
</div>;
|
||||
},
|
||||
|
||||
|
@ -418,23 +527,25 @@ module.exports = React.createClass({
|
|||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const RegistrationForm = sdk.getComponent('auth.RegistrationForm');
|
||||
|
||||
if (this.state.doingUIAuth) {
|
||||
if (this.state.matrixClient && this.state.doingUIAuth) {
|
||||
return <InteractiveAuth
|
||||
matrixClient={this._matrixClient}
|
||||
matrixClient={this.state.matrixClient}
|
||||
makeRequest={this._makeRegisterRequest}
|
||||
onAuthFinished={this._onUIAuthFinished}
|
||||
inputs={this._getUIAuthInputs()}
|
||||
makeRegistrationUrl={this.props.makeRegistrationUrl}
|
||||
requestEmailToken={this._requestEmailToken}
|
||||
sessionId={this.props.sessionId}
|
||||
clientSecret={this.props.clientSecret}
|
||||
emailSid={this.props.idSid}
|
||||
poll={true}
|
||||
/>;
|
||||
} else if (!this.state.matrixClient && !this.state.busy) {
|
||||
return null;
|
||||
} else if (this.state.busy || !this.state.flows) {
|
||||
return <div className="mx_AuthBody_spinner">
|
||||
<Spinner />
|
||||
</div>;
|
||||
} else {
|
||||
} else if (this.state.flows.length) {
|
||||
let onEditServerDetailsClick = null;
|
||||
// If custom URLs are allowed and we haven't selected the Free server type, wire
|
||||
// up the server details edit link.
|
||||
|
@ -446,13 +557,6 @@ module.exports = React.createClass({
|
|||
onEditServerDetailsClick = this.onEditServerDetailsClick;
|
||||
}
|
||||
|
||||
// If the current HS URL is the default HS URL, then we can label it
|
||||
// with the default HS name (if it exists).
|
||||
let hsName;
|
||||
if (this.state.hsUrl === this.props.defaultHsUrl) {
|
||||
hsName = this.props.defaultServerName;
|
||||
}
|
||||
|
||||
return <RegistrationForm
|
||||
defaultUsername={this.state.formVals.username}
|
||||
defaultEmail={this.state.formVals.email}
|
||||
|
@ -462,8 +566,9 @@ module.exports = React.createClass({
|
|||
onRegisterClick={this.onFormSubmit}
|
||||
onEditServerDetailsClick={onEditServerDetailsClick}
|
||||
flows={this.state.flows}
|
||||
hsName={hsName}
|
||||
hsUrl={this.state.hsUrl}
|
||||
serverConfig={this.props.serverConfig}
|
||||
canSubmit={!this.state.serverErrorIsFatal}
|
||||
serverRequiresIdServer={this.state.serverRequiresIdServer}
|
||||
/>;
|
||||
}
|
||||
},
|
||||
|
@ -472,13 +577,28 @@ module.exports = React.createClass({
|
|||
const AuthHeader = sdk.getComponent('auth.AuthHeader');
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
const AuthPage = sdk.getComponent('auth.AuthPage');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let errorText;
|
||||
const err = this.state.errorText || this.props.defaultServerDiscoveryError;
|
||||
const err = this.state.errorText;
|
||||
if (err) {
|
||||
errorText = <div className="mx_Login_error">{ err }</div>;
|
||||
}
|
||||
|
||||
let serverDeadSection;
|
||||
if (!this.state.serverIsAlive) {
|
||||
const classes = classNames({
|
||||
"mx_Login_error": true,
|
||||
"mx_Login_serverError": true,
|
||||
"mx_Login_serverErrorNonFatal": !this.state.serverErrorIsFatal,
|
||||
});
|
||||
serverDeadSection = (
|
||||
<div className={classes}>
|
||||
{this.state.serverDeadError}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const signIn = <a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#">
|
||||
{ _t('Sign in instead') }
|
||||
</a>;
|
||||
|
@ -491,16 +611,62 @@ module.exports = React.createClass({
|
|||
</a>;
|
||||
}
|
||||
|
||||
let body;
|
||||
if (this.state.completedNoSignin) {
|
||||
let regDoneText;
|
||||
if (this.state.differentLoggedInUserId) {
|
||||
regDoneText = <div>
|
||||
<p>{_t(
|
||||
"Your new account (%(newAccountId)s) is registered, but you're already " +
|
||||
"logged into a different account (%(loggedInUserId)s).", {
|
||||
newAccountId: this.state.registeredUsername,
|
||||
loggedInUserId: this.state.differentLoggedInUserId,
|
||||
},
|
||||
)}</p>
|
||||
<p><AccessibleButton element="span" className="mx_linkButton" onClick={this._onLoginClickWithCheck}>
|
||||
{_t("Continue with previous account")}
|
||||
</AccessibleButton></p>
|
||||
</div>;
|
||||
} else if (this.state.formVals.password) {
|
||||
// We're the client that started the registration
|
||||
regDoneText = <h3>{_t(
|
||||
"<a>Log in</a> to your new account.", {},
|
||||
{
|
||||
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
|
||||
},
|
||||
)}</h3>;
|
||||
} else {
|
||||
// We're not the original client: the user probably got to us by clicking the
|
||||
// email validation link. We can't offer a 'go straight to your account' link
|
||||
// as we don't have the original creds.
|
||||
regDoneText = <h3>{_t(
|
||||
"You can now close this window or <a>log in</a> to your new account.", {},
|
||||
{
|
||||
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
|
||||
},
|
||||
)}</h3>;
|
||||
}
|
||||
body = <div>
|
||||
<h2>{_t("Registration Successful")}</h2>
|
||||
{ regDoneText }
|
||||
</div>;
|
||||
} else {
|
||||
body = <div>
|
||||
<h2>{ _t('Create your account') }</h2>
|
||||
{ errorText }
|
||||
{ serverDeadSection }
|
||||
{ this.renderServerComponent() }
|
||||
{ this.renderRegisterComponent() }
|
||||
{ goBack }
|
||||
{ signIn }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<AuthHeader />
|
||||
<AuthBody>
|
||||
<h2>{ _t('Create your account') }</h2>
|
||||
{ errorText }
|
||||
{ this.renderServerComponent() }
|
||||
{ this.renderRegisterComponent() }
|
||||
{ goBack }
|
||||
{ signIn }
|
||||
{ body }
|
||||
</AuthBody>
|
||||
</AuthPage>
|
||||
);
|
||||
|
|
322
src/components/structures/auth/SoftLogout.js
Normal file
322
src/components/structures/auth/SoftLogout.js
Normal file
|
@ -0,0 +1,322 @@
|
|||
/*
|
||||
Copyright 2019 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 sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import Modal from '../../../Modal';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import {sendLoginRequest} from "../../../Login";
|
||||
import url from 'url';
|
||||
|
||||
const LOGIN_VIEW = {
|
||||
LOADING: 1,
|
||||
PASSWORD: 2,
|
||||
CAS: 3, // SSO, but old
|
||||
SSO: 4,
|
||||
UNSUPPORTED: 5,
|
||||
};
|
||||
|
||||
const FLOWS_TO_VIEWS = {
|
||||
"m.login.password": LOGIN_VIEW.PASSWORD,
|
||||
"m.login.cas": LOGIN_VIEW.CAS,
|
||||
"m.login.sso": LOGIN_VIEW.SSO,
|
||||
};
|
||||
|
||||
export default class SoftLogout extends React.Component {
|
||||
static propTypes = {
|
||||
// Query parameters from MatrixChat
|
||||
realQueryParams: PropTypes.object, // {homeserver, identityServer, loginToken}
|
||||
|
||||
// Called when the SSO login completes
|
||||
onTokenLoginCompleted: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
loginView: LOGIN_VIEW.LOADING,
|
||||
keyBackupNeeded: true, // assume we do while we figure it out (see componentWillMount)
|
||||
ssoUrl: null,
|
||||
|
||||
busy: false,
|
||||
password: "",
|
||||
errorText: "",
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
// We've ended up here when we don't need to - navigate to login
|
||||
if (!Lifecycle.isSoftLogout()) {
|
||||
dis.dispatch({action: "on_logged_in"});
|
||||
return;
|
||||
}
|
||||
|
||||
this._initLogin();
|
||||
|
||||
MatrixClientPeg.get().flagAllGroupSessionsForBackup().then(remaining => {
|
||||
this.setState({keyBackupNeeded: remaining > 0});
|
||||
});
|
||||
}
|
||||
|
||||
onClearAll = () => {
|
||||
const ConfirmWipeDeviceDialog = sdk.getComponent('dialogs.ConfirmWipeDeviceDialog');
|
||||
Modal.createTrackedDialog('Clear Data', 'Soft Logout', ConfirmWipeDeviceDialog, {
|
||||
onFinished: (wipeData) => {
|
||||
if (!wipeData) return;
|
||||
|
||||
console.log("Clearing data from soft-logged-out device");
|
||||
Lifecycle.logout();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
async _initLogin() {
|
||||
const queryParams = this.props.realQueryParams;
|
||||
const hasAllParams = queryParams && queryParams['homeserver'] && queryParams['loginToken'];
|
||||
if (hasAllParams) {
|
||||
this.setState({loginView: LOGIN_VIEW.LOADING});
|
||||
this.trySsoLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: we don't use the existing Login class because it is heavily flow-based. We don't
|
||||
// care about login flows here, unless it is the single flow we support.
|
||||
const client = MatrixClientPeg.get();
|
||||
const loginViews = (await client.loginFlows()).flows.map(f => FLOWS_TO_VIEWS[f.type]);
|
||||
|
||||
const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED;
|
||||
this.setState({loginView: chosenView});
|
||||
|
||||
if (chosenView === LOGIN_VIEW.CAS || chosenView === LOGIN_VIEW.SSO) {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
const appUrl = url.parse(window.location.href, true);
|
||||
appUrl.hash = ""; // Clear #/soft_logout off the URL
|
||||
appUrl.query["homeserver"] = client.getHomeserverUrl();
|
||||
appUrl.query["identityServer"] = client.getIdentityServerUrl();
|
||||
|
||||
const ssoUrl = client.getSsoLoginUrl(url.format(appUrl), chosenView === LOGIN_VIEW.CAS ? "cas" : "sso");
|
||||
this.setState({ssoUrl});
|
||||
}
|
||||
}
|
||||
|
||||
onPasswordChange = (ev) => {
|
||||
this.setState({password: ev.target.value});
|
||||
};
|
||||
|
||||
onForgotPassword = () => {
|
||||
dis.dispatch({action: 'start_password_recovery'});
|
||||
};
|
||||
|
||||
onPasswordLogin = async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.setState({busy: true});
|
||||
|
||||
const hsUrl = MatrixClientPeg.get().getHomeserverUrl();
|
||||
const isUrl = MatrixClientPeg.get().getIdentityServerUrl();
|
||||
const loginType = "m.login.password";
|
||||
const loginParams = {
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: MatrixClientPeg.get().getUserId(),
|
||||
},
|
||||
password: this.state.password,
|
||||
device_id: MatrixClientPeg.get().getDeviceId(),
|
||||
};
|
||||
|
||||
let credentials = null;
|
||||
try {
|
||||
credentials = await sendLoginRequest(hsUrl, isUrl, loginType, loginParams);
|
||||
} catch (e) {
|
||||
let errorText = _t("Failed to re-authenticate due to a homeserver problem");
|
||||
if (e.errcode === "M_FORBIDDEN" && (e.httpStatus === 401 || e.httpStatus === 403)) {
|
||||
errorText = _t("Incorrect password");
|
||||
}
|
||||
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: errorText,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Lifecycle.hydrateSession(credentials).catch((e) => {
|
||||
console.error(e);
|
||||
this.setState({busy: false, errorText: _t("Failed to re-authenticate")});
|
||||
});
|
||||
};
|
||||
|
||||
async trySsoLogin() {
|
||||
this.setState({busy: true});
|
||||
|
||||
const hsUrl = this.props.realQueryParams['homeserver'];
|
||||
const isUrl = this.props.realQueryParams['identityServer'] || MatrixClientPeg.get().getIdentityServerUrl();
|
||||
const loginType = "m.login.token";
|
||||
const loginParams = {
|
||||
token: this.props.realQueryParams['loginToken'],
|
||||
device_id: MatrixClientPeg.get().getDeviceId(),
|
||||
};
|
||||
|
||||
let credentials = null;
|
||||
try {
|
||||
credentials = await sendLoginRequest(hsUrl, isUrl, loginType, loginParams);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.setState({busy: false, loginView: LOGIN_VIEW.UNSUPPORTED});
|
||||
return;
|
||||
}
|
||||
|
||||
Lifecycle.hydrateSession(credentials).then(() => {
|
||||
if (this.props.onTokenLoginCompleted) this.props.onTokenLoginCompleted();
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
this.setState({busy: false, loginView: LOGIN_VIEW.UNSUPPORTED});
|
||||
});
|
||||
}
|
||||
|
||||
onSsoLogin = async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.setState({busy: true});
|
||||
window.location.href = this.state.ssoUrl;
|
||||
};
|
||||
|
||||
_renderSignInSection() {
|
||||
if (this.state.loginView === LOGIN_VIEW.LOADING) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
let introText = null; // null is translated to something area specific in this function
|
||||
if (this.state.keyBackupNeeded) {
|
||||
introText = _t(
|
||||
"Regain access to your account and recover encryption keys stored on this device. " +
|
||||
"Without them, you won’t be able to read all of your secure messages on any device.");
|
||||
}
|
||||
|
||||
if (this.state.loginView === LOGIN_VIEW.PASSWORD) {
|
||||
const Field = sdk.getComponent("elements.Field");
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let error = null;
|
||||
if (this.state.errorText) {
|
||||
error = <span className='mx_Login_error'>{this.state.errorText}</span>;
|
||||
}
|
||||
|
||||
if (!introText) {
|
||||
introText = _t("Enter your password to sign in and regain access to your account.");
|
||||
} // else we already have a message and should use it (key backup warning)
|
||||
|
||||
return (
|
||||
<form onSubmit={this.onPasswordLogin}>
|
||||
<p>{introText}</p>
|
||||
{error}
|
||||
<Field
|
||||
id="softlogout_password"
|
||||
type="password"
|
||||
label={_t("Password")}
|
||||
onChange={this.onPasswordChange}
|
||||
value={this.state.password}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={this.onPasswordLogin}
|
||||
kind="primary"
|
||||
type="submit"
|
||||
disabled={this.state.busy}
|
||||
>
|
||||
{_t("Sign In")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={this.onForgotPassword} kind="link">
|
||||
{_t("Forgotten your password?")}
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.loginView === LOGIN_VIEW.SSO || this.state.loginView === LOGIN_VIEW.CAS) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
if (!introText) {
|
||||
introText = _t("Sign in and regain access to your account.");
|
||||
} // else we already have a message and should use it (key backup warning)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{introText}</p>
|
||||
<AccessibleButton kind='primary' onClick={this.onSsoLogin}>
|
||||
{_t('Sign in with single sign-on')}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default: assume unsupported/error
|
||||
return (
|
||||
<p>
|
||||
{_t(
|
||||
"You cannot sign in to your account. Please contact your " +
|
||||
"homeserver admin for more information.",
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<AuthHeader />
|
||||
<AuthBody>
|
||||
<h2>
|
||||
{_t("You're signed out")}
|
||||
</h2>
|
||||
|
||||
<h3>{_t("Sign in")}</h3>
|
||||
<div>
|
||||
{this._renderSignInSection()}
|
||||
</div>
|
||||
|
||||
<h3>{_t("Clear personal data")}</h3>
|
||||
<p>
|
||||
{_t(
|
||||
"Warning: Your personal data (including encryption keys) is still stored " +
|
||||
"on this device. Clear it if you're finished using this device, or want to sign " +
|
||||
"in to another account.",
|
||||
)}
|
||||
</p>
|
||||
<div>
|
||||
<AccessibleButton onClick={this.onClearAll} kind="danger">
|
||||
{_t("Clear all data")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</AuthBody>
|
||||
</AuthPage>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -15,12 +15,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'AuthFooter',
|
||||
|
||||
render: function() {
|
||||
|
|
|
@ -15,12 +15,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import sdk from '../../../index';
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'AuthHeader',
|
||||
|
||||
render: function() {
|
||||
|
|
|
@ -15,12 +15,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import sdk from '../../../index';
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'AuthPage',
|
||||
|
||||
render: function() {
|
||||
|
|
|
@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
|
@ -25,7 +24,7 @@ const DIV_ID = 'mx_recaptcha';
|
|||
/**
|
||||
* A pure UI component which displays a captcha form.
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'CaptchaForm',
|
||||
|
||||
propTypes: {
|
||||
|
@ -90,7 +89,7 @@ module.exports = React.createClass({
|
|||
+ "authentication");
|
||||
}
|
||||
|
||||
console.log("Rendering to %s", divId);
|
||||
console.info("Rendering to %s", divId);
|
||||
this._captchaWidgetId = global.grecaptcha.render(divId, {
|
||||
sitekey: publicKey,
|
||||
callback: this.props.onCaptchaResponse,
|
||||
|
|
|
@ -20,6 +20,7 @@ import PropTypes from 'prop-types';
|
|||
import sdk from '../../../index';
|
||||
|
||||
import { COUNTRIES } from '../../../phonenumber';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
|
||||
const COUNTRIES_BY_ISO2 = {};
|
||||
for (const c of COUNTRIES) {
|
||||
|
@ -45,17 +46,25 @@ export default class CountryDropdown extends React.Component {
|
|||
this._onOptionChange = this._onOptionChange.bind(this);
|
||||
this._getShortOption = this._getShortOption.bind(this);
|
||||
|
||||
let defaultCountry = COUNTRIES[0];
|
||||
const defaultCountryCode = SdkConfig.get()["defaultCountryCode"];
|
||||
if (defaultCountryCode) {
|
||||
const country = COUNTRIES.find(c => c.iso2 === defaultCountryCode.toUpperCase());
|
||||
if (country) defaultCountry = country;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
searchQuery: '',
|
||||
defaultCountry,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (!this.props.value) {
|
||||
// If no value is given, we start with the first
|
||||
// If no value is given, we start with the default
|
||||
// country selected, but our parent component
|
||||
// doesn't know this, therefore we do this.
|
||||
this.props.onOptionChange(COUNTRIES[0]);
|
||||
this.props.onOptionChange(this.state.defaultCountry);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,7 +128,7 @@ export default class CountryDropdown extends React.Component {
|
|||
|
||||
// default value here too, otherwise we need to handle null / undefined
|
||||
// values between mounting and the initial value propgating
|
||||
const value = this.props.value || COUNTRIES[0].iso2;
|
||||
const value = this.props.value || this.state.defaultCountry.iso2;
|
||||
|
||||
return <Dropdown className={this.props.className + " mx_CountryDropdown"}
|
||||
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 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,9 +16,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'CustomServerDialog',
|
||||
|
||||
render: function() {
|
||||
|
@ -33,11 +35,6 @@ module.exports = React.createClass({
|
|||
"allows you to use this app with an existing Matrix account on a " +
|
||||
"different homeserver.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"You can also set a custom identity server, but you won't be " +
|
||||
"able to invite users by email address, or be invited by email " +
|
||||
"address yourself.",
|
||||
)}</p>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.props.onFinished} autoFocus={true}>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 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,6 +17,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import url from 'url';
|
||||
import classnames from 'classnames';
|
||||
|
@ -57,13 +59,12 @@ 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.
|
||||
* makeRegistrationUrl A function that makes a registration URL
|
||||
*
|
||||
* Each component may also provide the following functions (beyond the standard React ones):
|
||||
* focus: set the input focus appropriately in the form.
|
||||
*/
|
||||
|
||||
export const PasswordAuthEntry = React.createClass({
|
||||
export const PasswordAuthEntry = createReactClass({
|
||||
displayName: 'PasswordAuthEntry',
|
||||
|
||||
statics: {
|
||||
|
@ -81,40 +82,38 @@ export const PasswordAuthEntry = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
passwordValid: false,
|
||||
password: "",
|
||||
};
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.refs.passwordField) {
|
||||
this.refs.passwordField.focus();
|
||||
}
|
||||
},
|
||||
|
||||
_onSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
if (this.props.busy) return;
|
||||
|
||||
this.props.submitAuthDict({
|
||||
type: PasswordAuthEntry.LOGIN_TYPE,
|
||||
// TODO: Remove `user` once servers support proper UIA
|
||||
// See https://github.com/vector-im/riot-web/issues/10312
|
||||
user: this.props.matrixClient.credentials.userId,
|
||||
password: this.refs.passwordField.value,
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: this.props.matrixClient.credentials.userId,
|
||||
},
|
||||
password: this.state.password,
|
||||
});
|
||||
},
|
||||
|
||||
_onPasswordFieldChange: function(ev) {
|
||||
// enable the submit button iff the password is non-empty
|
||||
this.setState({
|
||||
passwordValid: Boolean(this.refs.passwordField.value),
|
||||
password: ev.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let passwordBoxClass = null;
|
||||
|
||||
if (this.props.errorText) {
|
||||
passwordBoxClass = 'error';
|
||||
}
|
||||
const passwordBoxClass = classnames({
|
||||
"error": this.props.errorText,
|
||||
});
|
||||
|
||||
let submitButtonOrSpinner;
|
||||
if (this.props.busy) {
|
||||
|
@ -124,7 +123,8 @@ export const PasswordAuthEntry = React.createClass({
|
|||
submitButtonOrSpinner = (
|
||||
<input type="submit"
|
||||
className="mx_Dialog_primary"
|
||||
disabled={!this.state.passwordValid}
|
||||
disabled={!this.state.password}
|
||||
value={_t("Continue")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -138,17 +138,21 @@ export const PasswordAuthEntry = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t("To continue, please enter your password.") }</p>
|
||||
<form onSubmit={this._onSubmit}>
|
||||
<label htmlFor="passwordField">{ _t("Password:") }</label>
|
||||
<input
|
||||
name="passwordField"
|
||||
ref="passwordField"
|
||||
<form onSubmit={this._onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
|
||||
<Field
|
||||
id="mx_InteractiveAuthEntryComponents_password"
|
||||
className={passwordBoxClass}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
type="password"
|
||||
name="passwordField"
|
||||
label={_t('Password')}
|
||||
autoFocus={true}
|
||||
value={this.state.password}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
/>
|
||||
<div className="mx_button_row">
|
||||
{ submitButtonOrSpinner }
|
||||
|
@ -160,7 +164,7 @@ export const PasswordAuthEntry = React.createClass({
|
|||
},
|
||||
});
|
||||
|
||||
export const RecaptchaAuthEntry = React.createClass({
|
||||
export const RecaptchaAuthEntry = createReactClass({
|
||||
displayName: 'RecaptchaAuthEntry',
|
||||
|
||||
statics: {
|
||||
|
@ -187,14 +191,24 @@ export const RecaptchaAuthEntry = React.createClass({
|
|||
return <Loader />;
|
||||
}
|
||||
|
||||
let errorText = this.props.errorText;
|
||||
|
||||
const CaptchaForm = sdk.getComponent("views.auth.CaptchaForm");
|
||||
const sitePublicKey = this.props.stageParams.public_key;
|
||||
let sitePublicKey;
|
||||
if (!this.props.stageParams || !this.props.stageParams.public_key) {
|
||||
errorText = _t(
|
||||
"Missing captcha public key in homeserver configuration. Please report " +
|
||||
"this to your homeserver administrator.",
|
||||
);
|
||||
} else {
|
||||
sitePublicKey = this.props.stageParams.public_key;
|
||||
}
|
||||
|
||||
let errorSection;
|
||||
if (this.props.errorText) {
|
||||
if (errorText) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{ this.props.errorText }
|
||||
{ errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -210,7 +224,7 @@ export const RecaptchaAuthEntry = React.createClass({
|
|||
},
|
||||
});
|
||||
|
||||
export const TermsAuthEntry = React.createClass({
|
||||
export const TermsAuthEntry = createReactClass({
|
||||
displayName: 'TermsAuthEntry',
|
||||
|
||||
statics: {
|
||||
|
@ -316,7 +330,7 @@ export const TermsAuthEntry = React.createClass({
|
|||
|
||||
checkboxes.push(
|
||||
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
|
||||
<input type="checkbox" onClick={() => this._togglePolicy(policy.id)} checked={checked} />
|
||||
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
|
||||
<a href={policy.url} target="_blank" rel="noopener">{ policy.name }</a>
|
||||
</label>,
|
||||
);
|
||||
|
@ -349,7 +363,7 @@ export const TermsAuthEntry = React.createClass({
|
|||
},
|
||||
});
|
||||
|
||||
export const EmailIdentityAuthEntry = React.createClass({
|
||||
export const EmailIdentityAuthEntry = createReactClass({
|
||||
displayName: 'EmailIdentityAuthEntry',
|
||||
|
||||
statics: {
|
||||
|
@ -365,7 +379,6 @@ export const EmailIdentityAuthEntry = React.createClass({
|
|||
stageState: PropTypes.object.isRequired,
|
||||
fail: PropTypes.func.isRequired,
|
||||
setEmailSid: PropTypes.func.isRequired,
|
||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -374,38 +387,6 @@ export const EmailIdentityAuthEntry = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
if (this.props.stageState.emailSid === null) {
|
||||
this.setState({requestingToken: true});
|
||||
this._requestEmailToken().catch((e) => {
|
||||
this.props.fail(e);
|
||||
}).finally(() => {
|
||||
this.setState({requestingToken: false});
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Requests a verification token by email.
|
||||
*/
|
||||
_requestEmailToken: function() {
|
||||
const nextLink = this.props.makeRegistrationUrl({
|
||||
client_secret: this.props.clientSecret,
|
||||
hs_url: this.props.matrixClient.getHomeserverUrl(),
|
||||
is_url: this.props.matrixClient.getIdentityServerUrl(),
|
||||
session_id: this.props.authSessionId,
|
||||
});
|
||||
|
||||
return this.props.matrixClient.requestRegisterEmailToken(
|
||||
this.props.inputs.emailAddress,
|
||||
this.props.clientSecret,
|
||||
1, // TODO: Multiple send attempts?
|
||||
nextLink,
|
||||
).then((result) => {
|
||||
this.props.setEmailSid(result.sid);
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.requestingToken) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
@ -424,7 +405,7 @@ export const EmailIdentityAuthEntry = React.createClass({
|
|||
},
|
||||
});
|
||||
|
||||
export const MsisdnAuthEntry = React.createClass({
|
||||
export const MsisdnAuthEntry = createReactClass({
|
||||
displayName: 'MsisdnAuthEntry',
|
||||
|
||||
statics: {
|
||||
|
@ -450,6 +431,7 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._submitUrl = null;
|
||||
this._sid = null;
|
||||
this._msisdn = null;
|
||||
this._tokenBox = null;
|
||||
|
@ -459,7 +441,7 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
this.props.fail(e);
|
||||
}).finally(() => {
|
||||
this.setState({requestingToken: false});
|
||||
}).done();
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
|
@ -472,6 +454,7 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
this.props.clientSecret,
|
||||
1, // TODO: Multiple send attempts?
|
||||
).then((result) => {
|
||||
this._submitUrl = result.submit_url;
|
||||
this._sid = result.sid;
|
||||
this._msisdn = result.msisdn;
|
||||
});
|
||||
|
@ -483,38 +466,56 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onFormSubmit: function(e) {
|
||||
_onFormSubmit: async function(e) {
|
||||
e.preventDefault();
|
||||
if (this.state.token == '') return;
|
||||
|
||||
this.setState({
|
||||
errorText: null,
|
||||
});
|
||||
this.setState({
|
||||
errorText: null,
|
||||
});
|
||||
|
||||
this.props.matrixClient.submitMsisdnToken(
|
||||
this._sid, this.props.clientSecret, this.state.token,
|
||||
).then((result) => {
|
||||
if (result.success) {
|
||||
const idServerParsedUrl = url.parse(
|
||||
this.props.matrixClient.getIdentityServerUrl(),
|
||||
try {
|
||||
const requiresIdServerParam =
|
||||
await this.props.matrixClient.doesServerRequireIdServerParam();
|
||||
let result;
|
||||
if (this._submitUrl) {
|
||||
result = await this.props.matrixClient.submitMsisdnTokenOtherUrl(
|
||||
this._submitUrl, this._sid, this.props.clientSecret, this.state.token,
|
||||
);
|
||||
} else if (requiresIdServerParam) {
|
||||
result = await this.props.matrixClient.submitMsisdnToken(
|
||||
this._sid, this.props.clientSecret, this.state.token,
|
||||
);
|
||||
} else {
|
||||
throw new Error("The registration with MSISDN flow is misconfigured");
|
||||
}
|
||||
if (result.success) {
|
||||
const creds = {
|
||||
sid: this._sid,
|
||||
client_secret: this.props.clientSecret,
|
||||
};
|
||||
if (requiresIdServerParam) {
|
||||
const idServerParsedUrl = url.parse(
|
||||
this.props.matrixClient.getIdentityServerUrl(),
|
||||
);
|
||||
creds.id_server = idServerParsedUrl.host;
|
||||
}
|
||||
this.props.submitAuthDict({
|
||||
type: MsisdnAuthEntry.LOGIN_TYPE,
|
||||
threepid_creds: {
|
||||
sid: this._sid,
|
||||
client_secret: this.props.clientSecret,
|
||||
id_server: idServerParsedUrl.host,
|
||||
},
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/vector-im/riot-web/issues/10312
|
||||
threepid_creds: creds,
|
||||
threepidCreds: creds,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
errorText: _t("Token incorrect"),
|
||||
});
|
||||
}
|
||||
}).catch((e) => {
|
||||
} catch (e) {
|
||||
this.props.fail(e);
|
||||
console.log("Failed to submit msisdn token");
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -564,7 +565,7 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
},
|
||||
});
|
||||
|
||||
export const FallbackAuthEntry = React.createClass({
|
||||
export const FallbackAuthEntry = createReactClass({
|
||||
displayName: 'FallbackAuthEntry',
|
||||
|
||||
propTypes: {
|
||||
|
|
|
@ -15,91 +15,82 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
||||
import * as ServerType from '../../views/auth/ServerTypeSelector';
|
||||
import ServerConfig from "./ServerConfig";
|
||||
|
||||
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
|
||||
|
||||
// TODO: TravisR - Can this extend ServerConfig for most things?
|
||||
|
||||
/*
|
||||
* Configure the Modular server name.
|
||||
*
|
||||
* This is a variant of ServerConfig with only the HS field and different body
|
||||
* text that is specific to the Modular case.
|
||||
*/
|
||||
export default class ModularServerConfig extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onServerConfigChange: PropTypes.func,
|
||||
export default class ModularServerConfig extends ServerConfig {
|
||||
static propTypes = ServerConfig.propTypes;
|
||||
|
||||
// default URLs are defined in config.json (or the hardcoded defaults)
|
||||
// they are used if the user has not overridden them with a custom URL.
|
||||
// In other words, if the custom URL is blank, the default is used.
|
||||
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
|
||||
|
||||
// This component always uses the default IS URL and doesn't allow it
|
||||
// to be changed. We still receive it as a prop here to simplify
|
||||
// consumers by still passing the IS URL via onServerConfigChange.
|
||||
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
|
||||
|
||||
// custom URLs are explicitly provided by the user and override the
|
||||
// default URLs. The user enters them via the component's input fields,
|
||||
// which is reflected on these properties whenever on..UrlChanged fires.
|
||||
// They are persisted in localStorage by MatrixClientPeg, and so can
|
||||
// override the default URLs when the component initially loads.
|
||||
customHsUrl: PropTypes.string,
|
||||
|
||||
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onServerConfigChange: function() {},
|
||||
customHsUrl: "",
|
||||
delayTimeMs: 0,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hsUrl: props.customHsUrl,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.customHsUrl === this.state.hsUrl) return;
|
||||
async validateAndApplyServer(hsUrl, isUrl) {
|
||||
// Always try and use the defaults first
|
||||
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
|
||||
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
|
||||
this.setState({busy: false, errorText: ""});
|
||||
this.props.onServerConfigChange(defaultConfig);
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
hsUrl: newProps.customHsUrl,
|
||||
hsUrl,
|
||||
isUrl,
|
||||
busy: true,
|
||||
errorText: "",
|
||||
});
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: newProps.customHsUrl,
|
||||
isUrl: this.props.defaultIsUrl,
|
||||
});
|
||||
}
|
||||
|
||||
onHomeserverBlur = (ev) => {
|
||||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: this.state.hsUrl,
|
||||
isUrl: this.props.defaultIsUrl,
|
||||
try {
|
||||
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
|
||||
this.setState({busy: false, errorText: ""});
|
||||
this.props.onServerConfigChange(result);
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
let message = _t("Unable to validate homeserver/identity server");
|
||||
if (e.translatedMessage) {
|
||||
message = e.translatedMessage;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: message,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onHomeserverChange = (ev) => {
|
||||
const hsUrl = ev.target.value;
|
||||
this.setState({ hsUrl });
|
||||
}
|
||||
|
||||
_waitThenInvoke(existingTimeoutId, fn) {
|
||||
if (existingTimeoutId) {
|
||||
clearTimeout(existingTimeoutId);
|
||||
return null;
|
||||
}
|
||||
return setTimeout(fn.bind(this), this.props.delayTimeMs);
|
||||
}
|
||||
|
||||
async validateServer() {
|
||||
// TODO: Do we want to support .well-known lookups here?
|
||||
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
|
||||
// find their homeserver without demanding they use "https://matrix.org"
|
||||
return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl);
|
||||
}
|
||||
|
||||
render() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const submitButton = this.props.submitText
|
||||
? <AccessibleButton
|
||||
element="button"
|
||||
type="submit"
|
||||
className={this.props.submitClass}
|
||||
onClick={this.onSubmit}
|
||||
disabled={this.state.busy}>{this.props.submitText}</AccessibleButton>
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="mx_ServerConfig">
|
||||
|
@ -113,15 +104,18 @@ export default class ModularServerConfig extends React.PureComponent {
|
|||
</a>,
|
||||
},
|
||||
)}
|
||||
<div className="mx_ServerConfig_fields">
|
||||
<Field id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Server Name")}
|
||||
placeholder={this.props.defaultHsUrl}
|
||||
value={this.state.hsUrl}
|
||||
onBlur={this.onHomeserverBlur}
|
||||
onChange={this.onHomeserverChange}
|
||||
/>
|
||||
</div>
|
||||
<form onSubmit={this.onSubmit} autoComplete="off" action={null}>
|
||||
<div className="mx_ServerConfig_fields">
|
||||
<Field id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Server Name")}
|
||||
placeholder={this.props.serverConfig.hsUrl}
|
||||
value={this.state.hsUrl}
|
||||
onBlur={this.onHomeserverBlur}
|
||||
onChange={this.onHomeserverChange}
|
||||
/>
|
||||
</div>
|
||||
{submitButton}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 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.
|
||||
|
@ -21,11 +22,30 @@ import classNames from 'classnames';
|
|||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a username/password form.
|
||||
*/
|
||||
class PasswordLogin extends React.Component {
|
||||
export default class PasswordLogin extends React.Component {
|
||||
static propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired, // fn(username, password)
|
||||
onError: PropTypes.func,
|
||||
onEditServerDetailsClick: PropTypes.func,
|
||||
onForgotPasswordClick: PropTypes.func, // fn()
|
||||
initialUsername: PropTypes.string,
|
||||
initialPhoneCountry: PropTypes.string,
|
||||
initialPhoneNumber: PropTypes.string,
|
||||
initialPassword: PropTypes.string,
|
||||
onUsernameChanged: PropTypes.func,
|
||||
onPhoneCountryChanged: PropTypes.func,
|
||||
onPhoneNumberChanged: PropTypes.func,
|
||||
onPasswordChanged: PropTypes.func,
|
||||
loginIncorrect: PropTypes.bool,
|
||||
disableSubmit: PropTypes.bool,
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onError: function() {},
|
||||
onEditServerDetailsClick: null,
|
||||
|
@ -40,13 +60,12 @@ class PasswordLogin extends React.Component {
|
|||
initialPhoneNumber: "",
|
||||
initialPassword: "",
|
||||
loginIncorrect: false,
|
||||
// This is optional and only set if we used a server name to determine
|
||||
// the HS URL via `.well-known` discovery. The server name is used
|
||||
// instead of the HS URL when talking about where to "sign in to".
|
||||
hsName: null,
|
||||
hsUrl: "",
|
||||
disableSubmit: false,
|
||||
}
|
||||
};
|
||||
|
||||
static LOGIN_FIELD_EMAIL = "login_field_email";
|
||||
static LOGIN_FIELD_MXID = "login_field_mxid";
|
||||
static LOGIN_FIELD_PHONE = "login_field_phone";
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -193,10 +212,7 @@ class PasswordLogin extends React.Component {
|
|||
name="username" // make it a little easier for browser's remember-password
|
||||
key="username_input"
|
||||
type="text"
|
||||
label={SdkConfig.get().disable_custom_urls ?
|
||||
_t("Username on %(hs)s", {
|
||||
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
|
||||
}) : _t("Username")}
|
||||
label={_t("Username")}
|
||||
value={this.state.username}
|
||||
onChange={this.onUsernameChanged}
|
||||
onBlur={this.onUsernameBlur}
|
||||
|
@ -242,6 +258,7 @@ class PasswordLogin extends React.Component {
|
|||
|
||||
render() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const SignInToText = sdk.getComponent('views.auth.SignInToText');
|
||||
|
||||
let forgotPasswordJsx;
|
||||
|
||||
|
@ -258,31 +275,6 @@ class PasswordLogin extends React.Component {
|
|||
</span>;
|
||||
}
|
||||
|
||||
let signInToText = _t('Sign in to your Matrix account');
|
||||
if (this.props.hsName) {
|
||||
signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.hsName,
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.props.hsUrl);
|
||||
signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
|
||||
serverName: parsedHsUrl.hostname,
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
let editLink = null;
|
||||
if (this.props.onEditServerDetailsClick) {
|
||||
editLink = <a className="mx_AuthBody_editServerDetails"
|
||||
href="#" onClick={this.props.onEditServerDetailsClick}
|
||||
>
|
||||
{_t('Change')}
|
||||
</a>;
|
||||
}
|
||||
|
||||
const pwFieldClass = classNames({
|
||||
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
|
||||
});
|
||||
|
@ -295,7 +287,6 @@ class PasswordLogin extends React.Component {
|
|||
<div className="mx_Login_type_container">
|
||||
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
|
||||
<Field
|
||||
className="mx_Login_type_dropdown"
|
||||
id="mx_PasswordLogin_type"
|
||||
element="select"
|
||||
value={this.state.loginType}
|
||||
|
@ -326,10 +317,8 @@ class PasswordLogin extends React.Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h3>
|
||||
{signInToText}
|
||||
{editLink}
|
||||
</h3>
|
||||
<SignInToText serverConfig={this.props.serverConfig}
|
||||
onEditServerDetailsClick={this.props.onEditServerDetailsClick} />
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
{loginType}
|
||||
{loginField}
|
||||
|
@ -353,27 +342,3 @@ class PasswordLogin extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email";
|
||||
PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid";
|
||||
PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
|
||||
|
||||
PasswordLogin.propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired, // fn(username, password)
|
||||
onError: PropTypes.func,
|
||||
onForgotPasswordClick: PropTypes.func, // fn()
|
||||
initialUsername: PropTypes.string,
|
||||
initialPhoneCountry: PropTypes.string,
|
||||
initialPhoneNumber: PropTypes.string,
|
||||
initialPassword: PropTypes.string,
|
||||
onUsernameChanged: PropTypes.func,
|
||||
onPhoneCountryChanged: PropTypes.func,
|
||||
onPhoneNumberChanged: PropTypes.func,
|
||||
onPasswordChanged: PropTypes.func,
|
||||
loginIncorrect: PropTypes.bool,
|
||||
hsName: PropTypes.string,
|
||||
hsUrl: PropTypes.string,
|
||||
disableSubmit: PropTypes.bool,
|
||||
};
|
||||
|
||||
module.exports = PasswordLogin;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,6 +18,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import Email from '../../../email';
|
||||
|
@ -26,6 +28,7 @@ import { _t } from '../../../languageHandler';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
||||
import withValidation from '../elements/Validation';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
|
||||
const FIELD_EMAIL = 'field_email';
|
||||
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
||||
|
@ -38,7 +41,7 @@ const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from of
|
|||
/**
|
||||
* A pure UI component which displays a registration form.
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'RegistrationForm',
|
||||
|
||||
propTypes: {
|
||||
|
@ -51,16 +54,15 @@ module.exports = React.createClass({
|
|||
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
|
||||
onEditServerDetailsClick: PropTypes.func,
|
||||
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
// This is optional and only set if we used a server name to determine
|
||||
// the HS URL via `.well-known` discovery. The server name is used
|
||||
// instead of the HS URL when talking about "your account".
|
||||
hsName: PropTypes.string,
|
||||
hsUrl: PropTypes.string,
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
canSubmit: PropTypes.bool,
|
||||
serverRequiresIdServer: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onValidationChange: console.error,
|
||||
canSubmit: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -70,33 +72,47 @@ module.exports = React.createClass({
|
|||
fieldValid: {},
|
||||
// The ISO2 country code selected in the phone number entry
|
||||
phoneCountry: this.props.defaultPhoneCountry,
|
||||
username: "",
|
||||
email: "",
|
||||
phoneNumber: "",
|
||||
password: "",
|
||||
username: this.props.defaultUsername || "",
|
||||
email: this.props.defaultEmail || "",
|
||||
phoneNumber: this.props.defaultPhoneNumber || "",
|
||||
password: this.props.defaultPassword || "",
|
||||
passwordConfirm: "",
|
||||
passwordComplexity: null,
|
||||
passwordSafe: false,
|
||||
};
|
||||
},
|
||||
|
||||
onSubmit: async function(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
if (!this.props.canSubmit) return;
|
||||
|
||||
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
||||
if (!allFieldsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
if (this.state.email == '') {
|
||||
if (this.state.email === '') {
|
||||
const haveIs = Boolean(this.props.serverConfig.isUrl);
|
||||
|
||||
let desc;
|
||||
if (this.props.serverRequiresIdServer && !haveIs) {
|
||||
desc = _t(
|
||||
"No identity server is configured so you cannot add an email address in order to " +
|
||||
"reset your password in the future.",
|
||||
);
|
||||
} else {
|
||||
desc = _t(
|
||||
"If you don't specify an email address, you won't be able to reset your password. " +
|
||||
"Are you sure?",
|
||||
);
|
||||
}
|
||||
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("If you don't specify an email address, you won't be able to reset your password. " +
|
||||
"Are you sure?") }
|
||||
</div>,
|
||||
description: desc,
|
||||
button: _t("Continue"),
|
||||
onFinished: function(confirmed) {
|
||||
if (confirmed) {
|
||||
|
@ -150,7 +166,11 @@ module.exports = React.createClass({
|
|||
if (!field) {
|
||||
continue;
|
||||
}
|
||||
field.validate({ allowEmpty: false });
|
||||
// We must wait for these validations to finish before queueing
|
||||
// up the setState below so our setState goes in the queue after
|
||||
// all the setStates from these validate calls (that's how we
|
||||
// know they've finished).
|
||||
await field.validate({ allowEmpty: false });
|
||||
}
|
||||
|
||||
// Validation and state updates are async, so we need to wait for them to complete
|
||||
|
@ -270,12 +290,23 @@ module.exports = React.createClass({
|
|||
}
|
||||
const { scorePassword } = await import('../../../utils/PasswordScorer');
|
||||
const complexity = scorePassword(value);
|
||||
const safe = complexity.score >= PASSWORD_MIN_SCORE;
|
||||
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
|
||||
this.setState({
|
||||
passwordComplexity: complexity,
|
||||
passwordSafe: safe,
|
||||
});
|
||||
return complexity.score >= PASSWORD_MIN_SCORE;
|
||||
return allowUnsafe || safe;
|
||||
},
|
||||
valid: function() {
|
||||
// Unsafe passwords that are valid are only possible through a
|
||||
// configuration flag. We'll print some helper text to signal
|
||||
// to the user that their password is allowed, but unsafe.
|
||||
if (!this.state.passwordSafe) {
|
||||
return _t("Password is allowed, but unsafe");
|
||||
}
|
||||
return _t("Nice, strong password!");
|
||||
},
|
||||
valid: () => _t("Nice, strong password!"),
|
||||
invalid: function() {
|
||||
const complexity = this.state.passwordComplexity;
|
||||
if (!complexity) {
|
||||
|
@ -367,7 +398,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
validateUsernameRules: withValidation({
|
||||
description: () => _t("Use letters, numbers, dashes and underscores only"),
|
||||
description: () => _t("Use lowercase letters, numbers, dashes and underscores only"),
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
|
@ -406,8 +437,32 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_showEmail() {
|
||||
const haveIs = Boolean(this.props.serverConfig.isUrl);
|
||||
if (
|
||||
(this.props.serverRequiresIdServer && !haveIs) ||
|
||||
!this._authStepIsUsed('m.login.email.identity')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
_showPhoneNumber() {
|
||||
const threePidLogin = !SdkConfig.get().disable_3pid_login;
|
||||
const haveIs = Boolean(this.props.serverConfig.isUrl);
|
||||
if (
|
||||
!threePidLogin ||
|
||||
(this.props.serverRequiresIdServer && !haveIs) ||
|
||||
!this._authStepIsUsed('m.login.msisdn')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
renderEmail() {
|
||||
if (!this._authStepIsUsed('m.login.email.identity')) {
|
||||
if (!this._showEmail()) {
|
||||
return null;
|
||||
}
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
@ -419,7 +474,6 @@ module.exports = React.createClass({
|
|||
ref={field => this[FIELD_EMAIL] = field}
|
||||
type="text"
|
||||
label={emailPlaceholder}
|
||||
defaultValue={this.props.defaultEmail}
|
||||
value={this.state.email}
|
||||
onChange={this.onEmailChange}
|
||||
onValidate={this.onEmailValidate}
|
||||
|
@ -433,7 +487,6 @@ module.exports = React.createClass({
|
|||
ref={field => this[FIELD_PASSWORD] = field}
|
||||
type="password"
|
||||
label={_t("Password")}
|
||||
defaultValue={this.props.defaultPassword}
|
||||
value={this.state.password}
|
||||
onChange={this.onPasswordChange}
|
||||
onValidate={this.onPasswordValidate}
|
||||
|
@ -447,7 +500,6 @@ module.exports = React.createClass({
|
|||
ref={field => this[FIELD_PASSWORD_CONFIRM] = field}
|
||||
type="password"
|
||||
label={_t("Confirm")}
|
||||
defaultValue={this.props.defaultPassword}
|
||||
value={this.state.passwordConfirm}
|
||||
onChange={this.onPasswordConfirmChange}
|
||||
onValidate={this.onPasswordConfirmValidate}
|
||||
|
@ -455,8 +507,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
renderPhoneNumber() {
|
||||
const threePidLogin = !SdkConfig.get().disable_3pid_login;
|
||||
if (!threePidLogin || !this._authStepIsUsed('m.login.msisdn')) {
|
||||
if (!this._showPhoneNumber()) {
|
||||
return null;
|
||||
}
|
||||
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
|
||||
|
@ -475,7 +526,6 @@ module.exports = React.createClass({
|
|||
ref={field => this[FIELD_PHONE_NUMBER] = field}
|
||||
type="text"
|
||||
label={phoneLabel}
|
||||
defaultValue={this.props.defaultPhoneNumber}
|
||||
value={this.state.phoneNumber}
|
||||
prefix={phoneCountry}
|
||||
onChange={this.onPhoneNumberChange}
|
||||
|
@ -491,7 +541,6 @@ module.exports = React.createClass({
|
|||
type="text"
|
||||
autoFocus={true}
|
||||
label={_t("Username")}
|
||||
defaultValue={this.props.defaultUsername}
|
||||
value={this.state.username}
|
||||
onChange={this.onUsernameChange}
|
||||
onValidate={this.onUsernameValidate}
|
||||
|
@ -499,20 +548,22 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
let yourMatrixAccountText = _t('Create your Matrix account');
|
||||
if (this.props.hsName) {
|
||||
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.hsName,
|
||||
let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.serverConfig.hsName,
|
||||
});
|
||||
if (this.props.serverConfig.hsNameIsDifferent) {
|
||||
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
|
||||
|
||||
yourMatrixAccountText = _t('Create your Matrix account on <underlinedServerName />', {}, {
|
||||
'underlinedServerName': () => {
|
||||
return <TextWithTooltip
|
||||
class="mx_Login_underlinedServerName"
|
||||
tooltip={this.props.serverConfig.hsUrl}
|
||||
>
|
||||
{this.props.serverConfig.hsName}
|
||||
</TextWithTooltip>;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.props.hsUrl);
|
||||
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
|
||||
serverName: parsedHsUrl.hostname,
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
let editLink = null;
|
||||
|
@ -525,9 +576,38 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
const registerButton = (
|
||||
<input className="mx_Login_submit" type="submit" value={_t("Register")} />
|
||||
<input className="mx_Login_submit" type="submit" value={_t("Register")} disabled={!this.props.canSubmit} />
|
||||
);
|
||||
|
||||
let emailHelperText = null;
|
||||
if (this._showEmail()) {
|
||||
if (this._showPhoneNumber()) {
|
||||
emailHelperText = <div>
|
||||
{_t(
|
||||
"Set an email for account recovery. " +
|
||||
"Use email or phone to optionally be discoverable by existing contacts.",
|
||||
)}
|
||||
</div>;
|
||||
} else {
|
||||
emailHelperText = <div>
|
||||
{_t(
|
||||
"Set an email for account recovery. " +
|
||||
"Use email to optionally be discoverable by existing contacts.",
|
||||
)}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
const haveIs = Boolean(this.props.serverConfig.isUrl);
|
||||
let noIsText = null;
|
||||
if (this.props.serverRequiresIdServer && !haveIs) {
|
||||
noIsText = <div>
|
||||
{_t(
|
||||
"No identity server is configured so you cannot add an email address in order to " +
|
||||
"reset your password in the future.",
|
||||
)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>
|
||||
|
@ -546,8 +626,8 @@ module.exports = React.createClass({
|
|||
{this.renderEmail()}
|
||||
{this.renderPhoneNumber()}
|
||||
</div>
|
||||
{_t("Use an email address to recover your account.") + " "}
|
||||
{_t("Other users can invite you to rooms using your contact details.")}
|
||||
{ emailHelperText }
|
||||
{ noIsText }
|
||||
{ registerButton }
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 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.
|
||||
|
@ -20,6 +21,11 @@ import PropTypes from 'prop-types';
|
|||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { createClient } from 'matrix-js-sdk/lib/matrix';
|
||||
import classNames from 'classnames';
|
||||
|
||||
/*
|
||||
* A pure UI component which displays the HS and IS to use.
|
||||
|
@ -27,82 +33,175 @@ import { _t } from '../../../languageHandler';
|
|||
|
||||
export default class ServerConfig extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onServerConfigChange: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
|
||||
// default URLs are defined in config.json (or the hardcoded defaults)
|
||||
// they are used if the user has not overridden them with a custom URL.
|
||||
// In other words, if the custom URL is blank, the default is used.
|
||||
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
|
||||
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
|
||||
|
||||
// custom URLs are explicitly provided by the user and override the
|
||||
// default URLs. The user enters them via the component's input fields,
|
||||
// which is reflected on these properties whenever on..UrlChanged fires.
|
||||
// They are persisted in localStorage by MatrixClientPeg, and so can
|
||||
// override the default URLs when the component initially loads.
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
// The current configuration that the user is expecting to change.
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
|
||||
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
|
||||
}
|
||||
|
||||
// Called after the component calls onServerConfigChange
|
||||
onAfterSubmit: PropTypes.func,
|
||||
|
||||
// Optional text for the submit button. If falsey, no button will be shown.
|
||||
submitText: PropTypes.string,
|
||||
|
||||
// Optional class for the submit button. Only applies if the submit button
|
||||
// is to be rendered.
|
||||
submitClass: PropTypes.string,
|
||||
|
||||
// Whether the flow this component is embedded in requires an identity
|
||||
// server when the homeserver says it will need one. Default false.
|
||||
showIdentityServerIfRequiredByHomeserver: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onServerConfigChange: function() {},
|
||||
customHsUrl: "",
|
||||
customIsUrl: "",
|
||||
delayTimeMs: 0,
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hsUrl: props.customHsUrl,
|
||||
isUrl: props.customIsUrl,
|
||||
busy: false,
|
||||
errorText: "",
|
||||
hsUrl: props.serverConfig.hsUrl,
|
||||
isUrl: props.serverConfig.isUrl,
|
||||
showIdentityServer: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.customHsUrl === this.state.hsUrl &&
|
||||
newProps.customIsUrl === this.state.isUrl) return;
|
||||
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
|
||||
newProps.serverConfig.isUrl === this.state.isUrl) return;
|
||||
|
||||
this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
|
||||
}
|
||||
|
||||
async validateServer() {
|
||||
// TODO: Do we want to support .well-known lookups here?
|
||||
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
|
||||
// find their homeserver without demanding they use "https://matrix.org"
|
||||
const result = this.validateAndApplyServer(this.state.hsUrl, this.state.isUrl);
|
||||
if (!result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// If the UI flow this component is embedded in requires an identity
|
||||
// server when the homeserver says it will need one, check first and
|
||||
// reveal this field if not already shown.
|
||||
// XXX: This a backward compatibility path for homeservers that require
|
||||
// an identity server to be passed during certain flows.
|
||||
// See also https://github.com/matrix-org/synapse/pull/5868.
|
||||
if (
|
||||
this.props.showIdentityServerIfRequiredByHomeserver &&
|
||||
!this.state.showIdentityServer &&
|
||||
await this.isIdentityServerRequiredByHomeserver()
|
||||
) {
|
||||
this.setState({
|
||||
showIdentityServer: true,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async validateAndApplyServer(hsUrl, isUrl) {
|
||||
// Always try and use the defaults first
|
||||
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
|
||||
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
|
||||
this.setState({
|
||||
hsUrl: defaultConfig.hsUrl,
|
||||
isUrl: defaultConfig.isUrl,
|
||||
busy: false,
|
||||
errorText: "",
|
||||
});
|
||||
this.props.onServerConfigChange(defaultConfig);
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
hsUrl: newProps.customHsUrl,
|
||||
isUrl: newProps.customIsUrl,
|
||||
});
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: newProps.customHsUrl,
|
||||
isUrl: newProps.customIsUrl,
|
||||
hsUrl,
|
||||
isUrl,
|
||||
busy: true,
|
||||
errorText: "",
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
|
||||
this.setState({busy: false, errorText: ""});
|
||||
this.props.onServerConfigChange(result);
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
|
||||
if (!stateForError.isFatalError) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
// carry on anyway
|
||||
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl, true);
|
||||
this.props.onServerConfigChange(result);
|
||||
return result;
|
||||
} else {
|
||||
let message = _t("Unable to validate homeserver/identity server");
|
||||
if (e.translatedMessage) {
|
||||
message = e.translatedMessage;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: message,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async isIdentityServerRequiredByHomeserver() {
|
||||
// XXX: We shouldn't have to create a whole new MatrixClient just to
|
||||
// check if the homeserver requires an identity server... Should it be
|
||||
// extracted to a static utils function...?
|
||||
return createClient({
|
||||
baseUrl: this.state.hsUrl,
|
||||
}).doesServerRequireIdServerParam();
|
||||
}
|
||||
|
||||
onHomeserverBlur = (ev) => {
|
||||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: this.state.hsUrl,
|
||||
isUrl: this.state.isUrl,
|
||||
});
|
||||
this.validateServer();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onHomeserverChange = (ev) => {
|
||||
const hsUrl = ev.target.value;
|
||||
this.setState({ hsUrl });
|
||||
}
|
||||
};
|
||||
|
||||
onIdentityServerBlur = (ev) => {
|
||||
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => {
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: this.state.hsUrl,
|
||||
isUrl: this.state.isUrl,
|
||||
});
|
||||
this.validateServer();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onIdentityServerChange = (ev) => {
|
||||
const isUrl = ev.target.value;
|
||||
this.setState({ isUrl });
|
||||
}
|
||||
};
|
||||
|
||||
onSubmit = async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const result = await this.validateServer();
|
||||
if (!result) return; // Do not continue.
|
||||
|
||||
if (this.props.onAfterSubmit) {
|
||||
this.props.onAfterSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
_waitThenInvoke(existingTimeoutId, fn) {
|
||||
if (existingTimeoutId) {
|
||||
|
@ -114,35 +213,75 @@ export default class ServerConfig extends React.PureComponent {
|
|||
showHelpPopup = () => {
|
||||
const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog');
|
||||
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
|
||||
};
|
||||
|
||||
_renderHomeserverSection() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
return <div>
|
||||
{_t("Enter your custom homeserver URL <a>What does this mean?</a>", {}, {
|
||||
a: sub => <a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
|
||||
{sub}
|
||||
</a>,
|
||||
})}
|
||||
<Field id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Homeserver URL")}
|
||||
placeholder={this.props.serverConfig.hsUrl}
|
||||
value={this.state.hsUrl}
|
||||
onBlur={this.onHomeserverBlur}
|
||||
onChange={this.onHomeserverChange}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderIdentityServerSection() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const classes = classNames({
|
||||
"mx_ServerConfig_identityServer": true,
|
||||
"mx_ServerConfig_identityServer_shown": this.state.showIdentityServer,
|
||||
});
|
||||
return <div className={classes}>
|
||||
{_t("Enter your custom identity server URL <a>What does this mean?</a>", {}, {
|
||||
a: sub => <a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
|
||||
{sub}
|
||||
</a>,
|
||||
})}
|
||||
<Field id="mx_ServerConfig_isUrl"
|
||||
label={_t("Identity Server URL")}
|
||||
placeholder={this.props.serverConfig.isUrl}
|
||||
value={this.state.isUrl || ''}
|
||||
onBlur={this.onIdentityServerBlur}
|
||||
onChange={this.onIdentityServerChange}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const errorText = this.state.errorText
|
||||
? <span className='mx_ServerConfig_error'>{this.state.errorText}</span>
|
||||
: null;
|
||||
|
||||
const submitButton = this.props.submitText
|
||||
? <AccessibleButton
|
||||
element="button"
|
||||
type="submit"
|
||||
className={this.props.submitClass}
|
||||
onClick={this.onSubmit}
|
||||
disabled={this.state.busy}>{this.props.submitText}</AccessibleButton>
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="mx_ServerConfig">
|
||||
<h3>{_t("Other servers")}</h3>
|
||||
{_t("Enter custom server URLs <a>What does this mean?</a>", {}, {
|
||||
a: sub => <a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
|
||||
{ sub }
|
||||
</a>,
|
||||
})}
|
||||
<div className="mx_ServerConfig_fields">
|
||||
<Field id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Homeserver URL")}
|
||||
placeholder={this.props.defaultHsUrl}
|
||||
value={this.state.hsUrl}
|
||||
onBlur={this.onHomeserverBlur}
|
||||
onChange={this.onHomeserverChange}
|
||||
/>
|
||||
<Field id="mx_ServerConfig_isUrl"
|
||||
label={_t("Identity Server URL")}
|
||||
placeholder={this.props.defaultIsUrl}
|
||||
value={this.state.isUrl}
|
||||
onBlur={this.onIdentityServerBlur}
|
||||
onChange={this.onIdentityServerChange}
|
||||
/>
|
||||
</div>
|
||||
{errorText}
|
||||
{this._renderHomeserverSection()}
|
||||
{this._renderIdentityServerSection()}
|
||||
<form onSubmit={this.onSubmit} autoComplete="off" action={null}>
|
||||
{submitButton}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ import PropTypes from 'prop-types';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import classnames from 'classnames';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import {makeType} from "../../../utils/TypeUtils";
|
||||
|
||||
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
|
||||
|
||||
|
@ -32,8 +34,12 @@ export const TYPES = {
|
|||
label: () => _t('Free'),
|
||||
logo: () => <img src={require('../../../../res/img/matrix-org-bw-logo.svg')} />,
|
||||
description: () => _t('Join millions for free on the largest public server'),
|
||||
hsUrl: 'https://matrix.org',
|
||||
isUrl: 'https://vector.im',
|
||||
serverConfig: makeType(ValidatedServerConfig, {
|
||||
hsUrl: "https://matrix-client.matrix.org",
|
||||
hsName: "matrix.org",
|
||||
hsNameIsDifferent: false,
|
||||
isUrl: "https://vector.im",
|
||||
}),
|
||||
},
|
||||
PREMIUM: {
|
||||
id: PREMIUM,
|
||||
|
@ -44,6 +50,7 @@ export const TYPES = {
|
|||
{sub}
|
||||
</a>,
|
||||
}),
|
||||
identityServerUrl: "https://vector.im",
|
||||
},
|
||||
ADVANCED: {
|
||||
id: ADVANCED,
|
||||
|
@ -56,10 +63,11 @@ export const TYPES = {
|
|||
},
|
||||
};
|
||||
|
||||
export function getTypeFromHsUrl(hsUrl) {
|
||||
export function getTypeFromServerConfig(config) {
|
||||
const {hsUrl} = config;
|
||||
if (!hsUrl) {
|
||||
return null;
|
||||
} else if (hsUrl === TYPES.FREE.hsUrl) {
|
||||
} else if (hsUrl === TYPES.FREE.serverConfig.hsUrl) {
|
||||
return FREE;
|
||||
} else if (new URL(hsUrl).hostname.endsWith('.modular.im')) {
|
||||
// This is an unlikely case to reach, as Modular defaults to hiding the
|
||||
|
@ -76,7 +84,7 @@ export default class ServerTypeSelector extends React.PureComponent {
|
|||
selected: PropTypes.string,
|
||||
// Handler called when the selected type changes.
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -106,7 +114,7 @@ export default class ServerTypeSelector extends React.PureComponent {
|
|||
e.stopPropagation();
|
||||
const type = e.currentTarget.dataset.id;
|
||||
this.updateSelectedType(type);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
|
62
src/components/views/auth/SignInToText.js
Normal file
62
src/components/views/auth/SignInToText.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
Copyright 2019 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 {_t} from "../../../languageHandler";
|
||||
import sdk from "../../../index";
|
||||
import PropTypes from "prop-types";
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
|
||||
export default class SignInToText extends React.PureComponent {
|
||||
static propTypes = {
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
onEditServerDetailsClick: PropTypes.func,
|
||||
};
|
||||
|
||||
render() {
|
||||
let signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
|
||||
serverName: this.props.serverConfig.hsName,
|
||||
});
|
||||
if (this.props.serverConfig.hsNameIsDifferent) {
|
||||
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
|
||||
|
||||
signInToText = _t('Sign in to your Matrix account on <underlinedServerName />', {}, {
|
||||
'underlinedServerName': () => {
|
||||
return <TextWithTooltip
|
||||
class="mx_Login_underlinedServerName"
|
||||
tooltip={this.props.serverConfig.hsUrl}
|
||||
>
|
||||
{this.props.serverConfig.hsName}
|
||||
</TextWithTooltip>;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let editLink = null;
|
||||
if (this.props.onEditServerDetailsClick) {
|
||||
editLink = <a className="mx_AuthBody_editServerDetails"
|
||||
href="#" onClick={this.props.onEditServerDetailsClick}
|
||||
>
|
||||
{_t('Change')}
|
||||
</a>;
|
||||
}
|
||||
|
||||
return <h3>
|
||||
{signInToText}
|
||||
{editLink}
|
||||
</h3>;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,12 +18,13 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import AvatarLogic from '../../../Avatar';
|
||||
import sdk from '../../../index';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'BaseAvatar',
|
||||
|
||||
propTypes: {
|
||||
|
@ -55,7 +57,7 @@ module.exports = React.createClass({
|
|||
return this._getState(this.props);
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
this.context.matrixClient.on('sync', this.onClientSync);
|
||||
},
|
||||
|
@ -104,9 +106,13 @@ module.exports = React.createClass({
|
|||
// work out the full set of urls to try to load. This is formed like so:
|
||||
// imageUrls: [ props.url, props.urls, default image ]
|
||||
|
||||
const urls = props.urls || [];
|
||||
if (props.url) {
|
||||
urls.unshift(props.url); // put in urls[0]
|
||||
let urls = [];
|
||||
if (!SettingsStore.getValue("lowBandwidth")) {
|
||||
urls = props.urls || [];
|
||||
|
||||
if (props.url) {
|
||||
urls.unshift(props.url); // put in urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
let defaultImageUrl = null;
|
||||
|
@ -116,6 +122,10 @@ module.exports = React.createClass({
|
|||
);
|
||||
urls.push(defaultImageUrl); // lowest priority
|
||||
}
|
||||
|
||||
// deduplicate URLs
|
||||
urls = Array.from(new Set(urls));
|
||||
|
||||
return {
|
||||
imageUrls: urls,
|
||||
defaultImageUrl: defaultImageUrl,
|
||||
|
@ -133,40 +143,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* returns the first (non-sigil) character of 'name',
|
||||
* converted to uppercase
|
||||
*/
|
||||
_getInitialLetter: function(name) {
|
||||
if (name.length < 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let idx = 0;
|
||||
const initial = name[0];
|
||||
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
// string.codePointAt(0) would do this, but that isn't supported by
|
||||
// some browsers (notably PhantomJS).
|
||||
let chars = 1;
|
||||
const first = name.charCodeAt(idx);
|
||||
|
||||
// check if it’s the start of a surrogate pair
|
||||
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
|
||||
const second = name.charCodeAt(idx+1);
|
||||
if (second >= 0xDC00 && second <= 0xDFFF) {
|
||||
chars++;
|
||||
}
|
||||
}
|
||||
|
||||
const firstChar = name.substring(idx, idx+chars);
|
||||
return firstChar.toUpperCase();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||
|
||||
const {
|
||||
|
@ -176,20 +153,20 @@ module.exports = React.createClass({
|
|||
} = this.props;
|
||||
|
||||
if (imageUrl === this.state.defaultImageUrl) {
|
||||
const initialLetter = this._getInitialLetter(name);
|
||||
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||
const textNode = (
|
||||
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||
<span className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||
style={{ fontSize: (width * 0.65) + "px",
|
||||
width: width + "px",
|
||||
lineHeight: height + "px" }}
|
||||
>
|
||||
{ initialLetter }
|
||||
</EmojiText>
|
||||
</span>
|
||||
);
|
||||
const imgNode = (
|
||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||
alt="" title={title} onError={this.onError}
|
||||
width={width} height={height} />
|
||||
width={width} height={height} aria-hidden="true" />
|
||||
);
|
||||
if (onClick != null) {
|
||||
return (
|
||||
|
|
|
@ -16,10 +16,11 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'GroupAvatar',
|
||||
|
||||
propTypes: {
|
||||
|
|
|
@ -14,15 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
const Avatar = require('../../../Avatar');
|
||||
const sdk = require("../../../index");
|
||||
const dispatcher = require("../../../dispatcher");
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'MemberAvatar',
|
||||
|
||||
propTypes: {
|
||||
|
|
|
@ -15,13 +15,14 @@ limitations under the License.
|
|||
*/
|
||||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import {ContentRepo} from "matrix-js-sdk";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from "../../../index";
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import Avatar from '../../../Avatar';
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'RoomAvatar',
|
||||
|
||||
// Room may be left unset here, but if it is,
|
||||
|
@ -51,7 +52,7 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
|
@ -89,7 +90,6 @@ module.exports = React.createClass({
|
|||
props.resizeMethod,
|
||||
), // highest priority
|
||||
this.getRoomAvatarUrl(props),
|
||||
this.getOneToOneAvatar(props), // lowest priority
|
||||
].filter(function(url) {
|
||||
return (url != null && url != "");
|
||||
});
|
||||
|
@ -98,41 +98,14 @@ module.exports = React.createClass({
|
|||
getRoomAvatarUrl: function(props) {
|
||||
if (!props.room) return null;
|
||||
|
||||
return props.room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
return Avatar.avatarUrlForRoom(
|
||||
props.room,
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false,
|
||||
);
|
||||
},
|
||||
|
||||
getOneToOneAvatar: function(props) {
|
||||
const room = props.room;
|
||||
if (!room) {
|
||||
return null;
|
||||
}
|
||||
let otherMember = null;
|
||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
if (otherUserId) {
|
||||
otherMember = room.getMember(otherUserId);
|
||||
} else {
|
||||
// if the room is not marked as a 1:1, but only has max 2 members
|
||||
// then still try to show any avatar (pref. other member)
|
||||
otherMember = room.getAvatarFallbackMember();
|
||||
}
|
||||
if (otherMember) {
|
||||
return otherMember.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
onRoomAvatarClick: function() {
|
||||
const avatarUrl = this.props.room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
|
|
|
@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
@ -26,7 +24,7 @@ import PropTypes from 'prop-types';
|
|||
|
||||
|
||||
export default class GenericElementContextMenu extends React.Component {
|
||||
static PropTypes = {
|
||||
static propTypes = {
|
||||
element: PropTypes.element.isRequired,
|
||||
// Function to be called when the parent window is resized
|
||||
// This can be used to reposition or close the menu on resize and
|
||||
|
|
|
@ -14,13 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class GenericTextContextMenu extends React.Component {
|
||||
static PropTypes = {
|
||||
static propTypes = {
|
||||
message: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 Vector Creations Ltd
|
||||
Copyright 2019 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.
|
||||
|
@ -77,11 +78,12 @@ export default class GroupInviteTileContextMenu extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return <div>
|
||||
<div className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject} >
|
||||
<AccessibleButton className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" />
|
||||
{ _t('Reject') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 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.
|
||||
|
@ -17,6 +19,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
@ -29,7 +32,11 @@ import SettingsStore from '../../../settings/SettingsStore';
|
|||
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
|
||||
module.exports = React.createClass({
|
||||
function canCancel(eventStatus) {
|
||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||
}
|
||||
|
||||
module.exports = createReactClass({
|
||||
displayName: 'MessageContextMenu',
|
||||
|
||||
propTypes: {
|
||||
|
@ -90,11 +97,36 @@ module.exports = React.createClass({
|
|||
this.closeMenu();
|
||||
},
|
||||
|
||||
onResendEditClick: function() {
|
||||
Resend.resend(this.props.mxEvent.replacingEvent());
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onResendRedactionClick: function() {
|
||||
Resend.resend(this.props.mxEvent.localRedactionEvent());
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onResendReactionsClick: function() {
|
||||
for (const reaction of this._getUnsentReactions()) {
|
||||
Resend.resend(reaction);
|
||||
}
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
e2eInfoClicked: function() {
|
||||
this.props.e2eInfoCallback();
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onReportEventClick: function() {
|
||||
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
|
||||
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
|
||||
mxEvent: this.props.mxEvent,
|
||||
}, 'mx_Dialog_reportEvent');
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onViewSourceClick: function() {
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
|
||||
|
@ -119,26 +151,54 @@ module.exports = React.createClass({
|
|||
onRedactClick: function() {
|
||||
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
|
||||
Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
|
||||
onFinished: (proceed) => {
|
||||
onFinished: async (proceed) => {
|
||||
if (!proceed) return;
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.redactEvent(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId()).catch(function(e) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// display error message stating you couldn't delete this.
|
||||
try {
|
||||
await cli.redactEvent(
|
||||
this.props.mxEvent.getRoomId(),
|
||||
this.props.mxEvent.getId(),
|
||||
);
|
||||
} catch (e) {
|
||||
const code = e.errcode || e.statusCode;
|
||||
Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: _t('You cannot delete this message. (%(code)s)', {code}),
|
||||
});
|
||||
}).done();
|
||||
// only show the dialog if failing for something other than a network error
|
||||
// (e.g. no errcode or statusCode) as in that case the redactions end up in the
|
||||
// detached queue and we show the room status bar to allow retry
|
||||
if (typeof code !== "undefined") {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// display error message stating you couldn't delete this.
|
||||
Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: _t('You cannot delete this message. (%(code)s)', {code}),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
}, 'mx_Dialog_confirmredact');
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onCancelSendClick: function() {
|
||||
Resend.removeFromQueue(this.props.mxEvent);
|
||||
const mxEvent = this.props.mxEvent;
|
||||
const editEvent = mxEvent.replacingEvent();
|
||||
const redactEvent = mxEvent.localRedactionEvent();
|
||||
const pendingReactions = this._getPendingReactions();
|
||||
|
||||
if (editEvent && canCancel(editEvent.status)) {
|
||||
Resend.removeFromQueue(editEvent);
|
||||
}
|
||||
if (redactEvent && canCancel(redactEvent.status)) {
|
||||
Resend.removeFromQueue(redactEvent);
|
||||
}
|
||||
if (pendingReactions.length) {
|
||||
for (const reaction of pendingReactions) {
|
||||
Resend.removeFromQueue(reaction);
|
||||
}
|
||||
}
|
||||
if (canCancel(mxEvent.status)) {
|
||||
Resend.removeFromQueue(this.props.mxEvent);
|
||||
}
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
|
@ -207,10 +267,46 @@ module.exports = React.createClass({
|
|||
this.closeMenu();
|
||||
},
|
||||
|
||||
_getReactions(filter) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.mxEvent.getRoomId());
|
||||
const eventId = this.props.mxEvent.getId();
|
||||
return room.getPendingEvents().filter(e => {
|
||||
const relation = e.getRelation();
|
||||
return relation &&
|
||||
relation.rel_type === "m.annotation" &&
|
||||
relation.event_id === eventId &&
|
||||
filter(e);
|
||||
});
|
||||
},
|
||||
|
||||
_getPendingReactions() {
|
||||
return this._getReactions(e => canCancel(e.status));
|
||||
},
|
||||
|
||||
_getUnsentReactions() {
|
||||
return this._getReactions(e => e.status === EventStatus.NOT_SENT);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const me = cli.getUserId();
|
||||
const mxEvent = this.props.mxEvent;
|
||||
const eventStatus = mxEvent.status;
|
||||
const editStatus = mxEvent.replacingEvent() && mxEvent.replacingEvent().status;
|
||||
const redactStatus = mxEvent.localRedactionEvent() && mxEvent.localRedactionEvent().status;
|
||||
const unsentReactionsCount = this._getUnsentReactions().length;
|
||||
const pendingReactionsCount = this._getPendingReactions().length;
|
||||
const allowCancel = canCancel(mxEvent.status) ||
|
||||
canCancel(editStatus) ||
|
||||
canCancel(redactStatus) ||
|
||||
pendingReactionsCount !== 0;
|
||||
let resendButton;
|
||||
let resendEditButton;
|
||||
let resendReactionsButton;
|
||||
let resendRedactionButton;
|
||||
let redactButton;
|
||||
let cancelButton;
|
||||
let forwardButton;
|
||||
|
@ -223,67 +319,92 @@ module.exports = React.createClass({
|
|||
|
||||
// status is SENT before remote-echo, null after
|
||||
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
||||
if (!mxEvent.isRedacted()) {
|
||||
if (eventStatus === EventStatus.NOT_SENT) {
|
||||
resendButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
|
||||
{ _t('Resend') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventStatus === EventStatus.NOT_SENT) {
|
||||
resendButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
|
||||
{ _t('Resend') }
|
||||
</div>
|
||||
if (editStatus === EventStatus.NOT_SENT) {
|
||||
resendEditButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendEditClick}>
|
||||
{ _t('Resend edit') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (unsentReactionsCount !== 0) {
|
||||
resendReactionsButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
|
||||
{ _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (redactStatus === EventStatus.NOT_SENT) {
|
||||
resendRedactionButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onResendRedactionClick}>
|
||||
{ _t('Resend removal') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSent && this.state.canRedact) {
|
||||
redactButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
||||
{ _t('Remove') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT) {
|
||||
if (allowCancel) {
|
||||
cancelButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
|
||||
{ _t('Cancel Sending') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (isContentActionable(mxEvent)) {
|
||||
forwardButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
|
||||
{ _t('Forward Message') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
if (this.state.canPin) {
|
||||
pinButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
|
||||
{ this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const viewSourceButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
|
||||
{ _t('View Source') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
if (mxEvent.getType() !== mxEvent.getWireType()) {
|
||||
viewClearSourceButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
|
||||
{ _t('View Decrypted Source') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.eventTileOps) {
|
||||
if (this.props.eventTileOps.isWidgetHidden()) {
|
||||
unhidePreviewButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
|
||||
{ _t('Unhide Preview') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -294,20 +415,19 @@ module.exports = React.createClass({
|
|||
}
|
||||
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
|
||||
const permalinkButton = (
|
||||
<div className="mx_MessageContextMenu_field">
|
||||
<a href={permalink}
|
||||
target="_blank" rel="noopener" onClick={this.onPermalinkClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field">
|
||||
<a href={permalink} target="_blank" rel="noopener" onClick={this.onPermalinkClick} tabIndex={-1}>
|
||||
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
|
||||
? _t('Share Permalink') : _t('Share Message') }
|
||||
</a>
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
if (this.props.eventTileOps && this.props.eventTileOps.getInnerText) {
|
||||
quoteButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
|
||||
{ _t('Quote') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -317,31 +437,52 @@ module.exports = React.createClass({
|
|||
isUrlPermitted(mxEvent.event.content.external_url)
|
||||
) {
|
||||
externalURLButton = (
|
||||
<div className="mx_MessageContextMenu_field">
|
||||
<a href={mxEvent.event.content.external_url}
|
||||
rel="noopener" target="_blank" onClick={this.closeMenu}>{ _t('Source URL') }</a>
|
||||
</div>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field">
|
||||
<a
|
||||
href={mxEvent.event.content.external_url}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
onClick={this.closeMenu}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{ _t('Source URL') }
|
||||
</a>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.collapseReplyThread) {
|
||||
collapseReplyThread = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
|
||||
{ _t('Collapse Reply Thread') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let e2eInfo;
|
||||
if (this.props.e2eInfoCallback) {
|
||||
e2eInfo = <div className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
|
||||
e2eInfo = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
|
||||
{ _t('End-to-end encryption information') }
|
||||
</div>;
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let reportEventButton;
|
||||
if (mxEvent.getSender() !== me) {
|
||||
reportEventButton = (
|
||||
<AccessibleButton className="mx_MessageContextMenu_field" onClick={this.onReportEventClick}>
|
||||
{ _t('Report Content') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_MessageContextMenu">
|
||||
{ resendButton }
|
||||
{ resendEditButton }
|
||||
{ resendReactionsButton }
|
||||
{ resendRedactionButton }
|
||||
{ redactButton }
|
||||
{ cancelButton }
|
||||
{ forwardButton }
|
||||
|
@ -354,6 +495,7 @@ module.exports = React.createClass({
|
|||
{ externalURLButton }
|
||||
{ collapseReplyThread }
|
||||
{ e2eInfo }
|
||||
{ reportEventButton }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 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,12 +17,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../../index';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
@ -30,8 +30,10 @@ import * as Rooms from '../../../Rooms';
|
|||
import * as RoomNotifs from '../../../RoomNotifs';
|
||||
import Modal from '../../../Modal';
|
||||
import RoomListActions from '../../../actions/RoomListActions';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import {sleep} from "../../../utils/promise";
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'RoomTileContextMenu',
|
||||
|
||||
propTypes: {
|
||||
|
@ -60,7 +62,7 @@ module.exports = React.createClass({
|
|||
|
||||
_toggleTag: function(tagNameOn, tagNameOff) {
|
||||
if (!MatrixClientPeg.get().isGuest()) {
|
||||
Promise.delay(500).then(() => {
|
||||
sleep(500).then(() => {
|
||||
dis.dispatch(RoomListActions.tagRoom(
|
||||
MatrixClientPeg.get(),
|
||||
this.props.room,
|
||||
|
@ -117,7 +119,7 @@ module.exports = React.createClass({
|
|||
|
||||
Rooms.guessAndSetDMRoom(
|
||||
this.props.room, newIsDirectMessage,
|
||||
).delay(500).finally(() => {
|
||||
).then(sleep(500)).finally(() => {
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
|
@ -158,8 +160,12 @@ module.exports = React.createClass({
|
|||
|
||||
_onClickForget: function() {
|
||||
// FIXME: duplicated with RoomSettings (and dead code in RoomView)
|
||||
MatrixClientPeg.get().forget(this.props.room.roomId).done(function() {
|
||||
dis.dispatch({ action: 'view_next_room' });
|
||||
MatrixClientPeg.get().forget(this.props.room.roomId).then(() => {
|
||||
// Switch to another room view if we're currently viewing the
|
||||
// historical room
|
||||
if (RoomViewStore.getRoomId() === this.props.room.roomId) {
|
||||
dis.dispatch({ action: 'view_next_room' });
|
||||
}
|
||||
}, function(err) {
|
||||
const errCode = err.errcode || _td("unknown error code");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
@ -184,10 +190,10 @@ module.exports = React.createClass({
|
|||
this.setState({
|
||||
roomNotifState: newState,
|
||||
});
|
||||
RoomNotifs.setRoomNotifsState(roomId, newState).done(() => {
|
||||
RoomNotifs.setRoomNotifsState(roomId, newState).then(() => {
|
||||
// delay slightly so that the user can see their state change
|
||||
// before closing the menu
|
||||
return Promise.delay(500).then(() => {
|
||||
return sleep(500).then(() => {
|
||||
if (this._unmounted) return;
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
|
@ -222,6 +228,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_renderNotifMenu: function() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const alertMeClasses = classNames({
|
||||
'mx_RoomTileContextMenu_notif_field': true,
|
||||
'mx_RoomTileContextMenu_notif_fieldSet': this.state.roomNotifState == RoomNotifs.ALL_MESSAGES_LOUD,
|
||||
|
@ -244,29 +252,29 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div className="mx_RoomTileContextMenu">
|
||||
<div className="mx_RoomTileContextMenu_notif_picker" >
|
||||
<div className="mx_RoomTileContextMenu_notif_picker">
|
||||
<img src={require("../../../../res/img/notif-slider.svg")} width="20" height="107" />
|
||||
</div>
|
||||
<div className={alertMeClasses} onClick={this._onClickAlertMe} >
|
||||
<AccessibleButton className={alertMeClasses} onClick={this._onClickAlertMe}>
|
||||
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
|
||||
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-off-copy.svg")} width="16" height="12" />
|
||||
{ _t('All messages (noisy)') }
|
||||
</div>
|
||||
<div className={allNotifsClasses} onClick={this._onClickAllNotifs} >
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className={allNotifsClasses} onClick={this._onClickAllNotifs}>
|
||||
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
|
||||
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-off.svg")} width="16" height="12" />
|
||||
{ _t('All messages') }
|
||||
</div>
|
||||
<div className={mentionsClasses} onClick={this._onClickMentions} >
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className={mentionsClasses} onClick={this._onClickMentions}>
|
||||
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
|
||||
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute-mentions.svg")} width="16" height="12" />
|
||||
{ _t('Mentions only') }
|
||||
</div>
|
||||
<div className={muteNotifsClasses} onClick={this._onClickMute} >
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className={muteNotifsClasses} onClick={this._onClickMute}>
|
||||
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" />
|
||||
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={require("../../../../res/img/icon-context-mute.svg")} width="16" height="12" />
|
||||
{ _t('Mute') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -282,12 +290,13 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_renderSettingsMenu: function() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div>
|
||||
<div className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings} >
|
||||
<AccessibleButton className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icons-settings-room.svg")} width="15" height="15" />
|
||||
{ _t('Settings') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -297,6 +306,8 @@ module.exports = React.createClass({
|
|||
return null;
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let leaveClickHandler = null;
|
||||
let leaveText = null;
|
||||
|
||||
|
@ -318,15 +329,17 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className="mx_RoomTileContextMenu_leave" onClick={leaveClickHandler} >
|
||||
<AccessibleButton className="mx_RoomTileContextMenu_leave" onClick={leaveClickHandler} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" />
|
||||
{ leaveText }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_renderRoomTagMenu: function() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const favouriteClasses = classNames({
|
||||
'mx_RoomTileContextMenu_tag_field': true,
|
||||
'mx_RoomTileContextMenu_tag_fieldSet': this.state.isFavourite,
|
||||
|
@ -347,21 +360,21 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className={favouriteClasses} onClick={this._onClickFavourite} >
|
||||
<AccessibleButton className={favouriteClasses} onClick={this._onClickFavourite} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_fave.svg")} width="15" height="15" />
|
||||
<img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_fave_on.svg")} width="15" height="15" />
|
||||
{ _t('Favourite') }
|
||||
</div>
|
||||
<div className={lowPriorityClasses} onClick={this._onClickLowPriority} >
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className={lowPriorityClasses} onClick={this._onClickLowPriority} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_low.svg")} width="15" height="15" />
|
||||
<img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_low_on.svg")} width="15" height="15" />
|
||||
{ _t('Low Priority') }
|
||||
</div>
|
||||
<div className={dmClasses} onClick={this._onClickDM} >
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className={dmClasses} onClick={this._onClickDM} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_person.svg")} width="15" height="15" />
|
||||
<img className="mx_RoomTileContextMenu_tag_icon_set" src={require("../../../../res/img/icon_context_person_on.svg")} width="15" height="15" />
|
||||
{ _t('Direct Chat') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -369,25 +382,27 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const myMembership = this.props.room.getMyMembership();
|
||||
|
||||
// Can't set notif level or tags on non-join rooms
|
||||
if (myMembership !== 'join') {
|
||||
return <div>
|
||||
{ this._renderLeaveMenu(myMembership) }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderSettingsMenu() }
|
||||
</div>;
|
||||
switch (myMembership) {
|
||||
case 'join':
|
||||
return <div>
|
||||
{ this._renderNotifMenu() }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderLeaveMenu(myMembership) }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderRoomTagMenu() }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderSettingsMenu() }
|
||||
</div>;
|
||||
case 'invite':
|
||||
return <div>
|
||||
{ this._renderLeaveMenu(myMembership) }
|
||||
</div>;
|
||||
default:
|
||||
return <div>
|
||||
{ this._renderLeaveMenu(myMembership) }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderSettingsMenu() }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ this._renderNotifMenu() }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderLeaveMenu(myMembership) }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderRoomTagMenu() }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderSettingsMenu() }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 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,12 +24,17 @@ import Modal from "../../../Modal";
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import { getHostingLink } from '../../../utils/HostingLink';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from "../../../index";
|
||||
|
||||
export class TopLeftMenu extends React.Component {
|
||||
static propTypes = {
|
||||
displayName: PropTypes.string.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func,
|
||||
|
||||
// Optional function to collect a reference to the container
|
||||
// of this component directly.
|
||||
containerRef: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
@ -52,6 +58,8 @@ export class TopLeftMenu extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const isGuest = MatrixClientPeg.get().isGuest();
|
||||
|
||||
const hostingSignupLink = getHostingLink('user-context-menu');
|
||||
|
@ -61,44 +69,56 @@ export class TopLeftMenu extends React.Component {
|
|||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
{
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener">{sub}</a>,
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener" tabIndex="0">{sub}</a>,
|
||||
},
|
||||
)}
|
||||
<a href={hostingSignupLink} target="_blank" rel="noopener">
|
||||
<a href={hostingSignupLink} target="_blank" rel="noopener" aria-hidden={true}>
|
||||
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||
</a>
|
||||
</div>;
|
||||
}
|
||||
|
||||
let homePageSection = null;
|
||||
let homePageItem = null;
|
||||
if (this.hasHomePage()) {
|
||||
homePageSection = <ul className="mx_TopLeftMenu_section_withIcon">
|
||||
<li className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>{_t("Home")}</li>
|
||||
</ul>;
|
||||
homePageItem = (
|
||||
<AccessibleButton element="li" className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>
|
||||
{_t("Home")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let signInOutSection;
|
||||
let signInOutItem;
|
||||
if (isGuest) {
|
||||
signInOutSection = <ul className="mx_TopLeftMenu_section_withIcon">
|
||||
<li className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>{_t("Sign in")}</li>
|
||||
</ul>;
|
||||
signInOutItem = (
|
||||
<AccessibleButton element="li" className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>
|
||||
{_t("Sign in")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
signInOutSection = <ul className="mx_TopLeftMenu_section_withIcon">
|
||||
<li className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>{_t("Sign out")}</li>
|
||||
</ul>;
|
||||
signInOutItem = (
|
||||
<AccessibleButton element="li" className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>
|
||||
{_t("Sign out")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="mx_TopLeftMenu">
|
||||
<div className="mx_TopLeftMenu_section_noIcon">
|
||||
const settingsItem = (
|
||||
<AccessibleButton element="li" className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>
|
||||
{_t("Settings")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
return <div className="mx_TopLeftMenu" ref={this.props.containerRef}>
|
||||
<div className="mx_TopLeftMenu_section_noIcon" aria-readonly={true}>
|
||||
<div>{this.props.displayName}</div>
|
||||
<div className="mx_TopLeftMenu_greyedText">{this.props.userId}</div>
|
||||
<div className="mx_TopLeftMenu_greyedText" aria-hidden={true}>{this.props.userId}</div>
|
||||
{hostingSignup}
|
||||
</div>
|
||||
{homePageSection}
|
||||
<ul className="mx_TopLeftMenu_section_withIcon">
|
||||
<li className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>{_t("Settings")}</li>
|
||||
{homePageItem}
|
||||
{settingsItem}
|
||||
{signInOutItem}
|
||||
</ul>
|
||||
{signInOutSection}
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
134
src/components/views/context_menus/WidgetContextMenu.js
Normal file
134
src/components/views/context_menus/WidgetContextMenu.js
Normal file
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
Copyright 2019 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 sdk from '../../../index';
|
||||
import {_t} from '../../../languageHandler';
|
||||
|
||||
export default class WidgetContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func,
|
||||
|
||||
// Callback for when the revoke button is clicked. Required.
|
||||
onRevokeClicked: PropTypes.func.isRequired,
|
||||
|
||||
// Callback for when the snapshot button is clicked. Button not shown
|
||||
// without a callback.
|
||||
onSnapshotClicked: PropTypes.func,
|
||||
|
||||
// Callback for when the reload button is clicked. Button not shown
|
||||
// without a callback.
|
||||
onReloadClicked: PropTypes.func,
|
||||
|
||||
// Callback for when the edit button is clicked. Button not shown
|
||||
// without a callback.
|
||||
onEditClicked: PropTypes.func,
|
||||
|
||||
// Callback for when the delete button is clicked. Button not shown
|
||||
// without a callback.
|
||||
onDeleteClicked: PropTypes.func,
|
||||
};
|
||||
|
||||
proxyClick(fn) {
|
||||
fn();
|
||||
if (this.props.onFinished) this.props.onFinished();
|
||||
}
|
||||
|
||||
// XXX: It's annoying that our context menus require us to hit onFinished() to close :(
|
||||
|
||||
onEditClicked = () => {
|
||||
this.proxyClick(this.props.onEditClicked);
|
||||
};
|
||||
|
||||
onReloadClicked = () => {
|
||||
this.proxyClick(this.props.onReloadClicked);
|
||||
};
|
||||
|
||||
onSnapshotClicked = () => {
|
||||
this.proxyClick(this.props.onSnapshotClicked);
|
||||
};
|
||||
|
||||
onDeleteClicked = () => {
|
||||
this.proxyClick(this.props.onDeleteClicked);
|
||||
};
|
||||
|
||||
onRevokeClicked = () => {
|
||||
this.proxyClick(this.props.onRevokeClicked);
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
|
||||
|
||||
const options = [];
|
||||
|
||||
if (this.props.onEditClicked) {
|
||||
options.push(
|
||||
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onEditClicked} key='edit'>
|
||||
{_t("Edit")}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.onReloadClicked) {
|
||||
options.push(
|
||||
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onReloadClicked}
|
||||
key='reload'>
|
||||
{_t("Reload")}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.onSnapshotClicked) {
|
||||
options.push(
|
||||
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onSnapshotClicked}
|
||||
key='snap'>
|
||||
{_t("Take picture")}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.onDeleteClicked) {
|
||||
options.push(
|
||||
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onDeleteClicked}
|
||||
key='delete'>
|
||||
{_t("Remove for everyone")}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
||||
// Push this last so it appears last. It's always present.
|
||||
options.push(
|
||||
<AccessibleButton className='mx_WidgetContextMenu_option' onClick={this.onRevokeClicked} key='revoke'>
|
||||
{_t("Remove for me")}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
|
||||
// Put separators between the options
|
||||
if (options.length > 1) {
|
||||
const length = options.length;
|
||||
for (let i = 0; i < length - 1; i++) {
|
||||
const sep = <hr key={i} className="mx_WidgetContextMenu_separator" />;
|
||||
|
||||
// Insert backwards so the insertions don't affect our math on where to place them.
|
||||
// We also use our cached length to avoid worrying about options.length changing
|
||||
options.splice(length - 1 - i, 0, sep);
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="mx_WidgetContextMenu">{options}</div>;
|
||||
}
|
||||
}
|
|
@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { _t } from '../../../languageHandler';
|
||||
module.exports = React.createClass({
|
||||
|
||||
module.exports = createReactClass({
|
||||
displayName: 'CreateRoomButton',
|
||||
propTypes: {
|
||||
onCreateRoom: PropTypes.func,
|
||||
|
|
|
@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const Presets = {
|
||||
|
@ -26,7 +25,7 @@ const Presets = {
|
|||
Custom: "custom",
|
||||
};
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'CreateRoomPresets',
|
||||
propTypes: {
|
||||
onChange: PropTypes.func,
|
||||
|
|
|
@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
const React = require('react');
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: 'RoomAlias',
|
||||
propTypes: {
|
||||
// Specifying a homeserver will make magical things happen when you,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 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.
|
||||
|
@ -17,13 +19,19 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Promise from 'bluebird';
|
||||
import dis from '../../../dispatcher';
|
||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import * as Email from "../../../email";
|
||||
import * as Email from '../../../email';
|
||||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
|
||||
import { abbreviateUrl } from '../../../utils/UrlUtils';
|
||||
import {sleep} from "../../../utils/promise";
|
||||
|
||||
const TRUNCATE_QUERY_LIST = 40;
|
||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||
|
@ -35,7 +43,7 @@ const addressTypeName = {
|
|||
};
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = createReactClass({
|
||||
displayName: "AddressPickerDialog",
|
||||
|
||||
propTypes: {
|
||||
|
@ -44,7 +52,7 @@ module.exports = React.createClass({
|
|||
// Extra node inserted after picker input, dropdown and errors
|
||||
extraNode: PropTypes.node,
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
roomId: PropTypes.string,
|
||||
button: PropTypes.string,
|
||||
focus: PropTypes.bool,
|
||||
|
@ -69,13 +77,18 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
error: false,
|
||||
let validAddressTypes = this.props.validAddressTypes;
|
||||
// Remove email from validAddressTypes if no IS is configured. It may be added at a later stage by the user
|
||||
if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes("email")) {
|
||||
validAddressTypes = validAddressTypes.filter(type => type !== "email");
|
||||
}
|
||||
|
||||
return {
|
||||
// Whether to show an error message because of an invalid address
|
||||
invalidAddressError: false,
|
||||
// List of UserAddressType objects representing
|
||||
// the list of addresses we're going to invite
|
||||
selectedList: [],
|
||||
|
||||
// Whether a search is ongoing
|
||||
busy: false,
|
||||
// An error message generated during the user directory search
|
||||
|
@ -87,6 +100,9 @@ module.exports = React.createClass({
|
|||
// List of UserAddressType objects representing the set of
|
||||
// auto-completion results for the current search query.
|
||||
suggestedList: [],
|
||||
// List of address types initialised from props, but may change while the
|
||||
// dialog is open and represents the supported list of address types at this time.
|
||||
validAddressTypes,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -97,12 +113,21 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
getPlaceholder() {
|
||||
const { placeholder } = this.props;
|
||||
if (typeof placeholder === "string") {
|
||||
return placeholder;
|
||||
}
|
||||
// Otherwise it's a function, as checked by prop types.
|
||||
return placeholder(this.state.validAddressTypes);
|
||||
},
|
||||
|
||||
onButtonClick: function() {
|
||||
let selectedList = this.state.selectedList.slice();
|
||||
// Check the text input field to see if user has an unconverted address
|
||||
// If there is and it's valid add it to the local selectedList
|
||||
if (this.refs.textinput.value !== '') {
|
||||
selectedList = this._addInputToList();
|
||||
selectedList = this._addAddressesToList([this.refs.textinput.value]);
|
||||
if (selectedList === null) return;
|
||||
}
|
||||
this.props.onFinished(true, selectedList);
|
||||
|
@ -140,12 +165,12 @@ module.exports = React.createClass({
|
|||
// if there's nothing in the input box, submit the form
|
||||
this.onButtonClick();
|
||||
} else {
|
||||
this._addInputToList();
|
||||
this._addAddressesToList([this.refs.textinput.value]);
|
||||
}
|
||||
} else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this._addInputToList();
|
||||
this._addAddressesToList([this.refs.textinput.value]);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -205,7 +230,7 @@ module.exports = React.createClass({
|
|||
|
||||
onSelected: function(index) {
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.push(this.state.suggestedList[index]);
|
||||
selectedList.push(this._getFilteredSuggestions()[index]);
|
||||
this.setState({
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
|
@ -241,7 +266,7 @@ module.exports = React.createClass({
|
|||
this.setState({
|
||||
searchError: err.errcode ? err.message : _t('Something went wrong!'),
|
||||
});
|
||||
}).done(() => {
|
||||
}).then(() => {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
|
@ -354,7 +379,7 @@ module.exports = React.createClass({
|
|||
// Do a local search immediately
|
||||
this._doLocalSearch(query);
|
||||
}
|
||||
}).done(() => {
|
||||
}).then(() => {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
|
@ -430,7 +455,7 @@ module.exports = React.createClass({
|
|||
// This is important, otherwise there's no way to invite
|
||||
// a perfectly valid address if there are close matches.
|
||||
const addrType = getAddressType(query);
|
||||
if (this.props.validAddressTypes.includes(addrType)) {
|
||||
if (this.state.validAddressTypes.includes(addrType)) {
|
||||
if (addrType === 'email' && !Email.looksValid(query)) {
|
||||
this.setState({searchError: _t("That doesn't look like a valid email address")});
|
||||
return;
|
||||
|
@ -442,56 +467,62 @@ module.exports = React.createClass({
|
|||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
if (addrType === 'email') {
|
||||
this._lookupThreepid(addrType, query).done();
|
||||
this._lookupThreepid(addrType, query);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
suggestedList,
|
||||
error: false,
|
||||
invalidAddressError: false,
|
||||
}, () => {
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionTop();
|
||||
});
|
||||
},
|
||||
|
||||
_addInputToList: function() {
|
||||
const addressText = this.refs.textinput.value.trim();
|
||||
const addrType = getAddressType(addressText);
|
||||
const addrObj = {
|
||||
addressType: addrType,
|
||||
address: addressText,
|
||||
isKnown: false,
|
||||
};
|
||||
if (!this.props.validAddressTypes.includes(addrType)) {
|
||||
this.setState({ error: true });
|
||||
return null;
|
||||
} else if (addrType === 'mx-user-id') {
|
||||
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||
if (user) {
|
||||
addrObj.displayName = user.displayName;
|
||||
addrObj.avatarMxc = user.avatarUrl;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
} else if (addrType === 'mx-room-id') {
|
||||
const room = MatrixClientPeg.get().getRoom(addrObj.address);
|
||||
if (room) {
|
||||
addrObj.displayName = room.name;
|
||||
addrObj.avatarMxc = room.avatarUrl;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
}
|
||||
|
||||
_addAddressesToList: function(addressTexts) {
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.push(addrObj);
|
||||
|
||||
let hasError = false;
|
||||
addressTexts.forEach((addressText) => {
|
||||
addressText = addressText.trim();
|
||||
const addrType = getAddressType(addressText);
|
||||
const addrObj = {
|
||||
addressType: addrType,
|
||||
address: addressText,
|
||||
isKnown: false,
|
||||
};
|
||||
|
||||
if (!this.state.validAddressTypes.includes(addrType)) {
|
||||
hasError = true;
|
||||
} else if (addrType === 'mx-user-id') {
|
||||
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||
if (user) {
|
||||
addrObj.displayName = user.displayName;
|
||||
addrObj.avatarMxc = user.avatarUrl;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
} else if (addrType === 'mx-room-id') {
|
||||
const room = MatrixClientPeg.get().getRoom(addrObj.address);
|
||||
if (room) {
|
||||
addrObj.displayName = room.name;
|
||||
addrObj.avatarMxc = room.avatarUrl;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
}
|
||||
|
||||
selectedList.push(addrObj);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
invalidAddressError: hasError ? true : this.state.invalidAddressError,
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
return selectedList;
|
||||
return hasError ? null : selectedList;
|
||||
},
|
||||
|
||||
_lookupThreepid: function(medium, address) {
|
||||
_lookupThreepid: async function(medium, address) {
|
||||
let cancelled = false;
|
||||
// Note that we can't safely remove this after we're done
|
||||
// because we don't know that it's the same one, so we just
|
||||
|
@ -502,36 +533,44 @@ module.exports = React.createClass({
|
|||
};
|
||||
|
||||
// wait a bit to let the user finish typing
|
||||
return Promise.delay(500).then(() => {
|
||||
if (cancelled) return null;
|
||||
return MatrixClientPeg.get().lookupThreePid(medium, address);
|
||||
}).then((res) => {
|
||||
if (res === null || !res.mxid) return null;
|
||||
await sleep(500);
|
||||
if (cancelled) return null;
|
||||
|
||||
try {
|
||||
const authClient = new IdentityAuthClient();
|
||||
const identityAccessToken = await authClient.getAccessToken();
|
||||
if (cancelled) return null;
|
||||
|
||||
return MatrixClientPeg.get().getProfileInfo(res.mxid);
|
||||
}).then((res) => {
|
||||
if (res === null) return null;
|
||||
if (cancelled) return null;
|
||||
const lookup = await MatrixClientPeg.get().lookupThreePid(
|
||||
medium,
|
||||
address,
|
||||
undefined /* callback */,
|
||||
identityAccessToken,
|
||||
);
|
||||
if (cancelled || lookup === null || !lookup.mxid) return null;
|
||||
|
||||
const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid);
|
||||
if (cancelled || profile === null) return null;
|
||||
|
||||
this.setState({
|
||||
suggestedList: [{
|
||||
// a UserAddressType
|
||||
addressType: medium,
|
||||
address: address,
|
||||
displayName: res.displayname,
|
||||
avatarMxc: res.avatar_url,
|
||||
displayName: profile.displayname,
|
||||
avatarMxc: profile.avatar_url,
|
||||
isKnown: true,
|
||||
}],
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.setState({
|
||||
searchError: _t('Something went wrong!'),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||
this.scrollElement = null;
|
||||
|
||||
_getFilteredSuggestions: function() {
|
||||
// map addressType => set of addresses to avoid O(n*m) operation
|
||||
const selectedAddresses = {};
|
||||
this.state.selectedList.forEach(({address, addressType}) => {
|
||||
|
@ -540,9 +579,50 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
// Filter out any addresses in the above already selected addresses (matching both type and address)
|
||||
const filteredSuggestedList = this.state.suggestedList.filter(({address, addressType}) => {
|
||||
return this.state.suggestedList.filter(({address, addressType}) => {
|
||||
return !(selectedAddresses[addressType] && selectedAddresses[addressType].has(address));
|
||||
});
|
||||
},
|
||||
|
||||
_onPaste: function(e) {
|
||||
// Prevent the text being pasted into the textarea
|
||||
e.preventDefault();
|
||||
const text = e.clipboardData.getData("text");
|
||||
// Process it as a list of addresses to add instead
|
||||
this._addAddressesToList(text.split(/[\s,]+/));
|
||||
},
|
||||
|
||||
onUseDefaultIdentityServerClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Update the IS in account data. Actually using it may trigger terms.
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useDefaultIdentityServer();
|
||||
|
||||
// Add email as a valid address type.
|
||||
const { validAddressTypes } = this.state;
|
||||
validAddressTypes.push('email');
|
||||
this.setState({ validAddressTypes });
|
||||
},
|
||||
|
||||
onManageSettingsClick(e) {
|
||||
e.preventDefault();
|
||||
dis.dispatch({ action: 'view_user_settings' });
|
||||
this.onCancel();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||
this.scrollElement = null;
|
||||
|
||||
let inputLabel;
|
||||
if (this.props.description) {
|
||||
inputLabel = <div className="mx_AddressPickerDialog_label">
|
||||
<label htmlFor="textinput">{this.props.description}</label>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const query = [];
|
||||
// create the invite list
|
||||
|
@ -562,22 +642,26 @@ module.exports = React.createClass({
|
|||
|
||||
// Add the query at the end
|
||||
query.push(
|
||||
<textarea key={this.state.selectedList.length}
|
||||
<textarea
|
||||
key={this.state.selectedList.length}
|
||||
onPaste={this._onPaste}
|
||||
rows="1"
|
||||
id="textinput"
|
||||
ref="textinput"
|
||||
className="mx_AddressPickerDialog_input"
|
||||
onChange={this.onQueryChanged}
|
||||
placeholder={this.props.placeholder}
|
||||
placeholder={this.getPlaceholder()}
|
||||
defaultValue={this.props.value}
|
||||
autoFocus={this.props.focus}>
|
||||
</textarea>,
|
||||
);
|
||||
|
||||
const filteredSuggestedList = this._getFilteredSuggestions();
|
||||
|
||||
let error;
|
||||
let addressSelector;
|
||||
if (this.state.error) {
|
||||
const validTypeDescriptions = this.props.validAddressTypes.map((t) => _t(addressTypeName[t]));
|
||||
if (this.state.invalidAddressError) {
|
||||
const validTypeDescriptions = this.state.validAddressTypes.map((t) => _t(addressTypeName[t]));
|
||||
error = <div className="mx_AddressPickerDialog_error">
|
||||
{ _t("You have entered an invalid address.") }
|
||||
<br />
|
||||
|
@ -600,17 +684,45 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
let identityServer;
|
||||
// If picker cannot currently accept e-mail but should be able to
|
||||
if (this.props.pickerType === 'user' && !this.state.validAddressTypes.includes('email')
|
||||
&& this.props.validAddressTypes.includes('email')) {
|
||||
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
|
||||
if (defaultIdentityServerUrl) {
|
||||
identityServer = <div className="mx_AddressPickerDialog_identityServer">{_t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
|
||||
"or manage in <settings>Settings</settings>.",
|
||||
{
|
||||
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
||||
},
|
||||
{
|
||||
default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{sub}</a>,
|
||||
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
||||
},
|
||||
)}</div>;
|
||||
} else {
|
||||
identityServer = <div className="mx_AddressPickerDialog_identityServer">{_t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"Manage in <settings>Settings</settings>.",
|
||||
{}, {
|
||||
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
||||
},
|
||||
)}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_AddressPickerDialog" onKeyDown={this.onKeyDown}
|
||||
onFinished={this.props.onFinished} title={this.props.title}>
|
||||
<div className="mx_AddressPickerDialog_label">
|
||||
<label htmlFor="textinput">{ this.props.description }</label>
|
||||
</div>
|
||||
{inputLabel}
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_AddressPickerDialog_inputContainer">{ query }</div>
|
||||
{ error }
|
||||
{ addressSelector }
|
||||
{ this.props.extraNode }
|
||||
{ identityServer }
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button}
|
||||
onPrimaryButtonClick={this.onButtonClick}
|
||||
|
|
|
@ -16,12 +16,13 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
propTypes: {
|
||||
unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ]
|
||||
onInviteAnyways: PropTypes.func.isRequired,
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
@ -25,6 +26,7 @@ import { MatrixClient } from 'matrix-js-sdk';
|
|||
import { KeyCode } from '../../../Keyboard';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
/**
|
||||
* Basic container for modal dialogs.
|
||||
|
@ -32,7 +34,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
|||
* Includes a div for the title, and a keypress handler which cancels the
|
||||
* dialog on escape.
|
||||
*/
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'BaseDialog',
|
||||
|
||||
propTypes: {
|
||||
|
@ -113,8 +115,9 @@ export default React.createClass({
|
|||
render: function() {
|
||||
let cancelButton;
|
||||
if (this.props.hasCancel) {
|
||||
cancelButton = <AccessibleButton onClick={this._onCancelClick} className="mx_Dialog_cancelButton">
|
||||
</AccessibleButton>;
|
||||
cancelButton = (
|
||||
<AccessibleButton onClick={this._onCancelClick} className="mx_Dialog_cancelButton" aria-label={_t("Close dialog")} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
/*
|
||||
Copyright 2017 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 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,6 +18,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -50,6 +53,13 @@ export default class BugReportDialog extends React.Component {
|
|||
}
|
||||
|
||||
_onSubmit(ev) {
|
||||
if ((!this.state.text || !this.state.text.trim()) && (!this.state.issueUrl || !this.state.issueUrl.trim())) {
|
||||
this.setState({
|
||||
err: _t("Please tell us what went wrong or, better, create a GitHub issue that describes the problem."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const userText =
|
||||
(this.state.text.length > 0 ? this.state.text + '\n\n': '') + 'Issue: ' +
|
||||
(this.state.issueUrl.length > 0 ? this.state.issueUrl : 'No issue link given');
|
||||
|
@ -62,6 +72,7 @@ export default class BugReportDialog extends React.Component {
|
|||
userText,
|
||||
sendLogs: true,
|
||||
progressCallback: this._sendProgressCallback,
|
||||
label: this.props.label,
|
||||
}).then(() => {
|
||||
if (!this._unmounted) {
|
||||
this.props.onFinished(false);
|
||||
|
@ -93,7 +104,7 @@ export default class BugReportDialog extends React.Component {
|
|||
this.setState({ issueUrl: ev.target.value });
|
||||
}
|
||||
|
||||
_onSendLogsChange(ev) {
|
||||
_onSendLogsChange(ev) {
|
||||
this.setState({ sendLogs: ev.target.checked });
|
||||
}
|
||||
|
||||
|
@ -165,6 +176,7 @@ 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")}
|
||||
|
@ -193,5 +205,5 @@ export default class BugReportDialog extends React.Component {
|
|||
}
|
||||
|
||||
BugReportDialog.propTypes = {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,6 +16,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import request from 'browser-request';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -99,7 +101,7 @@ export default class ChangelogDialog extends React.Component {
|
|||
}
|
||||
|
||||
ChangelogDialog.propTypes = {
|
||||
version: React.PropTypes.string.isRequired,
|
||||
newVersion: React.PropTypes.string.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
version: PropTypes.string.isRequired,
|
||||
newVersion: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
@ -1,198 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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 PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Unread from '../../../Unread';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class ChatCreateOrReuseDialog extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onFinished = this.onFinished.bind(this);
|
||||
this.onRoomTileClick = this.onRoomTileClick.bind(this);
|
||||
|
||||
this.state = {
|
||||
tiles: [],
|
||||
profile: {
|
||||
displayName: null,
|
||||
avatarUrl: null,
|
||||
},
|
||||
profileError: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
const dmRoomMap = new DMRoomMap(client);
|
||||
const dmRooms = dmRoomMap.getDMRoomsForUserId(this.props.userId);
|
||||
|
||||
const RoomTile = sdk.getComponent("rooms.RoomTile");
|
||||
|
||||
const tiles = [];
|
||||
for (const roomId of dmRooms) {
|
||||
const room = client.getRoom(roomId);
|
||||
if (room) {
|
||||
const isInvite = room.getMyMembership() === "invite";
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0 || isInvite;
|
||||
tiles.push(
|
||||
<RoomTile key={room.roomId} room={room}
|
||||
transparent={true}
|
||||
collapsed={false}
|
||||
selected={false}
|
||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
highlight={highlight}
|
||||
isInvite={isInvite}
|
||||
onClick={this.onRoomTileClick}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
tiles: tiles,
|
||||
});
|
||||
|
||||
if (tiles.length === 0) {
|
||||
this.setState({
|
||||
busyProfile: true,
|
||||
});
|
||||
MatrixClientPeg.get().getProfileInfo(this.props.userId).done((resp) => {
|
||||
const profile = {
|
||||
displayName: resp.displayname,
|
||||
avatarUrl: null,
|
||||
};
|
||||
if (resp.avatar_url) {
|
||||
profile.avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(
|
||||
resp.avatar_url, 48, 48, "crop",
|
||||
);
|
||||
}
|
||||
this.setState({
|
||||
busyProfile: false,
|
||||
profile: profile,
|
||||
});
|
||||
}, (err) => {
|
||||
console.error(
|
||||
'Unable to get profile for user ' + this.props.userId + ':',
|
||||
err,
|
||||
);
|
||||
this.setState({
|
||||
busyProfile: false,
|
||||
profileError: err,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onRoomTileClick(roomId) {
|
||||
this.props.onExistingRoomSelected(roomId);
|
||||
}
|
||||
|
||||
onFinished() {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
render() {
|
||||
let title = '';
|
||||
let content = null;
|
||||
if (this.state.tiles.length > 0) {
|
||||
// Show the existing rooms with a "+" to add a new dm
|
||||
title = _t('Create a new chat or reuse an existing one');
|
||||
const labelClasses = classNames({
|
||||
mx_MemberInfo_createRoom_label: true,
|
||||
mx_RoomTile_name: true,
|
||||
});
|
||||
const startNewChat = <AccessibleButton
|
||||
className="mx_MemberInfo_createRoom"
|
||||
onClick={this.props.onNewDMClick}
|
||||
>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
<img src={require("../../../../res/img/create-big.svg")} width="26" height="26" />
|
||||
</div>
|
||||
<div className={labelClasses}><i>{ _t("Start new chat") }</i></div>
|
||||
</AccessibleButton>;
|
||||
content = <div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ _t('You already have existing direct chats with this user:') }
|
||||
<div className="mx_ChatCreateOrReuseDialog_tiles">
|
||||
{ this.state.tiles }
|
||||
{ startNewChat }
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
// Show the avatar, name and a button to confirm that a new chat is requested
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
title = _t('Start chatting');
|
||||
|
||||
let profile = null;
|
||||
if (this.state.busyProfile) {
|
||||
profile = <Spinner />;
|
||||
} else if (this.state.profileError) {
|
||||
profile = <div className="error" role="alert">
|
||||
Unable to load profile information for { this.props.userId }
|
||||
</div>;
|
||||
} else {
|
||||
profile = <div className="mx_ChatCreateOrReuseDialog_profile">
|
||||
<BaseAvatar
|
||||
name={this.state.profile.displayName || this.props.userId}
|
||||
url={this.state.profile.avatarUrl}
|
||||
width={48} height={48}
|
||||
/>
|
||||
<div className="mx_ChatCreateOrReuseDialog_profile_name">
|
||||
{ this.state.profile.displayName || this.props.userId }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
content = <div>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<p>
|
||||
{ _t('Click on the button below to start chatting!') }
|
||||
</p>
|
||||
{ profile }
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t('Start Chatting')}
|
||||
onPrimaryButtonClick={this.props.onNewDMClick} focus={true} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className='mx_ChatCreateOrReuseDialog'
|
||||
onFinished={this.onFinished}
|
||||
title={title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChatCreateOrReuseDialog.propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
// Called when clicking outside of the dialog
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
onNewDMClick: PropTypes.func.isRequired,
|
||||
onExistingRoomSelected: PropTypes.func.isRequired,
|
||||
};
|
90
src/components/views/dialogs/ConfirmAndWaitRedactDialog.js
Normal file
90
src/components/views/dialogs/ConfirmAndWaitRedactDialog.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
Copyright 2019 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 sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
/*
|
||||
* A dialog for confirming a redaction.
|
||||
* Also shows a spinner (and possible error) while the redaction is ongoing,
|
||||
* and only closes the dialog when the redaction is done or failed.
|
||||
*
|
||||
* This is done to prevent the edit history dialog racing with the redaction:
|
||||
* if this dialog closes and the MessageEditHistoryDialog is shown again,
|
||||
* it will fetch the relations again, which will race with the ongoing /redact request.
|
||||
* which will cause the edit to appear unredacted.
|
||||
*
|
||||
* To avoid this, we keep the dialog open as long as /redact is in progress.
|
||||
*/
|
||||
export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isRedacting: false,
|
||||
redactionErrorCode: null,
|
||||
};
|
||||
}
|
||||
|
||||
onParentFinished = async (proceed) => {
|
||||
if (proceed) {
|
||||
this.setState({isRedacting: true});
|
||||
try {
|
||||
await this.props.redact();
|
||||
this.props.onFinished(true);
|
||||
} catch (error) {
|
||||
const code = error.errcode || error.statusCode;
|
||||
if (typeof code !== "undefined") {
|
||||
this.setState({redactionErrorCode: code});
|
||||
} else {
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.isRedacting) {
|
||||
if (this.state.redactionErrorCode) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const code = this.state.redactionErrorCode;
|
||||
return (
|
||||
<ErrorDialog
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Error')}
|
||||
description={_t('You cannot delete this message. (%(code)s)', {code})}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return (
|
||||
<BaseDialog
|
||||
onFinished={this.props.onFinished}
|
||||
hasCancel={false}
|
||||
title={_t("Removing…")}>
|
||||
<Spinner />
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
|
||||
return <ConfirmRedactDialog onFinished={this.onParentFinished} />;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,13 +15,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
/*
|
||||
* A dialog for confirming a redaction.
|
||||
*/
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'ConfirmRedactDialog',
|
||||
|
||||
render: function() {
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
|
@ -29,7 +30,7 @@ import { GroupMemberType } from '../../../groups';
|
|||
* to make it obvious what is going to happen.
|
||||
* Also tweaks the style for 'dangerous' actions (albeit only with colour)
|
||||
*/
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'ConfirmUserActionDialog',
|
||||
propTypes: {
|
||||
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||
|
@ -49,10 +50,10 @@ export default React.createClass({
|
|||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
defaultProps: {
|
||||
getDefaultProps: () => ({
|
||||
danger: false,
|
||||
askReason: false,
|
||||
},
|
||||
}),
|
||||
|
||||
componentWillMount: function() {
|
||||
this._reasonField = null;
|
||||
|
|
61
src/components/views/dialogs/ConfirmWipeDeviceDialog.js
Normal file
61
src/components/views/dialogs/ConfirmWipeDeviceDialog.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
Copyright 2019 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 sdk from "../../../index";
|
||||
|
||||
export default class ConfirmWipeDeviceDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_onConfirm = () => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
_onDecline = () => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_ConfirmWipeDeviceDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Clear all data on this device?")}>
|
||||
<div className='mx_ConfirmWipeDeviceDialog_content'>
|
||||
<p>
|
||||
{_t(
|
||||
"Clearing all data from this device is permanent. Encrypted messages will be lost " +
|
||||
"unless their keys have been backed up.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Clear all data")}
|
||||
onPrimaryButtonClick={this._onConfirm}
|
||||
primaryButtonClass="danger"
|
||||
cancelButton={_t("Cancel")}
|
||||
onCancel={this._onDecline}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -15,13 +15,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'CreateGroupDialog',
|
||||
propTypes: {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
@ -92,7 +93,7 @@ export default React.createClass({
|
|||
this.setState({createError: e});
|
||||
}).finally(() => {
|
||||
this.setState({creating: false});
|
||||
}).done();
|
||||
});
|
||||
},
|
||||
|
||||
_onCancel: function() {
|
||||
|
|
|
@ -15,58 +15,187 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import withValidation from '../elements/Validation';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'CreateRoomDialog',
|
||||
propTypes: {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
getInitialState() {
|
||||
const config = SdkConfig.get();
|
||||
// Dialog shows inverse of m.federate (noFederate) strict false check to skip undefined check (default = true)
|
||||
this.defaultNoFederate = config.default_federate === false;
|
||||
return {
|
||||
isPublic: false,
|
||||
name: "",
|
||||
topic: "",
|
||||
alias: "",
|
||||
detailsOpen: false,
|
||||
noFederate: config.default_federate === false,
|
||||
nameIsValid: false,
|
||||
};
|
||||
},
|
||||
|
||||
onOk: function() {
|
||||
this.props.onFinished(true, this.refs.textinput.value, this.refs.checkbox.checked);
|
||||
_roomCreateOptions() {
|
||||
const createOpts = {};
|
||||
createOpts.name = this.state.name;
|
||||
if (this.state.isPublic) {
|
||||
createOpts.visibility = "public";
|
||||
createOpts.preset = "public_chat";
|
||||
// to prevent createRoom from enabling guest access
|
||||
createOpts['initial_state'] = [];
|
||||
const {alias} = this.state;
|
||||
const localPart = alias.substr(1, alias.indexOf(":") - 1);
|
||||
createOpts['room_alias_name'] = localPart;
|
||||
}
|
||||
if (this.state.topic) {
|
||||
createOpts.topic = this.state.topic;
|
||||
}
|
||||
if (this.state.noFederate) {
|
||||
createOpts.creation_content = {'m.federate': false};
|
||||
}
|
||||
return createOpts;
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
this._detailsRef.addEventListener("toggle", this.onDetailsToggled);
|
||||
// move focus to first field when showing dialog
|
||||
this._nameFieldRef.focus();
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this._detailsRef.removeEventListener("toggle", this.onDetailsToggled);
|
||||
},
|
||||
|
||||
_onKeyDown: function(event) {
|
||||
if (event.key === Key.ENTER) {
|
||||
this.onOk();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
onOk: async function() {
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement) {
|
||||
activeElement.blur();
|
||||
}
|
||||
await this._nameFieldRef.validate({allowEmpty: false});
|
||||
if (this._aliasFieldRef) {
|
||||
await this._aliasFieldRef.validate({allowEmpty: false});
|
||||
}
|
||||
// Validation and state updates are async, so we need to wait for them to complete
|
||||
// first. Queue a `setState` callback and wait for it to resolve.
|
||||
await new Promise(resolve => this.setState({}, resolve));
|
||||
if (this.state.nameIsValid && (!this._aliasFieldRef || this._aliasFieldRef.isValid)) {
|
||||
this.props.onFinished(true, this._roomCreateOptions());
|
||||
} else {
|
||||
let field;
|
||||
if (!this.state.nameIsValid) {
|
||||
field = this._nameFieldRef;
|
||||
} else if (this._aliasFieldRef && !this._aliasFieldRef.isValid) {
|
||||
field = this._aliasFieldRef;
|
||||
}
|
||||
if (field) {
|
||||
field.focus();
|
||||
field.validate({ allowEmpty: false, focused: true });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
onNameChange(ev) {
|
||||
this.setState({name: ev.target.value});
|
||||
},
|
||||
|
||||
onTopicChange(ev) {
|
||||
this.setState({topic: ev.target.value});
|
||||
},
|
||||
|
||||
onPublicChange(isPublic) {
|
||||
this.setState({isPublic});
|
||||
},
|
||||
|
||||
onAliasChange(alias) {
|
||||
this.setState({alias});
|
||||
},
|
||||
|
||||
onDetailsToggled(ev) {
|
||||
this.setState({detailsOpen: ev.target.open});
|
||||
},
|
||||
|
||||
onNoFederateChange(noFederate) {
|
||||
this.setState({noFederate});
|
||||
},
|
||||
|
||||
collectDetailsRef(ref) {
|
||||
this._detailsRef = ref;
|
||||
},
|
||||
|
||||
async onNameValidate(fieldState) {
|
||||
const result = await this._validateRoomName(fieldState);
|
||||
this.setState({nameIsValid: result.valid});
|
||||
return result;
|
||||
},
|
||||
|
||||
_validateRoomName: withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: async ({ value }) => !!value,
|
||||
invalid: () => _t("Please enter a name for the room"),
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Field = sdk.getComponent('views.elements.Field');
|
||||
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
|
||||
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
|
||||
|
||||
let privateLabel;
|
||||
let publicLabel;
|
||||
let aliasField;
|
||||
if (this.state.isPublic) {
|
||||
publicLabel = (<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} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
privateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
|
||||
}
|
||||
|
||||
const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
|
||||
return (
|
||||
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
||||
title={_t('Create Room')}
|
||||
title={title}
|
||||
>
|
||||
<form onSubmit={this.onOk}>
|
||||
<form onSubmit={this.onOk} onKeyDown={this._onKeyDown}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateRoomDialog_label">
|
||||
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
||||
</div>
|
||||
<div className="mx_CreateRoomDialog_input_container">
|
||||
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} />
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<details className="mx_CreateRoomDialog_details">
|
||||
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
|
||||
<div>
|
||||
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
|
||||
<label htmlFor="checkbox">
|
||||
{ _t('Block users on other matrix homeservers from joining this room') }
|
||||
<br />
|
||||
({ _t('This setting cannot be changed later!') })
|
||||
</label>
|
||||
</div>
|
||||
<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} />
|
||||
<LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} />
|
||||
{ privateLabel }
|
||||
{ publicLabel }
|
||||
{ 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>
|
||||
<LabelledToggleSwitch label={ _t('Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)')} onChange={this.onNoFederateChange} value={this.state.noFederate} />
|
||||
</details>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -36,7 +36,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
confirmButtonEnabled: false,
|
||||
password: "",
|
||||
busy: false,
|
||||
shouldErase: false,
|
||||
errStr: null,
|
||||
|
@ -45,7 +45,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
|
||||
_onPasswordFieldChange(ev) {
|
||||
this.setState({
|
||||
confirmButtonEnabled: Boolean(ev.target.value),
|
||||
password: ev.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -63,14 +63,20 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
// 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,
|
||||
password: this._passwordField.value,
|
||||
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) {
|
||||
if (err.httpStatus === 401 || err.httpStatus === 403) {
|
||||
errStr = _t('Incorrect password');
|
||||
Velocity(this._passwordField, "callout.shake", 300);
|
||||
}
|
||||
|
@ -83,7 +89,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
|
||||
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||
Lifecycle.onLoggedOut();
|
||||
this.props.onFinished(false);
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
_onCancel() {
|
||||
|
@ -104,7 +110,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
}
|
||||
|
||||
const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account');
|
||||
const okEnabled = this.state.confirmButtonEnabled && !this.state.busy;
|
||||
const okEnabled = this.state.password && !this.state.busy;
|
||||
|
||||
let cancelButton = null;
|
||||
if (!this.state.busy) {
|
||||
|
@ -113,6 +119,8 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
</button>;
|
||||
}
|
||||
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_DeactivateAccountDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
|
@ -167,10 +175,12 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
</p>
|
||||
|
||||
<p>{ _t("To continue, please enter your password:") }</p>
|
||||
<input
|
||||
<Field
|
||||
id="mx_DeactivateAccountDialog_password"
|
||||
type="password"
|
||||
placeholder={_t("password")}
|
||||
label={_t('Password')}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
value={this.state.password}
|
||||
ref={(e) => {this._passwordField = e;}}
|
||||
className={passwordBoxClass}
|
||||
/>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -23,6 +24,10 @@ import sdk from '../../../index';
|
|||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import createRoom from "../../../createRoom";
|
||||
import dis from "../../../dispatcher";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
|
||||
const MODE_LEGACY = 'legacy';
|
||||
const MODE_SAS = 'sas';
|
||||
|
@ -85,25 +90,37 @@ export default class DeviceVerifyDialog extends React.Component {
|
|||
this.props.onFinished(confirm);
|
||||
}
|
||||
|
||||
_onSasRequestClick = () => {
|
||||
_onSasRequestClick = async () => {
|
||||
this.setState({
|
||||
phase: PHASE_WAIT_FOR_PARTNER_TO_ACCEPT,
|
||||
});
|
||||
this._verifier = MatrixClientPeg.get().beginKeyVerification(
|
||||
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
|
||||
);
|
||||
this._verifier.on('show_sas', this._onVerifierShowSas);
|
||||
this._verifier.verify().then(() => {
|
||||
const client = MatrixClientPeg.get();
|
||||
const verifyingOwnDevice = this.props.userId === client.getUserId();
|
||||
try {
|
||||
if (!verifyingOwnDevice && SettingsStore.getValue("feature_dm_verification")) {
|
||||
const roomId = await ensureDMExistsAndOpen(this.props.userId);
|
||||
// throws upon cancellation before having started
|
||||
this._verifier = await client.requestVerificationDM(
|
||||
this.props.userId, roomId, [verificationMethods.SAS],
|
||||
);
|
||||
} else {
|
||||
this._verifier = client.beginKeyVerification(
|
||||
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
|
||||
);
|
||||
}
|
||||
this._verifier.on('show_sas', this._onVerifierShowSas);
|
||||
// throws upon cancellation
|
||||
await this._verifier.verify();
|
||||
this.setState({phase: PHASE_VERIFIED});
|
||||
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
|
||||
this._verifier = null;
|
||||
}).catch((e) => {
|
||||
} catch (e) {
|
||||
console.log("Verification failed", e);
|
||||
this.setState({
|
||||
phase: PHASE_CANCELLED,
|
||||
});
|
||||
this._verifier = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onSasMatchesClick = () => {
|
||||
|
@ -241,6 +258,16 @@ export default class DeviceVerifyDialog extends React.Component {
|
|||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
||||
|
||||
let text;
|
||||
if (MatrixClientPeg.get().getUserId() === this.props.userId) {
|
||||
text = _t("To verify that this device can be trusted, please check that the key you see " +
|
||||
"in User Settings on that device matches the key below:");
|
||||
} else {
|
||||
text = _t("To verify that this device can be trusted, please contact its owner using some other " +
|
||||
"means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings " +
|
||||
"for this device matches the key below:");
|
||||
}
|
||||
|
||||
const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint());
|
||||
const body = (
|
||||
<div>
|
||||
|
@ -250,10 +277,7 @@ export default class DeviceVerifyDialog extends React.Component {
|
|||
{_t("Use two-way text verification")}
|
||||
</AccessibleButton>
|
||||
<p>
|
||||
{ _t("To verify that this device can be trusted, please contact its " +
|
||||
"owner using some other means (e.g. in person or a phone call) " +
|
||||
"and ask them whether the key they see in their User Settings " +
|
||||
"for this device matches the key below:") }
|
||||
{ text }
|
||||
</p>
|
||||
<div className="mx_DeviceVerifyDialog_cryptoSection">
|
||||
<ul>
|
||||
|
@ -291,3 +315,30 @@ export default class DeviceVerifyDialog extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
async function ensureDMExistsAndOpen(userId) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
||||
const rooms = roomIds.map(id => client.getRoom(id));
|
||||
const suitableDMRooms = rooms.filter(r => {
|
||||
if (r && r.getMyMembership() === "join") {
|
||||
const member = r.getMember(userId);
|
||||
return member && (member.membership === "invite" || member.membership === "join");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
let roomId;
|
||||
if (suitableDMRooms.length) {
|
||||
const room = suitableDMRooms[0];
|
||||
roomId = room.roomId;
|
||||
} else {
|
||||
roomId = await createRoom({dmUserId: userId, spinner: false, andView: false});
|
||||
}
|
||||
// don't use andView and spinner in createRoom, together, they cause this dialog to close and reopen,
|
||||
// we causes us to loose the verifier and restart, and we end up having two verification requests
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
should_peek: false,
|
||||
});
|
||||
return roomId;
|
||||
}
|
||||
|
|
|
@ -128,8 +128,10 @@ class SendCustomEvent extends GenericEditor {
|
|||
|
||||
return <div>
|
||||
<div className="mx_DevTools_content">
|
||||
{ this.textInput('eventType', _t('Event Type')) }
|
||||
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
|
||||
<div className="mx_DevTools_eventTypeStateKeyGroup">
|
||||
{ this.textInput('eventType', _t('Event Type')) }
|
||||
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
|
|
|
@ -26,11 +26,12 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'ErrorDialog',
|
||||
propTypes: {
|
||||
title: PropTypes.string,
|
||||
|
|
|
@ -34,9 +34,15 @@ export default class IncomingSasDialog extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
let phase = PHASE_START;
|
||||
if (this.props.verifier.cancelled) {
|
||||
console.log("Verifier was cancelled in the background.");
|
||||
phase = PHASE_CANCELLED;
|
||||
}
|
||||
|
||||
this._showSasEvent = null;
|
||||
this.state = {
|
||||
phase: PHASE_START,
|
||||
phase: phase,
|
||||
sasVerified: false,
|
||||
opponentProfile: null,
|
||||
opponentProfileError: null,
|
||||
|
|
|
@ -17,23 +17,28 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classNames from "classnames";
|
||||
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'InfoDialog',
|
||||
propTypes: {
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.node,
|
||||
button: PropTypes.string,
|
||||
onFinished: PropTypes.func,
|
||||
hasCloseButton: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: '',
|
||||
description: '',
|
||||
hasCloseButton: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -48,9 +53,9 @@ export default React.createClass({
|
|||
<BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={false}
|
||||
hasCancel={this.props.hasCloseButton}
|
||||
>
|
||||
<div className="mx_Dialog_content" id="mx_Dialog_content">
|
||||
<div className={classNames("mx_Dialog_content", this.props.className)} id="mx_Dialog_content">
|
||||
{ this.props.description }
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
|
|
57
src/components/views/dialogs/IntegrationsDisabledDialog.js
Normal file
57
src/components/views/dialogs/IntegrationsDisabledDialog.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
Copyright 2019 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 sdk from "../../../index";
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
export default class IntegrationsDisabledDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_onAcknowledgeClick = () => {
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_onOpenSettingsClick = () => {
|
||||
this.props.onFinished();
|
||||
dis.dispatch({action: "view_user_settings"});
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_IntegrationsDisabledDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Integrations are disabled")}>
|
||||
<div className='mx_IntegrationsDisabledDialog_content'>
|
||||
<p>{_t("Enable 'Manage Integrations' in Settings to do this.")}</p>
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Settings")}
|
||||
onPrimaryButtonClick={this._onOpenSettingsClick}
|
||||
cancelButton={_t("OK")}
|
||||
onCancel={this._onAcknowledgeClick}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
55
src/components/views/dialogs/IntegrationsImpossibleDialog.js
Normal file
55
src/components/views/dialogs/IntegrationsImpossibleDialog.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
Copyright 2019 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 sdk from "../../../index";
|
||||
|
||||
export default class IntegrationsImpossibleDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_onAcknowledgeClick = () => {
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_IntegrationsImpossibleDialog' hasCancel={false}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Integrations not allowed")}>
|
||||
<div className='mx_IntegrationsImpossibleDialog_content'>
|
||||
<p>
|
||||
{_t(
|
||||
"Your Riot doesn't allow you to use an Integration Manager to do this. " +
|
||||
"Please contact an admin.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("OK")}
|
||||
onPrimaryButtonClick={this._onAcknowledgeClick}
|
||||
hasCancel={false}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
|
@ -23,7 +24,7 @@ import { _t } from '../../../languageHandler';
|
|||
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'InteractiveAuthDialog',
|
||||
|
||||
propTypes: {
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
import Modal from '../../../Modal';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
|
||||
|
@ -29,7 +30,7 @@ import { _t, _td } from '../../../languageHandler';
|
|||
* should not, and `undefined` if the dialog is cancelled. (In other words:
|
||||
* truthy: do the key share. falsy: don't share the keys).
|
||||
*/
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
propTypes: {
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
|
@ -77,7 +78,7 @@ export default React.createClass({
|
|||
true,
|
||||
);
|
||||
}
|
||||
}).done();
|
||||
});
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
|
|
@ -20,11 +20,12 @@ import 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 = {
|
||||
onFinished: function() {},
|
||||
}
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -34,9 +35,11 @@ export default class LogoutDialog extends React.Component {
|
|||
this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this);
|
||||
this._onLogoutConfirm = this._onLogoutConfirm.bind(this);
|
||||
|
||||
const shouldLoadBackupStatus = !MatrixClientPeg.get().getKeyBackupEnabled();
|
||||
const lowBandwidth = SettingsStore.getValue("lowBandwidth");
|
||||
const shouldLoadBackupStatus = !lowBandwidth && !MatrixClientPeg.get().getKeyBackupEnabled();
|
||||
|
||||
this.state = {
|
||||
shouldLoadBackupStatus: shouldLoadBackupStatus,
|
||||
loading: shouldLoadBackupStatus,
|
||||
backupInfo: null,
|
||||
error: null,
|
||||
|
@ -110,17 +113,17 @@ export default class LogoutDialog extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const description = <div>
|
||||
<p>{_t(
|
||||
"Encrypted messages are secured with end-to-end encryption. " +
|
||||
"Only you and the recipient(s) have the keys to read these messages.",
|
||||
)}</p>
|
||||
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
|
||||
</div>;
|
||||
|
||||
if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||
if (this.state.shouldLoadBackupStatus) {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const description = <div>
|
||||
<p>{_t(
|
||||
"Encrypted messages are secured with end-to-end encryption. " +
|
||||
"Only you and the recipient(s) have the keys to read these messages.",
|
||||
)}</p>
|
||||
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
|
||||
</div>;
|
||||
|
||||
let dialogContent;
|
||||
if (this.state.loading) {
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
|
|
171
src/components/views/dialogs/MessageEditHistoryDialog.js
Normal file
171
src/components/views/dialogs/MessageEditHistoryDialog.js
Normal file
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
Copyright 2019 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 { _t } from '../../../languageHandler';
|
||||
import sdk from "../../../index";
|
||||
import {wantsDateSeparator} from '../../../DateUtils';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
|
||||
export default class MessageEditHistoryDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
originalEvent: null,
|
||||
error: null,
|
||||
events: [],
|
||||
nextBatch: null,
|
||||
isLoading: true,
|
||||
isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"),
|
||||
};
|
||||
}
|
||||
|
||||
loadMoreEdits = async (backwards) => {
|
||||
if (backwards || (!this.state.nextBatch && !this.state.isLoading)) {
|
||||
// bail out on backwards as we only paginate in one direction
|
||||
return false;
|
||||
}
|
||||
const opts = {from: this.state.nextBatch};
|
||||
const roomId = this.props.mxEvent.getRoomId();
|
||||
const eventId = this.props.mxEvent.getId();
|
||||
const client = MatrixClientPeg.get();
|
||||
let result;
|
||||
let resolve;
|
||||
let reject;
|
||||
const promise = new Promise((_resolve, _reject) => {resolve = _resolve; reject = _reject;});
|
||||
try {
|
||||
result = await client.relations(
|
||||
roomId, eventId, "m.replace", "m.room.message", opts);
|
||||
} catch (error) {
|
||||
// log if the server returned an error
|
||||
if (error.errcode) {
|
||||
console.error("fetching /relations failed with error", error);
|
||||
}
|
||||
this.setState({error}, () => reject(error));
|
||||
return promise;
|
||||
}
|
||||
|
||||
const newEvents = result.events;
|
||||
this._locallyRedactEventsIfNeeded(newEvents);
|
||||
this.setState({
|
||||
originalEvent: this.state.originalEvent || result.originalEvent,
|
||||
events: this.state.events.concat(newEvents),
|
||||
nextBatch: result.nextBatch,
|
||||
isLoading: false,
|
||||
}, () => {
|
||||
const hasMoreResults = !!this.state.nextBatch;
|
||||
resolve(hasMoreResults);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
_locallyRedactEventsIfNeeded(newEvents) {
|
||||
const roomId = this.props.mxEvent.getRoomId();
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(roomId);
|
||||
const pendingEvents = room.getPendingEvents();
|
||||
for (const e of newEvents) {
|
||||
const pendingRedaction = pendingEvents.find(pe => {
|
||||
return pe.getType() === "m.room.redaction" && pe.getAssociatedId() === e.getId();
|
||||
});
|
||||
if (pendingRedaction) {
|
||||
e.markLocallyRedacted(pendingRedaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadMoreEdits();
|
||||
}
|
||||
|
||||
_renderEdits() {
|
||||
const EditHistoryMessage = sdk.getComponent('messages.EditHistoryMessage');
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const nodes = [];
|
||||
let lastEvent;
|
||||
let allEvents = this.state.events;
|
||||
// append original event when we've done last pagination
|
||||
if (this.state.originalEvent && !this.state.nextBatch) {
|
||||
allEvents = allEvents.concat(this.state.originalEvent);
|
||||
}
|
||||
const baseEventId = this.props.mxEvent.getId();
|
||||
allEvents.forEach((e, i) => {
|
||||
if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) {
|
||||
nodes.push(<li key={e.getTs() + "~"}><DateSeparator ts={e.getTs()} /></li>);
|
||||
}
|
||||
const isBaseEvent = e.getId() === baseEventId;
|
||||
nodes.push((
|
||||
<EditHistoryMessage
|
||||
key={e.getId()}
|
||||
previousEdit={!isBaseEvent ? allEvents[i + 1] : null}
|
||||
isBaseEvent={isBaseEvent}
|
||||
mxEvent={e}
|
||||
isTwelveHour={this.state.isTwelveHour}
|
||||
/>));
|
||||
lastEvent = e;
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
|
||||
render() {
|
||||
let content;
|
||||
if (this.state.error) {
|
||||
const {error} = this.state;
|
||||
if (error.errcode === "M_UNRECOGNIZED") {
|
||||
content = (<p className="mx_MessageEditHistoryDialog_error">
|
||||
{_t("Your homeserver doesn't seem to support this feature.")}
|
||||
</p>);
|
||||
} else if (error.errcode) {
|
||||
// some kind of error from the homeserver
|
||||
content = (<p className="mx_MessageEditHistoryDialog_error">
|
||||
{_t("Something went wrong!")}
|
||||
</p>);
|
||||
} else {
|
||||
content = (<p className="mx_MessageEditHistoryDialog_error">
|
||||
{_t("Cannot reach homeserver")}
|
||||
<br />
|
||||
{_t("Ensure you have a stable internet connection, or get in touch with the server admin")}
|
||||
</p>);
|
||||
}
|
||||
} else if (this.state.isLoading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
content = <Spinner />;
|
||||
} else {
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
content = (<ScrollPanel
|
||||
className="mx_MessageEditHistoryDialog_scrollPanel"
|
||||
onFillRequest={ this.loadMoreEdits }
|
||||
stickyBottom={false}
|
||||
startAtBottom={false}
|
||||
>
|
||||
<ul className="mx_MessageEditHistoryDialog_edits mx_MessagePanel_alwaysShowTimestamps">{this._renderEdits()}</ul>
|
||||
</ScrollPanel>);
|
||||
}
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className='mx_MessageEditHistoryDialog' hasCancel={true}
|
||||
onFinished={this.props.onFinished} title={_t("Message edits")}>
|
||||
{content}
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -16,11 +16,12 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'QuestionDialog',
|
||||
propTypes: {
|
||||
title: PropTypes.string,
|
||||
|
|
137
src/components/views/dialogs/ReportEventDialog.js
Normal file
137
src/components/views/dialogs/ReportEventDialog.js
Normal file
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import PropTypes from "prop-types";
|
||||
import {MatrixEvent} from "matrix-js-sdk";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
|
||||
/*
|
||||
* A dialog for reporting an event.
|
||||
*/
|
||||
export default class ReportEventDialog extends PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
reason: "",
|
||||
busy: false,
|
||||
err: null,
|
||||
};
|
||||
}
|
||||
|
||||
_onReasonChange = ({target: {value: reason}}) => {
|
||||
this.setState({ reason });
|
||||
};
|
||||
|
||||
_onCancel = () => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
_onSubmit = async () => {
|
||||
if (!this.state.reason || !this.state.reason.trim()) {
|
||||
this.setState({
|
||||
err: _t("Please fill why you're reporting."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
busy: true,
|
||||
err: null,
|
||||
});
|
||||
|
||||
try {
|
||||
const ev = this.props.mxEvent;
|
||||
await MatrixClientPeg.get().reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim());
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
err: e.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Loader = sdk.getComponent('elements.Spinner');
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
let error = null;
|
||||
if (this.state.err) {
|
||||
error = <div className="error">
|
||||
{this.state.err}
|
||||
</div>;
|
||||
}
|
||||
|
||||
let progress = null;
|
||||
if (this.state.busy) {
|
||||
progress = (
|
||||
<div className="progress">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_BugReportDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Report Content to Your Homeserver Administrator')}
|
||||
contentId='mx_ReportEventDialog'
|
||||
>
|
||||
<div className="mx_ReportEventDialog" id="mx_ReportEventDialog">
|
||||
<p>
|
||||
{
|
||||
_t("Reporting this message will send its unique 'event ID' to the administrator of " +
|
||||
"your homeserver. If messages in this room are encrypted, your homeserver " +
|
||||
"administrator will not be able to read the message text or view any files or images.")
|
||||
}
|
||||
</p>
|
||||
|
||||
<Field
|
||||
id="mx_ReportEventDialog_reason"
|
||||
className="mx_ReportEventDialog_reason"
|
||||
element="textarea"
|
||||
label={_t("Reason")}
|
||||
rows={5}
|
||||
onChange={this._onReasonChange}
|
||||
value={this.state.reason}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
{progress}
|
||||
{error}
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("Send report")}
|
||||
onPrimaryButtonClick={this._onSubmit}
|
||||
focus={true}
|
||||
onCancel={this._onCancel}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -22,8 +23,10 @@ import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsT
|
|||
import RolesRoomSettingsTab from "../settings/tabs/room/RolesRoomSettingsTab";
|
||||
import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab";
|
||||
import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab";
|
||||
import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsTab";
|
||||
import sdk from "../../../index";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import dis from "../../../dispatcher";
|
||||
|
||||
export default class RoomSettingsDialog extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -31,6 +34,22 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
|
||||
_onAction = (payload) => {
|
||||
// When room changes below us, close the room settings
|
||||
// whilst the modal is open this can only be triggered when someone hits Leave Room
|
||||
if (payload.action === 'view_next_room') {
|
||||
this.props.onFinished();
|
||||
}
|
||||
};
|
||||
|
||||
_getTabs() {
|
||||
const tabs = [];
|
||||
|
||||
|
@ -49,6 +68,11 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
"mx_RoomSettingsDialog_rolesIcon",
|
||||
<RolesRoomSettingsTab roomId={this.props.roomId} />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
_td("Notifications"),
|
||||
"mx_RoomSettingsDialog_rolesIcon",
|
||||
<NotificationSettingsTab roomId={this.props.roomId} />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
_td("Advanced"),
|
||||
"mx_RoomSettingsDialog_warningIcon",
|
||||
|
|
|
@ -15,13 +15,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'RoomUpgradeDialog',
|
||||
|
||||
propTypes: {
|
||||
|
@ -92,7 +93,7 @@ export default React.createClass({
|
|||
<p>
|
||||
{_t(
|
||||
"Upgrading this room requires closing down the current " +
|
||||
"instance of the room and creating a new room it its place. " +
|
||||
"instance of the room and creating a new room in its place. " +
|
||||
"To give room members the best possible experience, we will:",
|
||||
)}
|
||||
</p>
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
|
@ -23,7 +24,7 @@ import Modal from '../../../Modal';
|
|||
import { _t } from '../../../languageHandler';
|
||||
|
||||
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'SessionRestoreErrorDialog',
|
||||
|
||||
propTypes: {
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import Email from '../../../email';
|
||||
|
@ -29,7 +30,7 @@ import Modal from '../../../Modal';
|
|||
*
|
||||
* On success, `onFinished(true)` is called.
|
||||
*/
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'SetEmailDialog',
|
||||
propTypes: {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
@ -61,9 +62,7 @@ export default React.createClass({
|
|||
return;
|
||||
}
|
||||
this._addThreepid = new AddThreepid();
|
||||
// we always bind emails when registering, so let's do the
|
||||
// same here.
|
||||
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
|
||||
this._addThreepid.addEmailAddress(emailAddress).then(() => {
|
||||
Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
|
||||
title: _t("Verification Pending"),
|
||||
description: _t(
|
||||
|
@ -97,7 +96,7 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
verifyEmailAddress: function() {
|
||||
this._addThreepid.checkEmailLinkClicked().done(() => {
|
||||
this._addThreepid.checkEmailLinkClicked().then(() => {
|
||||
this.props.onFinished(true);
|
||||
}, (err) => {
|
||||
this.setState({emailBusy: false});
|
||||
|
|
|
@ -15,8 +15,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
@ -34,7 +34,7 @@ const USERNAME_CHECK_DEBOUNCE_MS = 250;
|
|||
*
|
||||
* On success, `onFinished(true, newDisplayName)` is called.
|
||||
*/
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'SetMxIdDialog',
|
||||
propTypes: {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,6 +17,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -60,10 +63,10 @@ const WarmFuzzy = function(props) {
|
|||
*
|
||||
* On success, `onFinished()` when finished
|
||||
*/
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'SetPasswordDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
|
|
@ -20,7 +20,7 @@ import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
|
|||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import QRCode from 'qrcode-react';
|
||||
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../matrix-to";
|
||||
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import * as ContextualMenu from "../../structures/ContextualMenu";
|
||||
|
||||
const socials = [
|
||||
|
@ -114,7 +114,8 @@ export default class ShareDialog extends React.Component {
|
|||
top: y,
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
}, false);
|
||||
e.target.onmouseleave = close;
|
||||
// Drop a reference to this close handler for componentWillUnmount
|
||||
this.closeCopiedTooltip = e.target.onmouseleave = close;
|
||||
}
|
||||
|
||||
onLinkSpecificEventCheckboxClick() {
|
||||
|
@ -131,6 +132,12 @@ export default class ShareDialog extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
|
||||
}
|
||||
|
||||
render() {
|
||||
let title;
|
||||
let matrixToUrl;
|
||||
|
@ -146,7 +153,7 @@ export default class ShareDialog extends React.Component {
|
|||
<input type="checkbox"
|
||||
id="mx_ShareDialog_checkbox"
|
||||
checked={this.state.linkSpecificEvent}
|
||||
onClick={this.onLinkSpecificEventCheckboxClick} />
|
||||
onChange={this.onLinkSpecificEventCheckboxClick} />
|
||||
<label htmlFor="mx_ShareDialog_checkbox">
|
||||
{ _t('Link to most recent message') }
|
||||
</label>
|
||||
|
|
63
src/components/views/dialogs/SlashCommandHelpDialog.js
Normal file
63
src/components/views/dialogs/SlashCommandHelpDialog.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {CommandCategories, CommandMap} from "../../../SlashCommands";
|
||||
import sdk from "../../../index";
|
||||
|
||||
export default ({onFinished}) => {
|
||||
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
|
||||
|
||||
const categories = {};
|
||||
Object.values(CommandMap).forEach(cmd => {
|
||||
if (!categories[cmd.category]) {
|
||||
categories[cmd.category] = [];
|
||||
}
|
||||
categories[cmd.category].push(cmd);
|
||||
});
|
||||
|
||||
const body = Object.values(CommandCategories).filter(c => categories[c]).map((category) => {
|
||||
const rows = [
|
||||
<tr key={"_category_" + category} className="mx_SlashCommandHelpDialog_headerRow">
|
||||
<td colSpan={3}>
|
||||
<h2>{_t(category)}</h2>
|
||||
</td>
|
||||
</tr>,
|
||||
];
|
||||
|
||||
categories[category].forEach(cmd => {
|
||||
rows.push(<tr key={cmd.command}>
|
||||
<td><strong>{cmd.command}</strong></td>
|
||||
<td>{cmd.args}</td>
|
||||
<td>{cmd.description}</td>
|
||||
</tr>);
|
||||
});
|
||||
|
||||
return rows;
|
||||
});
|
||||
|
||||
return <InfoDialog
|
||||
className="mx_SlashCommandHelpDialog"
|
||||
title={_t("Command Help")}
|
||||
description={<table>
|
||||
<tbody>
|
||||
{body}
|
||||
</tbody>
|
||||
</table>}
|
||||
hasCloseButton={true}
|
||||
onFinished={onFinished} />;
|
||||
};
|
172
src/components/views/dialogs/TabbedIntegrationManagerDialog.js
Normal file
172
src/components/views/dialogs/TabbedIntegrationManagerDialog.js
Normal file
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
Copyright 2019 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 {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import {Room} from "matrix-js-sdk";
|
||||
import sdk from '../../../index';
|
||||
import {dialogTermsInteractionCallback, TermsNotSignedError} from "../../../Terms";
|
||||
import classNames from 'classnames';
|
||||
import ScalarMessaging from "../../../ScalarMessaging";
|
||||
|
||||
export default class TabbedIntegrationManagerDialog extends React.Component {
|
||||
static propTypes = {
|
||||
/**
|
||||
* Called with:
|
||||
* * success {bool} True if the user accepted any douments, false if cancelled
|
||||
* * agreedUrls {string[]} List of agreed URLs
|
||||
*/
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
||||
/**
|
||||
* Optional room where the integration manager should be open to
|
||||
*/
|
||||
room: PropTypes.instanceOf(Room),
|
||||
|
||||
/**
|
||||
* Optional screen to open on the integration manager
|
||||
*/
|
||||
screen: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Optional integration ID to open in the integration manager
|
||||
*/
|
||||
integrationId: PropTypes.string,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
managers: IntegrationManagers.sharedInstance().getOrderedManagers(),
|
||||
busy: true,
|
||||
currentIndex: 0,
|
||||
currentConnected: false,
|
||||
currentLoading: true,
|
||||
currentScalarClient: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.openManager(0, true);
|
||||
}
|
||||
|
||||
openManager = async (i: number, force = false) => {
|
||||
if (i === this.state.currentIndex && !force) return;
|
||||
|
||||
const manager = this.state.managers[i];
|
||||
const client = manager.getScalarClient();
|
||||
this.setState({
|
||||
busy: true,
|
||||
currentIndex: i,
|
||||
currentLoading: true,
|
||||
currentConnected: false,
|
||||
currentScalarClient: client,
|
||||
});
|
||||
|
||||
ScalarMessaging.setOpenManagerUrl(manager.uiUrl);
|
||||
|
||||
client.setTermsInteractionCallback((policyInfo, agreedUrls) => {
|
||||
// To avoid visual glitching of two modals stacking briefly, we customise the
|
||||
// terms dialog sizing when it will appear for the integration manager so that
|
||||
// it gets the same basic size as the IM's own modal.
|
||||
return dialogTermsInteractionCallback(
|
||||
policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationManager',
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
if (!client.hasCredentials()) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
currentLoading: false,
|
||||
currentConnected: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
busy: false,
|
||||
currentLoading: false,
|
||||
currentConnected: true,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof TermsNotSignedError) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(e);
|
||||
this.setState({
|
||||
busy: false,
|
||||
currentLoading: false,
|
||||
currentConnected: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_renderTabs() {
|
||||
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
|
||||
return this.state.managers.map((m, i) => {
|
||||
const classes = classNames({
|
||||
'mx_TabbedIntegrationManagerDialog_tab': true,
|
||||
'mx_TabbedIntegrationManagerDialog_currentTab': this.state.currentIndex === i,
|
||||
});
|
||||
return (
|
||||
<AccessibleButton
|
||||
className={classes}
|
||||
onClick={() => this.openManager(i)}
|
||||
key={`tab_${i}`}
|
||||
disabled={this.state.busy}
|
||||
>
|
||||
{m.name}
|
||||
</AccessibleButton>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_renderTab() {
|
||||
const IntegrationManager = sdk.getComponent("views.settings.IntegrationManager");
|
||||
let uiUrl = null;
|
||||
if (this.state.currentScalarClient) {
|
||||
uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom(
|
||||
this.props.room,
|
||||
this.props.screen,
|
||||
this.props.integrationId,
|
||||
);
|
||||
}
|
||||
return <IntegrationManager
|
||||
configured={true}
|
||||
loading={this.state.currentLoading}
|
||||
connected={this.state.currentConnected}
|
||||
url={uiUrl}
|
||||
onFinished={() => {/* no-op */}}
|
||||
/>;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='mx_TabbedIntegrationManagerDialog_container'>
|
||||
<div className='mx_TabbedIntegrationManagerDialog_tabs'>
|
||||
{this._renderTabs()}
|
||||
</div>
|
||||
<div className='mx_TabbedIntegrationManagerDialog_currentManager'>
|
||||
{this._renderTab()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
205
src/components/views/dialogs/TermsDialog.js
Normal file
205
src/components/views/dialogs/TermsDialog.js
Normal file
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
Copyright 2019 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 url from 'url';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t, pickBestLanguage } from '../../../languageHandler';
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
|
||||
class TermsCheckbox extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
checked: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
onChange = (ev) => {
|
||||
this.props.onChange(this.props.url, ev.target.checked);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <input type="checkbox"
|
||||
onChange={this.onChange}
|
||||
checked={this.props.checked}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
export default class TermsDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
/**
|
||||
* Array of [Service, policies] pairs, where policies is the response from the
|
||||
* /terms endpoint for that service
|
||||
*/
|
||||
policiesAndServicePairs: PropTypes.array.isRequired,
|
||||
|
||||
/**
|
||||
* urls that the user has already agreed to
|
||||
*/
|
||||
agreedUrls: PropTypes.arrayOf(PropTypes.string),
|
||||
|
||||
/**
|
||||
* Called with:
|
||||
* * success {bool} True if the user accepted any douments, false if cancelled
|
||||
* * agreedUrls {string[]} List of agreed URLs
|
||||
*/
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super();
|
||||
this.state = {
|
||||
// url -> boolean
|
||||
agreedUrls: {},
|
||||
};
|
||||
for (const url of props.agreedUrls) {
|
||||
this.state.agreedUrls[url] = true;
|
||||
}
|
||||
}
|
||||
|
||||
_onCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onNextClick = () => {
|
||||
this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url]));
|
||||
}
|
||||
|
||||
_nameForServiceType(serviceType, host) {
|
||||
switch (serviceType) {
|
||||
case Matrix.SERVICE_TYPES.IS:
|
||||
return <div>{_t("Identity Server")}<br />({host})</div>;
|
||||
case Matrix.SERVICE_TYPES.IM:
|
||||
return <div>{_t("Integration Manager")}<br />({host})</div>;
|
||||
}
|
||||
}
|
||||
|
||||
_summaryForServiceType(serviceType) {
|
||||
switch (serviceType) {
|
||||
case Matrix.SERVICE_TYPES.IS:
|
||||
return <div>
|
||||
{_t("Find others by phone or email")}
|
||||
<br />
|
||||
{_t("Be found by phone or email")}
|
||||
</div>;
|
||||
case Matrix.SERVICE_TYPES.IM:
|
||||
return <div>
|
||||
{_t("Use bots, bridges, widgets and sticker packs")}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
_onTermsCheckboxChange = (url, checked) => {
|
||||
this.setState({
|
||||
agreedUrls: Object.assign({}, this.state.agreedUrls, { [url]: checked }),
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
const rows = [];
|
||||
for (const policiesAndService of this.props.policiesAndServicePairs) {
|
||||
const parsedBaseUrl = url.parse(policiesAndService.service.baseUrl);
|
||||
|
||||
const policyValues = Object.values(policiesAndService.policies);
|
||||
for (let i = 0; i < policyValues.length; ++i) {
|
||||
const termDoc = policyValues[i];
|
||||
const termsLang = pickBestLanguage(Object.keys(termDoc).filter((k) => k !== 'version'));
|
||||
let serviceName;
|
||||
let summary;
|
||||
if (i === 0) {
|
||||
serviceName = this._nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host);
|
||||
summary = this._summaryForServiceType(
|
||||
policiesAndService.service.serviceType,
|
||||
);
|
||||
}
|
||||
|
||||
rows.push(<tr key={termDoc[termsLang].url}>
|
||||
<td className="mx_TermsDialog_service">{serviceName}</td>
|
||||
<td className="mx_TermsDialog_summary">{summary}</td>
|
||||
<td>{termDoc[termsLang].name} <a rel="noopener" target="_blank" href={termDoc[termsLang].url}>
|
||||
<span className="mx_TermsDialog_link" />
|
||||
</a></td>
|
||||
<td><TermsCheckbox
|
||||
url={termDoc[termsLang].url}
|
||||
onChange={this._onTermsCheckboxChange}
|
||||
checked={Boolean(this.state.agreedUrls[termDoc[termsLang].url])}
|
||||
/></td>
|
||||
</tr>);
|
||||
}
|
||||
}
|
||||
|
||||
// if all the documents for at least one service have been checked, we can enable
|
||||
// the submit button
|
||||
let enableSubmit = false;
|
||||
for (const policiesAndService of this.props.policiesAndServicePairs) {
|
||||
let docsAgreedForService = 0;
|
||||
for (const terms of Object.values(policiesAndService.policies)) {
|
||||
let docAgreed = false;
|
||||
for (const lang of Object.keys(terms)) {
|
||||
if (lang === 'version') continue;
|
||||
if (this.state.agreedUrls[terms[lang].url]) {
|
||||
docAgreed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (docAgreed) {
|
||||
++docsAgreedForService;
|
||||
}
|
||||
}
|
||||
if (docsAgreedForService === Object.keys(policiesAndService.policies).length) {
|
||||
enableSubmit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
fixedWidth={false}
|
||||
onFinished={this._onCancelClick}
|
||||
title={_t("Terms of Service")}
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={false}
|
||||
>
|
||||
<div id='mx_Dialog_content'>
|
||||
<p>{_t("To continue you need to accept the terms of this service.")}</p>
|
||||
|
||||
<table className="mx_TermsDialog_termsTable"><tbody>
|
||||
<tr className="mx_TermsDialog_termsTableHeader">
|
||||
<th>{_t("Service")}</th>
|
||||
<th>{_t("Summary")}</th>
|
||||
<th>{_t("Document")}</th>
|
||||
<th>{_t("Accept")}</th>
|
||||
</tr>
|
||||
{rows}
|
||||
</tbody></table>
|
||||
</div>
|
||||
|
||||
<DialogButtons primaryButton={_t('Next')}
|
||||
hasCancel={true}
|
||||
onCancel={this._onCancelClick}
|
||||
onPrimaryButtonClick={this._onNextClick}
|
||||
primaryDisabled={!enableSubmit}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -15,10 +15,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
|
||||
export default React.createClass({
|
||||
export default createReactClass({
|
||||
displayName: 'TextInputDialog',
|
||||
propTypes: {
|
||||
title: PropTypes.string,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue