Merge branch 'develop' into luke/fix-people-section2
This commit is contained in:
commit
dca02d916a
27 changed files with 2536 additions and 100 deletions
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -51,11 +52,36 @@ class AddThreepid {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to add a msisdn threepid. This will trigger a side-effect of
|
||||||
|
* sending a test message to the provided phone number.
|
||||||
|
* @param {string} phoneCountry The ISO 2 letter code of the country to resolve phoneNumber in
|
||||||
|
* @param {string} phoneNumber The national or international formatted phone number to add
|
||||||
|
* @param {boolean} bind If True, bind this phone number to this mxid on the Identity Server
|
||||||
|
* @return {Promise} Resolves when the text message has been sent. Then call haveMsisdnToken().
|
||||||
|
*/
|
||||||
|
addMsisdn(phoneCountry, phoneNumber, bind) {
|
||||||
|
this.bind = bind;
|
||||||
|
return MatrixClientPeg.get().requestAdd3pidMsisdnToken(
|
||||||
|
phoneCountry, phoneNumber, this.clientSecret, 1,
|
||||||
|
).then((res) => {
|
||||||
|
this.sessionId = res.sid;
|
||||||
|
return res;
|
||||||
|
}, function(err) {
|
||||||
|
if (err.errcode == 'M_THREEPID_IN_USE') {
|
||||||
|
err.message = "This phone number is already in use";
|
||||||
|
} else if (err.httpStatus) {
|
||||||
|
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the email link has been clicked by attempting to add the threepid
|
* Checks if the email link has been clicked by attempting to add the threepid
|
||||||
* @return {Promise} Resolves if the password was reset. Rejects with an object
|
* @return {Promise} Resolves if the email address was added. Rejects with an object
|
||||||
* with a "message" property which contains a human-readable message detailing why
|
* 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".
|
* the request failed.
|
||||||
*/
|
*/
|
||||||
checkEmailLinkClicked() {
|
checkEmailLinkClicked() {
|
||||||
var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||||
|
@ -73,6 +99,29 @@ class AddThreepid {
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a phone number verification code as entered by the user and validates
|
||||||
|
* it with the ID server, then if successful, adds the phone number.
|
||||||
|
* @return {Promise} Resolves if the phone number was added. Rejects with an object
|
||||||
|
* with a "message" property which contains a human-readable message detailing why
|
||||||
|
* the request failed.
|
||||||
|
*/
|
||||||
|
haveMsisdnToken(token) {
|
||||||
|
return MatrixClientPeg.get().submitMsisdnToken(
|
||||||
|
this.sessionId, this.clientSecret, token,
|
||||||
|
).then((result) => {
|
||||||
|
if (result.errcode) {
|
||||||
|
throw result;
|
||||||
|
}
|
||||||
|
const identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||||
|
return MatrixClientPeg.get().addThreePid({
|
||||||
|
sid: this.sessionId,
|
||||||
|
client_secret: this.clientSecret,
|
||||||
|
id_server: identityServerDomain
|
||||||
|
}, this.bind);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = AddThreepid;
|
module.exports = AddThreepid;
|
||||||
|
|
|
@ -58,6 +58,22 @@ export function unicodeToImage(str) {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given one or more unicode characters (represented by unicode
|
||||||
|
* character number), return an image node with the corresponding
|
||||||
|
* emoji.
|
||||||
|
*
|
||||||
|
* @param alt {string} String to use for the image alt text
|
||||||
|
* @param unicode {integer} One or more integers representing unicode characters
|
||||||
|
* @returns A img node with the corresponding emoji
|
||||||
|
*/
|
||||||
|
export function charactersToImageNode(alt, ...unicode) {
|
||||||
|
const fileName = unicode.map((u) => {
|
||||||
|
return u.toString(16);
|
||||||
|
}).join('-');
|
||||||
|
return <img alt={alt} src={`${emojione.imagePathSVG}${fileName}.svg${emojione.cacheBustParam}`}/>;
|
||||||
|
}
|
||||||
|
|
||||||
export function stripParagraphs(html: string): string {
|
export function stripParagraphs(html: string): string {
|
||||||
const contentDiv = document.createElement('div');
|
const contentDiv = document.createElement('div');
|
||||||
contentDiv.innerHTML = html;
|
contentDiv.innerHTML = html;
|
||||||
|
|
51
src/Login.js
51
src/Login.js
|
@ -105,21 +105,48 @@ export default class Login {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loginViaPassword(username, pass) {
|
loginViaPassword(username, phoneCountry, phoneNumber, pass) {
|
||||||
var self = this;
|
const self = this;
|
||||||
var isEmail = username.indexOf("@") > 0;
|
|
||||||
var loginParams = {
|
const isEmail = username.indexOf("@") > 0;
|
||||||
password: pass,
|
|
||||||
initial_device_display_name: this._defaultDeviceDisplayName,
|
let identifier;
|
||||||
};
|
let legacyParams; // parameters added to support old HSes
|
||||||
if (isEmail) {
|
if (phoneCountry && phoneNumber) {
|
||||||
loginParams.medium = 'email';
|
identifier = {
|
||||||
loginParams.address = username;
|
type: 'm.id.phone',
|
||||||
|
country: phoneCountry,
|
||||||
|
number: phoneNumber,
|
||||||
|
};
|
||||||
|
// No legacy support for phone number login
|
||||||
|
} else if (isEmail) {
|
||||||
|
identifier = {
|
||||||
|
type: 'm.id.thirdparty',
|
||||||
|
medium: 'email',
|
||||||
|
address: username,
|
||||||
|
};
|
||||||
|
legacyParams = {
|
||||||
|
medium: 'email',
|
||||||
|
address: username,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
loginParams.user = username;
|
identifier = {
|
||||||
|
type: 'm.id.user',
|
||||||
|
user: username,
|
||||||
|
};
|
||||||
|
legacyParams = {
|
||||||
|
user: username,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var client = this._createTemporaryClient();
|
const loginParams = {
|
||||||
|
password: pass,
|
||||||
|
identifier: identifier,
|
||||||
|
initial_device_display_name: this._defaultDeviceDisplayName,
|
||||||
|
};
|
||||||
|
Object.assign(loginParams, legacyParams);
|
||||||
|
|
||||||
|
const client = this._createTemporaryClient();
|
||||||
return client.login('m.login.password', loginParams).then(function(data) {
|
return client.login('m.login.password', loginParams).then(function(data) {
|
||||||
return q({
|
return q({
|
||||||
homeserverUrl: self._hsUrl,
|
homeserverUrl: self._hsUrl,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,8 +15,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
var PlatformPeg = require("./PlatformPeg");
|
var PlatformPeg = require("./PlatformPeg");
|
||||||
var TextForEvent = require('./TextForEvent');
|
var TextForEvent = require('./TextForEvent');
|
||||||
|
@ -103,7 +102,7 @@ var Notifier = {
|
||||||
},
|
},
|
||||||
|
|
||||||
stop: function() {
|
stop: function() {
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get() && this.boundOnRoomTimeline) {
|
||||||
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
|
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
|
||||||
MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt);
|
MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt);
|
||||||
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
|
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
|
||||||
|
|
|
@ -79,6 +79,8 @@ import views$dialogs$ChatCreateOrReuseDialog from './components/views/dialogs/Ch
|
||||||
views$dialogs$ChatCreateOrReuseDialog && (module.exports.components['views.dialogs.ChatCreateOrReuseDialog'] = views$dialogs$ChatCreateOrReuseDialog);
|
views$dialogs$ChatCreateOrReuseDialog && (module.exports.components['views.dialogs.ChatCreateOrReuseDialog'] = views$dialogs$ChatCreateOrReuseDialog);
|
||||||
import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog';
|
import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog';
|
||||||
views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog);
|
views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog);
|
||||||
|
import views$dialogs$ConfirmRedactDialog from './components/views/dialogs/ConfirmRedactDialog';
|
||||||
|
views$dialogs$ConfirmRedactDialog && (module.exports.components['views.dialogs.ConfirmRedactDialog'] = views$dialogs$ConfirmRedactDialog);
|
||||||
import views$dialogs$ConfirmUserActionDialog from './components/views/dialogs/ConfirmUserActionDialog';
|
import views$dialogs$ConfirmUserActionDialog from './components/views/dialogs/ConfirmUserActionDialog';
|
||||||
views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog);
|
views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog);
|
||||||
import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog';
|
import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog';
|
||||||
|
@ -109,6 +111,8 @@ import views$elements$DeviceVerifyButtons from './components/views/elements/Devi
|
||||||
views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons);
|
views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons);
|
||||||
import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox';
|
import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox';
|
||||||
views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = views$elements$DirectorySearchBox);
|
views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = views$elements$DirectorySearchBox);
|
||||||
|
import views$elements$Dropdown from './components/views/elements/Dropdown';
|
||||||
|
views$elements$Dropdown && (module.exports.components['views.elements.Dropdown'] = views$elements$Dropdown);
|
||||||
import views$elements$EditableText from './components/views/elements/EditableText';
|
import views$elements$EditableText from './components/views/elements/EditableText';
|
||||||
views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText);
|
views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText);
|
||||||
import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer';
|
import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer';
|
||||||
|
@ -131,6 +135,8 @@ import views$login$CaptchaForm from './components/views/login/CaptchaForm';
|
||||||
views$login$CaptchaForm && (module.exports.components['views.login.CaptchaForm'] = views$login$CaptchaForm);
|
views$login$CaptchaForm && (module.exports.components['views.login.CaptchaForm'] = views$login$CaptchaForm);
|
||||||
import views$login$CasLogin from './components/views/login/CasLogin';
|
import views$login$CasLogin from './components/views/login/CasLogin';
|
||||||
views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = views$login$CasLogin);
|
views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = views$login$CasLogin);
|
||||||
|
import views$login$CountryDropdown from './components/views/login/CountryDropdown';
|
||||||
|
views$login$CountryDropdown && (module.exports.components['views.login.CountryDropdown'] = views$login$CountryDropdown);
|
||||||
import views$login$CustomServerDialog from './components/views/login/CustomServerDialog';
|
import views$login$CustomServerDialog from './components/views/login/CustomServerDialog';
|
||||||
views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog);
|
views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog);
|
||||||
import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents';
|
import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents';
|
||||||
|
@ -223,6 +229,8 @@ import views$rooms$TopUnreadMessagesBar from './components/views/rooms/TopUnread
|
||||||
views$rooms$TopUnreadMessagesBar && (module.exports.components['views.rooms.TopUnreadMessagesBar'] = views$rooms$TopUnreadMessagesBar);
|
views$rooms$TopUnreadMessagesBar && (module.exports.components['views.rooms.TopUnreadMessagesBar'] = views$rooms$TopUnreadMessagesBar);
|
||||||
import views$rooms$UserTile from './components/views/rooms/UserTile';
|
import views$rooms$UserTile from './components/views/rooms/UserTile';
|
||||||
views$rooms$UserTile && (module.exports.components['views.rooms.UserTile'] = views$rooms$UserTile);
|
views$rooms$UserTile && (module.exports.components['views.rooms.UserTile'] = views$rooms$UserTile);
|
||||||
|
import views$settings$AddPhoneNumber from './components/views/settings/AddPhoneNumber';
|
||||||
|
views$settings$AddPhoneNumber && (module.exports.components['views.settings.AddPhoneNumber'] = views$settings$AddPhoneNumber);
|
||||||
import views$settings$ChangeAvatar from './components/views/settings/ChangeAvatar';
|
import views$settings$ChangeAvatar from './components/views/settings/ChangeAvatar';
|
||||||
views$settings$ChangeAvatar && (module.exports.components['views.settings.ChangeAvatar'] = views$settings$ChangeAvatar);
|
views$settings$ChangeAvatar && (module.exports.components['views.settings.ChangeAvatar'] = views$settings$ChangeAvatar);
|
||||||
import views$settings$ChangeDisplayName from './components/views/settings/ChangeDisplayName';
|
import views$settings$ChangeDisplayName from './components/views/settings/ChangeDisplayName';
|
||||||
|
|
|
@ -140,13 +140,20 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_requestCallback: function(auth) {
|
_requestCallback: function(auth, background) {
|
||||||
|
const makeRequestPromise = this.props.makeRequest(auth);
|
||||||
|
|
||||||
|
// if it's a background request, just do it: we don't want
|
||||||
|
// it to affect the state of our UI.
|
||||||
|
if (background) return makeRequestPromise;
|
||||||
|
|
||||||
|
// otherwise, manage the state of the spinner and error messages
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: true,
|
busy: true,
|
||||||
errorText: null,
|
errorText: null,
|
||||||
stageErrorText: null,
|
stageErrorText: null,
|
||||||
});
|
});
|
||||||
return this.props.makeRequest(auth).finally(() => {
|
return makeRequestPromise.finally(() => {
|
||||||
if (this._unmounted) {
|
if (this._unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,13 @@ export default React.createClass({
|
||||||
return this._scrollStateMap[roomId];
|
return this._scrollStateMap[roomId];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
canResetTimelineInRoom: function(roomId) {
|
||||||
|
if (!this.refs.roomView) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.refs.roomView.canResetTimeline();
|
||||||
|
},
|
||||||
|
|
||||||
_onKeyDown: function(ev) {
|
_onKeyDown: function(ev) {
|
||||||
/*
|
/*
|
||||||
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
|
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
|
||||||
|
|
|
@ -806,9 +806,31 @@ module.exports = React.createClass({
|
||||||
* (useful for setting listeners)
|
* (useful for setting listeners)
|
||||||
*/
|
*/
|
||||||
_onWillStartClient() {
|
_onWillStartClient() {
|
||||||
|
var self = this;
|
||||||
var cli = MatrixClientPeg.get();
|
var cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
var self = this;
|
// Allow the JS SDK to reap timeline events. This reduces the amount of
|
||||||
|
// memory consumed as the JS SDK stores multiple distinct copies of room
|
||||||
|
// state (each of which can be 10s of MBs) for each DISJOINT timeline. This is
|
||||||
|
// particularly noticeable when there are lots of 'limited' /sync responses
|
||||||
|
// such as when laptops unsleep.
|
||||||
|
// https://github.com/vector-im/riot-web/issues/3307#issuecomment-282895568
|
||||||
|
cli.setCanResetTimelineCallback(function(roomId) {
|
||||||
|
console.log("Request to reset timeline in room ", roomId, " viewing:", self.state.currentRoomId);
|
||||||
|
if (roomId !== self.state.currentRoomId) {
|
||||||
|
// It is safe to remove events from rooms we are not viewing.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// We are viewing the room which we want to reset. It is only safe to do
|
||||||
|
// this if we are not scrolled up in the view. To find out, delegate to
|
||||||
|
// the timeline panel. If the timeline panel doesn't exist, then we assume
|
||||||
|
// it is safe to reset the timeline.
|
||||||
|
if (!self.refs.loggedInView) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return self.refs.loggedInView.canResetTimelineInRoom(roomId);
|
||||||
|
});
|
||||||
|
|
||||||
cli.on('sync', function(state, prevState) {
|
cli.on('sync', function(state, prevState) {
|
||||||
self.updateStatusIndicator(state, prevState);
|
self.updateStatusIndicator(state, prevState);
|
||||||
if (state === "SYNCING" && prevState === "SYNCING") {
|
if (state === "SYNCING" && prevState === "SYNCING") {
|
||||||
|
|
|
@ -490,6 +490,13 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
canResetTimeline: function() {
|
||||||
|
if (!this.refs.messagePanel) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.refs.messagePanel.canResetTimeline();
|
||||||
|
},
|
||||||
|
|
||||||
// called when state.room is first initialised (either at initial load,
|
// called when state.room is first initialised (either at initial load,
|
||||||
// after a successful peek, or after we join the room).
|
// after a successful peek, or after we join the room).
|
||||||
_onRoomLoaded: function(room) {
|
_onRoomLoaded: function(room) {
|
||||||
|
|
|
@ -431,6 +431,10 @@ var TimelinePanel = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
canResetTimeline: function() {
|
||||||
|
return this.refs.messagePanel && this.refs.messagePanel.isAtBottom();
|
||||||
|
},
|
||||||
|
|
||||||
onRoomRedaction: function(ev, room) {
|
onRoomRedaction: function(ev, room) {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
|
||||||
|
|
|
@ -25,12 +25,13 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
this.mounted = true;
|
this.mounted = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
},
|
},
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction: function(payload) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -139,6 +140,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
|
this._addThreepid = null;
|
||||||
|
|
||||||
if (PlatformPeg.get()) {
|
if (PlatformPeg.get()) {
|
||||||
q().then(() => {
|
q().then(() => {
|
||||||
|
@ -321,12 +323,16 @@ module.exports = React.createClass({
|
||||||
UserSettingsStore.setEnableNotifications(event.target.checked);
|
UserSettingsStore.setEnableNotifications(event.target.checked);
|
||||||
},
|
},
|
||||||
|
|
||||||
onAddThreepidClicked: function(value, shouldSubmit) {
|
_onAddEmailEditFinished: function(value, shouldSubmit) {
|
||||||
if (!shouldSubmit) return;
|
if (!shouldSubmit) return;
|
||||||
|
this._addEmail();
|
||||||
|
},
|
||||||
|
|
||||||
|
_addEmail: function() {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
|
||||||
var email_address = this.refs.add_threepid_input.value;
|
var email_address = this.refs.add_email_input.value;
|
||||||
if (!Email.looksValid(email_address)) {
|
if (!Email.looksValid(email_address)) {
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Invalid Email Address",
|
title: "Invalid Email Address",
|
||||||
|
@ -334,10 +340,10 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.add_threepid = new AddThreepid();
|
this._addThreepid = new AddThreepid();
|
||||||
// we always bind emails when registering, so let's do the
|
// we always bind emails when registering, so let's do the
|
||||||
// same here.
|
// same here.
|
||||||
this.add_threepid.addEmailAddress(email_address, true).done(() => {
|
this._addThreepid.addEmailAddress(email_address, true).done(() => {
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createDialog(QuestionDialog, {
|
||||||
title: "Verification Pending",
|
title: "Verification Pending",
|
||||||
description: "Please check your email and click on the link it contains. Once this is done, click continue.",
|
description: "Please check your email and click on the link it contains. Once this is done, click continue.",
|
||||||
|
@ -352,7 +358,7 @@ module.exports = React.createClass({
|
||||||
description: "Unable to add email address"
|
description: "Unable to add email address"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ReactDOM.findDOMNode(this.refs.add_threepid_input).blur();
|
ReactDOM.findDOMNode(this.refs.add_email_input).blur();
|
||||||
this.setState({email_add_pending: true});
|
this.setState({email_add_pending: true});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -391,8 +397,8 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
verifyEmailAddress: function() {
|
verifyEmailAddress: function() {
|
||||||
this.add_threepid.checkEmailLinkClicked().done(() => {
|
this._addThreepid.checkEmailLinkClicked().done(() => {
|
||||||
this.add_threepid = undefined;
|
this._addThreepid = null;
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: "UserSettings.LOADING",
|
phase: "UserSettings.LOADING",
|
||||||
});
|
});
|
||||||
|
@ -761,6 +767,14 @@ module.exports = React.createClass({
|
||||||
return medium[0].toUpperCase() + medium.slice(1);
|
return medium[0].toUpperCase() + medium.slice(1);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
presentableTextForThreepid: function(threepid) {
|
||||||
|
if (threepid.medium == 'msisdn') {
|
||||||
|
return '+' + threepid.address;
|
||||||
|
} else {
|
||||||
|
return threepid.address;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
switch (this.state.phase) {
|
switch (this.state.phase) {
|
||||||
|
@ -793,7 +807,9 @@ module.exports = React.createClass({
|
||||||
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
|
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_profileInputCell">
|
<div className="mx_UserSettings_profileInputCell">
|
||||||
<input type="text" key={val.address} id={id} value={val.address} disabled />
|
<input type="text" key={val.address} id={id}
|
||||||
|
value={this.presentableTextForThreepid(val)} disabled
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||||
<img src="img/cancel-small.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />
|
<img src="img/cancel-small.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />
|
||||||
|
@ -801,30 +817,35 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
var addThreepidSection;
|
let addEmailSection;
|
||||||
if (this.state.email_add_pending) {
|
if (this.state.email_add_pending) {
|
||||||
addThreepidSection = <Loader />;
|
addEmailSection = <Loader key="_email_add_spinner" />;
|
||||||
} else if (!MatrixClientPeg.get().isGuest()) {
|
} else if (!MatrixClientPeg.get().isGuest()) {
|
||||||
addThreepidSection = (
|
addEmailSection = (
|
||||||
<div className="mx_UserSettings_profileTableRow" key="new">
|
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
|
||||||
<div className="mx_UserSettings_profileLabelCell">
|
<div className="mx_UserSettings_profileLabelCell">
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_profileInputCell">
|
<div className="mx_UserSettings_profileInputCell">
|
||||||
<EditableText
|
<EditableText
|
||||||
ref="add_threepid_input"
|
ref="add_email_input"
|
||||||
className="mx_UserSettings_editable"
|
className="mx_UserSettings_editable"
|
||||||
placeholderClassName="mx_UserSettings_threepidPlaceholder"
|
placeholderClassName="mx_UserSettings_threepidPlaceholder"
|
||||||
placeholder={ "Add email address" }
|
placeholder={ "Add email address" }
|
||||||
blurToCancel={ false }
|
blurToCancel={ false }
|
||||||
onValueChanged={ this.onAddThreepidClicked } />
|
onValueChanged={ this._onAddEmailEditFinished } />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||||
<img src="img/plus.svg" width="14" height="14" alt="Add" onClick={ this.onAddThreepidClicked.bind(this, undefined, true) }/>
|
<img src="img/plus.svg" width="14" height="14" alt="Add" onClick={this._addEmail} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
threepidsSection.push(addThreepidSection);
|
const AddPhoneNumber = sdk.getComponent('views.settings.AddPhoneNumber');
|
||||||
|
const addMsisdnSection = (
|
||||||
|
<AddPhoneNumber key="_addMsisdn" onThreepidAdded={this._refreshFromServer} />
|
||||||
|
);
|
||||||
|
threepidsSection.push(addEmailSection);
|
||||||
|
threepidsSection.push(addMsisdnSection);
|
||||||
|
|
||||||
var accountJsx;
|
var accountJsx;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -64,8 +65,10 @@ module.exports = React.createClass({
|
||||||
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||||
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||||
|
|
||||||
// used for preserving username when changing homeserver
|
// used for preserving form values when changing homeserver
|
||||||
username: "",
|
username: "",
|
||||||
|
phoneCountry: null,
|
||||||
|
phoneNumber: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -73,20 +76,21 @@ module.exports = React.createClass({
|
||||||
this._initLoginLogic();
|
this._initLoginLogic();
|
||||||
},
|
},
|
||||||
|
|
||||||
onPasswordLogin: function(username, password) {
|
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||||
var self = this;
|
this.setState({
|
||||||
self.setState({
|
|
||||||
busy: true,
|
busy: true,
|
||||||
errorText: null,
|
errorText: null,
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
this._loginLogic.loginViaPassword(username, password).then(function(data) {
|
this._loginLogic.loginViaPassword(
|
||||||
self.props.onLoggedIn(data);
|
username, phoneCountry, phoneNumber, password,
|
||||||
}, function(error) {
|
).then((data) => {
|
||||||
self._setStateFromError(error, true);
|
this.props.onLoggedIn(data);
|
||||||
}).finally(function() {
|
}, (error) => {
|
||||||
self.setState({
|
this._setStateFromError(error, true);
|
||||||
|
}).finally(() => {
|
||||||
|
this.setState({
|
||||||
busy: false
|
busy: false
|
||||||
});
|
});
|
||||||
}).done();
|
}).done();
|
||||||
|
@ -119,6 +123,14 @@ module.exports = React.createClass({
|
||||||
this.setState({ username: username });
|
this.setState({ username: username });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onPhoneCountryChanged: function(phoneCountry) {
|
||||||
|
this.setState({ phoneCountry: phoneCountry });
|
||||||
|
},
|
||||||
|
|
||||||
|
onPhoneNumberChanged: function(phoneNumber) {
|
||||||
|
this.setState({ phoneNumber: phoneNumber });
|
||||||
|
},
|
||||||
|
|
||||||
onHsUrlChanged: function(newHsUrl) {
|
onHsUrlChanged: function(newHsUrl) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -225,7 +237,11 @@ module.exports = React.createClass({
|
||||||
<PasswordLogin
|
<PasswordLogin
|
||||||
onSubmit={this.onPasswordLogin}
|
onSubmit={this.onPasswordLogin}
|
||||||
initialUsername={this.state.username}
|
initialUsername={this.state.username}
|
||||||
|
initialPhoneCountry={this.state.phoneCountry}
|
||||||
|
initialPhoneNumber={this.state.phoneNumber}
|
||||||
onUsernameChanged={this.onUsernameChanged}
|
onUsernameChanged={this.onUsernameChanged}
|
||||||
|
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
||||||
|
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||||
loginIncorrect={this.state.loginIncorrect}
|
loginIncorrect={this.state.loginIncorrect}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -155,10 +155,21 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_onUIAuthFinished: function(success, response, extra) {
|
_onUIAuthFinished: function(success, response, extra) {
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
let msg = response.message || response.toString();
|
||||||
|
// can we give a better error message?
|
||||||
|
if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
||||||
|
let msisdn_available = false;
|
||||||
|
for (const flow of response.available_flows) {
|
||||||
|
msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1;
|
||||||
|
}
|
||||||
|
if (!msisdn_available) {
|
||||||
|
msg = "This server does not support authentication with a phone number";
|
||||||
|
}
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false,
|
busy: false,
|
||||||
doingUIAuth: false,
|
doingUIAuth: false,
|
||||||
errorText: response.message || response.toString(),
|
errorText: msg,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -261,6 +272,9 @@ module.exports = React.createClass({
|
||||||
case "RegistrationForm.ERR_EMAIL_INVALID":
|
case "RegistrationForm.ERR_EMAIL_INVALID":
|
||||||
errMsg = "This doesn't look like a valid email address";
|
errMsg = "This doesn't look like a valid email address";
|
||||||
break;
|
break;
|
||||||
|
case "RegistrationForm.ERR_PHONE_NUMBER_INVALID":
|
||||||
|
errMsg = "This doesn't look like a valid phone number";
|
||||||
|
break;
|
||||||
case "RegistrationForm.ERR_USERNAME_INVALID":
|
case "RegistrationForm.ERR_USERNAME_INVALID":
|
||||||
errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores.";
|
errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores.";
|
||||||
break;
|
break;
|
||||||
|
@ -295,15 +309,20 @@ module.exports = React.createClass({
|
||||||
guestAccessToken = null;
|
guestAccessToken = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only send the bind params if we're sending username / pw params
|
||||||
|
// (Since we need to send no params at all to use the ones saved in the
|
||||||
|
// session).
|
||||||
|
const bindThreepids = this.state.formVals.password ? {
|
||||||
|
email: true,
|
||||||
|
msisdn: true,
|
||||||
|
} : {};
|
||||||
|
|
||||||
return this._matrixClient.register(
|
return this._matrixClient.register(
|
||||||
this.state.formVals.username,
|
this.state.formVals.username,
|
||||||
this.state.formVals.password,
|
this.state.formVals.password,
|
||||||
undefined, // session id: included in the auth dict already
|
undefined, // session id: included in the auth dict already
|
||||||
auth,
|
auth,
|
||||||
// Only send the bind_email param if we're sending username / pw params
|
bindThreepids,
|
||||||
// (Since we need to send no params at all to use the ones saved in the
|
|
||||||
// session).
|
|
||||||
Boolean(this.state.formVals.username) || undefined,
|
|
||||||
guestAccessToken,
|
guestAccessToken,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -354,6 +373,8 @@ module.exports = React.createClass({
|
||||||
<RegistrationForm
|
<RegistrationForm
|
||||||
defaultUsername={this.state.formVals.username}
|
defaultUsername={this.state.formVals.username}
|
||||||
defaultEmail={this.state.formVals.email}
|
defaultEmail={this.state.formVals.email}
|
||||||
|
defaultPhoneCountry={this.state.formVals.phoneCountry}
|
||||||
|
defaultPhoneNumber={this.state.formVals.phoneNumber}
|
||||||
defaultPassword={this.state.formVals.password}
|
defaultPassword={this.state.formVals.password}
|
||||||
teamsConfig={this.state.teamsConfig}
|
teamsConfig={this.state.teamsConfig}
|
||||||
guestUsername={guestUsername}
|
guestUsername={guestUsername}
|
||||||
|
|
73
src/components/views/dialogs/ConfirmRedactDialog.js
Normal file
73
src/components/views/dialogs/ConfirmRedactDialog.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations 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 classnames from 'classnames';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A dialog for confirming a redaction.
|
||||||
|
*/
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'ConfirmRedactDialog',
|
||||||
|
propTypes: {
|
||||||
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
defaultProps: {
|
||||||
|
danger: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
onOk: function() {
|
||||||
|
this.props.onFinished(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
onCancel: function() {
|
||||||
|
this.props.onFinished(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
||||||
|
const title = "Confirm Redaction";
|
||||||
|
|
||||||
|
const confirmButtonClass = classnames({
|
||||||
|
'mx_Dialog_primary': true,
|
||||||
|
'danger': false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||||
|
onEnterPressed={ this.onOk }
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
Are you sure you wish to redact (delete) this event?
|
||||||
|
Note that if you redact a room name or topic change, it could undo the change.
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_buttons">
|
||||||
|
<button className={confirmButtonClass} onClick={this.onOk}>
|
||||||
|
Redact
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onClick={this.onCancel}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -27,8 +27,8 @@ import React from 'react';
|
||||||
export default function AccessibleButton(props) {
|
export default function AccessibleButton(props) {
|
||||||
const {element, onClick, children, ...restProps} = props;
|
const {element, onClick, children, ...restProps} = props;
|
||||||
restProps.onClick = onClick;
|
restProps.onClick = onClick;
|
||||||
restProps.onKeyDown = function(e) {
|
restProps.onKeyUp = function(e) {
|
||||||
if (e.keyCode == 13 || e.keyCode == 32) return onClick();
|
if (e.keyCode == 13 || e.keyCode == 32) return onClick(e);
|
||||||
};
|
};
|
||||||
restProps.tabIndex = restProps.tabIndex || "0";
|
restProps.tabIndex = restProps.tabIndex || "0";
|
||||||
restProps.role = "button";
|
restProps.role = "button";
|
||||||
|
|
324
src/components/views/elements/Dropdown.js
Normal file
324
src/components/views/elements/Dropdown.js
Normal file
|
@ -0,0 +1,324 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations 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 classnames from 'classnames';
|
||||||
|
import AccessibleButton from './AccessibleButton';
|
||||||
|
|
||||||
|
class MenuOption extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this._onMouseEnter = this._onMouseEnter.bind(this);
|
||||||
|
this._onClick = this._onClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMouseEnter() {
|
||||||
|
this.props.onMouseEnter(this.props.dropdownKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.props.onClick(this.props.dropdownKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const optClasses = classnames({
|
||||||
|
mx_Dropdown_option: true,
|
||||||
|
mx_Dropdown_option_highlight: this.props.highlighted,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className={optClasses}
|
||||||
|
onClick={this._onClick} onKeyPress={this._onKeyPress}
|
||||||
|
onMouseEnter={this._onMouseEnter}
|
||||||
|
>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
MenuOption.propTypes = {
|
||||||
|
children: React.PropTypes.oneOfType([
|
||||||
|
React.PropTypes.arrayOf(React.PropTypes.node),
|
||||||
|
React.PropTypes.node
|
||||||
|
]),
|
||||||
|
highlighted: React.PropTypes.bool,
|
||||||
|
dropdownKey: React.PropTypes.string,
|
||||||
|
onClick: React.PropTypes.func.isRequired,
|
||||||
|
onMouseEnter: React.PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Reusable dropdown select control, akin to react-select,
|
||||||
|
* but somewhat simpler as react-select is 79KB of minified
|
||||||
|
* javascript.
|
||||||
|
*
|
||||||
|
* TODO: Port NetworkDropdown to use this.
|
||||||
|
*/
|
||||||
|
export default class Dropdown extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.dropdownRootElement = null;
|
||||||
|
this.ignoreEvent = null;
|
||||||
|
|
||||||
|
this._onInputClick = this._onInputClick.bind(this);
|
||||||
|
this._onRootClick = this._onRootClick.bind(this);
|
||||||
|
this._onDocumentClick = this._onDocumentClick.bind(this);
|
||||||
|
this._onMenuOptionClick = this._onMenuOptionClick.bind(this);
|
||||||
|
this._onInputKeyPress = this._onInputKeyPress.bind(this);
|
||||||
|
this._onInputKeyUp = this._onInputKeyUp.bind(this);
|
||||||
|
this._onInputChange = this._onInputChange.bind(this);
|
||||||
|
this._collectRoot = this._collectRoot.bind(this);
|
||||||
|
this._collectInputTextBox = this._collectInputTextBox.bind(this);
|
||||||
|
this._setHighlightedOption = this._setHighlightedOption.bind(this);
|
||||||
|
|
||||||
|
this.inputTextBox = null;
|
||||||
|
|
||||||
|
this._reindexChildren(this.props.children);
|
||||||
|
|
||||||
|
const firstChild = React.Children.toArray(props.children)[0];
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
// True if the menu is dropped-down
|
||||||
|
expanded: false,
|
||||||
|
// The key of the highlighted option
|
||||||
|
// (the option that would become selected if you pressed enter)
|
||||||
|
highlightedOption: firstChild ? firstChild.key : null,
|
||||||
|
// the current search query
|
||||||
|
searchQuery: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
// Listen for all clicks on the document so we can close the
|
||||||
|
// menu when the user clicks somewhere else
|
||||||
|
document.addEventListener('click', this._onDocumentClick, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('click', this._onDocumentClick, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this._reindexChildren(nextProps.children);
|
||||||
|
const firstChild = React.Children.toArray(nextProps.children)[0];
|
||||||
|
this.setState({
|
||||||
|
highlightedOption: firstChild ? firstChild.key : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_reindexChildren(children) {
|
||||||
|
this.childrenByKey = {};
|
||||||
|
React.Children.forEach(children, (child) => {
|
||||||
|
this.childrenByKey[child.key] = child;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDocumentClick(ev) {
|
||||||
|
// Close the dropdown if the user clicks anywhere that isn't
|
||||||
|
// within our root element
|
||||||
|
if (ev !== this.ignoreEvent) {
|
||||||
|
this.setState({
|
||||||
|
expanded: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRootClick(ev) {
|
||||||
|
// This captures any clicks that happen within our elements,
|
||||||
|
// such that we can then ignore them when they're seen by the
|
||||||
|
// click listener on the document handler, ie. not close the
|
||||||
|
// dropdown immediately after opening it.
|
||||||
|
// NB. We can't just stopPropagation() because then the event
|
||||||
|
// doesn't reach the React onClick().
|
||||||
|
this.ignoreEvent = ev;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onInputClick(ev) {
|
||||||
|
this.setState({
|
||||||
|
expanded: !this.state.expanded,
|
||||||
|
});
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMenuOptionClick(dropdownKey) {
|
||||||
|
this.setState({
|
||||||
|
expanded: false,
|
||||||
|
});
|
||||||
|
this.props.onOptionChange(dropdownKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onInputKeyPress(e) {
|
||||||
|
// This needs to be on the keypress event because otherwise
|
||||||
|
// it can't cancel the form submission
|
||||||
|
if (e.key == 'Enter') {
|
||||||
|
this.setState({
|
||||||
|
expanded: false,
|
||||||
|
});
|
||||||
|
this.props.onOptionChange(this.state.highlightedOption);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onInputKeyUp(e) {
|
||||||
|
// These keys don't generate keypress events and so needs to
|
||||||
|
// be on keyup
|
||||||
|
if (e.key == 'Escape') {
|
||||||
|
this.setState({
|
||||||
|
expanded: false,
|
||||||
|
});
|
||||||
|
} else if (e.key == 'ArrowDown') {
|
||||||
|
this.setState({
|
||||||
|
highlightedOption: this._nextOption(this.state.highlightedOption),
|
||||||
|
});
|
||||||
|
} else if (e.key == 'ArrowUp') {
|
||||||
|
this.setState({
|
||||||
|
highlightedOption: this._prevOption(this.state.highlightedOption),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onInputChange(e) {
|
||||||
|
this.setState({
|
||||||
|
searchQuery: e.target.value,
|
||||||
|
});
|
||||||
|
if (this.props.onSearchChange) {
|
||||||
|
this.props.onSearchChange(e.target.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_collectRoot(e) {
|
||||||
|
if (this.dropdownRootElement) {
|
||||||
|
this.dropdownRootElement.removeEventListener(
|
||||||
|
'click', this._onRootClick, false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (e) {
|
||||||
|
e.addEventListener('click', this._onRootClick, false);
|
||||||
|
}
|
||||||
|
this.dropdownRootElement = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
_collectInputTextBox(e) {
|
||||||
|
this.inputTextBox = e;
|
||||||
|
if (e) e.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
_setHighlightedOption(optionKey) {
|
||||||
|
this.setState({
|
||||||
|
highlightedOption: optionKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_nextOption(optionKey) {
|
||||||
|
const keys = Object.keys(this.childrenByKey);
|
||||||
|
const index = keys.indexOf(optionKey);
|
||||||
|
return keys[(index + 1) % keys.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
_prevOption(optionKey) {
|
||||||
|
const keys = Object.keys(this.childrenByKey);
|
||||||
|
const index = keys.indexOf(optionKey);
|
||||||
|
return keys[(index - 1) % keys.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
_getMenuOptions() {
|
||||||
|
const options = React.Children.map(this.props.children, (child) => {
|
||||||
|
return (
|
||||||
|
<MenuOption key={child.key} dropdownKey={child.key}
|
||||||
|
highlighted={this.state.highlightedOption == child.key}
|
||||||
|
onMouseEnter={this._setHighlightedOption}
|
||||||
|
onClick={this._onMenuOptionClick}
|
||||||
|
>
|
||||||
|
{child}
|
||||||
|
</MenuOption>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.state.searchQuery) {
|
||||||
|
options.push(
|
||||||
|
<div key="_searchprompt" className="mx_Dropdown_searchPrompt">
|
||||||
|
Type to search...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let currentValue;
|
||||||
|
|
||||||
|
const menuStyle = {};
|
||||||
|
if (this.props.menuWidth) menuStyle.width = this.props.menuWidth;
|
||||||
|
|
||||||
|
let menu;
|
||||||
|
if (this.state.expanded) {
|
||||||
|
currentValue = <input type="text" className="mx_Dropdown_option"
|
||||||
|
ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress}
|
||||||
|
onKeyUp={this._onInputKeyUp}
|
||||||
|
onChange={this._onInputChange}
|
||||||
|
value={this.state.searchQuery}
|
||||||
|
/>;
|
||||||
|
menu = <div className="mx_Dropdown_menu" style={menuStyle}>
|
||||||
|
{this._getMenuOptions()}
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
const selectedChild = this.props.getShortOption ?
|
||||||
|
this.props.getShortOption(this.props.value) :
|
||||||
|
this.childrenByKey[this.props.value];
|
||||||
|
currentValue = <div className="mx_Dropdown_option">
|
||||||
|
{selectedChild}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropdownClasses = {
|
||||||
|
mx_Dropdown: true,
|
||||||
|
};
|
||||||
|
if (this.props.className) {
|
||||||
|
dropdownClasses[this.props.className] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note the menu sits inside the AccessibleButton div so it's anchored
|
||||||
|
// to the input, but overflows below it. The root contains both.
|
||||||
|
return <div className={classnames(dropdownClasses)} ref={this._collectRoot}>
|
||||||
|
<AccessibleButton className="mx_Dropdown_input" onClick={this._onInputClick}>
|
||||||
|
{currentValue}
|
||||||
|
<span className="mx_Dropdown_arrow"></span>
|
||||||
|
{menu}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dropdown.propTypes = {
|
||||||
|
// The width that the dropdown should be. If specified,
|
||||||
|
// the dropped-down part of the menu will be set to this
|
||||||
|
// width.
|
||||||
|
menuWidth: React.PropTypes.number,
|
||||||
|
// Called when the selected option changes
|
||||||
|
onOptionChange: React.PropTypes.func.isRequired,
|
||||||
|
// Called when the value of the search field changes
|
||||||
|
onSearchChange: React.PropTypes.func,
|
||||||
|
// Function that, given the key of an option, returns
|
||||||
|
// a node representing that option to be displayed in the
|
||||||
|
// box itself as the currently-selected option (ie. as
|
||||||
|
// opposed to in the actual dropped-down part). If
|
||||||
|
// unspecified, the appropriate child element is used as
|
||||||
|
// in the dropped-down menu.
|
||||||
|
getShortOption: React.PropTypes.func,
|
||||||
|
value: React.PropTypes.string,
|
||||||
|
}
|
123
src/components/views/login/CountryDropdown.js
Normal file
123
src/components/views/login/CountryDropdown.js
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations 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 { COUNTRIES } from '../../../phonenumber';
|
||||||
|
import { charactersToImageNode } from '../../../HtmlUtils';
|
||||||
|
|
||||||
|
const COUNTRIES_BY_ISO2 = new Object(null);
|
||||||
|
for (const c of COUNTRIES) {
|
||||||
|
COUNTRIES_BY_ISO2[c.iso2] = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function countryMatchesSearchQuery(query, country) {
|
||||||
|
if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
|
||||||
|
if (country.iso2 == query.toUpperCase()) return true;
|
||||||
|
if (country.prefix == query) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_DISPLAYED_ROWS = 2;
|
||||||
|
|
||||||
|
export default class CountryDropdown extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this._onSearchChange = this._onSearchChange.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
searchQuery: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.value) {
|
||||||
|
// If no value is given, we start with the first
|
||||||
|
// country selected, but our parent component
|
||||||
|
// doesn't know this, therefore we do this.
|
||||||
|
this.props.onOptionChange(COUNTRIES[0].iso2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSearchChange(search) {
|
||||||
|
this.setState({
|
||||||
|
searchQuery: search,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_flagImgForIso2(iso2) {
|
||||||
|
// Unicode Regional Indicator Symbol letter 'A'
|
||||||
|
const RIS_A = 0x1F1E6;
|
||||||
|
const ASCII_A = 65;
|
||||||
|
return charactersToImageNode(iso2,
|
||||||
|
RIS_A + (iso2.charCodeAt(0) - ASCII_A),
|
||||||
|
RIS_A + (iso2.charCodeAt(1) - ASCII_A),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||||
|
|
||||||
|
let displayedCountries;
|
||||||
|
if (this.state.searchQuery) {
|
||||||
|
displayedCountries = COUNTRIES.filter(
|
||||||
|
countryMatchesSearchQuery.bind(this, this.state.searchQuery),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
this.state.searchQuery.length == 2 &&
|
||||||
|
COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()]
|
||||||
|
) {
|
||||||
|
// exact ISO2 country name match: make the first result the matches ISO2
|
||||||
|
const matched = COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()];
|
||||||
|
displayedCountries = displayedCountries.filter((c) => {
|
||||||
|
return c.iso2 != matched.iso2;
|
||||||
|
});
|
||||||
|
displayedCountries.unshift(matched);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
displayedCountries = COUNTRIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayedCountries.length > MAX_DISPLAYED_ROWS) {
|
||||||
|
displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = displayedCountries.map((country) => {
|
||||||
|
return <div key={country.iso2}>
|
||||||
|
{this._flagImgForIso2(country.iso2)}
|
||||||
|
{country.name}
|
||||||
|
</div>;
|
||||||
|
});
|
||||||
|
|
||||||
|
// default value here too, otherwise we need to handle null / undefined
|
||||||
|
// values between mounting and the initial value propgating
|
||||||
|
const value = this.props.value || COUNTRIES[0].iso2;
|
||||||
|
|
||||||
|
return <Dropdown className={this.props.className}
|
||||||
|
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
|
||||||
|
menuWidth={298} getShortOption={this._flagImgForIso2}
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
{options}
|
||||||
|
</Dropdown>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CountryDropdown.propTypes = {
|
||||||
|
className: React.PropTypes.string,
|
||||||
|
onOptionChange: React.PropTypes.func.isRequired,
|
||||||
|
value: React.PropTypes.string,
|
||||||
|
};
|
|
@ -16,6 +16,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import url from 'url';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
|
||||||
|
@ -158,6 +160,7 @@ export const RecaptchaAuthEntry = React.createClass({
|
||||||
submitAuthDict: React.PropTypes.func.isRequired,
|
submitAuthDict: React.PropTypes.func.isRequired,
|
||||||
stageParams: React.PropTypes.object.isRequired,
|
stageParams: React.PropTypes.object.isRequired,
|
||||||
errorText: React.PropTypes.string,
|
errorText: React.PropTypes.string,
|
||||||
|
busy: React.PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
_onCaptchaResponse: function(response) {
|
_onCaptchaResponse: function(response) {
|
||||||
|
@ -168,6 +171,11 @@ export const RecaptchaAuthEntry = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
if (this.props.busy) {
|
||||||
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
const CaptchaForm = sdk.getComponent("views.login.CaptchaForm");
|
const CaptchaForm = sdk.getComponent("views.login.CaptchaForm");
|
||||||
var sitePublicKey = this.props.stageParams.public_key;
|
var sitePublicKey = this.props.stageParams.public_key;
|
||||||
return (
|
return (
|
||||||
|
@ -255,6 +263,137 @@ export const EmailIdentityAuthEntry = React.createClass({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const MsisdnAuthEntry = React.createClass({
|
||||||
|
displayName: 'MsisdnAuthEntry',
|
||||||
|
|
||||||
|
statics: {
|
||||||
|
LOGIN_TYPE: "m.login.msisdn",
|
||||||
|
},
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
inputs: React.PropTypes.shape({
|
||||||
|
phoneCountry: React.PropTypes.string,
|
||||||
|
phoneNumber: React.PropTypes.string,
|
||||||
|
}),
|
||||||
|
fail: React.PropTypes.func,
|
||||||
|
clientSecret: React.PropTypes.func,
|
||||||
|
submitAuthDict: React.PropTypes.func.isRequired,
|
||||||
|
matrixClient: React.PropTypes.object,
|
||||||
|
submitAuthDict: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
token: '',
|
||||||
|
requestingToken: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this._sid = null;
|
||||||
|
this._msisdn = null;
|
||||||
|
this._tokenBox = null;
|
||||||
|
|
||||||
|
this.setState({requestingToken: true});
|
||||||
|
this._requestMsisdnToken().catch((e) => {
|
||||||
|
this.props.fail(e);
|
||||||
|
}).finally(() => {
|
||||||
|
this.setState({requestingToken: false});
|
||||||
|
}).done();
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Requests a verification token by SMS.
|
||||||
|
*/
|
||||||
|
_requestMsisdnToken: function() {
|
||||||
|
return this.props.matrixClient.requestRegisterMsisdnToken(
|
||||||
|
this.props.inputs.phoneCountry,
|
||||||
|
this.props.inputs.phoneNumber,
|
||||||
|
this.props.clientSecret,
|
||||||
|
1, // TODO: Multiple send attempts?
|
||||||
|
).then((result) => {
|
||||||
|
this._sid = result.sid;
|
||||||
|
this._msisdn = result.msisdn;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onTokenChange: function(e) {
|
||||||
|
this.setState({
|
||||||
|
token: e.target.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onFormSubmit: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.state.token == '') return;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
errorText: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.matrixClient.submitMsisdnToken(
|
||||||
|
this._sid, this.props.clientSecret, this.state.token
|
||||||
|
).then((result) => {
|
||||||
|
if (result.success) {
|
||||||
|
const idServerParsedUrl = url.parse(
|
||||||
|
this.props.matrixClient.getIdentityServerUrl(),
|
||||||
|
)
|
||||||
|
this.props.submitAuthDict({
|
||||||
|
type: MsisdnAuthEntry.LOGIN_TYPE,
|
||||||
|
threepid_creds: {
|
||||||
|
sid: this._sid,
|
||||||
|
client_secret: this.props.clientSecret,
|
||||||
|
id_server: idServerParsedUrl.host,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
errorText: "Token incorrect",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch((e) => {
|
||||||
|
this.props.fail(e);
|
||||||
|
console.log("Failed to submit msisdn token");
|
||||||
|
}).done();
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
if (this.state.requestingToken) {
|
||||||
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
return <Loader />;
|
||||||
|
} else {
|
||||||
|
const enableSubmit = Boolean(this.state.token);
|
||||||
|
const submitClasses = classnames({
|
||||||
|
mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
|
||||||
|
mx_UserSettings_button: true, // XXX button classes
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>A text message has been sent to +<i>{this._msisdn}</i></p>
|
||||||
|
<p>Please enter the code it contains:</p>
|
||||||
|
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
||||||
|
<form onSubmit={this._onFormSubmit}>
|
||||||
|
<input type="text"
|
||||||
|
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
|
||||||
|
value={this.state.token}
|
||||||
|
onChange={this._onTokenChange}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<input type="submit" value="Submit"
|
||||||
|
className={submitClasses}
|
||||||
|
disabled={!enableSubmit}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<div className="error">
|
||||||
|
{this.state.errorText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const FallbackAuthEntry = React.createClass({
|
export const FallbackAuthEntry = React.createClass({
|
||||||
displayName: 'FallbackAuthEntry',
|
displayName: 'FallbackAuthEntry',
|
||||||
|
|
||||||
|
@ -313,6 +452,7 @@ const AuthEntryComponents = [
|
||||||
PasswordAuthEntry,
|
PasswordAuthEntry,
|
||||||
RecaptchaAuthEntry,
|
RecaptchaAuthEntry,
|
||||||
EmailIdentityAuthEntry,
|
EmailIdentityAuthEntry,
|
||||||
|
MsisdnAuthEntry,
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getEntryComponentForLoginType(loginType) {
|
export function getEntryComponentForLoginType(loginType) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,6 +18,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import sdk from '../../../index';
|
||||||
import {field_input_incorrect} from '../../../UiEffects';
|
import {field_input_incorrect} from '../../../UiEffects';
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,8 +30,12 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
||||||
onForgotPasswordClick: React.PropTypes.func, // fn()
|
onForgotPasswordClick: React.PropTypes.func, // fn()
|
||||||
initialUsername: React.PropTypes.string,
|
initialUsername: React.PropTypes.string,
|
||||||
|
initialPhoneCountry: React.PropTypes.string,
|
||||||
|
initialPhoneNumber: React.PropTypes.string,
|
||||||
initialPassword: React.PropTypes.string,
|
initialPassword: React.PropTypes.string,
|
||||||
onUsernameChanged: React.PropTypes.func,
|
onUsernameChanged: React.PropTypes.func,
|
||||||
|
onPhoneCountryChanged: React.PropTypes.func,
|
||||||
|
onPhoneNumberChanged: React.PropTypes.func,
|
||||||
onPasswordChanged: React.PropTypes.func,
|
onPasswordChanged: React.PropTypes.func,
|
||||||
loginIncorrect: React.PropTypes.bool,
|
loginIncorrect: React.PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
@ -38,7 +44,11 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
return {
|
return {
|
||||||
onUsernameChanged: function() {},
|
onUsernameChanged: function() {},
|
||||||
onPasswordChanged: function() {},
|
onPasswordChanged: function() {},
|
||||||
|
onPhoneCountryChanged: function() {},
|
||||||
|
onPhoneNumberChanged: function() {},
|
||||||
initialUsername: "",
|
initialUsername: "",
|
||||||
|
initialPhoneCountry: "",
|
||||||
|
initialPhoneNumber: "",
|
||||||
initialPassword: "",
|
initialPassword: "",
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
};
|
};
|
||||||
|
@ -48,6 +58,8 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
return {
|
return {
|
||||||
username: this.props.initialUsername,
|
username: this.props.initialUsername,
|
||||||
password: this.props.initialPassword,
|
password: this.props.initialPassword,
|
||||||
|
phoneCountry: this.props.initialPhoneCountry,
|
||||||
|
phoneNumber: this.props.initialPhoneNumber,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -63,7 +75,12 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
|
|
||||||
onSubmitForm: function(ev) {
|
onSubmitForm: function(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.props.onSubmit(this.state.username, this.state.password);
|
this.props.onSubmit(
|
||||||
|
this.state.username,
|
||||||
|
this.state.phoneCountry,
|
||||||
|
this.state.phoneNumber,
|
||||||
|
this.state.password,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
onUsernameChanged: function(ev) {
|
onUsernameChanged: function(ev) {
|
||||||
|
@ -71,6 +88,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
this.props.onUsernameChanged(ev.target.value);
|
this.props.onUsernameChanged(ev.target.value);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onPhoneCountryChanged: function(country) {
|
||||||
|
this.setState({phoneCountry: country});
|
||||||
|
this.props.onPhoneCountryChanged(country);
|
||||||
|
},
|
||||||
|
|
||||||
|
onPhoneNumberChanged: function(ev) {
|
||||||
|
this.setState({phoneNumber: ev.target.value});
|
||||||
|
this.props.onPhoneNumberChanged(ev.target.value);
|
||||||
|
},
|
||||||
|
|
||||||
onPasswordChanged: function(ev) {
|
onPasswordChanged: function(ev) {
|
||||||
this.setState({password: ev.target.value});
|
this.setState({password: ev.target.value});
|
||||||
this.props.onPasswordChanged(ev.target.value);
|
this.props.onPasswordChanged(ev.target.value);
|
||||||
|
@ -92,13 +119,28 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
error: this.props.loginIncorrect,
|
error: this.props.loginIncorrect,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={this.onSubmitForm}>
|
<form onSubmit={this.onSubmitForm}>
|
||||||
<input className="mx_Login_field" type="text"
|
<input className="mx_Login_field mx_Login_username" type="text"
|
||||||
name="username" // make it a little easier for browser's remember-password
|
name="username" // make it a little easier for browser's remember-password
|
||||||
value={this.state.username} onChange={this.onUsernameChanged}
|
value={this.state.username} onChange={this.onUsernameChanged}
|
||||||
placeholder="Email or user name" autoFocus />
|
placeholder="Email or user name" autoFocus />
|
||||||
|
or
|
||||||
|
<div className="mx_Login_phoneSection">
|
||||||
|
<CountryDropdown ref="phone_country" onOptionChange={this.onPhoneCountryChanged}
|
||||||
|
className="mx_Login_phoneCountry"
|
||||||
|
value={this.state.phoneCountry}
|
||||||
|
/>
|
||||||
|
<input type="text" ref="phoneNumber"
|
||||||
|
onChange={this.onPhoneNumberChanged}
|
||||||
|
placeholder="Mobile phone number"
|
||||||
|
className="mx_Login_phoneNumberField mx_Login_field"
|
||||||
|
value={this.state.phoneNumber}
|
||||||
|
name="phoneNumber"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
||||||
name="password"
|
name="password"
|
||||||
|
|
|
@ -19,9 +19,12 @@ import React from 'react';
|
||||||
import { field_input_incorrect } from '../../../UiEffects';
|
import { field_input_incorrect } from '../../../UiEffects';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import Email from '../../../email';
|
import Email from '../../../email';
|
||||||
|
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
|
|
||||||
const FIELD_EMAIL = 'field_email';
|
const FIELD_EMAIL = 'field_email';
|
||||||
|
const FIELD_PHONE_COUNTRY = 'field_phone_country';
|
||||||
|
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
||||||
const FIELD_USERNAME = 'field_username';
|
const FIELD_USERNAME = 'field_username';
|
||||||
const FIELD_PASSWORD = 'field_password';
|
const FIELD_PASSWORD = 'field_password';
|
||||||
const FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
|
const FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
|
||||||
|
@ -35,6 +38,8 @@ module.exports = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// Values pre-filled in the input boxes when the component loads
|
// Values pre-filled in the input boxes when the component loads
|
||||||
defaultEmail: React.PropTypes.string,
|
defaultEmail: React.PropTypes.string,
|
||||||
|
defaultPhoneCountry: React.PropTypes.string,
|
||||||
|
defaultPhoneNumber: React.PropTypes.string,
|
||||||
defaultUsername: React.PropTypes.string,
|
defaultUsername: React.PropTypes.string,
|
||||||
defaultPassword: React.PropTypes.string,
|
defaultPassword: React.PropTypes.string,
|
||||||
teamsConfig: React.PropTypes.shape({
|
teamsConfig: React.PropTypes.shape({
|
||||||
|
@ -71,6 +76,8 @@ module.exports = React.createClass({
|
||||||
return {
|
return {
|
||||||
fieldValid: {},
|
fieldValid: {},
|
||||||
selectedTeam: null,
|
selectedTeam: null,
|
||||||
|
// The ISO2 country code selected in the phone number entry
|
||||||
|
phoneCountry: this.props.defaultPhoneCountry,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -85,6 +92,7 @@ module.exports = React.createClass({
|
||||||
this.validateField(FIELD_PASSWORD_CONFIRM);
|
this.validateField(FIELD_PASSWORD_CONFIRM);
|
||||||
this.validateField(FIELD_PASSWORD);
|
this.validateField(FIELD_PASSWORD);
|
||||||
this.validateField(FIELD_USERNAME);
|
this.validateField(FIELD_USERNAME);
|
||||||
|
this.validateField(FIELD_PHONE_NUMBER);
|
||||||
this.validateField(FIELD_EMAIL);
|
this.validateField(FIELD_EMAIL);
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -118,6 +126,8 @@ module.exports = React.createClass({
|
||||||
username: this.refs.username.value.trim() || this.props.guestUsername,
|
username: this.refs.username.value.trim() || this.props.guestUsername,
|
||||||
password: this.refs.password.value.trim(),
|
password: this.refs.password.value.trim(),
|
||||||
email: email,
|
email: email,
|
||||||
|
phoneCountry: this.state.phoneCountry,
|
||||||
|
phoneNumber: this.refs.phoneNumber.value.trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (promise) {
|
if (promise) {
|
||||||
|
@ -174,6 +184,11 @@ module.exports = React.createClass({
|
||||||
const emailValid = email === '' || Email.looksValid(email);
|
const emailValid = email === '' || Email.looksValid(email);
|
||||||
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
|
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||||
break;
|
break;
|
||||||
|
case FIELD_PHONE_NUMBER:
|
||||||
|
const phoneNumber = this.refs.phoneNumber.value;
|
||||||
|
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
|
||||||
|
this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
|
||||||
|
break;
|
||||||
case FIELD_USERNAME:
|
case FIELD_USERNAME:
|
||||||
// XXX: SPEC-1
|
// XXX: SPEC-1
|
||||||
var username = this.refs.username.value.trim() || this.props.guestUsername;
|
var username = this.refs.username.value.trim() || this.props.guestUsername;
|
||||||
|
@ -233,6 +248,8 @@ module.exports = React.createClass({
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case FIELD_EMAIL:
|
case FIELD_EMAIL:
|
||||||
return this.refs.email;
|
return this.refs.email;
|
||||||
|
case FIELD_PHONE_NUMBER:
|
||||||
|
return this.refs.phoneNumber;
|
||||||
case FIELD_USERNAME:
|
case FIELD_USERNAME:
|
||||||
return this.refs.username;
|
return this.refs.username;
|
||||||
case FIELD_PASSWORD:
|
case FIELD_PASSWORD:
|
||||||
|
@ -251,6 +268,12 @@ module.exports = React.createClass({
|
||||||
return cls;
|
return cls;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onPhoneCountryChange(newVal) {
|
||||||
|
this.setState({
|
||||||
|
phoneCountry: newVal,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
@ -286,6 +309,25 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||||
|
const phoneSection = (
|
||||||
|
<div className="mx_Login_phoneSection">
|
||||||
|
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
|
||||||
|
className="mx_Login_phoneCountry"
|
||||||
|
value={this.state.phoneCountry}
|
||||||
|
/>
|
||||||
|
<input type="text" ref="phoneNumber"
|
||||||
|
placeholder="Mobile phone number (optional)"
|
||||||
|
defaultValue={this.props.defaultPhoneNumber}
|
||||||
|
className={this._classForField(
|
||||||
|
FIELD_PHONE_NUMBER, 'mx_Login_phoneNumberField', 'mx_Login_field'
|
||||||
|
)}
|
||||||
|
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
|
||||||
|
value={self.state.phoneNumber}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const registerButton = (
|
const registerButton = (
|
||||||
<input className="mx_Login_submit" type="submit" value="Register" />
|
<input className="mx_Login_submit" type="submit" value="Register" />
|
||||||
);
|
);
|
||||||
|
@ -300,6 +342,7 @@ module.exports = React.createClass({
|
||||||
<form onSubmit={this.onSubmit}>
|
<form onSubmit={this.onSubmit}>
|
||||||
{emailSection}
|
{emailSection}
|
||||||
{belowEmailSection}
|
{belowEmailSection}
|
||||||
|
{phoneSection}
|
||||||
<input type="text" ref="username"
|
<input type="text" ref="username"
|
||||||
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
||||||
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
||||||
|
|
|
@ -24,7 +24,7 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
const text = this.props.mxEvent.getContent().body;
|
const text = this.props.mxEvent.getContent().body;
|
||||||
return (
|
return (
|
||||||
<span className="mx_UnknownBody">
|
<span className="mx_UnknownBody" title="Redacted or unknown message type">
|
||||||
{text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
@ -541,9 +541,9 @@ export default class MessageComposerInput extends React.Component {
|
||||||
let sendTextFn = this.client.sendTextMessage;
|
let sendTextFn = this.client.sendTextMessage;
|
||||||
|
|
||||||
if (contentText.startsWith('/me')) {
|
if (contentText.startsWith('/me')) {
|
||||||
contentText = contentText.replace('/me', '');
|
contentText = contentText.replace('/me ', '');
|
||||||
// bit of a hack, but the alternative would be quite complicated
|
// bit of a hack, but the alternative would be quite complicated
|
||||||
if (contentHTML) contentHTML = contentHTML.replace('/me', '');
|
if (contentHTML) contentHTML = contentHTML.replace('/me ', '');
|
||||||
sendHtmlFn = this.client.sendHtmlEmote;
|
sendHtmlFn = this.client.sendHtmlEmote;
|
||||||
sendTextFn = this.client.sendEmoteMessage;
|
sendTextFn = this.client.sendEmoteMessage;
|
||||||
}
|
}
|
||||||
|
|
172
src/components/views/settings/AddPhoneNumber.js
Normal file
172
src/components/views/settings/AddPhoneNumber.js
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations 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 AddThreepid from '../../../AddThreepid';
|
||||||
|
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
|
||||||
|
import Modal from '../../../Modal';
|
||||||
|
|
||||||
|
|
||||||
|
export default WithMatrixClient(React.createClass({
|
||||||
|
displayName: 'AddPhoneNumber',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
matrixClient: React.PropTypes.object.isRequired,
|
||||||
|
onThreepidAdded: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
busy: false,
|
||||||
|
phoneCountry: null,
|
||||||
|
phoneNumber: "",
|
||||||
|
msisdn_add_pending: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this._addThreepid = null;
|
||||||
|
this._addMsisdnInput = null;
|
||||||
|
this._unmounted = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
this._unmounted = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
_onPhoneCountryChange: function(phoneCountry) {
|
||||||
|
this.setState({ phoneCountry: phoneCountry });
|
||||||
|
},
|
||||||
|
|
||||||
|
_onPhoneNumberChange: function(ev) {
|
||||||
|
this.setState({ phoneNumber: ev.target.value });
|
||||||
|
},
|
||||||
|
|
||||||
|
_onAddMsisdnEditFinished: function(value, shouldSubmit) {
|
||||||
|
if (!shouldSubmit) return;
|
||||||
|
this._addMsisdn();
|
||||||
|
},
|
||||||
|
|
||||||
|
_onAddMsisdnSubmit: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this._addMsisdn();
|
||||||
|
},
|
||||||
|
|
||||||
|
_collectAddMsisdnInput: function(e) {
|
||||||
|
this._addMsisdnInput = e;
|
||||||
|
},
|
||||||
|
|
||||||
|
_addMsisdn: function() {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
|
||||||
|
this._addThreepid = new AddThreepid();
|
||||||
|
// we always bind phone numbers when registering, so let's do the
|
||||||
|
// same here.
|
||||||
|
this._addThreepid.addMsisdn(this.state.phoneCountry, this.state.phoneNumber, true).then((resp) => {
|
||||||
|
this._promptForMsisdnVerificationCode(resp.msisdn);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error("Unable to add phone number: " + err);
|
||||||
|
let msg = err.message;
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Error",
|
||||||
|
description: msg,
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
if (this._unmounted) return;
|
||||||
|
this.setState({msisdn_add_pending: false});
|
||||||
|
}).done();
|
||||||
|
this._addMsisdnInput.blur();
|
||||||
|
this.setState({msisdn_add_pending: true});
|
||||||
|
},
|
||||||
|
|
||||||
|
_promptForMsisdnVerificationCode:function (msisdn, err) {
|
||||||
|
if (this._unmounted) return;
|
||||||
|
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
||||||
|
let msgElements = [
|
||||||
|
<div key="_static" >A text message has been sent to +{msisdn}.
|
||||||
|
Please enter the verification code it contains</div>
|
||||||
|
];
|
||||||
|
if (err) {
|
||||||
|
let msg = err.error;
|
||||||
|
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
|
||||||
|
msg = "Incorrect verification code";
|
||||||
|
}
|
||||||
|
msgElements.push(<div key="_error" className="error">{msg}</div>);
|
||||||
|
}
|
||||||
|
Modal.createDialog(TextInputDialog, {
|
||||||
|
title: "Enter Code",
|
||||||
|
description: <div>{msgElements}</div>,
|
||||||
|
button: "Submit",
|
||||||
|
onFinished: (should_verify, token) => {
|
||||||
|
if (!should_verify) {
|
||||||
|
this._addThreepid = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._unmounted) return;
|
||||||
|
this.setState({msisdn_add_pending: true});
|
||||||
|
this._addThreepid.haveMsisdnToken(token).then(() => {
|
||||||
|
this._addThreepid = null;
|
||||||
|
this.setState({phoneNumber: ''});
|
||||||
|
if (this.props.onThreepidAdded) this.props.onThreepidAdded();
|
||||||
|
}).catch((err) => {
|
||||||
|
this._promptForMsisdnVerificationCode(msisdn, err);
|
||||||
|
}).finally(() => {
|
||||||
|
if (this._unmounted) return;
|
||||||
|
this.setState({msisdn_add_pending: false});
|
||||||
|
}).done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
if (this.state.msisdn_add_pending) {
|
||||||
|
return <Loader />;
|
||||||
|
} else if (this.props.matrixClient.isGuest()) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||||
|
// XXX: This CSS relies on the CSS surrounding it in UserSettings as its in
|
||||||
|
// a tabular format to align the submit buttons
|
||||||
|
return (
|
||||||
|
<form className="mx_UserSettings_profileTableRow" onSubmit={this._onAddMsisdnSubmit}>
|
||||||
|
<div className="mx_UserSettings_profileLabelCell">
|
||||||
|
</div>
|
||||||
|
<div className="mx_UserSettings_profileInputCell">
|
||||||
|
<div className="mx_Login_phoneSection">
|
||||||
|
<CountryDropdown onOptionChange={this._onPhoneCountryChange}
|
||||||
|
className="mx_Login_phoneCountry"
|
||||||
|
value={this.state.phoneCountry}
|
||||||
|
/>
|
||||||
|
<input type="text"
|
||||||
|
ref={this._collectAddMsisdnInput}
|
||||||
|
className="mx_UserSettings_phoneNumberField"
|
||||||
|
placeholder="Add phone number"
|
||||||
|
value={this.state.phoneNumber}
|
||||||
|
onChange={this._onPhoneNumberChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||||
|
<input type="image" value="Add" src="img/plus.svg" width="14" height="14" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}))
|
1273
src/phonenumber.js
Normal file
1273
src/phonenumber.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -68,48 +68,49 @@ describe('InteractiveAuthDialog', function () {
|
||||||
onFinished={onFinished}
|
onFinished={onFinished}
|
||||||
/>, parentDiv);
|
/>, parentDiv);
|
||||||
|
|
||||||
// at this point there should be a password box and a submit button
|
// wait for a password box and a submit button
|
||||||
const formNode = ReactTestUtils.findRenderedDOMComponentWithTag(dlg, "form");
|
test_utils.waitForRenderedDOMComponentWithTag(dlg, "form").then((formNode) => {
|
||||||
const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag(
|
const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag(
|
||||||
dlg, "input"
|
dlg, "input"
|
||||||
);
|
);
|
||||||
let passwordNode;
|
let passwordNode;
|
||||||
let submitNode;
|
let submitNode;
|
||||||
for (const node of inputNodes) {
|
for (const node of inputNodes) {
|
||||||
if (node.type == 'password') {
|
if (node.type == 'password') {
|
||||||
passwordNode = node;
|
passwordNode = node;
|
||||||
} else if (node.type == 'submit') {
|
} else if (node.type == 'submit') {
|
||||||
submitNode = node;
|
submitNode = node;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
expect(passwordNode).toExist();
|
||||||
expect(passwordNode).toExist();
|
expect(submitNode).toExist();
|
||||||
expect(submitNode).toExist();
|
|
||||||
|
|
||||||
// submit should be disabled
|
// submit should be disabled
|
||||||
expect(submitNode.disabled).toBe(true);
|
expect(submitNode.disabled).toBe(true);
|
||||||
|
|
||||||
// put something in the password box, and hit enter; that should
|
// put something in the password box, and hit enter; that should
|
||||||
// trigger a request
|
// trigger a request
|
||||||
passwordNode.value = "s3kr3t";
|
passwordNode.value = "s3kr3t";
|
||||||
ReactTestUtils.Simulate.change(passwordNode);
|
ReactTestUtils.Simulate.change(passwordNode);
|
||||||
expect(submitNode.disabled).toBe(false);
|
expect(submitNode.disabled).toBe(false);
|
||||||
ReactTestUtils.Simulate.submit(formNode, {});
|
ReactTestUtils.Simulate.submit(formNode, {});
|
||||||
|
|
||||||
expect(doRequest.callCount).toEqual(1);
|
expect(doRequest.callCount).toEqual(1);
|
||||||
expect(doRequest.calledWithExactly({
|
expect(doRequest.calledWithExactly({
|
||||||
session: "sess",
|
session: "sess",
|
||||||
type: "m.login.password",
|
type: "m.login.password",
|
||||||
password: "s3kr3t",
|
password: "s3kr3t",
|
||||||
user: "@user:id",
|
user: "@user:id",
|
||||||
})).toBe(true);
|
})).toBe(true);
|
||||||
|
|
||||||
// there should now be a spinner
|
// there should now be a spinner
|
||||||
ReactTestUtils.findRenderedComponentWithType(
|
ReactTestUtils.findRenderedComponentWithType(
|
||||||
dlg, sdk.getComponent('elements.Spinner'),
|
dlg, sdk.getComponent('elements.Spinner'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// let the request complete
|
// let the request complete
|
||||||
q.delay(1).then(() => {
|
return q.delay(1);
|
||||||
|
}).then(() => {
|
||||||
expect(onFinished.callCount).toEqual(1);
|
expect(onFinished.callCount).toEqual(1);
|
||||||
expect(onFinished.calledWithExactly(true, {a:1})).toBe(true);
|
expect(onFinished.calledWithExactly(true, {a:1})).toBe(true);
|
||||||
}).done(done, done);
|
}).done(done, done);
|
||||||
|
|
|
@ -1,11 +1,51 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var sinon = require('sinon');
|
import sinon from 'sinon';
|
||||||
var q = require('q');
|
import q from 'q';
|
||||||
|
import ReactTestUtils from 'react-addons-test-utils';
|
||||||
|
|
||||||
var peg = require('../src/MatrixClientPeg.js');
|
import peg from '../src/MatrixClientPeg.js';
|
||||||
var jssdk = require('matrix-js-sdk');
|
import jssdk from 'matrix-js-sdk';
|
||||||
var MatrixEvent = jssdk.MatrixEvent;
|
const MatrixEvent = jssdk.MatrixEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around window.requestAnimationFrame that returns a promise
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function _waitForFrame() {
|
||||||
|
const def = q.defer();
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
def.resolve();
|
||||||
|
});
|
||||||
|
return def.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits a small number of animation frames for a component to appear
|
||||||
|
* in the DOM. Like findRenderedDOMComponentWithTag(), but allows
|
||||||
|
* for the element to appear a short time later, eg. if a promise needs
|
||||||
|
* to resolve first.
|
||||||
|
* @return a promise that resolves once the component appears, or rejects
|
||||||
|
* if it doesn't appear after a nominal number of animation frames.
|
||||||
|
*/
|
||||||
|
export function waitForRenderedDOMComponentWithTag(tree, tag, attempts) {
|
||||||
|
if (attempts === undefined) {
|
||||||
|
// Let's start by assuming we'll only need to wait a single frame, and
|
||||||
|
// we can try increasing this if necessary.
|
||||||
|
attempts = 1;
|
||||||
|
} else if (attempts == 0) {
|
||||||
|
return q.reject("Gave up waiting for component with tag: " + tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _waitForFrame().then(() => {
|
||||||
|
const result = ReactTestUtils.scryRenderedDOMComponentsWithTag(tree, tag);
|
||||||
|
if (result.length > 0) {
|
||||||
|
return result[0];
|
||||||
|
} else {
|
||||||
|
return waitForRenderedDOMComponentWithTag(tree, tag, attempts - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform common actions before each test case, e.g. printing the test case
|
* Perform common actions before each test case, e.g. printing the test case
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue