Merge remote-tracking branch 'upstream/develop' into hs/upload-limits
This commit is contained in:
commit
2b077b4f5d
142 changed files with 5235 additions and 671 deletions
|
@ -33,12 +33,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return({
|
||||
directoryHover : false,
|
||||
roomsHover : false,
|
||||
return ({
|
||||
directoryHover: false,
|
||||
roomsHover: false,
|
||||
homeHover: false,
|
||||
peopleHover : false,
|
||||
settingsHover : false,
|
||||
peopleHover: false,
|
||||
settingsHover: false,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -145,7 +145,7 @@ module.exports = React.createClass({
|
|||
// Get the label/tooltip to show
|
||||
getLabel: function(label, show) {
|
||||
if (show) {
|
||||
var RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
return <RoomTooltip className="mx_BottomLeftMenu_tooltip" label={label} />;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -16,18 +16,18 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'CompatibilityPage',
|
||||
propTypes: {
|
||||
onAccept: React.PropTypes.func
|
||||
onAccept: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onAccept: function() {} // NOP
|
||||
onAccept: function() {}, // NOP
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -36,7 +36,6 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
return (
|
||||
<div className="mx_CompatibilityPage">
|
||||
<div className="mx_CompatibilityPage_box">
|
||||
|
@ -69,5 +68,5 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -36,10 +36,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
phases: {
|
||||
CONFIG: "CONFIG", // We're waiting for user to configure and hit create.
|
||||
CREATING: "CREATING", // We're sending the request.
|
||||
CREATED: "CREATED", // We successfully created the room.
|
||||
ERROR: "ERROR", // There was an error while trying to create room.
|
||||
CONFIG: "CONFIG", // We're waiting for user to configure and hit create.
|
||||
CREATING: "CREATING", // We're sending the request.
|
||||
CREATED: "CREATED", // We successfully created the room.
|
||||
ERROR: "ERROR", // There was an error while trying to create room.
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
|
|
@ -746,14 +746,38 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_leaveGroupWarnings: function() {
|
||||
const warnings = [];
|
||||
|
||||
if (this.state.isUserPrivileged) {
|
||||
warnings.push((
|
||||
<span className="warning">
|
||||
{ " " /* Whitespace, otherwise the sentences get smashed together */ }
|
||||
{ _t("You are an administrator of this community. You will not be " +
|
||||
"able to rejoin without an invite from another administrator.") }
|
||||
</span>
|
||||
));
|
||||
}
|
||||
|
||||
return warnings;
|
||||
},
|
||||
|
||||
|
||||
_onLeaveClick: function() {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const warnings = this._leaveGroupWarnings();
|
||||
|
||||
Modal.createTrackedDialog('Leave Group', '', QuestionDialog, {
|
||||
title: _t("Leave Community"),
|
||||
description: _t("Leave %(groupName)s?", {groupName: this.props.groupId}),
|
||||
description: (
|
||||
<span>
|
||||
{ _t("Leave %(groupName)s?", {groupName: this.props.groupId}) }
|
||||
{ warnings }
|
||||
</span>
|
||||
),
|
||||
button: _t("Leave"),
|
||||
danger: true,
|
||||
onFinished: async (confirmed) => {
|
||||
danger: this.state.isUserPrivileged,
|
||||
onFinished: async(confirmed) => {
|
||||
if (!confirmed) return;
|
||||
|
||||
this.setState({membershipBusy: true});
|
||||
|
|
|
@ -23,6 +23,8 @@ import request from 'browser-request';
|
|||
import { _t } from '../../languageHandler';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import sdk from '../../index';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import dis from '../../dispatcher';
|
||||
|
||||
class HomePage extends React.Component {
|
||||
static displayName = 'HomePage';
|
||||
|
@ -37,6 +39,10 @@ class HomePage extends React.Component {
|
|||
homePageUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
};
|
||||
|
||||
state = {
|
||||
iframeSrc: '',
|
||||
page: '',
|
||||
|
@ -52,15 +58,14 @@ class HomePage extends React.Component {
|
|||
|
||||
if (this.props.teamToken && this.props.teamServerUrl) {
|
||||
this.setState({
|
||||
iframeSrc: `${this.props.teamServerUrl}/static/${this.props.teamToken}/home.html`
|
||||
iframeSrc: `${this.props.teamServerUrl}/static/${this.props.teamToken}/home.html`,
|
||||
});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// we use request() to inline the homepage into the react component
|
||||
// so that it can inherit CSS and theming easily rather than mess around
|
||||
// with iframes and trying to synchronise document.stylesheets.
|
||||
|
||||
let src = this.props.homePageUrl || 'home.html';
|
||||
const src = this.props.homePageUrl || 'home.html';
|
||||
|
||||
request(
|
||||
{ method: "GET", url: src },
|
||||
|
@ -77,7 +82,7 @@ class HomePage extends React.Component {
|
|||
|
||||
body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1));
|
||||
this.setState({ page: body });
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -86,18 +91,55 @@ class HomePage extends React.Component {
|
|||
this._unmounted = true;
|
||||
}
|
||||
|
||||
onLoginClick() {
|
||||
dis.dispatch({ action: 'start_login' });
|
||||
}
|
||||
|
||||
onRegisterClick() {
|
||||
dis.dispatch({ action: 'start_registration' });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.iframeSrc) {
|
||||
return (
|
||||
<div className="mx_HomePage">
|
||||
<iframe src={ this.state.iframeSrc } />
|
||||
let guestWarning = "";
|
||||
if (this.context.matrixClient.isGuest()) {
|
||||
guestWarning = (
|
||||
<div className="mx_HomePage_guest_warning">
|
||||
<img src="img/warning.svg" width="24" height="23" />
|
||||
<div>
|
||||
<div>
|
||||
{ _t("You are currently using Riot anonymously as a guest.") }
|
||||
</div>
|
||||
<div>
|
||||
{ _t(
|
||||
'If you would like to create a Matrix account you can <a>register</a> now.',
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#" onClick={this.onRegisterClick}>{ sub }</a> },
|
||||
) }
|
||||
</div>
|
||||
<div>
|
||||
{ _t(
|
||||
'If you already have a Matrix account you can <a>log in</a> instead.',
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#" onClick={this.onLoginClick}>{ sub }</a> },
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
|
||||
if (this.state.iframeSrc) {
|
||||
return (
|
||||
<div className="mx_HomePage">
|
||||
{ guestWarning }
|
||||
<iframe src={ this.state.iframeSrc } />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
return (
|
||||
<GeminiScrollbarWrapper autoshow={true} className="mx_HomePage">
|
||||
{ guestWarning }
|
||||
<div className="mx_HomePage_body" dangerouslySetInnerHTML={{ __html: this.state.page }}>
|
||||
</div>
|
||||
</GeminiScrollbarWrapper>
|
||||
|
@ -106,4 +148,4 @@ class HomePage extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = HomePage;
|
||||
module.exports = HomePage;
|
||||
|
|
|
@ -68,6 +68,11 @@ export default React.createClass({
|
|||
// If true, poll to see if the auth flow has been completed
|
||||
// out-of-band
|
||||
poll: PropTypes.bool,
|
||||
|
||||
// If true, components will be told that the 'Continue' button
|
||||
// is managed by some other party and should not be managed by
|
||||
// the component itself.
|
||||
continueIsManaged: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -128,6 +133,12 @@ export default React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
tryContinue: function() {
|
||||
if (this.refs.stageComponent && this.refs.stageComponent.tryContinue) {
|
||||
this.refs.stageComponent.tryContinue();
|
||||
}
|
||||
},
|
||||
|
||||
_authStateUpdated: function(stageType, stageState) {
|
||||
const oldStage = this.state.authStage;
|
||||
this.setState({
|
||||
|
@ -192,6 +203,7 @@ export default React.createClass({
|
|||
fail={this._onAuthStageFailed}
|
||||
setEmailSid={this._setEmailSid}
|
||||
makeRegistrationUrl={this.props.makeRegistrationUrl}
|
||||
showContinue={!this.props.continueIsManaged}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -28,7 +28,7 @@ import VectorConferenceHandler from '../../VectorConferenceHandler';
|
|||
import SettingsStore from '../../settings/SettingsStore';
|
||||
|
||||
|
||||
var LeftPanel = React.createClass({
|
||||
const LeftPanel = React.createClass({
|
||||
displayName: 'LeftPanel',
|
||||
|
||||
// NB. If you add props, don't forget to update
|
||||
|
@ -181,14 +181,8 @@ var LeftPanel = React.createClass({
|
|||
const BottomLeftMenu = sdk.getComponent('structures.BottomLeftMenu');
|
||||
const CallPreview = sdk.getComponent('voip.CallPreview');
|
||||
|
||||
let topBox;
|
||||
if (this.context.matrixClient.isGuest()) {
|
||||
const LoginBox = sdk.getComponent('structures.LoginBox');
|
||||
topBox = <LoginBox collapsed={ this.props.collapsed }/>;
|
||||
} else {
|
||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
||||
topBox = <SearchBox collapsed={ this.props.collapsed } onSearch={ this.onSearch } />;
|
||||
}
|
||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
||||
const topBox = <SearchBox collapsed={ this.props.collapsed } onSearch={ this.onSearch } />;
|
||||
|
||||
const classes = classNames(
|
||||
"mx_LeftPanel",
|
||||
|
@ -220,11 +214,11 @@ var LeftPanel = React.createClass({
|
|||
collapsed={this.props.collapsed}
|
||||
searchFilter={this.state.searchFilter}
|
||||
ConferenceHandler={VectorConferenceHandler} />
|
||||
<BottomLeftMenu collapsed={this.props.collapsed}/>
|
||||
<BottomLeftMenu collapsed={this.props.collapsed} />
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = LeftPanel;
|
||||
|
|
|
@ -64,6 +64,9 @@ const LoggedInView = React.createClass({
|
|||
|
||||
teamToken: PropTypes.string,
|
||||
|
||||
// Used by the RoomView to handle joining rooms
|
||||
viaServers: PropTypes.arrayOf(PropTypes.string),
|
||||
|
||||
// and lots and lots of other stuff.
|
||||
},
|
||||
|
||||
|
@ -186,13 +189,13 @@ const LoggedInView = React.createClass({
|
|||
_updateServerNoticeEvents: async function() {
|
||||
const roomLists = RoomListStore.getRoomLists();
|
||||
if (!roomLists['m.server_notice']) return [];
|
||||
|
||||
|
||||
const pinnedEvents = [];
|
||||
for (const room of roomLists['m.server_notice']) {
|
||||
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
|
||||
|
||||
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
|
||||
|
||||
|
||||
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
|
||||
for (const eventId of pinnedEventIds) {
|
||||
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
|
||||
|
@ -204,7 +207,7 @@ const LoggedInView = React.createClass({
|
|||
serverNoticeEvents: pinnedEvents,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
/*
|
||||
|
@ -389,6 +392,7 @@ const LoggedInView = React.createClass({
|
|||
onRegistered={this.props.onRegistered}
|
||||
thirdPartyInvite={this.props.thirdPartyInvite}
|
||||
oobData={this.props.roomOobData}
|
||||
viaServers={this.props.viaServers}
|
||||
eventPixelOffset={this.props.initialEventPixelOffset}
|
||||
key={this.props.currentRoomId || 'roomview'}
|
||||
disabled={this.props.middleDisabled}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -16,31 +17,15 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
import { _t } from '../../languageHandler';
|
||||
var sdk = require('../../index')
|
||||
var dis = require('../../dispatcher');
|
||||
var rate_limited_func = require('../../ratelimitedfunc');
|
||||
var AccessibleButton = require('../../components/views/elements/AccessibleButton');
|
||||
const dis = require('../../dispatcher');
|
||||
const AccessibleButton = require('../../components/views/elements/AccessibleButton');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'LoginBox',
|
||||
|
||||
propTypes: {
|
||||
collapsed: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
onToggleCollapse: function(show) {
|
||||
if (show) {
|
||||
dis.dispatch({
|
||||
action: 'show_left_panel',
|
||||
});
|
||||
}
|
||||
else {
|
||||
dis.dispatch({
|
||||
action: 'hide_left_panel',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onLoginClick: function() {
|
||||
|
@ -52,42 +37,21 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
|
||||
var toggleCollapse;
|
||||
if (this.props.collapsed) {
|
||||
toggleCollapse =
|
||||
<AccessibleButton className="mx_SearchBox_maximise" onClick={ this.onToggleCollapse.bind(this, true) }>
|
||||
<TintableSvg src="img/maximise.svg" width="10" height="16" alt="Expand panel"/>
|
||||
const loginButton = (
|
||||
<div className="mx_LoginBox_loginButton_wrapper">
|
||||
<AccessibleButton className="mx_LoginBox_loginButton" element="button" onClick={this.onLoginClick}>
|
||||
{ _t("Login") }
|
||||
</AccessibleButton>
|
||||
}
|
||||
else {
|
||||
toggleCollapse =
|
||||
<AccessibleButton className="mx_SearchBox_minimise" onClick={ this.onToggleCollapse.bind(this, false) }>
|
||||
<TintableSvg src="img/minimise.svg" width="10" height="16" alt="Collapse panel"/>
|
||||
<AccessibleButton className="mx_LoginBox_registerButton" element="button" onClick={this.onRegisterClick}>
|
||||
{ _t("Register") }
|
||||
</AccessibleButton>
|
||||
}
|
||||
|
||||
var loginButton;
|
||||
if (!this.props.collapsed) {
|
||||
loginButton = (
|
||||
<div className="mx_LoginBox_loginButton_wrapper">
|
||||
<AccessibleButton className="mx_LoginBox_loginButton" element="button" onClick={this.onLoginClick}>
|
||||
{ _t("Login") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_LoginBox_registerButton" element="button" onClick={this.onRegisterClick}>
|
||||
{ _t("Register") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return (
|
||||
<div className="mx_SearchBox mx_LoginBox">
|
||||
{ loginButton }
|
||||
{ toggleCollapse }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_LoginBox">
|
||||
{ loginButton }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -840,6 +840,7 @@ export default React.createClass({
|
|||
page_type: PageTypes.RoomView,
|
||||
thirdPartyInvite: roomInfo.third_party_invite,
|
||||
roomOobData: roomInfo.oob_data,
|
||||
viaServers: roomInfo.via_servers,
|
||||
};
|
||||
|
||||
if (roomInfo.room_alias) {
|
||||
|
@ -1034,6 +1035,7 @@ export default React.createClass({
|
|||
{ warnings }
|
||||
</span>
|
||||
),
|
||||
button: _t("Leave"),
|
||||
onFinished: (shouldLeave) => {
|
||||
if (shouldLeave) {
|
||||
const d = MatrixClientPeg.get().leave(roomId);
|
||||
|
@ -1373,6 +1375,7 @@ export default React.createClass({
|
|||
cli.on("crypto.roomKeyRequestCancellation", (req) => {
|
||||
krh.handleKeyRequestCancellation(req);
|
||||
});
|
||||
|
||||
cli.on("Room", (room) => {
|
||||
if (MatrixClientPeg.get().isCryptoEnabled()) {
|
||||
const blacklistEnabled = SettingsStore.getValueAt(
|
||||
|
@ -1403,6 +1406,11 @@ export default React.createClass({
|
|||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Fire the tinter right on startup to ensure the default theme is applied
|
||||
// A later sync can/will correct the tint to be the right value for the user
|
||||
const colorScheme = SettingsStore.getValue("roomColor");
|
||||
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1483,9 +1491,21 @@ export default React.createClass({
|
|||
inviterName: params.inviter_name,
|
||||
};
|
||||
|
||||
// on our URLs there might be a ?via=matrix.org or similar to help
|
||||
// joins to the room succeed. We'll pass these through as an array
|
||||
// to other levels. If there's just one ?via= then params.via is a
|
||||
// single string. If someone does something like ?via=one.com&via=two.com
|
||||
// then params.via is an array of strings.
|
||||
let via = [];
|
||||
if (params.via) {
|
||||
if (typeof(params.via) === 'string') via = [params.via];
|
||||
else via = params.via;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
action: 'view_room',
|
||||
event_id: eventId,
|
||||
via_servers: via,
|
||||
// If an event ID is given in the URL hash, notify RoomViewStore to mark
|
||||
// it as highlighted, which will propagate to RoomView and highlight the
|
||||
// associated EventTile.
|
||||
|
|
|
@ -16,18 +16,18 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
|
||||
var MatrixClientPeg = require('../../MatrixClientPeg');
|
||||
var ContentRepo = require("matrix-js-sdk").ContentRepo;
|
||||
var Modal = require('../../Modal');
|
||||
var sdk = require('../../index');
|
||||
var dis = require('../../dispatcher');
|
||||
const MatrixClientPeg = require('../../MatrixClientPeg');
|
||||
const ContentRepo = require("matrix-js-sdk").ContentRepo;
|
||||
const Modal = require('../../Modal');
|
||||
const sdk = require('../../index');
|
||||
const dis = require('../../dispatcher');
|
||||
|
||||
var linkify = require('linkifyjs');
|
||||
var linkifyString = require('linkifyjs/string');
|
||||
var linkifyMatrix = require('../../linkify-matrix');
|
||||
var sanitizeHtml = require('sanitize-html');
|
||||
const linkify = require('linkifyjs');
|
||||
const linkifyString = require('linkifyjs/string');
|
||||
const linkifyMatrix = require('../../linkify-matrix');
|
||||
const sanitizeHtml = require('sanitize-html');
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import { _t } from '../../languageHandler';
|
||||
|
@ -46,7 +46,7 @@ module.exports = React.createClass({
|
|||
getDefaultProps: function() {
|
||||
return {
|
||||
config: {},
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -58,7 +58,7 @@ module.exports = React.createClass({
|
|||
includeAll: false,
|
||||
roomServer: null,
|
||||
filterString: null,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -134,13 +134,12 @@ module.exports = React.createClass({
|
|||
opts.include_all_networks = true;
|
||||
}
|
||||
if (this.nextBatch) opts.since = this.nextBatch;
|
||||
if (my_filter_string) opts.filter = { generic_search_term: my_filter_string } ;
|
||||
if (my_filter_string) opts.filter = { generic_search_term: my_filter_string };
|
||||
return MatrixClientPeg.get().publicRooms(opts).then((data) => {
|
||||
if (
|
||||
my_filter_string != this.state.filterString ||
|
||||
my_server != this.state.roomServer ||
|
||||
my_next_batch != this.nextBatch)
|
||||
{
|
||||
my_next_batch != this.nextBatch) {
|
||||
// if the filter or server has changed since this request was sent,
|
||||
// throw away the result (don't even clear the busy flag
|
||||
// since we must still have a request in flight)
|
||||
|
@ -163,8 +162,7 @@ module.exports = React.createClass({
|
|||
if (
|
||||
my_filter_string != this.state.filterString ||
|
||||
my_server != this.state.roomServer ||
|
||||
my_next_batch != this.nextBatch)
|
||||
{
|
||||
my_next_batch != this.nextBatch) {
|
||||
// as above: we don't care about errors for old
|
||||
// requests either
|
||||
return;
|
||||
|
@ -177,10 +175,10 @@ module.exports = React.createClass({
|
|||
|
||||
this.setState({ loading: false });
|
||||
console.error("Failed to get publicRooms: %s", JSON.stringify(err));
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to get public room list', '', ErrorDialog, {
|
||||
title: _t('Failed to get public room list'),
|
||||
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded'))
|
||||
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded')),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -193,13 +191,13 @@ module.exports = React.createClass({
|
|||
* this needs SPEC-417.
|
||||
*/
|
||||
removeFromDirectory: function(room) {
|
||||
var alias = get_display_alias_for_room(room);
|
||||
var name = room.name || alias || _t('Unnamed room');
|
||||
const alias = get_display_alias_for_room(room);
|
||||
const name = room.name || alias || _t('Unnamed room');
|
||||
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
var desc;
|
||||
let desc;
|
||||
if (alias) {
|
||||
desc = _t('Delete the room alias %(alias)s and remove %(name)s from the directory?', {alias: alias, name: name});
|
||||
} else {
|
||||
|
@ -212,9 +210,9 @@ module.exports = React.createClass({
|
|||
onFinished: (should_delete) => {
|
||||
if (!should_delete) return;
|
||||
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
var modal = Modal.createDialog(Loader);
|
||||
var step = _t('remove %(name)s from the directory.', {name: name});
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const modal = Modal.createDialog(Loader);
|
||||
let step = _t('remove %(name)s from the directory.', {name: name});
|
||||
|
||||
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
|
||||
if (!alias) return;
|
||||
|
@ -229,10 +227,10 @@ module.exports = React.createClass({
|
|||
console.error("Failed to " + step + ": " + err);
|
||||
Modal.createTrackedDialog('Remove from Directory Error', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded'))
|
||||
description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded')),
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -347,7 +345,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
showRoom: function(room, room_alias) {
|
||||
var payload = {action: 'view_room'};
|
||||
const payload = {action: 'view_room'};
|
||||
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
|
||||
|
@ -383,16 +381,16 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getRows: function() {
|
||||
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
|
||||
if (!this.state.publicRooms) return [];
|
||||
|
||||
var rooms = this.state.publicRooms;
|
||||
var rows = [];
|
||||
var self = this;
|
||||
var guestRead, guestJoin, perms;
|
||||
for (var i = 0; i < rooms.length; i++) {
|
||||
var name = rooms[i].name || get_display_alias_for_room(rooms[i]) || _t('Unnamed room');
|
||||
const rooms = this.state.publicRooms;
|
||||
const rows = [];
|
||||
const self = this;
|
||||
let guestRead; let guestJoin; let perms;
|
||||
for (let i = 0; i < rooms.length; i++) {
|
||||
const name = rooms[i].name || get_display_alias_for_room(rooms[i]) || _t('Unnamed room');
|
||||
guestRead = null;
|
||||
guestJoin = null;
|
||||
|
||||
|
@ -412,7 +410,7 @@ module.exports = React.createClass({
|
|||
perms = <div className="mx_RoomDirectory_perms">{guestRead}{guestJoin}</div>;
|
||||
}
|
||||
|
||||
var topic = rooms[i].topic || '';
|
||||
let topic = rooms[i].topic || '';
|
||||
topic = linkifyString(sanitizeHtml(topic));
|
||||
|
||||
rows.push(
|
||||
|
@ -432,14 +430,14 @@ module.exports = React.createClass({
|
|||
<div className="mx_RoomDirectory_name">{ name }</div>
|
||||
{ perms }
|
||||
<div className="mx_RoomDirectory_topic"
|
||||
onClick={ function(e) { e.stopPropagation() } }
|
||||
dangerouslySetInnerHTML={{ __html: 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>
|
||||
</tr>,
|
||||
);
|
||||
}
|
||||
return rows;
|
||||
|
@ -524,7 +522,7 @@ module.exports = React.createClass({
|
|||
onFillRequest={ this.onFillRequest }
|
||||
stickyBottom={false}
|
||||
startAtBottom={false}
|
||||
onResize={function(){}}
|
||||
onResize={function() {}}
|
||||
>
|
||||
{ scrollpanel_content }
|
||||
</ScrollPanel>;
|
||||
|
@ -577,11 +575,11 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
|
||||
// but works with the objects we get from the public room list
|
||||
function get_display_alias_for_room(room) {
|
||||
return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
|
||||
return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ function getUnsentMessages(room) {
|
|||
return room.getPendingEvents().filter(function(ev) {
|
||||
return ev.status === Matrix.EventStatus.NOT_SENT;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomStatusBar',
|
||||
|
@ -66,6 +66,10 @@ module.exports = React.createClass({
|
|||
// result in "X, Y, Z and 100 others are typing."
|
||||
whoIsTypingLimit: PropTypes.number,
|
||||
|
||||
// true if the room is being peeked at. This affects components that shouldn't
|
||||
// logically be shown when peeking, such as a prompt to invite people to a room.
|
||||
isPeeking: PropTypes.bool,
|
||||
|
||||
// callback for when the user clicks on the 'resend all' button in the
|
||||
// 'unsent messages' bar
|
||||
onResendAllClick: PropTypes.func,
|
||||
|
@ -299,7 +303,7 @@ module.exports = React.createClass({
|
|||
const errorIsMauError = Boolean(
|
||||
this.state.syncStateData &&
|
||||
this.state.syncStateData.error &&
|
||||
this.state.syncStateData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED'
|
||||
this.state.syncStateData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED',
|
||||
);
|
||||
return this.state.syncState === "ERROR" && !errorIsMauError;
|
||||
},
|
||||
|
@ -457,7 +461,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
// If you're alone in the room, and have sent a message, suggest to invite someone
|
||||
if (this.props.sentMessageAndIsAlone) {
|
||||
if (this.props.sentMessageAndIsAlone && !this.props.isPeeking) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_isAlone">
|
||||
{ _t("There's no one else here! Would you like to <inviteText>invite others</inviteText> " +
|
||||
|
|
|
@ -89,6 +89,9 @@ module.exports = React.createClass({
|
|||
|
||||
// is the RightPanel collapsed?
|
||||
collapsedRhs: PropTypes.bool,
|
||||
|
||||
// Servers the RoomView can use to try and assist joins
|
||||
viaServers: PropTypes.arrayOf(PropTypes.string),
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -708,8 +711,8 @@ module.exports = React.createClass({
|
|||
if (!room) return;
|
||||
|
||||
console.log("Tinter.tint from updateTint");
|
||||
const color_scheme = SettingsStore.getValue("roomColor", room.roomId);
|
||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
const colorScheme = SettingsStore.getValue("roomColor", room.roomId);
|
||||
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
|
||||
},
|
||||
|
||||
onAccountData: function(event) {
|
||||
|
@ -724,10 +727,10 @@ module.exports = React.createClass({
|
|||
if (room.roomId == this.state.roomId) {
|
||||
const type = event.getType();
|
||||
if (type === "org.matrix.room.color_scheme") {
|
||||
const color_scheme = event.getContent();
|
||||
const colorScheme = event.getContent();
|
||||
// XXX: we should validate the event
|
||||
console.log("Tinter.tint from onRoomAccountData");
|
||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
|
||||
} else if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
|
||||
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
|
||||
this._updatePreviewUrlVisibility(room);
|
||||
|
@ -863,7 +866,7 @@ module.exports = React.createClass({
|
|||
action: 'do_after_sync_prepared',
|
||||
deferred_action: {
|
||||
action: 'join_room',
|
||||
opts: { inviteSignUrl: signUrl },
|
||||
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -905,7 +908,7 @@ module.exports = React.createClass({
|
|||
this.props.thirdPartyInvite.inviteSignUrl : undefined;
|
||||
dis.dispatch({
|
||||
action: 'join_room',
|
||||
opts: { inviteSignUrl: signUrl },
|
||||
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
|
||||
});
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
@ -1553,6 +1556,7 @@ module.exports = React.createClass({
|
|||
canPreview={false} error={this.state.roomLoadError}
|
||||
roomAlias={roomAlias}
|
||||
spinner={this.state.joining}
|
||||
spinnerState="joining"
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
room={this.state.room}
|
||||
|
@ -1597,6 +1601,7 @@ module.exports = React.createClass({
|
|||
inviterName={inviterName}
|
||||
canPreview={false}
|
||||
spinner={this.state.joining}
|
||||
spinnerState="joining"
|
||||
room={this.state.room}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1634,6 +1639,7 @@ module.exports = React.createClass({
|
|||
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
|
||||
sentMessageAndIsAlone={this.state.isAlone}
|
||||
hasActiveCall={inCall}
|
||||
isPeeking={myMembership !== "join"}
|
||||
onInviteClick={this.onInviteButtonClick}
|
||||
onStopWarningClick={this.onStopAloneWarningClick}
|
||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||
|
@ -1683,6 +1689,7 @@ module.exports = React.createClass({
|
|||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
spinner={this.state.joining}
|
||||
spinnerState="joining"
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
canPreview={this.state.canPeek}
|
||||
|
@ -1705,10 +1712,10 @@ module.exports = React.createClass({
|
|||
</AuxPanel>
|
||||
);
|
||||
|
||||
let messageComposer, searchInfo;
|
||||
let messageComposer; let searchInfo;
|
||||
const canSpeak = (
|
||||
// joined and not showing search results
|
||||
myMembership == 'join' && !this.state.searchResults
|
||||
myMembership === 'join' && !this.state.searchResults
|
||||
);
|
||||
if (canSpeak) {
|
||||
messageComposer =
|
||||
|
@ -1723,6 +1730,11 @@ module.exports = React.createClass({
|
|||
/>;
|
||||
}
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
const LoginBox = sdk.getComponent('structures.LoginBox');
|
||||
messageComposer = <LoginBox />;
|
||||
}
|
||||
|
||||
// TODO: Why aren't we storing the term/scope/count in this format
|
||||
// in this.state if this is what RoomHeader desires?
|
||||
if (this.state.searchResults) {
|
||||
|
@ -1734,7 +1746,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
if (inCall) {
|
||||
let zoomButton, voiceMuteButton, videoMuteButton;
|
||||
let zoomButton; let voiceMuteButton; let videoMuteButton;
|
||||
|
||||
if (call.type === "video") {
|
||||
zoomButton = (
|
||||
|
|
|
@ -72,7 +72,7 @@ module.exports = React.createClass({
|
|||
function() {
|
||||
this.props.onSearch(this.refs.search.value);
|
||||
},
|
||||
100
|
||||
100,
|
||||
),
|
||||
|
||||
onToggleCollapse: function(show) {
|
||||
|
@ -80,8 +80,7 @@ module.exports = React.createClass({
|
|||
dis.dispatch({
|
||||
action: 'show_left_panel',
|
||||
});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
dis.dispatch({
|
||||
action: 'hide_left_panel',
|
||||
});
|
||||
|
@ -103,25 +102,24 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
|
||||
var collapseTabIndex = this.refs.search && this.refs.search.value !== "" ? "-1" : "0";
|
||||
const collapseTabIndex = this.refs.search && this.refs.search.value !== "" ? "-1" : "0";
|
||||
|
||||
var toggleCollapse;
|
||||
let toggleCollapse;
|
||||
if (this.props.collapsed) {
|
||||
toggleCollapse =
|
||||
<AccessibleButton className="mx_SearchBox_maximise" tabIndex={collapseTabIndex} onClick={ this.onToggleCollapse.bind(this, true) }>
|
||||
<TintableSvg src="img/maximise.svg" width="10" height="16" alt={ _t("Expand panel") }/>
|
||||
</AccessibleButton>
|
||||
}
|
||||
else {
|
||||
<TintableSvg src="img/maximise.svg" width="10" height="16" alt={ _t("Expand panel") } />
|
||||
</AccessibleButton>;
|
||||
} else {
|
||||
toggleCollapse =
|
||||
<AccessibleButton className="mx_SearchBox_minimise" tabIndex={collapseTabIndex} onClick={ this.onToggleCollapse.bind(this, false) }>
|
||||
<TintableSvg src="img/minimise.svg" width="10" height="16" alt={ _t("Collapse panel") }/>
|
||||
</AccessibleButton>
|
||||
<TintableSvg src="img/minimise.svg" width="10" height="16" alt={ _t("Collapse panel") } />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
var searchControls;
|
||||
let searchControls;
|
||||
if (!this.props.collapsed) {
|
||||
searchControls = [
|
||||
this.state.searchTerm.length > 0 ?
|
||||
|
@ -148,16 +146,16 @@ module.exports = React.createClass({
|
|||
onChange={ this.onChange }
|
||||
onKeyDown={ this._onKeyDown }
|
||||
placeholder={ _t('Filter room names') }
|
||||
/>
|
||||
/>,
|
||||
];
|
||||
}
|
||||
|
||||
var self = this;
|
||||
const self = this;
|
||||
return (
|
||||
<div className="mx_SearchBox">
|
||||
{ searchControls }
|
||||
{ toggleCollapse }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -829,7 +829,7 @@ var TimelinePanel = React.createClass({
|
|||
// 4. Also, if pos === null, the event might not be paginated - show the unread bar
|
||||
const pos = this.getReadMarkerPosition();
|
||||
return this.state.readMarkerEventId !== null && // 1.
|
||||
this.state.readMarkerEventId !== this._getCurrentReadReceipt() && // 2.
|
||||
this.state.readMarkerEventId !== this._getCurrentReadReceipt() && // 2.
|
||||
(pos < 0 || pos === null); // 3., 4.
|
||||
},
|
||||
|
||||
|
|
|
@ -82,6 +82,9 @@ const SIMPLE_SETTINGS = [
|
|||
{ id: "TagPanel.disableTagPanel" },
|
||||
{ id: "enableWidgetScreenshots" },
|
||||
{ id: "RoomSubList.showEmpty" },
|
||||
{ id: "pinMentionedRooms" },
|
||||
{ id: "pinUnreadRooms" },
|
||||
{ id: "showDeveloperTools" },
|
||||
];
|
||||
|
||||
// These settings must be defined in SettingsStore
|
||||
|
@ -586,23 +589,21 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_onExportE2eKeysClicked: function() {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', '', (cb) => {
|
||||
require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
||||
cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
||||
}, "e2e-export");
|
||||
}, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
});
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
||||
import('../../async-components/views/dialogs/ExportE2eKeysDialog'),
|
||||
{
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
_onImportE2eKeysClicked: function() {
|
||||
Modal.createTrackedDialogAsync('Import E2E Keys', '', (cb) => {
|
||||
require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => {
|
||||
cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog'));
|
||||
}, "e2e-export");
|
||||
}, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
});
|
||||
Modal.createTrackedDialogAsync('Import E2E Keys', '',
|
||||
import('../../async-components/views/dialogs/ImportE2eKeysDialog'),
|
||||
{
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
_renderGroupSettings: function() {
|
||||
|
@ -736,6 +737,16 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let keyBackupSection;
|
||||
if (SettingsStore.isFeatureEnabled("feature_keybackup")) {
|
||||
const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
|
||||
keyBackupSection = <div className="mx_UserSettings_section">
|
||||
<h3>{ _t("Key Backup") }</h3>
|
||||
<KeyBackupPanel />
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>{ _t("Cryptography") }</h3>
|
||||
|
@ -751,6 +762,7 @@ module.exports = React.createClass({
|
|||
<div className="mx_UserSettings_section">
|
||||
{ CRYPTO_SETTINGS.map( this._renderDeviceSetting ) }
|
||||
</div>
|
||||
{keyBackupSection}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -844,7 +856,7 @@ module.exports = React.createClass({
|
|||
SettingsStore.getLabsFeatures().forEach((featureId) => {
|
||||
// TODO: this ought to be a separate component so that we don't need
|
||||
// to rebind the onChange each time we render
|
||||
const onChange = async (e) => {
|
||||
const onChange = async(e) => {
|
||||
const checked = e.target.checked;
|
||||
if (featureId === "feature_lazyloading") {
|
||||
const confirmed = await this._onLazyLoadChanging(checked);
|
||||
|
@ -1297,7 +1309,7 @@ module.exports = React.createClass({
|
|||
// If the olmVersion is not defined then either crypto is disabled, or
|
||||
// we are using a version old version of olm. We assume the former.
|
||||
let olmVersionString = "<not-enabled>";
|
||||
if (olmVersion !== undefined) {
|
||||
if (olmVersion) {
|
||||
olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -53,5 +53,5 @@ module.exports = React.createClass({
|
|||
</SyntaxHighlight>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -121,13 +121,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_onExportE2eKeysClicked: function() {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', 'Forgot Password', (cb) => {
|
||||
require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
||||
cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
||||
}, "e2e-export");
|
||||
}, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
});
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', 'Forgot Password',
|
||||
import('../../../async-components/views/dialogs/ExportE2eKeysDialog'),
|
||||
{
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
onInputChanged: function(stateKey, ev) {
|
||||
|
|
|
@ -26,6 +26,7 @@ import Login from '../../../Login';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
import request from 'browser-request';
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
|
@ -74,6 +75,11 @@ module.exports = React.createClass({
|
|||
phoneCountry: null,
|
||||
phoneNumber: "",
|
||||
currentFlow: "m.login.password",
|
||||
|
||||
// .well-known discovery
|
||||
discoveredHsUrl: "",
|
||||
discoveredIsUrl: "",
|
||||
discoveryError: "",
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -84,7 +90,10 @@ module.exports = React.createClass({
|
|||
// letting you do that login type
|
||||
this._stepRendererMap = {
|
||||
'm.login.password': this._renderPasswordStep,
|
||||
'm.login.cas': this._renderCasStep,
|
||||
|
||||
// CAS and SSO are the same thing, modulo the url we link to
|
||||
'm.login.cas': () => this._renderSsoStep(this._loginLogic.getSsoLoginUrl("cas")),
|
||||
'm.login.sso': () => this._renderSsoStep(this._loginLogic.getSsoLoginUrl("sso")),
|
||||
};
|
||||
|
||||
this._initLoginLogic();
|
||||
|
@ -102,6 +111,10 @@ 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) return;
|
||||
|
||||
this.setState({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
|
@ -186,10 +199,6 @@ module.exports = React.createClass({
|
|||
}).done();
|
||||
},
|
||||
|
||||
onCasLogin: function() {
|
||||
this._loginLogic.redirectToCas();
|
||||
},
|
||||
|
||||
_onLoginAsGuestClick: function() {
|
||||
const self = this;
|
||||
self.setState({
|
||||
|
@ -222,6 +231,22 @@ module.exports = React.createClass({
|
|||
this.setState({ username: username });
|
||||
},
|
||||
|
||||
onUsernameBlur: function(username) {
|
||||
this.setState({ username: username });
|
||||
if (username[0] === "@") {
|
||||
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);
|
||||
} catch (e) {
|
||||
console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e);
|
||||
this.setState({discoveryError: _t("Failed to perform homeserver discovery")});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onPhoneCountryChanged: function(phoneCountry) {
|
||||
this.setState({ phoneCountry: phoneCountry });
|
||||
},
|
||||
|
@ -257,6 +282,122 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_tryWellKnownDiscovery: async function(serverName) {
|
||||
if (!serverName.trim()) {
|
||||
// Nothing to discover
|
||||
this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: ""});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const wellknown = await this._getWellKnownObject(`https://${serverName}/.well-known/matrix/client`);
|
||||
if (!wellknown["m.homeserver"]) {
|
||||
console.error("No m.homeserver key in well-known response");
|
||||
this.setState({discoveryError: _t("Invalid homeserver discovery response")});
|
||||
return;
|
||||
}
|
||||
|
||||
const hsUrl = this._sanitizeWellKnownUrl(wellknown["m.homeserver"]["base_url"]);
|
||||
if (!hsUrl) {
|
||||
console.error("Invalid base_url for m.homeserver");
|
||||
this.setState({discoveryError: _t("Invalid homeserver discovery response")});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Verifying homeserver URL: " + hsUrl);
|
||||
const hsVersions = await this._getWellKnownObject(`${hsUrl}/_matrix/client/versions`);
|
||||
if (!hsVersions["versions"]) {
|
||||
console.error("Invalid /versions response");
|
||||
this.setState({discoveryError: _t("Invalid homeserver discovery response")});
|
||||
return;
|
||||
}
|
||||
|
||||
let isUrl = "";
|
||||
if (wellknown["m.identity_server"]) {
|
||||
isUrl = this._sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]);
|
||||
if (!isUrl) {
|
||||
console.error("Invalid base_url for m.identity_server");
|
||||
this.setState({discoveryError: _t("Invalid homeserver discovery response")});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Verifying identity server URL: " + isUrl);
|
||||
const isResponse = await this._getWellKnownObject(`${isUrl}/_matrix/identity/api/v1`);
|
||||
if (!isResponse) {
|
||||
console.error("Invalid /api/v1 response");
|
||||
this.setState({discoveryError: _t("Invalid homeserver discovery response")});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({discoveredHsUrl: hsUrl, discoveredIsUrl: isUrl, discoveryError: ""});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e.wkAction) {
|
||||
if (e.wkAction === "FAIL_ERROR" || e.wkAction === "FAIL_PROMPT") {
|
||||
// We treat FAIL_ERROR and FAIL_PROMPT the same to avoid having the user
|
||||
// submit their details to the wrong homeserver. In practice, the custom
|
||||
// server options will show up to try and guide the user into entering
|
||||
// the required information.
|
||||
this.setState({discoveryError: _t("Cannot find homeserver")});
|
||||
return;
|
||||
} else if (e.wkAction === "IGNORE") {
|
||||
// Nothing to discover
|
||||
this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: ""});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
_sanitizeWellKnownUrl: function(url) {
|
||||
if (!url) return false;
|
||||
|
||||
const parser = document.createElement('a');
|
||||
parser.href = url;
|
||||
|
||||
if (parser.protocol !== "http:" && parser.protocol !== "https:") return false;
|
||||
if (!parser.hostname) return false;
|
||||
|
||||
const port = parser.port ? `:${parser.port}` : "";
|
||||
const path = parser.pathname ? parser.pathname : "";
|
||||
let saferUrl = `${parser.protocol}//${parser.hostname}${port}${path}`;
|
||||
if (saferUrl.endsWith("/")) saferUrl = saferUrl.substring(0, saferUrl.length - 1);
|
||||
return saferUrl;
|
||||
},
|
||||
|
||||
_getWellKnownObject: function(url) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
request(
|
||||
{ method: "GET", url: url },
|
||||
(err, response, body) => {
|
||||
if (err || response.status < 200 || response.status >= 300) {
|
||||
let action = "FAIL_ERROR";
|
||||
if (response.status === 404) {
|
||||
// We could just resolve with an empty object, but that
|
||||
// causes a different series of branches when the m.homeserver
|
||||
// bit of the JSON is missing.
|
||||
action = "IGNORE";
|
||||
}
|
||||
reject({err: err, response: response, wkAction: action});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
resolve(JSON.parse(body));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e.name === "SyntaxError") {
|
||||
reject({wkAction: "FAIL_PROMPT", wkError: "Invalid JSON"});
|
||||
} else throw e;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
_initLoginLogic: function(hsUrl, isUrl) {
|
||||
const self = this;
|
||||
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
|
||||
|
@ -394,6 +535,7 @@ module.exports = React.createClass({
|
|||
initialPhoneCountry={this.state.phoneCountry}
|
||||
initialPhoneNumber={this.state.phoneNumber}
|
||||
onUsernameChanged={this.onUsernameChanged}
|
||||
onUsernameBlur={this.onUsernameBlur}
|
||||
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
||||
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
|
@ -403,10 +545,9 @@ module.exports = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
_renderCasStep: function() {
|
||||
const CasLogin = sdk.getComponent('login.CasLogin');
|
||||
_renderSsoStep: function(url) {
|
||||
return (
|
||||
<CasLogin onSubmit={this.onCasLogin} />
|
||||
<a href={url} className="mx_Login_sso_link">{ _t('Sign in with single sign-on') }</a>
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -418,6 +559,8 @@ module.exports = React.createClass({
|
|||
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
|
||||
const errorText = this.state.discoveryError || this.state.errorText;
|
||||
|
||||
let loginAsGuestJsx;
|
||||
if (this.props.enableGuest) {
|
||||
loginAsGuestJsx =
|
||||
|
@ -432,8 +575,8 @@ module.exports = React.createClass({
|
|||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
serverConfig = <ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
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}
|
||||
|
@ -445,16 +588,16 @@ module.exports = React.createClass({
|
|||
if (theme !== "status") {
|
||||
header = <h2>{ _t('Sign in') } { loader }</h2>;
|
||||
} else {
|
||||
if (!this.state.errorText) {
|
||||
if (!errorText) {
|
||||
header = <h2>{ _t('Sign in to get started') } { loader }</h2>;
|
||||
}
|
||||
}
|
||||
|
||||
let errorTextSection;
|
||||
if (this.state.errorText) {
|
||||
if (errorText) {
|
||||
errorTextSection = (
|
||||
<div className="mx_Login_error">
|
||||
{ this.state.errorText }
|
||||
{ errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ module.exports = React.createClass({
|
|||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
let {member, fallbackUserId, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||
let userId = member ? member.userId : fallbackUserId;
|
||||
const userId = member ? member.userId : fallbackUserId;
|
||||
|
||||
if (viewUserOnClick) {
|
||||
onClick = () => {
|
||||
|
|
|
@ -48,7 +48,7 @@ export default class GroupInviteTileContextMenu extends React.Component {
|
|||
Modal.createTrackedDialog('Reject community invite', '', QuestionDialog, {
|
||||
title: _t('Reject invitation'),
|
||||
description: _t('Are you sure you want to reject the invitation?'),
|
||||
onFinished: async (shouldLeave) => {
|
||||
onFinished: async(shouldLeave) => {
|
||||
if (!shouldLeave) return;
|
||||
|
||||
// FIXME: controller shouldn't be loading a view :(
|
||||
|
|
|
@ -31,13 +31,13 @@ export default class ChangelogDialog extends React.Component {
|
|||
componentDidMount() {
|
||||
const version = this.props.newVersion.split('-');
|
||||
const version2 = this.props.version.split('-');
|
||||
if(version == null || version2 == null) return;
|
||||
if (version == null || version2 == null) return;
|
||||
// parse versions of form: [vectorversion]-react-[react-sdk-version]-js-[js-sdk-version]
|
||||
for(let i=0; i<REPOS.length; i++) {
|
||||
for (let i=0; i<REPOS.length; i++) {
|
||||
const oldVersion = version2[2*i];
|
||||
const newVersion = version[2*i];
|
||||
request(`https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`, (a, b, body) => {
|
||||
if(body == null) return;
|
||||
if (body == null) return;
|
||||
this.setState({[REPOS[i]]: JSON.parse(body).commits});
|
||||
});
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ export default class ChangelogDialog extends React.Component {
|
|||
{this.state[repo].map(this._elementsForCommit)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const content = (
|
||||
|
@ -83,7 +83,7 @@ export default class ChangelogDialog extends React.Component {
|
|||
button={_t("Update")}
|
||||
onFinished={this.props.onFinished}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ 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);
|
||||
|
|
|
@ -57,7 +57,7 @@ export default React.createClass({
|
|||
let error = null;
|
||||
if (!this.state.groupId) {
|
||||
error = _t("Community IDs cannot be empty.");
|
||||
} else if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
|
||||
} else if (!/^[a-z0-9=_\-./]*$/.test(this.state.groupId)) {
|
||||
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
|
||||
}
|
||||
this.setState({
|
||||
|
|
71
src/components/views/dialogs/CryptoStoreTooNewDialog.js
Normal file
71
src/components/views/dialogs/CryptoStoreTooNewDialog.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
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 sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
export default (props) => {
|
||||
const _onLogoutClicked = () => {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Logout e2e db too new', '', QuestionDialog, {
|
||||
title: _t("Sign out"),
|
||||
description: _t(
|
||||
"To avoid losing your chat history, you must export your room keys " +
|
||||
"before logging out. You will need to go back to the newer version of " +
|
||||
"Riot to do this",
|
||||
),
|
||||
button: _t("Sign out"),
|
||||
focus: false,
|
||||
onFinished: (doLogout) => {
|
||||
if (doLogout) {
|
||||
dis.dispatch({action: 'logout'});
|
||||
props.onFinished();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const description =
|
||||
_t("You've previously used a newer version of Riot on %(host)s. " +
|
||||
"To use this version again with end to end encryption, you will " +
|
||||
"need to sign out and back in again. ",
|
||||
{host: props.host},
|
||||
);
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (<BaseDialog className="mx_CryptoStoreTooNewDialog"
|
||||
contentId='mx_Dialog_content'
|
||||
title={_t("Incompatible Database")}
|
||||
hasCancel={false}
|
||||
onFinished={props.onFinished}
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ description }
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t('Continue With Encryption Disabled')}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={props.onFinished}
|
||||
>
|
||||
<button onClick={_onLogoutClicked} >
|
||||
{ _t('Sign out') }
|
||||
</button>
|
||||
</DialogButtons>
|
||||
</BaseDialog>);
|
||||
};
|
|
@ -101,6 +101,9 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
onSubmit: function(ev) {
|
||||
if (this.refs.uiAuth) {
|
||||
this.refs.uiAuth.tryContinue();
|
||||
}
|
||||
this.setState({
|
||||
doingUIAuth: true,
|
||||
});
|
||||
|
@ -217,6 +220,8 @@ export default React.createClass({
|
|||
onAuthFinished={this._onUIAuthFinished}
|
||||
inputs={{}}
|
||||
poll={true}
|
||||
ref="uiAuth"
|
||||
continueIsManaged={true}
|
||||
/>;
|
||||
}
|
||||
const inputClasses = classnames({
|
||||
|
|
309
src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js
Normal file
309
src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js
Normal file
|
@ -0,0 +1,309 @@
|
|||
/*
|
||||
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 sdk from '../../../../index';
|
||||
import MatrixClientPeg from '../../../../MatrixClientPeg';
|
||||
import Modal from '../../../../Modal';
|
||||
|
||||
import { _t } from '../../../../languageHandler';
|
||||
|
||||
/**
|
||||
* Dialog for restoring e2e keys from a backup and the user's recovery key
|
||||
*/
|
||||
export default React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
backupInfo: null,
|
||||
loading: false,
|
||||
loadError: null,
|
||||
restoreError: null,
|
||||
recoveryKey: "",
|
||||
recoverInfo: null,
|
||||
recoveryKeyValid: false,
|
||||
forceRecoveryKey: false,
|
||||
passPhrase: '',
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._loadBackupStatus();
|
||||
},
|
||||
|
||||
_onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
_onDone: function() {
|
||||
this.props.onFinished(true);
|
||||
},
|
||||
|
||||
_onUseRecoveryKeyClick: function() {
|
||||
this.setState({
|
||||
forceRecoveryKey: true,
|
||||
});
|
||||
},
|
||||
|
||||
_onResetRecoveryClick: function() {
|
||||
this.props.onFinished(false);
|
||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||
{
|
||||
onFinished: () => {
|
||||
this._loadBackupStatus();
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
_onRecoveryKeyChange: function(e) {
|
||||
this.setState({
|
||||
recoveryKey: e.target.value,
|
||||
recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value),
|
||||
});
|
||||
},
|
||||
|
||||
_onPassPhraseNext: async function() {
|
||||
this.setState({
|
||||
loading: true,
|
||||
restoreError: null,
|
||||
});
|
||||
try {
|
||||
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword(
|
||||
this.state.passPhrase, undefined, undefined, this.state.backupInfo.version,
|
||||
);
|
||||
this.setState({
|
||||
loading: false,
|
||||
recoverInfo,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error restoring backup", e);
|
||||
this.setState({
|
||||
loading: false,
|
||||
restoreError: e,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_onRecoveryKeyNext: async function() {
|
||||
this.setState({
|
||||
loading: true,
|
||||
restoreError: null,
|
||||
});
|
||||
try {
|
||||
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey(
|
||||
this.state.recoveryKey, undefined, undefined, this.state.backupInfo.version,
|
||||
);
|
||||
this.setState({
|
||||
loading: false,
|
||||
recoverInfo,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error restoring backup", e);
|
||||
this.setState({
|
||||
loading: false,
|
||||
restoreError: e,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_onPassPhraseChange: function(e) {
|
||||
this.setState({
|
||||
passPhrase: e.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
_onPassPhraseKeyPress: function(e) {
|
||||
if (e.key === "Enter") {
|
||||
this._onPassPhraseNext();
|
||||
}
|
||||
},
|
||||
|
||||
_onRecoveryKeyKeyPress: function(e) {
|
||||
if (e.key === "Enter" && this.state.recoveryKeyValid) {
|
||||
this._onRecoveryKeyNext();
|
||||
}
|
||||
},
|
||||
|
||||
_loadBackupStatus: async function() {
|
||||
this.setState({
|
||||
loading: true,
|
||||
loadError: null,
|
||||
});
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
this.setState({
|
||||
loadError: null,
|
||||
loading: false,
|
||||
backupInfo,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error loading backup status", e);
|
||||
this.setState({
|
||||
loadError: e,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
const backupHasPassphrase = (
|
||||
this.state.backupInfo &&
|
||||
this.state.backupInfo.auth_data &&
|
||||
this.state.backupInfo.auth_data.private_key_salt &&
|
||||
this.state.backupInfo.auth_data.private_key_iterations
|
||||
);
|
||||
|
||||
let content;
|
||||
let title;
|
||||
if (this.state.loading) {
|
||||
title = _t("Loading...");
|
||||
content = <Spinner />;
|
||||
} else if (this.state.loadError) {
|
||||
title = _t("Error");
|
||||
content = _t("Unable to load backup status");
|
||||
} else if (this.state.restoreError) {
|
||||
title = _t("Error");
|
||||
content = _t("Unable to restore backup");
|
||||
} else if (this.state.backupInfo === null) {
|
||||
title = _t("Error");
|
||||
content = _t("No backup found!");
|
||||
} else if (this.state.recoverInfo) {
|
||||
title = _t("Backup Restored");
|
||||
let failedToDecrypt;
|
||||
if (this.state.recoverInfo.total > this.state.recoverInfo.imported) {
|
||||
failedToDecrypt = <p>{_t(
|
||||
"Failed to decrypt %(failedCount)s sessions!",
|
||||
{failedCount: this.state.recoverInfo.total - this.state.recoverInfo.imported},
|
||||
)}</p>;
|
||||
}
|
||||
content = <div>
|
||||
<p>{_t("Restored %(sessionCount)s session keys", {sessionCount: this.state.recoverInfo.imported})}</p>
|
||||
{failedToDecrypt}
|
||||
</div>;
|
||||
} else if (backupHasPassphrase && !this.state.forceRecoveryKey) {
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
title = _t("Enter Recovery Passphrase");
|
||||
content = <div>
|
||||
{_t(
|
||||
"Access your secure message history and set up secure " +
|
||||
"messaging by entering your recovery passphrase.",
|
||||
)}<br />
|
||||
|
||||
<div className="mx_RestoreKeyBackupDialog_primaryContainer">
|
||||
<input type="password"
|
||||
className="mx_RestoreKeyBackupDialog_passPhraseInput"
|
||||
onChange={this._onPassPhraseChange}
|
||||
onKeyPress={this._onPassPhraseKeyPress}
|
||||
value={this.state.passPhrase}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<DialogButtons primaryButton={_t('Next')}
|
||||
onPrimaryButtonClick={this._onPassPhraseNext}
|
||||
hasCancel={true}
|
||||
onCancel={this._onCancel}
|
||||
focus={false}
|
||||
/>
|
||||
</div>
|
||||
{_t(
|
||||
"If you've forgotten your recovery passphrase you can "+
|
||||
"<button1>use your recovery key</button1> or " +
|
||||
"<button2>set up new recovery options</button2>"
|
||||
, {}, {
|
||||
button1: s => <AccessibleButton className="mx_linkButton"
|
||||
element="span"
|
||||
onClick={this._onUseRecoveryKeyClick}
|
||||
>
|
||||
{s}
|
||||
</AccessibleButton>,
|
||||
button2: s => <AccessibleButton className="mx_linkButton"
|
||||
element="span"
|
||||
onClick={this._onResetRecoveryClick}
|
||||
>
|
||||
{s}
|
||||
</AccessibleButton>,
|
||||
})}
|
||||
</div>;
|
||||
} else {
|
||||
title = _t("Enter Recovery Key");
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let keyStatus;
|
||||
if (this.state.recoveryKey.length === 0) {
|
||||
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus"></div>;
|
||||
} else if (this.state.recoveryKeyValid) {
|
||||
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
|
||||
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
|
||||
</div>;
|
||||
} else {
|
||||
keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
|
||||
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
|
||||
</div>;
|
||||
}
|
||||
|
||||
content = <div>
|
||||
{_t(
|
||||
"Access your secure message history and set up secure " +
|
||||
"messaging by entering your recovery key.",
|
||||
)}<br />
|
||||
|
||||
<div className="mx_RestoreKeyBackupDialog_primaryContainer">
|
||||
<input className="mx_RestoreKeyBackupDialog_recoveryKeyInput"
|
||||
onChange={this._onRecoveryKeyChange}
|
||||
onKeyPress={this._onRecoveryKeyKeyPress}
|
||||
value={this.state.recoveryKey}
|
||||
autoFocus={true}
|
||||
/>
|
||||
{keyStatus}
|
||||
<DialogButtons primaryButton={_t('Next')}
|
||||
onPrimaryButtonClick={this._onRecoveryKeyNext}
|
||||
hasCancel={true}
|
||||
onCancel={this._onCancel}
|
||||
focus={false}
|
||||
primaryDisabled={!this.state.recoveryKeyValid}
|
||||
/>
|
||||
</div>
|
||||
{_t(
|
||||
"If you've forgotten your recovery passphrase you can "+
|
||||
"<button>set up new recovery options</button>"
|
||||
, {}, {
|
||||
button: s => <AccessibleButton className="mx_linkButton"
|
||||
element="span"
|
||||
onClick={this._onResetRecoveryClick}
|
||||
>
|
||||
{s}
|
||||
</AccessibleButton>,
|
||||
})}
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_RestoreKeyBackupDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
title={title}
|
||||
>
|
||||
<div>
|
||||
{content}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -153,8 +153,8 @@ export default class NetworkDropdown extends React.Component {
|
|||
|
||||
const sortedInstances = this.props.protocols[proto].instances;
|
||||
sortedInstances.sort(function(x, y) {
|
||||
const a = x.desc
|
||||
const b = y.desc
|
||||
const a = x.desc;
|
||||
const b = y.desc;
|
||||
if (a < b) {
|
||||
return -1;
|
||||
} else if (a > b) {
|
||||
|
@ -208,7 +208,7 @@ export default class NetworkDropdown extends React.Component {
|
|||
return <div key={key} className="mx_NetworkDropdown_networkoption" onClick={click_handler}>
|
||||
{icon}
|
||||
<span className="mx_NetworkDropdown_menu_network">{name}</span>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -223,11 +223,11 @@ export default class NetworkDropdown extends React.Component {
|
|||
current_value = <input type="text" className="mx_NetworkDropdown_networkoption"
|
||||
ref={this.collectInputTextBox} onKeyUp={this.onInputKeyUp}
|
||||
placeholder="matrix.org" // 'matrix.org' as an example of an HS name
|
||||
/>
|
||||
/>;
|
||||
} else {
|
||||
const instance = instanceForInstanceId(this.props.protocols, this.state.selectedInstanceId);
|
||||
current_value = this._makeMenuOption(
|
||||
this.state.selectedServer, instance, this.state.includeAll, false
|
||||
this.state.selectedServer, instance, this.state.includeAll, false,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -318,6 +318,19 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
this.setState({deleting: true});
|
||||
|
||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
||||
// its hold on the webcam. Without this, the widget holds a media
|
||||
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
|
||||
if (this.refs.appFrame) {
|
||||
// In practice we could just do `+= ''` to trick the browser
|
||||
// into thinking the URL changed, however I can foresee this
|
||||
// being optimized out by a browser. Instead, we'll just point
|
||||
// the iframe at a page that is reasonably safe to use in the
|
||||
// event the iframe doesn't wink away.
|
||||
// This is relative to where the Riot instance is located.
|
||||
this.refs.appFrame.src = 'about:blank';
|
||||
}
|
||||
|
||||
WidgetUtils.setRoomWidget(
|
||||
this.props.room.roomId,
|
||||
this.props.id,
|
||||
|
|
|
@ -78,7 +78,7 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
let blacklistButton = null, verifyButton = null;
|
||||
let blacklistButton = null; let verifyButton = null;
|
||||
|
||||
if (this.state.device.isBlocked()) {
|
||||
blacklistButton = (
|
||||
|
|
|
@ -43,7 +43,11 @@ module.exports = React.createClass({
|
|||
|
||||
focus: PropTypes.bool,
|
||||
|
||||
// disables the primary and cancel buttons
|
||||
disabled: PropTypes.bool,
|
||||
|
||||
// disables only the primary button
|
||||
primaryDisabled: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -70,15 +74,15 @@ module.exports = React.createClass({
|
|||
}
|
||||
return (
|
||||
<div className="mx_Dialog_buttons">
|
||||
{ cancelButton }
|
||||
{ this.props.children }
|
||||
<button className={primaryButtonClassName}
|
||||
onClick={this.props.onPrimaryButtonClick}
|
||||
autoFocus={this.props.focus}
|
||||
disabled={this.props.disabled}
|
||||
disabled={this.props.disabled || this.props.primaryDisabled}
|
||||
>
|
||||
{ this.props.primaryButton }
|
||||
</button>
|
||||
{ this.props.children }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -122,7 +122,6 @@ export default class EditableTextContainer extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
EditableTextContainer.propTypes = {
|
||||
|
|
|
@ -16,13 +16,13 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
|
||||
import {formatDate} from '../../../DateUtils';
|
||||
var filesize = require('filesize');
|
||||
var AccessibleButton = require('../../../components/views/elements/AccessibleButton');
|
||||
const filesize = require('filesize');
|
||||
const AccessibleButton = require('../../../components/views/elements/AccessibleButton');
|
||||
const Modal = require('../../../Modal');
|
||||
const sdk = require('../../../index');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -69,24 +69,24 @@ module.exports = React.createClass({
|
|||
Modal.createTrackedDialog('Confirm Redact Dialog', 'Image View', ConfirmRedactDialog, {
|
||||
onFinished: (proceed) => {
|
||||
if (!proceed) return;
|
||||
var self = this;
|
||||
const self = this;
|
||||
MatrixClientPeg.get().redactEvent(
|
||||
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId()
|
||||
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId(),
|
||||
).catch(function(e) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// display error message stating you couldn't delete this.
|
||||
var code = e.errcode || e.statusCode;
|
||||
const code = e.errcode || e.statusCode;
|
||||
Modal.createTrackedDialog('You cannot delete this image.', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: _t('You cannot delete this image. (%(code)s)', {code: code})
|
||||
description: _t('You cannot delete this image. (%(code)s)', {code: code}),
|
||||
});
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
getName: function () {
|
||||
var name = this.props.name;
|
||||
getName: function() {
|
||||
let name = this.props.name;
|
||||
if (name && this.props.link) {
|
||||
name = <a href={ this.props.link } target="_blank" rel="noopener">{ name }</a>;
|
||||
}
|
||||
|
@ -94,7 +94,6 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
/*
|
||||
// In theory max-width: 80%, max-height: 80% on the CSS should work
|
||||
// but in practice, it doesn't, so do it manually:
|
||||
|
@ -123,7 +122,7 @@ module.exports = React.createClass({
|
|||
height: displayHeight
|
||||
};
|
||||
*/
|
||||
var style, res;
|
||||
let style; let res;
|
||||
|
||||
if (this.props.width && this.props.height) {
|
||||
style = {
|
||||
|
@ -133,23 +132,22 @@ module.exports = React.createClass({
|
|||
res = style.width + "x" + style.height + "px";
|
||||
}
|
||||
|
||||
var size;
|
||||
let size;
|
||||
if (this.props.fileSize) {
|
||||
size = filesize(this.props.fileSize);
|
||||
}
|
||||
|
||||
var size_res;
|
||||
let size_res;
|
||||
if (size && res) {
|
||||
size_res = size + ", " + res;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
size_res = size || res;
|
||||
}
|
||||
|
||||
var showEventMeta = !!this.props.mxEvent;
|
||||
const showEventMeta = !!this.props.mxEvent;
|
||||
|
||||
var eventMeta;
|
||||
if(showEventMeta) {
|
||||
let eventMeta;
|
||||
if (showEventMeta) {
|
||||
// Figure out the sender, defaulting to mxid
|
||||
let sender = this.props.mxEvent.getSender();
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
|
@ -163,8 +161,8 @@ module.exports = React.createClass({
|
|||
</div>);
|
||||
}
|
||||
|
||||
var eventRedact;
|
||||
if(showEventMeta) {
|
||||
let eventRedact;
|
||||
if (showEventMeta) {
|
||||
eventRedact = (<div className="mx_ImageView_button" onClick={this.onRedactClick}>
|
||||
{ _t('Remove') }
|
||||
</div>);
|
||||
|
@ -175,10 +173,10 @@ module.exports = React.createClass({
|
|||
<div className="mx_ImageView_lhs">
|
||||
</div>
|
||||
<div className="mx_ImageView_content">
|
||||
<img src={this.props.src} style={style}/>
|
||||
<img src={this.props.src} style={style} />
|
||||
<div className="mx_ImageView_labelWrapper">
|
||||
<div className="mx_ImageView_label">
|
||||
<AccessibleButton className="mx_ImageView_cancel" onClick={ this.props.onFinished }><img src="img/cancel-white.svg" width="18" height="18" alt={ _t('Close') }/></AccessibleButton>
|
||||
<AccessibleButton className="mx_ImageView_cancel" onClick={ this.props.onFinished }><img src="img/cancel-white.svg" width="18" height="18" alt={ _t('Close') } /></AccessibleButton>
|
||||
<div className="mx_ImageView_shim">
|
||||
</div>
|
||||
<div className="mx_ImageView_name">
|
||||
|
@ -187,7 +185,7 @@ module.exports = React.createClass({
|
|||
{ eventMeta }
|
||||
<a className="mx_ImageView_link" href={ this.props.src } download={ this.props.name } target="_blank" rel="noopener">
|
||||
<div className="mx_ImageView_download">
|
||||
{ _t('Download this file') }<br/>
|
||||
{ _t('Download this file') }<br />
|
||||
<span className="mx_ImageView_size">{ size_res }</span>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -201,5 +199,5 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -20,14 +20,14 @@ module.exports = React.createClass({
|
|||
displayName: 'InlineSpinner',
|
||||
|
||||
render: function() {
|
||||
var w = this.props.w || 16;
|
||||
var h = this.props.h || 16;
|
||||
var imgClass = this.props.imgClassName || "";
|
||||
const w = this.props.w || 16;
|
||||
const h = this.props.h || 16;
|
||||
const imgClass = this.props.imgClassName || "";
|
||||
|
||||
return (
|
||||
<div className="mx_InlineSpinner">
|
||||
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
|
||||
<img src="img/spinner.gif" width={w} height={h} className={imgClass} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -54,7 +54,6 @@ function getOrCreateContainer(containerId) {
|
|||
* bounding rect as the parent of PE.
|
||||
*/
|
||||
export default class PersistedElement extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
// Unique identifier for this PersistedElement instance
|
||||
// Any PersistedElements with the same persistKey will use
|
||||
|
|
|
@ -29,7 +29,7 @@ const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
|||
|
||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
||||
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room|group)\/(([#!@+])[^\/]*)$/;
|
||||
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room|group)\/(([#!@+])[^/]*)$/;
|
||||
|
||||
const Pill = React.createClass({
|
||||
statics: {
|
||||
|
|
|
@ -16,19 +16,19 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'Spinner',
|
||||
|
||||
render: function() {
|
||||
var w = this.props.w || 32;
|
||||
var h = this.props.h || 32;
|
||||
var imgClass = this.props.imgClassName || "";
|
||||
const w = this.props.w || 32;
|
||||
const h = this.props.h || 32;
|
||||
const imgClass = this.props.imgClassName || "";
|
||||
return (
|
||||
<div className="mx_Spinner">
|
||||
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
|
||||
<img src="img/spinner.gif" width={w} height={h} className={imgClass} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -20,7 +20,6 @@ import TintableSvg from './TintableSvg';
|
|||
import AccessibleButton from './AccessibleButton';
|
||||
|
||||
export default class TintableSvgButton extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ module.exports = React.createClass({
|
|||
<div className="mx_MatrixToolbar_content">
|
||||
{ _t('You are not receiving desktop notifications') } <a className="mx_MatrixToolbar_link" onClick={ this.onClick }> { _t('Enable them now') }</a>
|
||||
</div>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={ this.hideToolbar } ><img src="img/cancel.svg" width="18" height="18" alt={_t('Close')}/></AccessibleButton>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={ this.hideToolbar } ><img src="img/cancel.svg" width="18" height="18" alt={_t('Close')} /></AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -45,10 +45,10 @@ export default React.createClass({
|
|||
description: <div className="mx_MatrixToolbar_changelog">{releaseNotes}</div>,
|
||||
button: _t("Update"),
|
||||
onFinished: (update) => {
|
||||
if(update && PlatformPeg.get()) {
|
||||
if (update && PlatformPeg.get()) {
|
||||
PlatformPeg.get().installUpdate();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -58,10 +58,10 @@ export default React.createClass({
|
|||
version: this.props.version,
|
||||
newVersion: this.props.newVersion,
|
||||
onFinished: (update) => {
|
||||
if(update && PlatformPeg.get()) {
|
||||
if (update && PlatformPeg.get()) {
|
||||
PlatformPeg.get().installUpdate();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -103,5 +103,5 @@ export default React.createClass({
|
|||
{action_button}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -32,14 +32,14 @@ export default React.createClass({
|
|||
getDefaultProps: function() {
|
||||
return {
|
||||
detail: '',
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getStatusText: function() {
|
||||
// we can't import the enum from riot-web as we don't want matrix-react-sdk
|
||||
// to depend on riot-web. so we grab it as a normal object via API instead.
|
||||
const updateCheckStatusEnum = PlatformPeg.get().getUpdateCheckStatusEnum();
|
||||
switch(this.props.status) {
|
||||
switch (this.props.status) {
|
||||
case updateCheckStatusEnum.ERROR:
|
||||
return _t('Error encountered (%(errorDetail)s).', { errorDetail: this.props.detail });
|
||||
case updateCheckStatusEnum.CHECKING:
|
||||
|
@ -59,7 +59,7 @@ export default React.createClass({
|
|||
const message = this.getStatusText();
|
||||
const warning = _t('Warning');
|
||||
|
||||
if (!'getUpdateCheckStatusEnum' in PlatformPeg.get()) {
|
||||
if (!('getUpdateCheckStatusEnum' in PlatformPeg.get())) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
|
@ -83,9 +83,9 @@ export default React.createClass({
|
|||
{message}
|
||||
</div>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.hideToolbar}>
|
||||
<img src="img/cancel.svg" width="18" height="18" alt={_t('Close')}/>
|
||||
<img src="img/cancel.svg" width="18" height="18" alt={_t('Close')} />
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -165,7 +165,7 @@ export default React.createClass({
|
|||
return (
|
||||
<div className="mx_MemberList">
|
||||
{ inputBox }
|
||||
<GeminiScrollbarWrapper autoshow={true} className="mx_MemberList_outerWrapper">
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
{ joined }
|
||||
{ invited }
|
||||
</GeminiScrollbarWrapper>
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'CasLogin',
|
||||
|
||||
propTypes: {
|
||||
onSubmit: PropTypes.func, // fn()
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={this.props.onSubmit}>{ _t("Sign in with CAS") }</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
});
|
|
@ -22,6 +22,7 @@ import classnames from 'classnames';
|
|||
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
|
@ -209,6 +210,145 @@ export const RecaptchaAuthEntry = React.createClass({
|
|||
},
|
||||
});
|
||||
|
||||
export const TermsAuthEntry = React.createClass({
|
||||
displayName: 'TermsAuthEntry',
|
||||
|
||||
statics: {
|
||||
LOGIN_TYPE: "m.login.terms",
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
stageParams: PropTypes.object.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
busy: PropTypes.bool,
|
||||
showContinue: PropTypes.bool,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// example stageParams:
|
||||
//
|
||||
// {
|
||||
// "policies": {
|
||||
// "privacy_policy": {
|
||||
// "version": "1.0",
|
||||
// "en": {
|
||||
// "name": "Privacy Policy",
|
||||
// "url": "https://example.org/privacy-1.0-en.html",
|
||||
// },
|
||||
// "fr": {
|
||||
// "name": "Politique de confidentialité",
|
||||
// "url": "https://example.org/privacy-1.0-fr.html",
|
||||
// },
|
||||
// },
|
||||
// "other_policy": { ... },
|
||||
// }
|
||||
// }
|
||||
|
||||
const allPolicies = this.props.stageParams.policies || {};
|
||||
const prefLang = SettingsStore.getValue("language");
|
||||
const initToggles = {};
|
||||
const pickedPolicies = [];
|
||||
for (const policyId of Object.keys(allPolicies)) {
|
||||
const policy = allPolicies[policyId];
|
||||
|
||||
// Pick a language based on the user's language, falling back to english,
|
||||
// and finally to the first language available. If there's still no policy
|
||||
// available then the homeserver isn't respecting the spec.
|
||||
let langPolicy = policy[prefLang];
|
||||
if (!langPolicy) langPolicy = policy["en"];
|
||||
if (!langPolicy) {
|
||||
// last resort
|
||||
const firstLang = Object.keys(policy).find(e => e !== "version");
|
||||
langPolicy = policy[firstLang];
|
||||
}
|
||||
if (!langPolicy) throw new Error("Failed to find a policy to show the user");
|
||||
|
||||
initToggles[policyId] = false;
|
||||
|
||||
langPolicy.id = policyId;
|
||||
pickedPolicies.push(langPolicy);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
"toggledPolicies": initToggles,
|
||||
"policies": pickedPolicies,
|
||||
});
|
||||
},
|
||||
|
||||
tryContinue: function() {
|
||||
this._trySubmit();
|
||||
},
|
||||
|
||||
_togglePolicy: function(policyId) {
|
||||
const newToggles = {};
|
||||
for (const policy of this.state.policies) {
|
||||
let checked = this.state.toggledPolicies[policy.id];
|
||||
if (policy.id === policyId) checked = !checked;
|
||||
|
||||
newToggles[policy.id] = checked;
|
||||
}
|
||||
this.setState({"toggledPolicies": newToggles});
|
||||
},
|
||||
|
||||
_trySubmit: function() {
|
||||
let allChecked = true;
|
||||
for (const policy of this.state.policies) {
|
||||
let checked = this.state.toggledPolicies[policy.id];
|
||||
allChecked = allChecked && checked;
|
||||
}
|
||||
|
||||
if (allChecked) this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE});
|
||||
else this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.props.busy) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
const checkboxes = [];
|
||||
let allChecked = true;
|
||||
for (const policy of this.state.policies) {
|
||||
const checked = this.state.toggledPolicies[policy.id];
|
||||
allChecked = allChecked && checked;
|
||||
|
||||
checkboxes.push(
|
||||
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
|
||||
<input type="checkbox" onClick={() => this._togglePolicy(policy.id)} checked={checked} />
|
||||
<a href={policy.url} target="_blank" rel="noopener">{ policy.name }</a>
|
||||
</label>,
|
||||
);
|
||||
}
|
||||
|
||||
let errorSection;
|
||||
if (this.props.errorText || this.state.errorText) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
{ this.props.errorText || this.state.errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let submitButton;
|
||||
if (this.props.showContinue !== false) {
|
||||
// XXX: button classes
|
||||
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_UserSettings_button"
|
||||
onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("Please review and accept the policies of this homeserver:")}</p>
|
||||
{ checkboxes }
|
||||
{ errorSection }
|
||||
{ submitButton }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const EmailIdentityAuthEntry = React.createClass({
|
||||
displayName: 'EmailIdentityAuthEntry',
|
||||
|
||||
|
@ -496,6 +636,7 @@ const AuthEntryComponents = [
|
|||
RecaptchaAuthEntry,
|
||||
EmailIdentityAuthEntry,
|
||||
MsisdnAuthEntry,
|
||||
TermsAuthEntry,
|
||||
];
|
||||
|
||||
export function getEntryComponentForLoginType(loginType) {
|
||||
|
|
|
@ -30,6 +30,7 @@ class PasswordLogin extends React.Component {
|
|||
static defaultProps = {
|
||||
onError: function() {},
|
||||
onUsernameChanged: function() {},
|
||||
onUsernameBlur: function() {},
|
||||
onPasswordChanged: function() {},
|
||||
onPhoneCountryChanged: function() {},
|
||||
onPhoneNumberChanged: function() {},
|
||||
|
@ -53,6 +54,7 @@ class PasswordLogin extends React.Component {
|
|||
|
||||
this.onSubmitForm = this.onSubmitForm.bind(this);
|
||||
this.onUsernameChanged = this.onUsernameChanged.bind(this);
|
||||
this.onUsernameBlur = this.onUsernameBlur.bind(this);
|
||||
this.onLoginTypeChange = this.onLoginTypeChange.bind(this);
|
||||
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
|
||||
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
|
||||
|
@ -124,6 +126,10 @@ class PasswordLogin extends React.Component {
|
|||
this.props.onUsernameChanged(ev.target.value);
|
||||
}
|
||||
|
||||
onUsernameBlur(ev) {
|
||||
this.props.onUsernameBlur(this.state.username);
|
||||
}
|
||||
|
||||
onLoginTypeChange(loginType) {
|
||||
this.props.onError(null); // send a null error to clear any error messages
|
||||
this.setState({
|
||||
|
@ -167,6 +173,7 @@ class PasswordLogin extends React.Component {
|
|||
type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
onChange={this.onUsernameChanged}
|
||||
onBlur={this.onUsernameBlur}
|
||||
placeholder="joe@example.com"
|
||||
value={this.state.username}
|
||||
autoFocus
|
||||
|
@ -182,6 +189,7 @@ class PasswordLogin extends React.Component {
|
|||
type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
onChange={this.onUsernameChanged}
|
||||
onBlur={this.onUsernameBlur}
|
||||
placeholder={SdkConfig.get().disable_custom_urls ?
|
||||
_t("Username on %(hs)s", {
|
||||
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
|
||||
|
|
|
@ -70,6 +70,23 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
if (newProps.customHsUrl === this.state.hs_url &&
|
||||
newProps.customIsUrl === this.state.is_url) return;
|
||||
|
||||
this.setState({
|
||||
hs_url: newProps.customHsUrl,
|
||||
is_url: newProps.customIsUrl,
|
||||
configVisible: !newProps.withToggleButton ||
|
||||
(newProps.customHsUrl !== newProps.defaultHsUrl) ||
|
||||
(newProps.customIsUrl !== newProps.defaultIsUrl),
|
||||
});
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl: newProps.customHsUrl,
|
||||
isUrl: newProps.customIsUrl,
|
||||
});
|
||||
},
|
||||
|
||||
onHomeserverChanged: function(ev) {
|
||||
this.setState({hs_url: ev.target.value}, function() {
|
||||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
|
||||
|
|
|
@ -278,6 +278,7 @@ export default class MImageBody extends React.Component {
|
|||
|
||||
let img = null;
|
||||
let placeholder = null;
|
||||
let gifLabel = null;
|
||||
|
||||
// e2e image hasn't been decrypted yet
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
|
@ -302,11 +303,14 @@ export default class MImageBody extends React.Component {
|
|||
onMouseLeave={this.onImageLeave} />;
|
||||
}
|
||||
|
||||
if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) {
|
||||
gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>;
|
||||
}
|
||||
|
||||
const thumbnail = (
|
||||
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight + "px" }} >
|
||||
{ /* Calculate aspect ratio, using %padding will size _container correctly */ }
|
||||
<div style={{ paddingBottom: (100 * infoHeight / infoWidth) + '%' }} />
|
||||
|
||||
{ showPlaceholder &&
|
||||
<div className="mx_MImageBody_thumbnail" style={{
|
||||
// Constrain width here so that spinner appears central to the loaded thumbnail
|
||||
|
@ -320,6 +324,7 @@ export default class MImageBody extends React.Component {
|
|||
|
||||
<div style={{display: !showPlaceholder ? undefined : 'none'}}>
|
||||
{ img }
|
||||
{ gifLabel }
|
||||
</div>
|
||||
|
||||
{ this.state.hover && this.getTooltip() }
|
||||
|
|
|
@ -103,7 +103,7 @@ module.exports = React.createClass({
|
|||
oldCanonicalAlias = this.props.canonicalAliasEvent.getContent().alias;
|
||||
}
|
||||
|
||||
let newCanonicalAlias = this.state.canonicalAlias;
|
||||
const newCanonicalAlias = this.state.canonicalAlias;
|
||||
|
||||
if (this.props.canSetCanonicalAlias && oldCanonicalAlias !== newCanonicalAlias) {
|
||||
console.log("AliasSettings: Updating canonical alias");
|
||||
|
@ -167,7 +167,7 @@ module.exports = React.createClass({
|
|||
|
||||
if (!this.props.canonicalAlias) {
|
||||
this.setState({
|
||||
canonicalAlias: alias
|
||||
canonicalAlias: alias,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -220,8 +220,9 @@ module.exports = React.createClass({
|
|||
let canonical_alias_section;
|
||||
if (this.props.canSetCanonicalAlias) {
|
||||
let found = false;
|
||||
const canonicalValue = this.state.canonicalAlias || "";
|
||||
canonical_alias_section = (
|
||||
<select onChange={this.onCanonicalAliasChange} value={this.state.canonicalAlias}>
|
||||
<select onChange={this.onCanonicalAliasChange} value={canonicalValue}>
|
||||
<option value="" key="unset">{ _t('not specified') }</option>
|
||||
{
|
||||
Object.keys(self.state.domainToAliases).map((domain, i) => {
|
||||
|
|
|
@ -22,7 +22,7 @@ import { _t } from '../../../languageHandler';
|
|||
import Modal from '../../../Modal';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
const GROUP_ID_REGEX = /\+\S+\:\S+/;
|
||||
const GROUP_ID_REGEX = /\+\S+:\S+/;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RelatedGroupSettings',
|
||||
|
|
|
@ -33,7 +33,6 @@ import Autocompleter from '../../../autocomplete/Autocompleter';
|
|||
const COMPOSER_SELECTED = 0;
|
||||
|
||||
export default class Autocomplete extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
|
|
@ -416,11 +416,10 @@ module.exports = withMatrixClient(React.createClass({
|
|||
onCryptoClicked: function(e) {
|
||||
const event = this.props.mxEvent;
|
||||
|
||||
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', (cb) => {
|
||||
require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb);
|
||||
}, {
|
||||
event: event,
|
||||
});
|
||||
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
||||
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
|
||||
{event},
|
||||
);
|
||||
},
|
||||
|
||||
onRequestKeysClick: function() {
|
||||
|
|
|
@ -107,7 +107,7 @@ module.exports = React.createClass({
|
|||
|
||||
// FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing?
|
||||
let image = p["og:image"];
|
||||
let imageMaxWidth = 100, imageMaxHeight = 100;
|
||||
const imageMaxWidth = 100; const imageMaxHeight = 100;
|
||||
if (image && image.startsWith("mxc://")) {
|
||||
image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight);
|
||||
}
|
||||
|
|
|
@ -712,7 +712,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
if (!member || !member.membership || member.membership === 'leave') {
|
||||
const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId();
|
||||
const onInviteUserButton = async () => {
|
||||
const onInviteUserButton = async() => {
|
||||
try {
|
||||
await cli.invite(roomId, member.userId);
|
||||
} catch (err) {
|
||||
|
|
|
@ -447,7 +447,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_MemberList">
|
||||
{ inputBox }
|
||||
<GeminiScrollbarWrapper autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
|
||||
<GeminiScrollbarWrapper autoshow={true} className="mx_MemberList_joined">
|
||||
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAtJoined}
|
||||
createOverflowElement={this._createOverflowTileJoined}
|
||||
getChildren={this._getChildrenJoined}
|
||||
|
|
|
@ -305,7 +305,7 @@ export default class MessageComposer extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let e2eImg, e2eTitle, e2eClass;
|
||||
let e2eImg; let e2eTitle; let e2eClass;
|
||||
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
if (roomIsEncrypted) {
|
||||
// FIXME: show a /!\ if there are untrusted devices in the room...
|
||||
|
@ -465,7 +465,7 @@ export default class MessageComposer extends React.Component {
|
|||
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
|
||||
src="img/icon-text-cancel.svg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -67,7 +67,7 @@ const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
|||
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
|
||||
const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g');
|
||||
|
||||
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
||||
const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000;
|
||||
|
||||
const ENTITY_TYPES = {
|
||||
AT_ROOM_PILL: 'ATROOMPILL',
|
||||
|
@ -175,8 +175,8 @@ export default class MessageComposerInput extends React.Component {
|
|||
// see https://github.com/ianstormtaylor/slate/issues/762#issuecomment-304855095
|
||||
this.direction = '';
|
||||
|
||||
this.plainWithMdPills = new PlainWithPillsSerializer({ pillFormat: 'md' });
|
||||
this.plainWithIdPills = new PlainWithPillsSerializer({ pillFormat: 'id' });
|
||||
this.plainWithMdPills = new PlainWithPillsSerializer({ pillFormat: 'md' });
|
||||
this.plainWithIdPills = new PlainWithPillsSerializer({ pillFormat: 'id' });
|
||||
this.plainWithPlainPills = new PlainWithPillsSerializer({ pillFormat: 'plain' });
|
||||
|
||||
this.md = new Md({
|
||||
|
@ -544,7 +544,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
if (editorState.startText !== null) {
|
||||
const text = editorState.startText.text;
|
||||
const currentStartOffset = editorState.startOffset;
|
||||
const currentStartOffset = editorState.selection.start.offset;
|
||||
|
||||
// Automatic replacement of plaintext emoji to Unicode emoji
|
||||
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||
|
@ -558,11 +558,11 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
const range = Range.create({
|
||||
anchor: {
|
||||
key: editorState.selection.startKey,
|
||||
key: editorState.startText.key,
|
||||
offset: currentStartOffset - emojiMatch[1].length - 1,
|
||||
},
|
||||
focus: {
|
||||
key: editorState.selection.startKey,
|
||||
key: editorState.startText.key,
|
||||
offset: currentStartOffset - 1,
|
||||
},
|
||||
});
|
||||
|
@ -573,29 +573,42 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
// emojioneify any emoji
|
||||
editorState.document.getTexts().forEach(node => {
|
||||
if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) {
|
||||
let match;
|
||||
while ((match = EMOJI_REGEX.exec(node.text)) !== null) {
|
||||
const range = Range.create({
|
||||
anchor: {
|
||||
key: node.key,
|
||||
offset: match.index,
|
||||
},
|
||||
focus: {
|
||||
key: node.key,
|
||||
offset: match.index + match[0].length,
|
||||
},
|
||||
});
|
||||
const inline = Inline.create({
|
||||
type: 'emoji',
|
||||
data: { emojiUnicode: match[0] },
|
||||
});
|
||||
change = change.insertInlineAtRange(range, inline);
|
||||
editorState = change.value;
|
||||
let foundEmoji;
|
||||
do {
|
||||
foundEmoji = false;
|
||||
|
||||
for (const node of editorState.document.getTexts()) {
|
||||
if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) {
|
||||
let match;
|
||||
EMOJI_REGEX.lastIndex = 0;
|
||||
while ((match = EMOJI_REGEX.exec(node.text)) !== null) {
|
||||
const range = Range.create({
|
||||
anchor: {
|
||||
key: node.key,
|
||||
offset: match.index,
|
||||
},
|
||||
focus: {
|
||||
key: node.key,
|
||||
offset: match.index + match[0].length,
|
||||
},
|
||||
});
|
||||
const inline = Inline.create({
|
||||
type: 'emoji',
|
||||
data: { emojiUnicode: match[0] },
|
||||
});
|
||||
change = change.insertInlineAtRange(range, inline);
|
||||
editorState = change.value;
|
||||
|
||||
// if we replaced an emoji, start again looking for more
|
||||
// emoji in the new editor state since doing the replacement
|
||||
// will change the node structure & offsets so we can't compute
|
||||
// insertion ranges from node.key / match.index anymore.
|
||||
foundEmoji = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} while (foundEmoji);
|
||||
|
||||
// work around weird bug where inserting emoji via the macOS
|
||||
// emoji picker can leave the selection stuck in the emoji's
|
||||
|
@ -1065,7 +1078,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
// only look for commands if the first block contains simple unformatted text
|
||||
// i.e. no pills or rich-text formatting and begins with a /.
|
||||
let cmd, commandText;
|
||||
let cmd; let commandText;
|
||||
const firstChild = editorState.document.nodes.get(0);
|
||||
const firstGrandChild = firstChild && firstChild.nodes.get(0);
|
||||
if (firstChild && firstGrandChild &&
|
||||
|
@ -1247,7 +1260,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
selectHistory = async (up) => {
|
||||
selectHistory = async(up) => {
|
||||
const delta = up ? -1 : 1;
|
||||
|
||||
// True if we are not currently selecting history, but composing a message
|
||||
|
@ -1295,7 +1308,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
return true;
|
||||
};
|
||||
|
||||
onTab = async (e) => {
|
||||
onTab = async(e) => {
|
||||
this.setState({
|
||||
someCompletions: null,
|
||||
});
|
||||
|
@ -1317,7 +1330,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
|
||||
};
|
||||
|
||||
onEscape = async (e) => {
|
||||
onEscape = async(e) => {
|
||||
e.preventDefault();
|
||||
if (this.autocomplete) {
|
||||
this.autocomplete.onEscape(e);
|
||||
|
@ -1336,7 +1349,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
/* If passed null, restores the original editor content from state.originalEditorState.
|
||||
* If passed a non-null displayedCompletion, modifies state.originalEditorState to compute new state.editorState.
|
||||
*/
|
||||
setDisplayedCompletion = async (displayedCompletion: ?Completion): boolean => {
|
||||
setDisplayedCompletion = async(displayedCompletion: ?Completion): boolean => {
|
||||
const activeEditorState = this.state.originalEditorState || this.state.editorState;
|
||||
|
||||
if (displayedCompletion == null) {
|
||||
|
|
|
@ -44,9 +44,13 @@ module.exports = React.createClass({
|
|||
error: PropTypes.object,
|
||||
|
||||
canPreview: PropTypes.bool,
|
||||
spinner: PropTypes.bool,
|
||||
room: PropTypes.object,
|
||||
|
||||
// When a spinner is present, a spinnerState can be specified to indicate the
|
||||
// purpose of the spinner.
|
||||
spinner: PropTypes.bool,
|
||||
spinnerState: PropTypes.oneOf(["joining"]),
|
||||
|
||||
// The alias that was used to access this room, if appropriate
|
||||
// If given, this will be how the room is referred to (eg.
|
||||
// in error messages).
|
||||
|
@ -89,11 +93,16 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
let joinBlock, previewBlock;
|
||||
let joinBlock; let previewBlock;
|
||||
|
||||
if (this.props.spinner || this.state.busy) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
let spinnerIntro = "";
|
||||
if (this.props.spinnerState === "joining") {
|
||||
spinnerIntro = _t("Joining room...");
|
||||
}
|
||||
return (<div className="mx_RoomPreviewBar">
|
||||
<p className="mx_RoomPreviewBar_spinnerIntro">{ spinnerIntro }</p>
|
||||
<Spinner />
|
||||
</div>);
|
||||
}
|
||||
|
|
|
@ -590,6 +590,11 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_openDevtools: function() {
|
||||
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
|
||||
Modal.createDialog(DevtoolsDialog, {roomId: this.props.room.roomId});
|
||||
},
|
||||
|
||||
_renderEncryptionSection: function() {
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
|
||||
|
@ -652,31 +657,31 @@ module.exports = React.createClass({
|
|||
const userLevels = powerLevels.users || {};
|
||||
|
||||
const powerLevelDescriptors = {
|
||||
users_default: {
|
||||
"users_default": {
|
||||
desc: _t('The default role for new room members is'),
|
||||
defaultValue: 0,
|
||||
},
|
||||
events_default: {
|
||||
"events_default": {
|
||||
desc: _t('To send messages, you must be a'),
|
||||
defaultValue: 0,
|
||||
},
|
||||
invite: {
|
||||
"invite": {
|
||||
desc: _t('To invite users into the room, you must be a'),
|
||||
defaultValue: 50,
|
||||
},
|
||||
state_default: {
|
||||
"state_default": {
|
||||
desc: _t('To configure the room, you must be a'),
|
||||
defaultValue: 50,
|
||||
},
|
||||
kick: {
|
||||
"kick": {
|
||||
desc: _t('To kick users, you must be a'),
|
||||
defaultValue: 50,
|
||||
},
|
||||
ban: {
|
||||
"ban": {
|
||||
desc: _t('To ban users, you must be a'),
|
||||
defaultValue: 50,
|
||||
},
|
||||
redact: {
|
||||
"redact": {
|
||||
desc: _t('To remove other users\' messages, you must be a'),
|
||||
defaultValue: 50,
|
||||
},
|
||||
|
@ -942,6 +947,11 @@ module.exports = React.createClass({
|
|||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const devtoolsButton = SettingsStore.getValue("showDeveloperTools") ?
|
||||
(<AccessibleButton className="mx_RoomSettings_devtoolsButton" onClick={this._openDevtools}>
|
||||
{ _t("Open Devtools") }
|
||||
</AccessibleButton>) : null;
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSettings">
|
||||
|
||||
|
@ -1055,6 +1065,7 @@ module.exports = React.createClass({
|
|||
{ _t('Internal room ID: ') } <code>{ this.props.room.roomId }</code><br />
|
||||
{ _t('Room version number: ') } <code>{ this.props.room.getVersion() }</code><br />
|
||||
{ roomUpgradeButton }
|
||||
{ devtoolsButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -179,13 +179,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_onExportE2eKeysClicked: function() {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password', (cb) => {
|
||||
require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
||||
cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
||||
}, "e2e-export");
|
||||
}, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
});
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
|
||||
import('../../../async-components/views/dialogs/ExportE2eKeysDialog'),
|
||||
{
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
onClickChange: function(ev) {
|
||||
|
|
242
src/components/views/settings/KeyBackupPanel.js
Normal file
242
src/components/views/settings/KeyBackupPanel.js
Normal file
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
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 sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
export default class KeyBackupPanel extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._startNewBackup = this._startNewBackup.bind(this);
|
||||
this._deleteBackup = this._deleteBackup.bind(this);
|
||||
this._verifyDevice = this._verifyDevice.bind(this);
|
||||
this._onKeyBackupStatus = this._onKeyBackupStatus.bind(this);
|
||||
this._restoreBackup = this._restoreBackup.bind(this);
|
||||
|
||||
this._unmounted = false;
|
||||
this.state = {
|
||||
loading: true,
|
||||
error: null,
|
||||
backupInfo: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._loadBackupStatus();
|
||||
|
||||
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatus);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatus);
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyBackupStatus() {
|
||||
this._loadBackupStatus();
|
||||
}
|
||||
|
||||
async _loadBackupStatus() {
|
||||
this.setState({loading: true});
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
backupInfo,
|
||||
backupSigStatus,
|
||||
loading: false,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Unable to fetch key backup status", e);
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
error: e,
|
||||
loading: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_startNewBackup() {
|
||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||
{
|
||||
onFinished: () => {
|
||||
this._loadBackupStatus();
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_deleteBackup() {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, {
|
||||
title: _t('Delete Backup'),
|
||||
description: _t(
|
||||
"Delete your backed up encryption keys from the server? " +
|
||||
"You will no longer be able to use your recovery key to read encrypted message history",
|
||||
),
|
||||
button: _t('Delete backup'),
|
||||
danger: true,
|
||||
onFinished: (proceed) => {
|
||||
if (!proceed) return;
|
||||
this.setState({loading: true});
|
||||
MatrixClientPeg.get().deleteKeyBackupVersion(this.state.backupInfo.version).then(() => {
|
||||
this._loadBackupStatus();
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
_restoreBackup() {
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
|
||||
});
|
||||
}
|
||||
|
||||
_verifyDevice(e) {
|
||||
const device = this.state.backupSigStatus.sigs[e.target.getAttribute('data-sigindex')].device;
|
||||
|
||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
|
||||
userId: MatrixClientPeg.get().credentials.userId,
|
||||
device: device,
|
||||
onFinished: () => {
|
||||
this._loadBackupStatus();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<div className="error">
|
||||
{_t("Unable to load key backup status")}
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.loading) {
|
||||
return <Spinner />;
|
||||
} else if (this.state.backupInfo) {
|
||||
let clientBackupStatus;
|
||||
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||
clientBackupStatus = _t("This device is uploading keys to this backup");
|
||||
} else {
|
||||
// XXX: display why and how to fix it
|
||||
clientBackupStatus = _t(
|
||||
"This device is <b>not</b> uploading keys to this backup", {},
|
||||
{b: x => <b>{x}</b>},
|
||||
);
|
||||
}
|
||||
|
||||
let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => {
|
||||
const sigStatusSubstitutions = {
|
||||
validity: sub =>
|
||||
<span className={sig.valid ? 'mx_KeyBackupPanel_sigValid' : 'mx_KeyBackupPanel_sigInvalid'}>
|
||||
{sub}
|
||||
</span>,
|
||||
verify: sub =>
|
||||
<span className={sig.device.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}>
|
||||
{sub}
|
||||
</span>,
|
||||
device: sub => <span className="mx_KeyBackupPanel_deviceName">{sig.device.getDisplayName()}</span>,
|
||||
};
|
||||
let sigStatus;
|
||||
if (sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()) {
|
||||
sigStatus = _t(
|
||||
"Backup has a <validity>valid</validity> signature from this device",
|
||||
{}, sigStatusSubstitutions,
|
||||
);
|
||||
} else if (sig.valid && sig.device.isVerified()) {
|
||||
sigStatus = _t(
|
||||
"Backup has a <validity>valid</validity> signature from " +
|
||||
"<verify>verified</verify> device <device>x</device>",
|
||||
{}, sigStatusSubstitutions,
|
||||
);
|
||||
} else if (sig.valid && !sig.device.isVerified()) {
|
||||
sigStatus = _t(
|
||||
"Backup has a <validity>valid</validity> signature from " +
|
||||
"<verify>unverified</verify> device <device></device>",
|
||||
{}, sigStatusSubstitutions,
|
||||
);
|
||||
} else if (!sig.valid && sig.device.isVerified()) {
|
||||
sigStatus = _t(
|
||||
"Backup has an <validity>invalid</validity> signature from " +
|
||||
"<verify>verified</verify> device <device></device>",
|
||||
{}, sigStatusSubstitutions,
|
||||
);
|
||||
} else if (!sig.valid && !sig.device.isVerified()) {
|
||||
sigStatus = _t(
|
||||
"Backup has an <validity>invalid</validity> signature from " +
|
||||
"<verify>unverified</verify> device <device></device>",
|
||||
{}, sigStatusSubstitutions,
|
||||
);
|
||||
}
|
||||
|
||||
let verifyButton;
|
||||
if (!sig.device.isVerified()) {
|
||||
verifyButton = <div><br /><AccessibleButton className="mx_UserSettings_button"
|
||||
onClick={this._verifyDevice} data-sigindex={i}>
|
||||
{ _t("Verify...") }
|
||||
</AccessibleButton></div>;
|
||||
}
|
||||
|
||||
return <div key={i}>
|
||||
{sigStatus}
|
||||
{verifyButton}
|
||||
</div>;
|
||||
});
|
||||
if (this.state.backupSigStatus.sigs.length === 0) {
|
||||
backupSigStatuses = _t("Backup is not signed by any of your devices");
|
||||
}
|
||||
|
||||
return <div>
|
||||
{_t("Backup version: ")}{this.state.backupInfo.version}<br />
|
||||
{_t("Algorithm: ")}{this.state.backupInfo.algorithm}<br />
|
||||
{clientBackupStatus}<br />
|
||||
<div>{backupSigStatuses}</div><br />
|
||||
<br />
|
||||
<AccessibleButton className="mx_UserSettings_button"
|
||||
onClick={this._restoreBackup}>
|
||||
{ _t("Restore backup") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_UserSettings_button danger"
|
||||
onClick={this._deleteBackup}>
|
||||
{ _t("Delete backup") }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
} else {
|
||||
return <div>
|
||||
{_t("No backup is present")}<br /><br />
|
||||
<AccessibleButton className="mx_UserSettings_button"
|
||||
onClick={this._startNewBackup}>
|
||||
{ _t("Start a new backup") }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -67,7 +67,7 @@ module.exports = React.createClass({
|
|||
phases: {
|
||||
LOADING: "LOADING", // The component is loading or sending data to the hs
|
||||
DISPLAY: "DISPLAY", // The component is ready and display data
|
||||
ERROR: "ERROR", // There was an error
|
||||
ERROR: "ERROR", // There was an error
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
|
@ -86,14 +86,14 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
phase: this.phases.LOADING,
|
||||
masterPushRule: undefined, // The master rule ('.m.rule.master')
|
||||
vectorPushRules: [], // HS default push rules displayed in Vector UI
|
||||
vectorContentRules: { // Keyword push rules displayed in Vector UI
|
||||
masterPushRule: undefined, // The master rule ('.m.rule.master')
|
||||
vectorPushRules: [], // HS default push rules displayed in Vector UI
|
||||
vectorContentRules: { // Keyword push rules displayed in Vector UI
|
||||
vectorState: PushRuleVectorState.ON,
|
||||
rules: [],
|
||||
},
|
||||
externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI
|
||||
externalContentRules: [], // Keyword push rules that have been defined outside Vector UI
|
||||
externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI
|
||||
externalContentRules: [], // Keyword push rules that have been defined outside Vector UI
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -290,7 +290,7 @@ module.exports = React.createClass({
|
|||
for (const i in this.state.vectorContentRules.rules) {
|
||||
const rule = this.state.vectorContentRules.rules[i];
|
||||
|
||||
let enabled, actions;
|
||||
let enabled; let actions;
|
||||
switch (newPushRuleVectorState) {
|
||||
case PushRuleVectorState.ON:
|
||||
if (rule.actions.length !== 1) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue