mirror of
https://github.com/element-hq/element-web
synced 2024-11-28 12:28:50 +03:00
merge develop
This commit is contained in:
commit
5f763f8ebb
23 changed files with 813 additions and 86 deletions
51
src/GuestAccess.js
Normal file
51
src/GuestAccess.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
Copyright 2015 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.
|
||||
*/
|
||||
const IS_GUEST_KEY = "matrix-is-guest";
|
||||
|
||||
class GuestAccess {
|
||||
|
||||
constructor(localStorage) {
|
||||
this.localStorage = localStorage;
|
||||
try {
|
||||
this._isGuest = localStorage.getItem(IS_GUEST_KEY) === "true";
|
||||
}
|
||||
catch (e) {} // don't care
|
||||
}
|
||||
|
||||
setPeekedRoom(roomId) {
|
||||
// we purposefully do not persist this to local storage as peeking is
|
||||
// entirely transient.
|
||||
this._peekedRoomId = roomId;
|
||||
}
|
||||
|
||||
getPeekedRoom() {
|
||||
return this._peekedRoomId;
|
||||
}
|
||||
|
||||
isGuest() {
|
||||
return this._isGuest;
|
||||
}
|
||||
|
||||
markAsGuest(isGuest) {
|
||||
try {
|
||||
this.localStorage.setItem(IS_GUEST_KEY, JSON.stringify(isGuest));
|
||||
} catch (e) {} // ignore. If they don't do LS, they'll just get a new account.
|
||||
this._isGuest = isGuest;
|
||||
this._peekedRoomId = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuestAccess;
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
|
||||
// A thing that holds your Matrix Client
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
var GuestAccess = require("./GuestAccess");
|
||||
|
||||
var matrixClient = null;
|
||||
|
||||
|
@ -33,7 +34,7 @@ function deviceId() {
|
|||
return id;
|
||||
}
|
||||
|
||||
function createClient(hs_url, is_url, user_id, access_token) {
|
||||
function createClient(hs_url, is_url, user_id, access_token, guestAccess) {
|
||||
var opts = {
|
||||
baseUrl: hs_url,
|
||||
idBaseUrl: is_url,
|
||||
|
@ -47,6 +48,15 @@ function createClient(hs_url, is_url, user_id, access_token) {
|
|||
}
|
||||
|
||||
matrixClient = Matrix.createClient(opts);
|
||||
if (guestAccess) {
|
||||
console.log("Guest: %s", guestAccess.isGuest());
|
||||
matrixClient.setGuest(guestAccess.isGuest());
|
||||
var peekedRoomId = guestAccess.getPeekedRoom();
|
||||
if (peekedRoomId) {
|
||||
console.log("Peeking in room %s", peekedRoomId);
|
||||
matrixClient.peekInRoom(peekedRoomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (localStorage) {
|
||||
|
@ -54,12 +64,18 @@ if (localStorage) {
|
|||
var is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
||||
var access_token = localStorage.getItem("mx_access_token");
|
||||
var user_id = localStorage.getItem("mx_user_id");
|
||||
var guestAccess = new GuestAccess(localStorage);
|
||||
if (access_token && user_id && hs_url) {
|
||||
createClient(hs_url, is_url, user_id, access_token);
|
||||
createClient(hs_url, is_url, user_id, access_token, guestAccess);
|
||||
}
|
||||
}
|
||||
|
||||
class MatrixClient {
|
||||
|
||||
constructor(guestAccess) {
|
||||
this.guestAccess = guestAccess;
|
||||
}
|
||||
|
||||
get() {
|
||||
return matrixClient;
|
||||
}
|
||||
|
@ -97,7 +113,7 @@ class MatrixClient {
|
|||
}
|
||||
}
|
||||
|
||||
replaceUsingAccessToken(hs_url, is_url, user_id, access_token) {
|
||||
replaceUsingAccessToken(hs_url, is_url, user_id, access_token, isGuest) {
|
||||
if (localStorage) {
|
||||
try {
|
||||
localStorage.clear();
|
||||
|
@ -105,7 +121,8 @@ class MatrixClient {
|
|||
console.warn("Error using local storage");
|
||||
}
|
||||
}
|
||||
createClient(hs_url, is_url, user_id, access_token);
|
||||
this.guestAccess.markAsGuest(Boolean(isGuest));
|
||||
createClient(hs_url, is_url, user_id, access_token, this.guestAccess);
|
||||
if (localStorage) {
|
||||
try {
|
||||
localStorage.setItem("mx_hs_url", hs_url);
|
||||
|
@ -122,6 +139,6 @@ class MatrixClient {
|
|||
}
|
||||
|
||||
if (!global.mxMatrixClient) {
|
||||
global.mxMatrixClient = new MatrixClient();
|
||||
global.mxMatrixClient = new MatrixClient(new GuestAccess(localStorage));
|
||||
}
|
||||
module.exports = global.mxMatrixClient;
|
||||
|
|
104
src/PasswordReset.js
Normal file
104
src/PasswordReset.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
|
||||
/**
|
||||
* Allows a user to reset their password on a homeserver.
|
||||
*
|
||||
* This involves getting an email token from the identity server to "prove" that
|
||||
* the client owns the given email address, which is then passed to the password
|
||||
* API on the homeserver in question with the new password.
|
||||
*/
|
||||
class PasswordReset {
|
||||
|
||||
/**
|
||||
* Configure the endpoints for password resetting.
|
||||
* @param {string} homeserverUrl The URL to the HS which has the account to reset.
|
||||
* @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping.
|
||||
*/
|
||||
constructor(homeserverUrl, identityUrl) {
|
||||
this.client = Matrix.createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
idBaseUrl: identityUrl
|
||||
});
|
||||
this.clientSecret = generateClientSecret();
|
||||
this.identityServerDomain = identityUrl.split("://")[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to reset the user's password. This will trigger a side-effect of
|
||||
* sending an email to the provided email address.
|
||||
* @param {string} emailAddress The email address
|
||||
* @param {string} newPassword The new password for the account.
|
||||
* @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
|
||||
*/
|
||||
resetPassword(emailAddress, newPassword) {
|
||||
this.password = newPassword;
|
||||
return this.client.requestEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
|
||||
this.sessionId = res.sid;
|
||||
return res;
|
||||
}, function(err) {
|
||||
if (err.httpStatus) {
|
||||
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the email link has been clicked by attempting to change the password
|
||||
* for the mxid linked to the email.
|
||||
* @return {Promise} Resolves if the password was reset. Rejects with an object
|
||||
* with a "message" property which contains a human-readable message detailing why
|
||||
* the reset failed, e.g. "There is no mapped matrix user ID for the given email address".
|
||||
*/
|
||||
checkEmailLinkClicked() {
|
||||
return this.client.setPassword({
|
||||
type: "m.login.email.identity",
|
||||
threepid_creds: {
|
||||
sid: this.sessionId,
|
||||
client_secret: this.clientSecret,
|
||||
id_server: this.identityServerDomain
|
||||
}
|
||||
}, this.password).catch(function(err) {
|
||||
if (err.httpStatus === 401) {
|
||||
err.message = "Failed to verify email address: make sure you clicked the link in the email";
|
||||
}
|
||||
else if (err.httpStatus === 404) {
|
||||
err.message = "Your email address does not appear to be associated with a Matrix ID on this Homeserver.";
|
||||
}
|
||||
else if (err.httpStatus) {
|
||||
err.message += ` (Status ${err.httpStatus})`;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// from Angular SDK
|
||||
function generateClientSecret() {
|
||||
var ret = "";
|
||||
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (var i = 0; i < 32; i++) {
|
||||
ret += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
module.exports = PasswordReset;
|
|
@ -73,6 +73,11 @@ class Presence {
|
|||
}
|
||||
var old_state = this.state;
|
||||
this.state = newState;
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return; // don't try to set presence when a guest; it won't work.
|
||||
}
|
||||
|
||||
var self = this;
|
||||
MatrixClientPeg.get().setPresence(this.state).done(function() {
|
||||
console.log("Presence: %s", newState);
|
||||
|
|
|
@ -69,6 +69,10 @@ class Register extends Signup {
|
|||
this.params.idSid = idSid;
|
||||
}
|
||||
|
||||
setGuestAccessToken(token) {
|
||||
this.guestAccessToken = token;
|
||||
}
|
||||
|
||||
getStep() {
|
||||
return this._step;
|
||||
}
|
||||
|
@ -126,7 +130,8 @@ class Register extends Signup {
|
|||
}
|
||||
|
||||
return MatrixClientPeg.get().register(
|
||||
this.username, this.password, this.params.sessionId, authDict, bindEmail
|
||||
this.username, this.password, this.params.sessionId, authDict, bindEmail,
|
||||
this.guestAccessToken
|
||||
).then(function(result) {
|
||||
self.credentials = result;
|
||||
self.setStep("COMPLETE");
|
||||
|
|
|
@ -31,6 +31,11 @@ class UserActivity {
|
|||
start() {
|
||||
document.onmousemove = this._onUserActivity.bind(this);
|
||||
document.onkeypress = this._onUserActivity.bind(this);
|
||||
// can't use document.scroll here because that's only the document
|
||||
// itself being scrolled. Need to use addEventListener's useCapture.
|
||||
// also this needs to be the wheel event, not scroll, as scroll is
|
||||
// fired when the view scrolls down for a new message.
|
||||
window.addEventListener('wheel', this._onUserActivity.bind(this), true);
|
||||
this.lastActivityAtTs = new Date().getTime();
|
||||
this.lastDispatchAtTs = 0;
|
||||
}
|
||||
|
@ -41,10 +46,11 @@ class UserActivity {
|
|||
stop() {
|
||||
document.onmousemove = undefined;
|
||||
document.onkeypress = undefined;
|
||||
window.removeEventListener('wheel', this._onUserActivity.bind(this), true);
|
||||
}
|
||||
|
||||
_onUserActivity(event) {
|
||||
if (event.screenX) {
|
||||
if (event.screenX && event.type == "mousemove") {
|
||||
if (event.screenX === this.lastScreenX &&
|
||||
event.screenY === this.lastScreenY)
|
||||
{
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var q = require("q");
|
||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||
var Notifier = require("./Notifier");
|
||||
|
||||
|
@ -35,6 +35,11 @@ module.exports = {
|
|||
},
|
||||
|
||||
loadThreePids: function() {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return q({
|
||||
threepids: []
|
||||
}); // guests can't poke 3pid endpoint
|
||||
}
|
||||
return MatrixClientPeg.get().getThreePids();
|
||||
},
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ module.exports = React.createClass({
|
|||
var old = oldChildren[c.key];
|
||||
var oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
|
||||
|
||||
if (oldNode.style.left != c.props.style.left) {
|
||||
if (oldNode && oldNode.style.left != c.props.style.left) {
|
||||
Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() {
|
||||
// special case visibility because it's nonsensical to animate an invisible element
|
||||
// so we always hidden->visible pre-transition and visible->hidden after
|
||||
|
@ -73,6 +73,7 @@ module.exports = React.createClass({
|
|||
|
||||
collectNode: function(k, node) {
|
||||
if (
|
||||
node &&
|
||||
this.nodes[k] === undefined &&
|
||||
node.props.startStyle &&
|
||||
Object.keys(node.props.startStyle).length
|
||||
|
|
|
@ -23,14 +23,15 @@ limitations under the License.
|
|||
|
||||
module.exports.components = {};
|
||||
module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom');
|
||||
module.exports.components['structures.login.ForgotPassword'] = require('./components/structures/login/ForgotPassword');
|
||||
module.exports.components['structures.login.Login'] = require('./components/structures/login/Login');
|
||||
module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration');
|
||||
module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration');
|
||||
module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat');
|
||||
module.exports.components['structures.RoomView'] = require('./components/structures/RoomView');
|
||||
module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel');
|
||||
module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar');
|
||||
module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings');
|
||||
module.exports.components['structures.login.Login'] = require('./components/structures/login/Login');
|
||||
module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration');
|
||||
module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration');
|
||||
module.exports.components['views.avatars.MemberAvatar'] = require('./components/views/avatars/MemberAvatar');
|
||||
module.exports.components['views.avatars.RoomAvatar'] = require('./components/views/avatars/RoomAvatar');
|
||||
module.exports.components['views.create_room.CreateRoomButton'] = require('./components/views/create_room/CreateRoomButton');
|
||||
|
@ -52,10 +53,10 @@ module.exports.components['views.login.LoginHeader'] = require('./components/vie
|
|||
module.exports.components['views.login.PasswordLogin'] = require('./components/views/login/PasswordLogin');
|
||||
module.exports.components['views.login.RegistrationForm'] = require('./components/views/login/RegistrationForm');
|
||||
module.exports.components['views.login.ServerConfig'] = require('./components/views/login/ServerConfig');
|
||||
module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent');
|
||||
module.exports.components['views.messages.MFileBody'] = require('./components/views/messages/MFileBody');
|
||||
module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody');
|
||||
module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody');
|
||||
module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent');
|
||||
module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody');
|
||||
module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent');
|
||||
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');
|
||||
|
|
|
@ -43,6 +43,7 @@ module.exports = React.createClass({
|
|||
ConferenceHandler: React.PropTypes.any,
|
||||
onNewScreen: React.PropTypes.func,
|
||||
registrationUrl: React.PropTypes.string,
|
||||
enableGuest: React.PropTypes.bool,
|
||||
startingQueryParams: React.PropTypes.object
|
||||
},
|
||||
|
||||
|
@ -84,8 +85,21 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._autoRegisterAsGuest = false;
|
||||
if (this.props.enableGuest) {
|
||||
if (!this.props.config || !this.props.config.default_hs_url) {
|
||||
console.error("Cannot enable guest access: No supplied config prop for HS/IS URLs");
|
||||
}
|
||||
else {
|
||||
this._autoRegisterAsGuest = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
if (this.state.logged_in) {
|
||||
// Don't auto-register as a guest. This applies if you refresh the page on a
|
||||
// logged in client THEN hit the Sign Out button.
|
||||
this._autoRegisterAsGuest = false;
|
||||
this.startMatrixClient();
|
||||
}
|
||||
this.focusComposer = false;
|
||||
|
@ -94,8 +108,11 @@ module.exports = React.createClass({
|
|||
this.scrollStateMap = {};
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
window.addEventListener("focus", this.onFocus);
|
||||
|
||||
if (this.state.logged_in) {
|
||||
this.notifyNewScreen('');
|
||||
} else if (this._autoRegisterAsGuest) {
|
||||
this._registerAsGuest();
|
||||
} else {
|
||||
this.notifyNewScreen('login');
|
||||
}
|
||||
|
@ -127,6 +144,34 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_registerAsGuest: function() {
|
||||
var self = this;
|
||||
var config = this.props.config;
|
||||
console.log("Doing guest login on %s", config.default_hs_url);
|
||||
MatrixClientPeg.replaceUsingUrls(
|
||||
config.default_hs_url, config.default_is_url
|
||||
);
|
||||
MatrixClientPeg.get().registerGuest().done(function(creds) {
|
||||
console.log("Registered as guest: %s", creds.user_id);
|
||||
self._setAutoRegisterAsGuest(false);
|
||||
self.onLoggedIn({
|
||||
userId: creds.user_id,
|
||||
accessToken: creds.access_token,
|
||||
homeserverUrl: config.default_hs_url,
|
||||
identityServerUrl: config.default_is_url,
|
||||
guest: true
|
||||
});
|
||||
}, function(err) {
|
||||
console.error(err.data);
|
||||
self._setAutoRegisterAsGuest(false);
|
||||
});
|
||||
},
|
||||
|
||||
_setAutoRegisterAsGuest: function(shouldAutoRegister) {
|
||||
this._autoRegisterAsGuest = shouldAutoRegister;
|
||||
this.forceUpdate();
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
var roomIndexDelta = 1;
|
||||
|
||||
|
@ -181,6 +226,21 @@ module.exports = React.createClass({
|
|||
screen: 'post_registration'
|
||||
});
|
||||
break;
|
||||
case 'start_upgrade_registration':
|
||||
this.replaceState({
|
||||
screen: "register",
|
||||
upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(),
|
||||
guestAccessToken: MatrixClientPeg.get().getAccessToken()
|
||||
});
|
||||
this.notifyNewScreen('register');
|
||||
break;
|
||||
case 'start_password_recovery':
|
||||
if (this.state.logged_in) return;
|
||||
this.replaceState({
|
||||
screen: 'forgot_password'
|
||||
});
|
||||
this.notifyNewScreen('forgot_password');
|
||||
break;
|
||||
case 'token_login':
|
||||
if (this.state.logged_in) return;
|
||||
|
||||
|
@ -392,10 +452,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onLoggedIn: function(credentials) {
|
||||
console.log("onLoggedIn => %s", credentials.userId);
|
||||
credentials.guest = Boolean(credentials.guest);
|
||||
console.log("onLoggedIn => %s (guest=%s)", credentials.userId, credentials.guest);
|
||||
MatrixClientPeg.replaceUsingAccessToken(
|
||||
credentials.homeserverUrl, credentials.identityServerUrl,
|
||||
credentials.userId, credentials.accessToken
|
||||
credentials.userId, credentials.accessToken, credentials.guest
|
||||
);
|
||||
this.setState({
|
||||
screen: undefined,
|
||||
|
@ -515,6 +576,11 @@ module.exports = React.createClass({
|
|||
action: 'token_login',
|
||||
params: params
|
||||
});
|
||||
} else if (screen == 'forgot_password') {
|
||||
dis.dispatch({
|
||||
action: 'start_password_recovery',
|
||||
params: params
|
||||
});
|
||||
} else if (screen == 'new') {
|
||||
dis.dispatch({
|
||||
action: 'view_create_room',
|
||||
|
@ -624,6 +690,10 @@ module.exports = React.createClass({
|
|||
this.showScreen("login");
|
||||
},
|
||||
|
||||
onForgotPasswordClick: function() {
|
||||
this.showScreen("forgot_password");
|
||||
},
|
||||
|
||||
onRegistered: function(credentials) {
|
||||
this.onLoggedIn(credentials);
|
||||
// do post-registration stuff
|
||||
|
@ -662,6 +732,7 @@ module.exports = React.createClass({
|
|||
var CreateRoom = sdk.getComponent('structures.CreateRoom');
|
||||
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
||||
var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||
var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
|
||||
|
||||
// needs to be before normal PageTypes as you are logged in technically
|
||||
if (this.state.screen == 'post_registration') {
|
||||
|
@ -725,12 +796,20 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
} else if (this.state.logged_in) {
|
||||
} else if (this.state.logged_in || (!this.state.logged_in && this._autoRegisterAsGuest)) {
|
||||
var Spinner = sdk.getComponent('elements.Spinner');
|
||||
var logoutLink;
|
||||
if (this.state.logged_in) {
|
||||
logoutLink = (
|
||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
|
||||
Logout
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
<Spinner />
|
||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>Logout</a>
|
||||
{logoutLink}
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.screen == 'register') {
|
||||
|
@ -740,19 +819,30 @@ module.exports = React.createClass({
|
|||
sessionId={this.state.register_session_id}
|
||||
idSid={this.state.register_id_sid}
|
||||
email={this.props.startingQueryParams.email}
|
||||
username={this.state.upgradeUsername}
|
||||
disableUsernameChanges={Boolean(this.state.upgradeUsername)}
|
||||
guestAccessToken={this.state.guestAccessToken}
|
||||
hsUrl={this.props.config.default_hs_url}
|
||||
isUrl={this.props.config.default_is_url}
|
||||
registrationUrl={this.props.registrationUrl}
|
||||
onLoggedIn={this.onRegistered}
|
||||
onLoginClick={this.onLoginClick} />
|
||||
);
|
||||
} else if (this.state.screen == 'forgot_password') {
|
||||
return (
|
||||
<ForgotPassword
|
||||
homeserverUrl={this.props.config.default_hs_url}
|
||||
identityServerUrl={this.props.config.default_is_url}
|
||||
onComplete={this.onLoginClick} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Login
|
||||
onLoggedIn={this.onLoggedIn}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
homeserverUrl={this.props.config.default_hs_url}
|
||||
identityServerUrl={this.props.config.default_is_url} />
|
||||
identityServerUrl={this.props.config.default_is_url}
|
||||
onForgotPasswordClick={this.onForgotPasswordClick} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ var Tinter = require("../../Tinter");
|
|||
|
||||
var PAGINATE_SIZE = 20;
|
||||
var INITIAL_SIZE = 20;
|
||||
var SEND_READ_RECEIPT_DELAY = 2000;
|
||||
|
||||
var DEBUG_SCROLL = false;
|
||||
|
||||
|
@ -75,6 +76,8 @@ module.exports = React.createClass({
|
|||
syncState: MatrixClientPeg.get().getSyncState(),
|
||||
hasUnsentMessages: this._hasUnsentMessages(room),
|
||||
callState: null,
|
||||
readMarkerEventId: room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId),
|
||||
readMarkerGhostEventId: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -99,9 +102,33 @@ module.exports = React.createClass({
|
|||
this.forceUpdate();
|
||||
}
|
||||
});
|
||||
// if this is an unknown room then we're in one of three states:
|
||||
// - This is a room we can peek into (search engine) (we can /peek)
|
||||
// - This is a room we can publicly join or were invited to. (we can /join)
|
||||
// - This is a room we cannot join at all. (no action can help us)
|
||||
// We can't try to /join because this may implicitly accept invites (!)
|
||||
// We can /peek though. If it fails then we present the join UI. If it
|
||||
// succeeds then great, show the preview (but we still may be able to /join!).
|
||||
if (!this.state.room) {
|
||||
console.log("Attempting to peek into room %s", this.props.roomId);
|
||||
MatrixClientPeg.get().peekInRoom(this.props.roomId).done(function() {
|
||||
// we don't need to do anything - JS SDK will emit Room events
|
||||
// which will update the UI.
|
||||
}, function(err) {
|
||||
console.error("Failed to peek into room: %s", err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// set a boolean to say we've been unmounted, which any pending
|
||||
// promises can use to throw away their results.
|
||||
//
|
||||
// (We could use isMounted, but facebook have deprecated that.)
|
||||
this.unmounted = true;
|
||||
|
||||
if (this.refs.messagePanel) {
|
||||
// disconnect the D&D event listeners from the message panel. This
|
||||
// is really just for hygiene - the messagePanel is going to be
|
||||
|
@ -201,7 +228,7 @@ module.exports = React.createClass({
|
|||
},*/
|
||||
|
||||
onRoomTimeline: function(ev, room, toStartOfTimeline) {
|
||||
if (!this.isMounted()) return;
|
||||
if (this.unmounted) return;
|
||||
|
||||
// ignore anything that comes in whilst paginating: we get one
|
||||
// event for each new matrix event so this would cause a huge
|
||||
|
@ -265,7 +292,33 @@ module.exports = React.createClass({
|
|||
|
||||
onRoomReceipt: function(receiptEvent, room) {
|
||||
if (room.roomId == this.props.roomId) {
|
||||
this.forceUpdate();
|
||||
var readMarkerEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId);
|
||||
var readMarkerGhostEventId = this.state.readMarkerGhostEventId;
|
||||
if (this.state.readMarkerEventId !== undefined && this.state.readMarkerEventId != readMarkerEventId) {
|
||||
readMarkerGhostEventId = this.state.readMarkerEventId;
|
||||
}
|
||||
|
||||
|
||||
// if the event after the one referenced in the read receipt if sent by us, do nothing since
|
||||
// this is a temporary period before the synthesized receipt for our own message arrives
|
||||
var readMarkerGhostEventIndex;
|
||||
for (var i = 0; i < room.timeline.length; ++i) {
|
||||
if (room.timeline[i].getId() == readMarkerGhostEventId) {
|
||||
readMarkerGhostEventIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (readMarkerGhostEventIndex + 1 < room.timeline.length) {
|
||||
var nextEvent = room.timeline[readMarkerGhostEventIndex + 1];
|
||||
if (nextEvent.sender && nextEvent.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
||||
readMarkerGhostEventId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
readMarkerEventId: readMarkerEventId,
|
||||
readMarkerGhostEventId: readMarkerGhostEventId,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -383,11 +436,14 @@ module.exports = React.createClass({
|
|||
_paginateCompleted: function() {
|
||||
debuglog("paginate complete");
|
||||
|
||||
this.setState({
|
||||
room: MatrixClientPeg.get().getRoom(this.props.roomId)
|
||||
});
|
||||
// we might have switched rooms since the paginate started - just bin
|
||||
// the results if so.
|
||||
if (this.unmounted) return;
|
||||
|
||||
this.setState({paginating: false});
|
||||
this.setState({
|
||||
room: MatrixClientPeg.get().getRoom(this.props.roomId),
|
||||
paginating: false,
|
||||
});
|
||||
},
|
||||
|
||||
onSearchResultsFillRequest: function(backwards) {
|
||||
|
@ -452,6 +508,12 @@ module.exports = React.createClass({
|
|||
joining: false,
|
||||
joinError: error
|
||||
});
|
||||
var msg = error.message ? error.message : JSON.stringify(error);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to join room",
|
||||
description: msg
|
||||
});
|
||||
});
|
||||
this.setState({
|
||||
joining: true
|
||||
|
@ -565,7 +627,7 @@ module.exports = React.createClass({
|
|||
|
||||
return searchPromise.then(function(results) {
|
||||
debuglog("search complete");
|
||||
if (!self.state.searching || self.searchId != localSearchId) {
|
||||
if (self.unmounted || !self.state.searching || self.searchId != localSearchId) {
|
||||
console.error("Discarding stale search results");
|
||||
return;
|
||||
}
|
||||
|
@ -583,7 +645,8 @@ module.exports = React.createClass({
|
|||
|
||||
// For overlapping highlights,
|
||||
// favour longer (more specific) terms first
|
||||
highlights = highlights.sort(function(a, b) { b.length - a.length });
|
||||
highlights = highlights.sort(function(a, b) {
|
||||
return b.length - a.length });
|
||||
|
||||
self.setState({
|
||||
searchHighlights: highlights,
|
||||
|
@ -678,10 +741,10 @@ module.exports = React.createClass({
|
|||
|
||||
var EventTile = sdk.getComponent('rooms.EventTile');
|
||||
|
||||
|
||||
var prevEvent = null; // the last event we showed
|
||||
var readReceiptEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId);
|
||||
var startIdx = Math.max(0, this.state.room.timeline.length - this.state.messageCap);
|
||||
var readMarkerIndex;
|
||||
var ghostIndex;
|
||||
for (var i = startIdx; i < this.state.room.timeline.length; i++) {
|
||||
var mxEv = this.state.room.timeline[i];
|
||||
|
||||
|
@ -695,6 +758,25 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
// now we've decided whether or not to show this message,
|
||||
// add the read up to marker if appropriate
|
||||
// doing this here means we implicitly do not show the marker
|
||||
// if it's at the bottom
|
||||
// NB. it would be better to decide where the read marker was going
|
||||
// when the state changed rather than here in the render method, but
|
||||
// this is where we decide what messages we show so it's the only
|
||||
// place we know whether we're at the bottom or not.
|
||||
var self = this;
|
||||
var mxEvSender = mxEv.sender ? mxEv.sender.userId : null;
|
||||
if (prevEvent && prevEvent.getId() == this.state.readMarkerEventId && mxEvSender != MatrixClientPeg.get().credentials.userId) {
|
||||
var hr;
|
||||
hr = (<hr className="mx_RoomView_myReadMarker" style={{opacity: 1, width: '99%'}} ref={function(n) {
|
||||
self.readMarkerNode = n;
|
||||
}} />);
|
||||
readMarkerIndex = ret.length;
|
||||
ret.push(<li key="_readupto" className="mx_RoomView_myReadMarker_container">{hr}</li>);
|
||||
}
|
||||
|
||||
// is this a continuation of the previous message?
|
||||
var continuation = false;
|
||||
if (prevEvent !== null) {
|
||||
|
@ -731,17 +813,33 @@ module.exports = React.createClass({
|
|||
</li>
|
||||
);
|
||||
|
||||
if (eventId == readReceiptEventId) {
|
||||
ret.push(<hr className="mx_RoomView_myReadMarker" />);
|
||||
// A read up to marker has died and returned as a ghost!
|
||||
// Lives in the dom as the ghost of the previous one while it fades away
|
||||
if (eventId == this.state.readMarkerGhostEventId) {
|
||||
ghostIndex = ret.length;
|
||||
}
|
||||
|
||||
prevEvent = mxEv;
|
||||
}
|
||||
|
||||
// splice the read marker ghost in now that we know whether the read receipt
|
||||
// is the last element or not, because we only decide as we're going along.
|
||||
if (readMarkerIndex === undefined && ghostIndex && ghostIndex <= ret.length) {
|
||||
var hr;
|
||||
hr = (<hr className="mx_RoomView_myReadMarker" style={{opacity: 1, width: '85%'}} ref={function(n) {
|
||||
Velocity(n, {opacity: '0', width: '10%'}, {duration: 400, easing: 'easeInSine', delay: 1000, complete: function() {
|
||||
self.setState({readMarkerGhostEventId: undefined});
|
||||
}});
|
||||
}} />);
|
||||
ret.splice(ghostIndex, 0, (
|
||||
<li key="_readuptoghost" className="mx_RoomView_myReadMarker_container">{hr}</li>
|
||||
));
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
uploadNewState: function(new_name, new_topic, new_join_rule, new_history_visibility, new_power_levels, new_color_scheme) {
|
||||
uploadNewState: function(newVals) {
|
||||
var old_name = this.state.room.name;
|
||||
|
||||
var old_topic = this.state.room.currentState.getStateEvents('m.room.topic', '');
|
||||
|
@ -767,54 +865,63 @@ module.exports = React.createClass({
|
|||
|
||||
var deferreds = [];
|
||||
|
||||
if (old_name != new_name && new_name != undefined) {
|
||||
if (old_name != newVals.name && newVals.name != undefined) {
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().setRoomName(this.state.room.roomId, new_name)
|
||||
MatrixClientPeg.get().setRoomName(this.state.room.roomId, newVals.name)
|
||||
);
|
||||
}
|
||||
|
||||
if (old_topic != new_topic && new_topic != undefined) {
|
||||
if (old_topic != newVals.topic && newVals.topic != undefined) {
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().setRoomTopic(this.state.room.roomId, new_topic)
|
||||
MatrixClientPeg.get().setRoomTopic(this.state.room.roomId, newVals.topic)
|
||||
);
|
||||
}
|
||||
|
||||
if (old_join_rule != new_join_rule && new_join_rule != undefined) {
|
||||
if (old_join_rule != newVals.join_rule && newVals.join_rule != undefined) {
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.state.room.roomId, "m.room.join_rules", {
|
||||
join_rule: new_join_rule,
|
||||
join_rule: newVals.join_rule,
|
||||
}, ""
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (old_history_visibility != new_history_visibility && new_history_visibility != undefined) {
|
||||
if (old_history_visibility != newVals.history_visibility &&
|
||||
newVals.history_visibility != undefined) {
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.state.room.roomId, "m.room.history_visibility", {
|
||||
history_visibility: new_history_visibility,
|
||||
history_visibility: newVals.history_visibility,
|
||||
}, ""
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (new_power_levels) {
|
||||
if (newVals.power_levels) {
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.state.room.roomId, "m.room.power_levels", new_power_levels, ""
|
||||
this.state.room.roomId, "m.room.power_levels", newVals.power_levels, ""
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (new_color_scheme) {
|
||||
if (newVals.color_scheme) {
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().setRoomAccountData(
|
||||
this.state.room.roomId, "org.matrix.room.color_scheme", new_color_scheme
|
||||
this.state.room.roomId, "org.matrix.room.color_scheme", newVals.color_scheme
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().setGuestAccess(this.state.room.roomId, {
|
||||
allowRead: newVals.guest_read,
|
||||
allowJoin: newVals.guest_join
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
if (deferreds.length) {
|
||||
var self = this;
|
||||
q.all(deferreds).fail(function(err) {
|
||||
|
@ -899,21 +1006,16 @@ module.exports = React.createClass({
|
|||
uploadingRoomSettings: true,
|
||||
});
|
||||
|
||||
var new_name = this.refs.header.getRoomName();
|
||||
var new_topic = this.refs.header.getTopic();
|
||||
var new_join_rule = this.refs.room_settings.getJoinRules();
|
||||
var new_history_visibility = this.refs.room_settings.getHistoryVisibility();
|
||||
var new_power_levels = this.refs.room_settings.getPowerLevels();
|
||||
var new_color_scheme = this.refs.room_settings.getColorScheme();
|
||||
|
||||
this.uploadNewState(
|
||||
new_name,
|
||||
new_topic,
|
||||
new_join_rule,
|
||||
new_history_visibility,
|
||||
new_power_levels,
|
||||
new_color_scheme
|
||||
);
|
||||
this.uploadNewState({
|
||||
name: this.refs.header.getRoomName(),
|
||||
topic: this.refs.room_settings.getTopic(),
|
||||
join_rule: this.refs.room_settings.getJoinRules(),
|
||||
history_visibility: this.refs.room_settings.getHistoryVisibility(),
|
||||
power_levels: this.refs.room_settings.getPowerLevels(),
|
||||
guest_join: this.refs.room_settings.canGuestsJoin(),
|
||||
guest_read: this.refs.room_settings.canGuestsRead(),
|
||||
color_scheme: this.refs.room_settings.getColorScheme(),
|
||||
});
|
||||
},
|
||||
|
||||
onCancelClick: function() {
|
||||
|
@ -1074,10 +1176,23 @@ module.exports = React.createClass({
|
|||
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
|
||||
|
||||
if (this.refs.callView) {
|
||||
// XXX: don't understand why we have to call findDOMNode here in react 0.14 - it should already be a DOM node.
|
||||
var video = ReactDOM.findDOMNode(this.refs.callView.refs.video.refs.remote);
|
||||
var video = this.refs.callView.getVideoView().getRemoteVideoElement();
|
||||
|
||||
// header + footer + status + give us at least 100px of scrollback at all times.
|
||||
auxPanelMaxHeight = window.innerHeight -
|
||||
(83 + 72 +
|
||||
sdk.getComponent('rooms.MessageComposer').MAX_HEIGHT +
|
||||
100);
|
||||
|
||||
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
|
||||
// but it's better than the video going missing entirely
|
||||
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
|
||||
|
||||
video.style.maxHeight = auxPanelMaxHeight + "px";
|
||||
|
||||
// the above might have made the video panel resize itself, so now
|
||||
// we need to tell the gemini panel to adapt.
|
||||
this.onChildResize();
|
||||
}
|
||||
|
||||
// we need to do this for general auxPanels too
|
||||
|
@ -1117,6 +1232,15 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onChildResize: function() {
|
||||
// When the video or the message composer resizes, the scroll panel
|
||||
// also changes size. Work around GeminiScrollBar fail by telling it
|
||||
// about it. This also ensures that the scroll offset is updated.
|
||||
if (this.refs.messagePanel) {
|
||||
this.refs.messagePanel.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
||||
var MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||
|
@ -1307,7 +1431,7 @@ module.exports = React.createClass({
|
|||
if (canSpeak) {
|
||||
messageComposer =
|
||||
<MessageComposer
|
||||
room={this.state.room} roomView={this} uploadFile={this.uploadFile}
|
||||
room={this.state.room} onResize={this.onChildResize} uploadFile={this.uploadFile}
|
||||
callState={this.state.callState} tabComplete={this.tabComplete} />
|
||||
}
|
||||
|
||||
|
@ -1410,7 +1534,8 @@ module.exports = React.createClass({
|
|||
} />
|
||||
{ fileDropTarget }
|
||||
<div className="mx_RoomView_auxPanel" ref="auxPanel">
|
||||
<CallView ref="callView" room={this.state.room} ConferenceHandler={this.props.ConferenceHandler}/>
|
||||
<CallView ref="callView" room={this.state.room} ConferenceHandler={this.props.ConferenceHandler}
|
||||
onResize={this.onChildResize} />
|
||||
{ conferenceCallNotification }
|
||||
{ aux }
|
||||
</div>
|
||||
|
|
|
@ -112,6 +112,14 @@ module.exports = React.createClass({
|
|||
this.checkFillState();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// set a boolean to say we've been unmounted, which any pending
|
||||
// promises can use to throw away their results.
|
||||
//
|
||||
// (We could use isMounted(), but facebook have deprecated that.)
|
||||
this.unmounted = true;
|
||||
},
|
||||
|
||||
onScroll: function(ev) {
|
||||
var sn = this._getScrollNode();
|
||||
debuglog("Scroll event: offset now:", sn.scrollTop, "recentEventScroll:", this.recentEventScroll);
|
||||
|
@ -158,6 +166,10 @@ module.exports = React.createClass({
|
|||
|
||||
// check the scroll state and send out backfill requests if necessary.
|
||||
checkFillState: function() {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
var sn = this._getScrollNode();
|
||||
|
||||
// if there is less than a screenful of messages above or below the
|
||||
|
@ -346,6 +358,12 @@ module.exports = React.createClass({
|
|||
* message panel.
|
||||
*/
|
||||
_getScrollNode: function() {
|
||||
if (this.unmounted) {
|
||||
// this shouldn't happen, but when it does, turn the NPE into
|
||||
// something more meaningful.
|
||||
throw new Error("ScrollPanel._getScrollNode called when unmounted");
|
||||
}
|
||||
|
||||
var panel = ReactDOM.findDOMNode(this.refs.geminiPanel);
|
||||
|
||||
// If the gemini scrollbar is doing its thing, this will be a div within
|
||||
|
|
|
@ -135,6 +135,12 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onUpgradeClicked: function() {
|
||||
dis.dispatch({
|
||||
action: "start_upgrade_registration"
|
||||
});
|
||||
},
|
||||
|
||||
onLogoutPromptCancel: function() {
|
||||
this.logoutModal.closeDialog();
|
||||
},
|
||||
|
@ -164,6 +170,28 @@ module.exports = React.createClass({
|
|||
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
|
||||
);
|
||||
|
||||
var accountJsx;
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
accountJsx = (
|
||||
<div className="mx_UserSettings_button" onClick={this.onUpgradeClicked}>
|
||||
Upgrade (It's free!)
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
accountJsx = (
|
||||
<ChangePassword
|
||||
className="mx_UserSettings_accountTable"
|
||||
rowClassName="mx_UserSettings_profileTableRow"
|
||||
rowLabelClassName="mx_UserSettings_profileLabelCell"
|
||||
rowInputClassName="mx_UserSettings_profileInputCell"
|
||||
buttonClassName="mx_UserSettings_button"
|
||||
onError={this.onPasswordChangeError}
|
||||
onFinished={this.onPasswordChanged} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_UserSettings">
|
||||
<RoomHeader simpleHeader="Settings" />
|
||||
|
@ -213,14 +241,7 @@ module.exports = React.createClass({
|
|||
<h2>Account</h2>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<ChangePassword
|
||||
className="mx_UserSettings_accountTable"
|
||||
rowClassName="mx_UserSettings_profileTableRow"
|
||||
rowLabelClassName="mx_UserSettings_profileLabelCell"
|
||||
rowInputClassName="mx_UserSettings_profileInputCell"
|
||||
buttonClassName="mx_UserSettings_button"
|
||||
onError={this.onPasswordChangeError}
|
||||
onFinished={this.onPasswordChanged} />
|
||||
{accountJsx}
|
||||
</div>
|
||||
|
||||
<div className="mx_UserSettings_logout">
|
||||
|
|
199
src/components/structures/login/ForgotPassword.js
Normal file
199
src/components/structures/login/ForgotPassword.js
Normal file
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
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';
|
||||
|
||||
var React = require('react');
|
||||
var sdk = require('../../../index');
|
||||
var Modal = require("../../../Modal");
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
|
||||
var PasswordReset = require("../../../PasswordReset");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ForgotPassword',
|
||||
|
||||
propTypes: {
|
||||
homeserverUrl: React.PropTypes.string,
|
||||
identityServerUrl: React.PropTypes.string,
|
||||
onComplete: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
enteredHomeserverUrl: this.props.homeserverUrl,
|
||||
enteredIdentityServerUrl: this.props.identityServerUrl,
|
||||
progress: null
|
||||
};
|
||||
},
|
||||
|
||||
submitPasswordReset: function(hsUrl, identityUrl, email, password) {
|
||||
this.setState({
|
||||
progress: "sending_email"
|
||||
});
|
||||
this.reset = new PasswordReset(hsUrl, identityUrl);
|
||||
this.reset.resetPassword(email, password).done(() => {
|
||||
this.setState({
|
||||
progress: "sent_email"
|
||||
});
|
||||
}, (err) => {
|
||||
this.showErrorDialog("Failed to send email: " + err.message);
|
||||
this.setState({
|
||||
progress: null
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
onVerify: function(ev) {
|
||||
ev.preventDefault();
|
||||
if (!this.reset) {
|
||||
console.error("onVerify called before submitPasswordReset!");
|
||||
return;
|
||||
}
|
||||
this.reset.checkEmailLinkClicked().done((res) => {
|
||||
this.setState({ progress: "complete" });
|
||||
}, (err) => {
|
||||
this.showErrorDialog(err.message);
|
||||
})
|
||||
},
|
||||
|
||||
onSubmitForm: function(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
if (!this.state.email) {
|
||||
this.showErrorDialog("The email address linked to your account must be entered.");
|
||||
}
|
||||
else if (!this.state.password || !this.state.password2) {
|
||||
this.showErrorDialog("A new password must be entered.");
|
||||
}
|
||||
else if (this.state.password !== this.state.password2) {
|
||||
this.showErrorDialog("New passwords must match each other.");
|
||||
}
|
||||
else {
|
||||
this.submitPasswordReset(
|
||||
this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
|
||||
this.state.email, this.state.password
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
onInputChanged: function(stateKey, ev) {
|
||||
this.setState({
|
||||
[stateKey]: ev.target.value
|
||||
});
|
||||
},
|
||||
|
||||
onHsUrlChanged: function(newHsUrl) {
|
||||
this.setState({
|
||||
enteredHomeserverUrl: newHsUrl
|
||||
});
|
||||
},
|
||||
|
||||
onIsUrlChanged: function(newIsUrl) {
|
||||
this.setState({
|
||||
enteredIdentityServerUrl: newIsUrl
|
||||
});
|
||||
},
|
||||
|
||||
showErrorDialog: function(body, title) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: title,
|
||||
description: body
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||
var LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||
var ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||
var Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
var resetPasswordJsx;
|
||||
|
||||
if (this.state.progress === "sending_email") {
|
||||
resetPasswordJsx = <Spinner />
|
||||
}
|
||||
else if (this.state.progress === "sent_email") {
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
An email has been sent to {this.state.email}. Once you've followed
|
||||
the link it contains, click below.
|
||||
<br />
|
||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||
value="I have verified my email address" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else if (this.state.progress === "complete") {
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
<p>Your password has been reset.</p>
|
||||
<p>You have been logged out of all devices and will no longer receive push notifications.
|
||||
To re-enable notifications, re-log in on each device.</p>
|
||||
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
||||
value="Return to login screen" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
To reset your password, enter the email address linked to your account:
|
||||
<br />
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
<input className="mx_Login_field" ref="user" type="text"
|
||||
value={this.state.email}
|
||||
onChange={this.onInputChanged.bind(this, "email")}
|
||||
placeholder="Email address" autoFocus />
|
||||
<br />
|
||||
<input className="mx_Login_field" ref="pass" type="password"
|
||||
value={this.state.password}
|
||||
onChange={this.onInputChanged.bind(this, "password")}
|
||||
placeholder="New password" />
|
||||
<br />
|
||||
<input className="mx_Login_field" ref="pass" type="password"
|
||||
value={this.state.password2}
|
||||
onChange={this.onInputChanged.bind(this, "password2")}
|
||||
placeholder="Confirm your new password" />
|
||||
<br />
|
||||
<input className="mx_Login_submit" type="submit" value="Send Reset Email" />
|
||||
</form>
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
defaultHsUrl={this.props.homeserverUrl}
|
||||
defaultIsUrl={this.props.identityServerUrl}
|
||||
onHsUrlChanged={this.onHsUrlChanged}
|
||||
onIsUrlChanged={this.onIsUrlChanged}
|
||||
delayTimeMs={0}/>
|
||||
<LoginFooter />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<div className="mx_Login_box">
|
||||
<LoginHeader />
|
||||
{resetPasswordJsx}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -33,7 +33,9 @@ module.exports = React.createClass({displayName: 'Login',
|
|||
homeserverUrl: React.PropTypes.string,
|
||||
identityServerUrl: React.PropTypes.string,
|
||||
// login shouldn't know or care how registration is done.
|
||||
onRegisterClick: React.PropTypes.func.isRequired
|
||||
onRegisterClick: React.PropTypes.func.isRequired,
|
||||
// login shouldn't care how password recovery is done.
|
||||
onForgotPasswordClick: React.PropTypes.func
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -138,7 +140,9 @@ module.exports = React.createClass({displayName: 'Login',
|
|||
switch (step) {
|
||||
case 'm.login.password':
|
||||
return (
|
||||
<PasswordLogin onSubmit={this.onPasswordLogin} />
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick} />
|
||||
);
|
||||
case 'm.login.cas':
|
||||
return (
|
||||
|
|
|
@ -19,7 +19,6 @@ limitations under the License.
|
|||
var React = require('react');
|
||||
|
||||
var sdk = require('../../../index');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var dis = require('../../../dispatcher');
|
||||
var Signup = require("../../../Signup");
|
||||
var ServerConfig = require("../../views/login/ServerConfig");
|
||||
|
@ -40,6 +39,9 @@ module.exports = React.createClass({
|
|||
hsUrl: React.PropTypes.string,
|
||||
isUrl: React.PropTypes.string,
|
||||
email: React.PropTypes.string,
|
||||
username: React.PropTypes.string,
|
||||
guestAccessToken: React.PropTypes.string,
|
||||
disableUsernameChanges: React.PropTypes.bool,
|
||||
// registration shouldn't know or care how login is done.
|
||||
onLoginClick: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
@ -63,6 +65,7 @@ module.exports = React.createClass({
|
|||
this.registerLogic.setSessionId(this.props.sessionId);
|
||||
this.registerLogic.setRegistrationUrl(this.props.registrationUrl);
|
||||
this.registerLogic.setIdSid(this.props.idSid);
|
||||
this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
|
||||
this.registerLogic.recheckState();
|
||||
},
|
||||
|
||||
|
@ -186,7 +189,9 @@ module.exports = React.createClass({
|
|||
registerStep = (
|
||||
<RegistrationForm
|
||||
showEmail={true}
|
||||
defaultUsername={this.props.username}
|
||||
defaultEmail={this.props.email}
|
||||
disableUsernameChanges={this.props.disableUsernameChanges}
|
||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||
onError={this.onFormValidationFailed}
|
||||
onRegisterClick={this.onFormSubmit} />
|
||||
|
|
|
@ -22,7 +22,8 @@ var ReactDOM = require('react-dom');
|
|||
*/
|
||||
module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||
propTypes: {
|
||||
onSubmit: React.PropTypes.func.isRequired // fn(username, password)
|
||||
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
||||
onForgotPasswordClick: React.PropTypes.func // fn()
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -46,6 +47,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var forgotPasswordJsx;
|
||||
|
||||
if (this.props.onForgotPasswordClick) {
|
||||
forgotPasswordJsx = (
|
||||
<a className="mx_Login_forgot" onClick={this.props.onForgotPasswordClick} href="#">
|
||||
Forgot your password?
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
|
@ -57,6 +68,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
value={this.state.password} onChange={this.onPasswordChanged}
|
||||
placeholder="Password" />
|
||||
<br />
|
||||
{forgotPasswordJsx}
|
||||
<input className="mx_Login_submit" type="submit" value="Log in" />
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -30,6 +30,7 @@ module.exports = React.createClass({
|
|||
defaultUsername: React.PropTypes.string,
|
||||
showEmail: React.PropTypes.bool,
|
||||
minPasswordLength: React.PropTypes.number,
|
||||
disableUsernameChanges: React.PropTypes.bool,
|
||||
onError: React.PropTypes.func,
|
||||
onRegisterClick: React.PropTypes.func // onRegisterClick(Object) => ?Promise
|
||||
},
|
||||
|
@ -109,7 +110,8 @@ module.exports = React.createClass({
|
|||
{emailSection}
|
||||
<br />
|
||||
<input className="mx_Login_field" type="text" ref="username"
|
||||
placeholder="User name" defaultValue={this.state.username} />
|
||||
placeholder="User name" defaultValue={this.state.username}
|
||||
disabled={this.props.disableUsernameChanges} />
|
||||
<br />
|
||||
<input className="mx_Login_field" type="password" ref="password"
|
||||
placeholder="Password" defaultValue={this.state.password} />
|
||||
|
|
|
@ -65,8 +65,17 @@ function mdownToHtml(mdown) {
|
|||
module.exports = React.createClass({
|
||||
displayName: 'MessageComposer',
|
||||
|
||||
statics: {
|
||||
// the height we limit the composer to
|
||||
MAX_HEIGHT: 100,
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
tabComplete: React.PropTypes.any
|
||||
tabComplete: React.PropTypes.any,
|
||||
|
||||
// a callback which is called when the height of the composer is
|
||||
// changed due to a change in content.
|
||||
onResize: React.PropTypes.func,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -237,13 +246,15 @@ module.exports = React.createClass({
|
|||
// scrollHeight is at least equal to clientHeight, so we have to
|
||||
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
|
||||
this.refs.textarea.style.height = "0px";
|
||||
var newHeight = this.refs.textarea.scrollHeight < 100 ? this.refs.textarea.scrollHeight : 100;
|
||||
var newHeight = Math.min(this.refs.textarea.scrollHeight,
|
||||
this.constructor.MAX_HEIGHT);
|
||||
this.refs.textarea.style.height = Math.ceil(newHeight) + "px";
|
||||
if (this.props.roomView) {
|
||||
// kick gemini-scrollbar to re-layout
|
||||
this.props.roomView.forceUpdate();
|
||||
}
|
||||
this.oldScrollHeight = this.refs.textarea.scrollHeight;
|
||||
|
||||
if (this.props.onResize) {
|
||||
// kick gemini-scrollbar to re-layout
|
||||
this.props.onResize();
|
||||
}
|
||||
},
|
||||
|
||||
onKeyUp: function(ev) {
|
||||
|
|
|
@ -77,6 +77,14 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
canGuestsJoin: function() {
|
||||
return this.refs.guests_join.checked;
|
||||
},
|
||||
|
||||
canGuestsRead: function() {
|
||||
return this.refs.guests_read.checked;
|
||||
},
|
||||
|
||||
getTopic: function() {
|
||||
return this.refs.topic.value;
|
||||
},
|
||||
|
@ -162,6 +170,10 @@ module.exports = React.createClass({
|
|||
if (history_visibility) history_visibility = history_visibility.getContent().history_visibility;
|
||||
|
||||
var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
|
||||
var guest_access = this.props.room.currentState.getStateEvents('m.room.guest_access', '');
|
||||
if (guest_access) {
|
||||
guest_access = guest_access.getContent().guest_access;
|
||||
}
|
||||
|
||||
var events_levels = power_levels.events || {};
|
||||
|
||||
|
@ -361,8 +373,15 @@ module.exports = React.createClass({
|
|||
<div className="mx_RoomSettings">
|
||||
<label><input type="checkbox" ref="is_private" defaultChecked={join_rule != "public"}/> Make this room private</label> <br/>
|
||||
<label><input type="checkbox" ref="share_history" defaultChecked={history_visibility == "shared"}/> Share message history with new users</label> <br/>
|
||||
<label><input type="checkbox" ref="guest_access" disabled={join_rule != "public"} defaultChecked={history_visibility == "world_readable"}/> Allow guest access</label> <br/>
|
||||
<label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label>
|
||||
<label>
|
||||
<input type="checkbox" ref="guests_read" defaultChecked={history_visibility === "world_readable"}/>
|
||||
Allow guests to read messages in this room
|
||||
</label> <br/>
|
||||
<label>
|
||||
<input type="checkbox" ref="guests_join" defaultChecked={guest_access === "can_join"}/>
|
||||
Allow guests to join this room
|
||||
</label> <br/>
|
||||
<label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label> <br/>
|
||||
|
||||
{ room_colors_section }
|
||||
|
||||
|
|
|
@ -33,6 +33,12 @@ var MatrixClientPeg = require("../../../MatrixClientPeg");
|
|||
module.exports = React.createClass({
|
||||
displayName: 'CallView',
|
||||
|
||||
propTypes: {
|
||||
// a callback which is called when the video within the callview
|
||||
// due to a change in video metadata
|
||||
onResize: React.PropTypes.func,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
if (this.props.room) {
|
||||
|
@ -97,7 +103,7 @@ module.exports = React.createClass({
|
|||
render: function(){
|
||||
var VideoView = sdk.getComponent('voip.VideoView');
|
||||
return (
|
||||
<VideoView ref="video" onClick={ this.props.onClick }/>
|
||||
<VideoView ref="video" onClick={ this.props.onClick } onResize={ this.props.onResize }/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -21,9 +21,29 @@ var React = require('react');
|
|||
module.exports = React.createClass({
|
||||
displayName: 'VideoFeed',
|
||||
|
||||
propTypes: {
|
||||
// a callback which is called when the video element is resized
|
||||
// due to a change in video metadata
|
||||
onResize: React.PropTypes.func,
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
this.refs.vid.addEventListener('resize', this.onResize);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.refs.vid.removeEventListener('resize', this.onResize);
|
||||
},
|
||||
|
||||
onResize: function(e) {
|
||||
if(this.props.onResize) {
|
||||
this.props.onResize(e);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<video>
|
||||
<video ref="vid">
|
||||
</video>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -85,7 +85,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_VideoView" ref={this.setContainer} onClick={ this.props.onClick }>
|
||||
<div className="mx_VideoView_remoteVideoFeed">
|
||||
<VideoFeed ref="remote"/>
|
||||
<VideoFeed ref="remote" onResize={this.props.onResize}/>
|
||||
<audio ref="remoteAudio"/>
|
||||
</div>
|
||||
<div className="mx_VideoView_localVideoFeed">
|
||||
|
|
Loading…
Reference in a new issue