diff --git a/src/AddThreepid.js b/src/AddThreepid.js
index d6a1d58aa0..c89de4f5fa 100644
--- a/src/AddThreepid.js
+++ b/src/AddThreepid.js
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
+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.
@@ -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
- * @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
- * the reset failed, e.g. "There is no mapped matrix user ID for the given email address".
+ * the request failed.
*/
checkEmailLinkClicked() {
var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
@@ -73,6 +99,29 @@ class AddThreepid {
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;
diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js
index c500076783..f1420d0a22 100644
--- a/src/HtmlUtils.js
+++ b/src/HtmlUtils.js
@@ -58,6 +58,22 @@ export function unicodeToImage(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 ;
+}
+
export function stripParagraphs(html: string): string {
const contentDiv = document.createElement('div');
contentDiv.innerHTML = html;
diff --git a/src/Login.js b/src/Login.js
index 96f953c130..107a8825e9 100644
--- a/src/Login.js
+++ b/src/Login.js
@@ -105,21 +105,48 @@ export default class Login {
});
}
- loginViaPassword(username, pass) {
- var self = this;
- var isEmail = username.indexOf("@") > 0;
- var loginParams = {
- password: pass,
- initial_device_display_name: this._defaultDeviceDisplayName,
- };
- if (isEmail) {
- loginParams.medium = 'email';
- loginParams.address = username;
+ loginViaPassword(username, phoneCountry, phoneNumber, pass) {
+ const self = this;
+
+ const isEmail = username.indexOf("@") > 0;
+
+ let identifier;
+ let legacyParams; // parameters added to support old HSes
+ if (phoneCountry && phoneNumber) {
+ identifier = {
+ 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 {
- 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 q({
homeserverUrl: self._hsUrl,
diff --git a/src/Notifier.js b/src/Notifier.js
index 67642e734a..7fc7d3e338 100644
--- a/src/Notifier.js
+++ b/src/Notifier.js
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
+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.
@@ -14,8 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-'use strict';
-
var MatrixClientPeg = require("./MatrixClientPeg");
var PlatformPeg = require("./PlatformPeg");
var TextForEvent = require('./TextForEvent');
@@ -103,7 +102,7 @@ var Notifier = {
},
stop: function() {
- if (MatrixClientPeg.get()) {
+ if (MatrixClientPeg.get() && this.boundOnRoomTimeline) {
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt);
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
diff --git a/src/component-index.js b/src/component-index.js
index 2644f1a379..d6873c6dfd 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -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);
import views$dialogs$ChatInviteDialog from './components/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';
views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog);
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);
import views$elements$DirectorySearchBox from './components/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';
views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText);
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);
import views$login$CasLogin from './components/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';
views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog);
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);
import views$rooms$UserTile from './components/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';
views$settings$ChangeAvatar && (module.exports.components['views.settings.ChangeAvatar'] = views$settings$ChangeAvatar);
import views$settings$ChangeDisplayName from './components/views/settings/ChangeDisplayName';
diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js
index 71fee883be..7c8a5b8065 100644
--- a/src/components/structures/InteractiveAuth.js
+++ b/src/components/structures/InteractiveAuth.js
@@ -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({
busy: true,
errorText: null,
stageErrorText: null,
});
- return this.props.makeRequest(auth).finally(() => {
+ return makeRequestPromise.finally(() => {
if (this._unmounted) {
return;
}
diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js
index c2243820cd..6e2f0a3a5b 100644
--- a/src/components/structures/LoggedInView.js
+++ b/src/components/structures/LoggedInView.js
@@ -81,6 +81,13 @@ export default React.createClass({
return this._scrollStateMap[roomId];
},
+ canResetTimelineInRoom: function(roomId) {
+ if (!this.refs.roomView) {
+ return true;
+ }
+ return this.refs.roomView.canResetTimeline();
+ },
+
_onKeyDown: function(ev) {
/*
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 2337d62fd8..9b51e7f3fb 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -806,9 +806,31 @@ module.exports = React.createClass({
* (useful for setting listeners)
*/
_onWillStartClient() {
+ var self = this;
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) {
self.updateStatusIndicator(state, prevState);
if (state === "SYNCING" && prevState === "SYNCING") {
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 345d0f6b80..b22d867acf 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -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,
// after a successful peek, or after we join the room).
_onRoomLoaded: function(room) {
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index cb42f701a3..8ef0e7631f 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -431,6 +431,10 @@ var TimelinePanel = React.createClass({
}
},
+ canResetTimeline: function() {
+ return this.refs.messagePanel && this.refs.messagePanel.isAtBottom();
+ },
+
onRoomRedaction: function(ev, room) {
if (this.unmounted) return;
diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.js
index 8266a11bc8..01a879fd1b 100644
--- a/src/components/structures/UploadBar.js
+++ b/src/components/structures/UploadBar.js
@@ -25,12 +25,13 @@ module.exports = React.createClass({displayName: 'UploadBar',
},
componentDidMount: function() {
- dis.register(this.onAction);
+ this.dispatcherRef = dis.register(this.onAction);
this.mounted = true;
},
componentWillUnmount: function() {
this.mounted = false;
+ dis.unregister(this.dispatcherRef);
},
onAction: function(payload) {
diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js
index 9e6d454fe9..0cb120019e 100644
--- a/src/components/structures/UserSettings.js
+++ b/src/components/structures/UserSettings.js
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
+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.
@@ -139,6 +140,7 @@ module.exports = React.createClass({
componentWillMount: function() {
this._unmounted = false;
+ this._addThreepid = null;
if (PlatformPeg.get()) {
q().then(() => {
@@ -321,12 +323,16 @@ module.exports = React.createClass({
UserSettingsStore.setEnableNotifications(event.target.checked);
},
- onAddThreepidClicked: function(value, shouldSubmit) {
+ _onAddEmailEditFinished: function(value, shouldSubmit) {
if (!shouldSubmit) return;
+ this._addEmail();
+ },
+
+ _addEmail: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
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)) {
Modal.createDialog(ErrorDialog, {
title: "Invalid Email Address",
@@ -334,10 +340,10 @@ module.exports = React.createClass({
});
return;
}
- this.add_threepid = new AddThreepid();
+ this._addThreepid = new AddThreepid();
// we always bind emails when registering, so let's do the
// same here.
- this.add_threepid.addEmailAddress(email_address, true).done(() => {
+ this._addThreepid.addEmailAddress(email_address, true).done(() => {
Modal.createDialog(QuestionDialog, {
title: "Verification Pending",
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"
});
});
- ReactDOM.findDOMNode(this.refs.add_threepid_input).blur();
+ ReactDOM.findDOMNode(this.refs.add_email_input).blur();
this.setState({email_add_pending: true});
},
@@ -391,8 +397,8 @@ module.exports = React.createClass({
},
verifyEmailAddress: function() {
- this.add_threepid.checkEmailLinkClicked().done(() => {
- this.add_threepid = undefined;
+ this._addThreepid.checkEmailLinkClicked().done(() => {
+ this._addThreepid = null;
this.setState({
phase: "UserSettings.LOADING",
});
@@ -761,6 +767,14 @@ module.exports = React.createClass({
return medium[0].toUpperCase() + medium.slice(1);
},
+ presentableTextForThreepid: function(threepid) {
+ if (threepid.medium == 'msisdn') {
+ return '+' + threepid.address;
+ } else {
+ return threepid.address;
+ }
+ },
+
render: function() {
var Loader = sdk.getComponent("elements.Spinner");
switch (this.state.phase) {
@@ -793,7 +807,9 @@ module.exports = React.createClass({
A text message has been sent to +{this._msisdn}
+Please enter the code it contains:
+