diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index 5008ddfcf53..00000000000
Binary files a/.DS_Store and /dev/null differ
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000000..880331a09e5
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,23 @@
+# Copyright 2017 Aviral Dasgupta
+#
+# 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.
+
+root = true
+
+[*]
+charset=utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
diff --git a/.eslintrc.js b/.eslintrc.js
index 34d3af270c9..6cd0e1015ec 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -13,6 +13,7 @@ module.exports = {
plugins: [
"react",
"flowtype",
+ "babel"
],
env: {
es6: true,
@@ -23,6 +24,11 @@ module.exports = {
}
},
rules: {
+ // eslint's built in no-invalid-this rule breaks with class properties
+ "no-invalid-this": "off",
+ // so we replace it with a version that is class property aware
+ "babel/no-invalid-this": "error",
+
/** react **/
// This just uses the react plugin to help eslint known when
// variables have been used in JSX
diff --git a/jenkins.sh b/jenkins.sh
index c1fba19e945..6a77911c272 100755
--- a/jenkins.sh
+++ b/jenkins.sh
@@ -3,7 +3,7 @@
set -e
export KARMAFLAGS="--no-colors"
-export NVM_DIR="/home/jenkins/.nvm"
+export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 4
diff --git a/package.json b/package.json
index 95bfafbb74a..a07e2236aa8 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,7 @@
"isomorphic-fetch": "^2.2.1",
"linkifyjs": "^2.1.3",
"lodash": "^4.13.1",
- "matrix-js-sdk": "0.7.5",
+ "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
"optimist": "^0.6.1",
"q": "^1.4.1",
"react": "^15.4.0",
@@ -90,6 +90,7 @@
"babel-preset-react": "^6.11.1",
"eslint": "^3.13.1",
"eslint-config-google": "^0.7.1",
+ "eslint-plugin-babel": "^4.0.1",
"eslint-plugin-flowtype": "^2.30.0",
"eslint-plugin-react": "^6.9.0",
"expect": "^1.16.0",
diff --git a/src/CallHandler.js b/src/CallHandler.js
index 268a599d8ef..42cc681d085 100644
--- a/src/CallHandler.js
+++ b/src/CallHandler.js
@@ -105,6 +105,15 @@ function _setCallListeners(call) {
call.hangup();
_setCallState(undefined, call.roomId, "ended");
});
+ call.on('send_event_error', function(err) {
+ if (err.name === "UnknownDeviceError") {
+ dis.dispatch({
+ action: 'unknown_device_error',
+ err: err,
+ room: MatrixClientPeg.get().getRoom(call.roomId),
+ });
+ }
+ });
call.on("hangup", function() {
_setCallState(undefined, call.roomId, "ended");
});
@@ -301,9 +310,10 @@ function _onAction(payload) {
placeCall(call);
}, function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ console.error("Conference call failed: " + err);
Modal.createDialog(ErrorDialog, {
title: "Failed to set up conference call",
- description: "Conference call failed: " + err,
+ description: "Conference call failed.",
});
});
}
diff --git a/src/ContentMessages.js b/src/ContentMessages.js
index 17c8155c1bc..4ab982c98f3 100644
--- a/src/ContentMessages.js
+++ b/src/ContentMessages.js
@@ -276,7 +276,7 @@ class ContentMessages {
sendContentToRoom(file, roomId, matrixClient) {
const content = {
- body: file.name,
+ body: file.name || 'Attachment',
info: {
size: file.size,
}
@@ -316,7 +316,7 @@ class ContentMessages {
}
const upload = {
- fileName: file.name,
+ fileName: file.name || 'Attachment',
roomId: roomId,
total: 0,
loaded: 0,
diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js
index c7b13bc0713..f1420d0a221 100644
--- a/src/HtmlUtils.js
+++ b/src/HtmlUtils.js
@@ -28,6 +28,7 @@ emojione.imagePathSVG = 'emojione/svg/';
emojione.imageType = 'svg';
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
+const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js
* because we want to include emoji shortnames in title text
@@ -57,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;
@@ -87,11 +104,12 @@ var sanitizeHtmlParams = {
// deliberately no h1/h2 to stop people shouting.
'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
- 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre'
+ 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span',
],
allowedAttributes: {
// custom ones first:
- font: ['color'], // custom to matrix
+ font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
+ span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
// We don't currently allow img itself by default, but this
// would make sense if we did
@@ -136,6 +154,38 @@ var sanitizeHtmlParams = {
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
return { tagName: tagName, attribs : attribs };
},
+ '*': function(tagName, attribs) {
+ // Delete any style previously assigned, style is an allowedTag for font and span
+ // because attributes are stripped after transforming
+ delete attribs.style;
+
+ // Sanitise and transform data-mx-color and data-mx-bg-color to their CSS
+ // equivalents
+ const customCSSMapper = {
+ 'data-mx-color': 'color',
+ 'data-mx-bg-color': 'background-color',
+ // $customAttributeKey: $cssAttributeKey
+ };
+
+ let style = "";
+ Object.keys(customCSSMapper).forEach((customAttributeKey) => {
+ const cssAttributeKey = customCSSMapper[customAttributeKey];
+ const customAttributeValue = attribs[customAttributeKey];
+ if (customAttributeValue &&
+ typeof customAttributeValue === 'string' &&
+ COLOR_REGEX.test(customAttributeValue)
+ ) {
+ style += cssAttributeKey + ":" + customAttributeValue + ";";
+ delete attribs[customAttributeKey];
+ }
+ });
+
+ if (style) {
+ attribs.style = style;
+ }
+
+ return { tagName: tagName, attribs: attribs };
+ },
},
};
@@ -290,7 +340,7 @@ export function bodyToHtml(content, highlights, opts) {
}
EMOJI_REGEX.lastIndex = 0;
- let contentBodyTrimmed = content.body.trim();
+ let contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
let match = EMOJI_REGEX.exec(contentBodyTrimmed);
let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
diff --git a/src/Invite.js b/src/Invite.js
index d1f03fe211e..0e8aca2cb55 100644
--- a/src/Invite.js
+++ b/src/Invite.js
@@ -19,8 +19,7 @@ import MultiInviter from './utils/MultiInviter';
const emailRegex = /^\S+@\S+\.\S+$/;
-// We allow localhost for mxids to avoid confusion
-const mxidRegex = /^@\S+:(?:\S+\.\S+|localhost)$/
+const mxidRegex = /^@\S+:\S+$/
export function getAddressType(inputText) {
const isEmailAddress = emailRegex.test(inputText);
diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index 493bbf12aa5..fc8087e12d4 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.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.
@@ -23,6 +24,9 @@ import UserActivity from './UserActivity';
import Presence from './Presence';
import dis from './dispatcher';
import DMRoomMap from './utils/DMRoomMap';
+import RtsClient from './RtsClient';
+import Modal from './Modal';
+import sdk from './index';
/**
* Called at startup, to attempt to build a logged-in Matrix session. It tries
@@ -108,16 +112,17 @@ export function loadSession(opts) {
return q();
}
- if (_restoreFromLocalStorage()) {
- return q();
- }
+ return _restoreFromLocalStorage().then((success) => {
+ if (success) {
+ return;
+ }
- if (enableGuest) {
- return _registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName);
- }
+ if (enableGuest) {
+ return _registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName);
+ }
- // fall back to login screen
- return q();
+ // fall back to login screen
+ });
}
function _loginWithToken(queryParams, defaultDeviceDisplayName) {
@@ -150,7 +155,7 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) {
function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
console.log("Doing guest login on %s", hsUrl);
- // TODO: we should probably de-duplicate this and Signup.Login.loginAsGuest.
+ // TODO: we should probably de-duplicate this and Login.loginAsGuest.
// Not really sure where the right home for it is.
// create a temporary MatrixClient to do the login
@@ -177,10 +182,11 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
});
}
-// returns true if a session is found in localstorage
+// returns a promise which resolves to true if a session is found in
+// localstorage
function _restoreFromLocalStorage() {
if (!localStorage) {
- return false;
+ return q(false);
}
const hs_url = localStorage.getItem("mx_hs_url");
const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
@@ -207,28 +213,60 @@ function _restoreFromLocalStorage() {
identityServerUrl: is_url,
guest: is_guest,
});
- return true;
+ return q(true);
} catch (e) {
- console.log("Unable to restore session", e);
-
- var msg = e.message;
- if (msg == "OLM.BAD_LEGACY_ACCOUNT_PICKLE") {
- msg = "You need to log back in to generate end-to-end encryption keys "
- + "for this device and submit the public key to your homeserver. "
- + "This is a once off; sorry for the inconvenience.";
- }
-
- // don't leak things into the new session
- _clearLocalStorage();
-
- throw new Error("Unable to restore previous session: " + msg);
+ return _handleRestoreFailure(e);
}
} else {
console.log("No previous session found.");
- return false;
+ return q(false);
}
}
+function _handleRestoreFailure(e) {
+ console.log("Unable to restore session", e);
+
+ let msg = e.message;
+ if (msg == "OLM.BAD_LEGACY_ACCOUNT_PICKLE") {
+ msg = "You need to log back in to generate end-to-end encryption keys "
+ + "for this device and submit the public key to your homeserver. "
+ + "This is a once off; sorry for the inconvenience.";
+
+ _clearLocalStorage();
+
+ return q.reject(new Error(
+ "Unable to restore previous session: " + msg,
+ ));
+ }
+
+ const def = q.defer();
+ const SessionRestoreErrorDialog =
+ sdk.getComponent('views.dialogs.SessionRestoreErrorDialog');
+
+ Modal.createDialog(SessionRestoreErrorDialog, {
+ error: msg,
+ onFinished: (success) => {
+ def.resolve(success);
+ },
+ });
+
+ return def.promise.then((success) => {
+ if (success) {
+ // user clicked continue.
+ _clearLocalStorage();
+ return false;
+ }
+
+ // try, try again
+ return _restoreFromLocalStorage();
+ });
+}
+
+let rtsClient = null;
+export function initRtsClient(url) {
+ rtsClient = new RtsClient(url);
+}
+
/**
* Transitions to a logged-in state using the given credentials
* @param {MatrixClientCreds} credentials The credentials to use
@@ -239,6 +277,9 @@ export function setLoggedIn(credentials) {
credentials.userId, credentials.guest,
credentials.homeserverUrl);
+ // Resolves by default
+ let teamPromise = Promise.resolve(null);
+
// persist the session
if (localStorage) {
try {
@@ -261,13 +302,30 @@ export function setLoggedIn(credentials) {
} catch (e) {
console.warn("Error using local storage: can't persist session!", e);
}
+
+ if (rtsClient && !credentials.guest) {
+ teamPromise = rtsClient.login(credentials.userId).then((body) => {
+ if (body.team_token) {
+ localStorage.setItem("mx_team_token", body.team_token);
+ }
+ return body.team_token;
+ });
+ }
} else {
console.warn("No local storage available: can't persist session!");
}
+ // stop any running clients before we create a new one with these new credentials
+ stopMatrixClient();
+
MatrixClientPeg.replaceUsingCreds(credentials);
- dis.dispatch({action: 'on_logged_in'});
+ teamPromise.then((teamToken) => {
+ dis.dispatch({action: 'on_logged_in', teamToken: teamToken});
+ }, (err) => {
+ console.warn("Failed to get team token on login", err);
+ dis.dispatch({action: 'on_logged_in', teamToken: null});
+ });
startMatrixClient();
}
@@ -361,6 +419,7 @@ export function stopMatrixClient() {
if (cli) {
cli.stopClient();
cli.removeAllListeners();
+ cli.store.deleteAllData();
MatrixClientPeg.unset();
}
}
diff --git a/src/Login.js b/src/Login.js
new file mode 100644
index 00000000000..107a8825e94
--- /dev/null
+++ b/src/Login.js
@@ -0,0 +1,205 @@
+/*
+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.
+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 Matrix from "matrix-js-sdk";
+
+import q from 'q';
+import url from 'url';
+
+export default class Login {
+ constructor(hsUrl, isUrl, fallbackHsUrl, opts) {
+ this._hsUrl = hsUrl;
+ this._isUrl = isUrl;
+ this._fallbackHsUrl = fallbackHsUrl;
+ this._currentFlowIndex = 0;
+ this._flows = [];
+ this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
+ }
+
+ getHomeserverUrl() {
+ return this._hsUrl;
+ }
+
+ getIdentityServerUrl() {
+ return this._isUrl;
+ }
+
+ setHomeserverUrl(hsUrl) {
+ this._hsUrl = hsUrl;
+ }
+
+ setIdentityServerUrl(isUrl) {
+ this._isUrl = isUrl;
+ }
+
+ /**
+ * Get a temporary MatrixClient, which can be used for login or register
+ * requests.
+ */
+ _createTemporaryClient() {
+ return Matrix.createClient({
+ baseUrl: this._hsUrl,
+ idBaseUrl: this._isUrl,
+ });
+ }
+
+ getFlows() {
+ var self = this;
+ var client = this._createTemporaryClient();
+ return client.loginFlows().then(function(result) {
+ self._flows = result.flows;
+ self._currentFlowIndex = 0;
+ // technically the UI should display options for all flows for the
+ // user to then choose one, so return all the flows here.
+ return self._flows;
+ });
+ }
+
+ chooseFlow(flowIndex) {
+ this._currentFlowIndex = flowIndex;
+ }
+
+ getCurrentFlowStep() {
+ // technically the flow can have multiple steps, but no one does this
+ // for login so we can ignore it.
+ var flowStep = this._flows[this._currentFlowIndex];
+ return flowStep ? flowStep.type : null;
+ }
+
+ loginAsGuest() {
+ var client = this._createTemporaryClient();
+ return client.registerGuest({
+ body: {
+ initial_device_display_name: this._defaultDeviceDisplayName,
+ },
+ }).then((creds) => {
+ return {
+ userId: creds.user_id,
+ deviceId: creds.device_id,
+ accessToken: creds.access_token,
+ homeserverUrl: this._hsUrl,
+ identityServerUrl: this._isUrl,
+ guest: true
+ };
+ }, (error) => {
+ if (error.httpStatus === 403) {
+ error.friendlyText = "Guest access is disabled on this Home Server.";
+ } else {
+ error.friendlyText = "Failed to register as guest: " + error.data;
+ }
+ throw error;
+ });
+ }
+
+ 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 {
+ identifier = {
+ type: 'm.id.user',
+ user: username,
+ };
+ legacyParams = {
+ user: username,
+ };
+ }
+
+ 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,
+ identityServerUrl: self._isUrl,
+ userId: data.user_id,
+ deviceId: data.device_id,
+ accessToken: data.access_token
+ });
+ }, function(error) {
+ if (error.httpStatus == 400 && loginParams.medium) {
+ error.friendlyText = (
+ 'This Home Server does not support login using email address.'
+ );
+ }
+ else if (error.httpStatus === 403) {
+ error.friendlyText = (
+ 'Incorrect username and/or password.'
+ );
+ if (self._fallbackHsUrl) {
+ var fbClient = Matrix.createClient({
+ baseUrl: self._fallbackHsUrl,
+ idBaseUrl: this._isUrl,
+ });
+
+ return fbClient.login('m.login.password', loginParams).then(function(data) {
+ return q({
+ homeserverUrl: self._fallbackHsUrl,
+ identityServerUrl: self._isUrl,
+ userId: data.user_id,
+ deviceId: data.device_id,
+ accessToken: data.access_token
+ });
+ }, function(fallback_error) {
+ // throw the original error
+ throw error;
+ });
+ }
+ }
+ else {
+ error.friendlyText = (
+ 'There was a problem logging in. (HTTP ' + error.httpStatus + ")"
+ );
+ }
+ throw error;
+ });
+ }
+
+ redirectToCas() {
+ var client = this._createTemporaryClient();
+ var parsedUrl = url.parse(window.location.href, true);
+ parsedUrl.query["homeserver"] = client.getHomeserverUrl();
+ parsedUrl.query["identityServer"] = client.getIdentityServerUrl();
+ var casUrl = client.getCasLoginUrl(url.format(parsedUrl));
+ window.location.href = casUrl;
+ }
+}
diff --git a/src/Markdown.js b/src/Markdown.js
index d6dc979a5a8..4a46ce4f249 100644
--- a/src/Markdown.js
+++ b/src/Markdown.js
@@ -92,7 +92,16 @@ export default class Markdown {
}
toHTML() {
- const renderer = new commonmark.HtmlRenderer({safe: false});
+ const renderer = new commonmark.HtmlRenderer({
+ safe: false,
+
+ // Set soft breaks to hard HTML breaks: commonmark
+ // puts softbreaks in for multiple lines in a blockquote,
+ // so if these are just newline characters then the
+ // block quote ends up all on one line
+ // (https://github.com/vector-im/riot-web/issues/3154)
+ softbreak: '
',
+ });
const real_paragraph = renderer.paragraph;
renderer.paragraph = function(node, entering) {
diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js
index 9c0daf47263..baa32930735 100644
--- a/src/MatrixClientPeg.js
+++ b/src/MatrixClientPeg.js
@@ -16,6 +16,7 @@ limitations under the License.
'use strict';
+import q from "q";
import Matrix from 'matrix-js-sdk';
import utils from 'matrix-js-sdk/lib/utils';
import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline';
@@ -71,7 +72,16 @@ class MatrixClientPeg {
const opts = utils.deepCopy(this.opts);
// the react sdk doesn't work without this, so don't allow
opts.pendingEventOrdering = "detached";
- this.get().startClient(opts);
+
+ let promise = this.matrixClient.store.startup();
+ // log any errors when starting up the database (if one exists)
+ promise.catch((err) => { console.error(err); });
+
+ // regardless of errors, start the client. If we did error out, we'll
+ // just end up doing a full initial /sync.
+ promise.finally(() => {
+ this.get().startClient(opts);
+ });
}
getCredentials(): MatrixClientCreds {
@@ -111,6 +121,17 @@ class MatrixClientPeg {
if (localStorage) {
opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
}
+ if (window.indexedDB && localStorage) {
+ // FIXME: bodge to remove old database. Remove this after a few weeks.
+ window.indexedDB.deleteDatabase("matrix-js-sdk:default");
+
+ opts.store = new Matrix.IndexedDBStore(
+ new Matrix.IndexedDBStoreBackend(window.indexedDB, "riot-web-sync"),
+ new Matrix.SyncAccumulator(), {
+ localStorage: localStorage,
+ }
+ );
+ }
this.matrixClient = Matrix.createClient(opts);
diff --git a/src/Modal.js b/src/Modal.js
index b6cc46ed452..7be37da92ee 100644
--- a/src/Modal.js
+++ b/src/Modal.js
@@ -43,7 +43,13 @@ const AsyncWrapper = React.createClass({
componentWillMount: function() {
this._unmounted = false;
+ // XXX: temporary logging to try to diagnose
+ // https://github.com/vector-im/riot-web/issues/3148
+ console.log('Starting load of AsyncWrapper for modal');
this.props.loader((e) => {
+ // XXX: temporary logging to try to diagnose
+ // https://github.com/vector-im/riot-web/issues/3148
+ console.log('AsyncWrapper load completed with '+e.displayName);
if (this._unmounted) {
return;
}
diff --git a/src/PageTypes.js b/src/PageTypes.js
index b2e2ecf4bc4..d87b363a6f0 100644
--- a/src/PageTypes.js
+++ b/src/PageTypes.js
@@ -16,6 +16,7 @@ limitations under the License.
/** The types of page which can be shown by the LoggedInView */
export default {
+ HomePage: "home_page",
RoomView: "room_view",
UserSettings: "user_settings",
CreateRoom: "create_room",
diff --git a/src/Resend.js b/src/Resend.js
index ad0f58eb9b1..bbd980ea7f3 100644
--- a/src/Resend.js
+++ b/src/Resend.js
@@ -18,23 +18,42 @@ var MatrixClientPeg = require('./MatrixClientPeg');
var dis = require('./dispatcher');
var sdk = require('./index');
var Modal = require('./Modal');
+import { EventStatus } from 'matrix-js-sdk';
module.exports = {
+ resendUnsentEvents: function(room) {
+ room.getPendingEvents().filter(function(ev) {
+ return ev.status === EventStatus.NOT_SENT;
+ }).forEach(function(event) {
+ module.exports.resend(event);
+ });
+ },
+ cancelUnsentEvents: function(room) {
+ room.getPendingEvents().filter(function(ev) {
+ return ev.status === EventStatus.NOT_SENT;
+ }).forEach(function(event) {
+ module.exports.removeFromQueue(event);
+ });
+ },
resend: function(event) {
+ const room = MatrixClientPeg.get().getRoom(event.getRoomId());
MatrixClientPeg.get().resendEvent(
- event, MatrixClientPeg.get().getRoom(event.getRoomId())
+ event, room
).done(function(res) {
dis.dispatch({
action: 'message_sent',
event: event
});
}, function(err) {
+ // XXX: temporary logging to try to diagnose
+ // https://github.com/vector-im/riot-web/issues/3148
+ console.log('Resend got send failure: ' + err.name + '('+err+')');
if (err.name === "UnknownDeviceError") {
- var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
- Modal.createDialog(UnknownDeviceDialog, {
- devices: err.devices,
- room: MatrixClientPeg.get().getRoom(event.getRoomId()),
- }, "mx_Dialog_unknownDevice");
+ dis.dispatch({
+ action: 'unknown_device_error',
+ err: err,
+ room: room,
+ });
}
dis.dispatch({
@@ -43,7 +62,6 @@ module.exports = {
});
});
},
-
removeFromQueue: function(event) {
MatrixClientPeg.get().cancelPendingEvent(event);
dis.dispatch({
diff --git a/src/RtsClient.js b/src/RtsClient.js
index ae62fb8b22f..8c3ce54b375 100644
--- a/src/RtsClient.js
+++ b/src/RtsClient.js
@@ -50,18 +50,18 @@ export default class RtsClient {
* Track a referral with the Riot Team Server. This should be called once a referred
* user has been successfully registered.
* @param {string} referrer the user ID of one who referred the user to Riot.
- * @param {string} userId the user ID of the user being referred.
- * @param {string} userEmail the email address linked to `userId`.
+ * @param {string} sid the sign-up identity server session ID .
+ * @param {string} clientSecret the sign-up client secret.
* @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon
* success.
*/
- trackReferral(referrer, userId, userEmail) {
+ trackReferral(referrer, sid, clientSecret) {
return request(this._url + '/register',
{
body: {
referrer: referrer,
- user_id: userId,
- user_email: userEmail,
+ session_id: sid,
+ client_secret: clientSecret,
},
method: 'POST',
}
@@ -77,4 +77,21 @@ export default class RtsClient {
}
);
}
+
+ /**
+ * Signal to the RTS that a login has occurred and that a user requires their team's
+ * token.
+ * @param {string} userId the user ID of the user who is a member of a team.
+ * @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon
+ * success.
+ */
+ login(userId) {
+ return request(this._url + '/login',
+ {
+ qs: {
+ user_id: userId,
+ },
+ }
+ );
+ }
}
diff --git a/src/Signup.js b/src/Signup.js
deleted file mode 100644
index d3643bd7498..00000000000
--- a/src/Signup.js
+++ /dev/null
@@ -1,461 +0,0 @@
-"use strict";
-
-import Matrix from "matrix-js-sdk";
-
-var MatrixClientPeg = require("./MatrixClientPeg");
-var SignupStages = require("./SignupStages");
-var dis = require("./dispatcher");
-var q = require("q");
-var url = require("url");
-
-const EMAIL_STAGE_TYPE = "m.login.email.identity";
-
-/**
- * A base class for common functionality between Registration and Login e.g.
- * storage of HS/IS URLs.
- */
-class Signup {
- constructor(hsUrl, isUrl, opts) {
- this._hsUrl = hsUrl;
- this._isUrl = isUrl;
- this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
- }
-
- getHomeserverUrl() {
- return this._hsUrl;
- }
-
- getIdentityServerUrl() {
- return this._isUrl;
- }
-
- setHomeserverUrl(hsUrl) {
- this._hsUrl = hsUrl;
- }
-
- setIdentityServerUrl(isUrl) {
- this._isUrl = isUrl;
- }
-
- /**
- * Get a temporary MatrixClient, which can be used for login or register
- * requests.
- */
- _createTemporaryClient() {
- return Matrix.createClient({
- baseUrl: this._hsUrl,
- idBaseUrl: this._isUrl,
- });
- }
-}
-
-/**
- * Registration logic class
- * This exists for the lifetime of a user's attempt to register an account,
- * so if their registration attempt fails for whatever reason and they
- * try again, call register() on the same instance again.
- *
- * TODO: parts of this overlap heavily with InteractiveAuth in the js-sdk. It
- * would be nice to make use of that rather than rolling our own version of it.
- */
-class Register extends Signup {
- constructor(hsUrl, isUrl, opts) {
- super(hsUrl, isUrl, opts);
- this.setStep("START");
- this.data = null; // from the server
- // random other stuff (e.g. query params, NOT params from the server)
- this.params = {};
- this.credentials = null;
- this.activeStage = null;
- this.registrationPromise = null;
- // These values MUST be undefined else we'll send "username: null" which
- // will error on Synapse rather than having the key absent.
- this.username = undefined; // desired
- this.email = undefined; // desired
- this.password = undefined; // desired
- }
-
- setClientSecret(secret) {
- this.params.clientSecret = secret;
- }
-
- setSessionId(sessionId) {
- this.params.sessionId = sessionId;
- }
-
- setRegistrationUrl(regUrl) {
- this.params.registrationUrl = regUrl;
- }
-
- setIdSid(idSid) {
- this.params.idSid = idSid;
- }
-
- setGuestAccessToken(token) {
- this.guestAccessToken = token;
- }
-
- getStep() {
- return this._step;
- }
-
- getCredentials() {
- return this.credentials;
- }
-
- getServerData() {
- return this.data || {};
- }
-
- getPromise() {
- return this.registrationPromise;
- }
-
- setStep(step) {
- this._step = 'Register.' + step;
- // TODO:
- // It's a shame this is going to the global dispatcher, we only really
- // want things which have an instance of this class to be able to add
- // listeners...
- console.log("Dispatching 'registration_step_update' for step %s", this._step);
- dis.dispatch({
- action: "registration_step_update"
- });
- }
-
- /**
- * Starts the registration process from the first stage
- */
- register(formVals) {
- var {username, password, email} = formVals;
- this.email = email;
- this.username = username;
- this.password = password;
- const client = this._createTemporaryClient();
- this.activeStage = null;
-
- // If there hasn't been a client secret set by this point,
- // generate one for this session. It will only be used if
- // we do email verification, but far simpler to just make
- // sure we have one.
- // We re-use this same secret over multiple calls to register
- // so that the identity server can honour the sendAttempt
- // parameter and not re-send email unless we actually want
- // another mail to be sent.
- if (!this.params.clientSecret) {
- this.params.clientSecret = client.generateClientSecret();
- }
- return this._tryRegister(client);
- }
-
- _tryRegister(client, authDict, poll_for_success) {
- var self = this;
-
- var bindEmail;
-
- if (this.username && this.password) {
- // only need to bind_email when sending u/p - sending it at other
- // times clobbers the u/p resulting in M_MISSING_PARAM (password)
- bindEmail = true;
- }
-
- // TODO need to figure out how to send the device display name to /register.
- return client.register(
- this.username, this.password, this.params.sessionId, authDict, bindEmail,
- this.guestAccessToken
- ).then(function(result) {
- self.credentials = result;
- self.setStep("COMPLETE");
- return result; // contains the credentials
- }, function(error) {
- if (error.httpStatus === 401) {
- if (error.data && error.data.flows) {
- // Remember the session ID from the server:
- // Either this is our first 401 in which case we need to store the
- // session ID for future calls, or it isn't in which case this
- // is just a no-op since it ought to be the same (or if it isn't,
- // we should use the latest one from the server in any case).
- self.params.sessionId = error.data.session;
- self.data = error.data || {};
- var flow = self.chooseFlow(error.data.flows);
-
- if (flow) {
- console.log("Active flow => %s", JSON.stringify(flow));
- var flowStage = self.firstUncompletedStage(flow);
- if (!self.activeStage || flowStage != self.activeStage.type) {
- return self._startStage(client, flowStage).catch(function(err) {
- self.setStep('START');
- throw err;
- });
- }
- }
- }
- if (poll_for_success) {
- return q.delay(2000).then(function() {
- return self._tryRegister(client, authDict, poll_for_success);
- });
- } else {
- throw new Error("Authorisation failed!");
- }
- } else {
- if (error.errcode === 'M_USER_IN_USE') {
- throw new Error("Username in use");
- } else if (error.errcode == 'M_INVALID_USERNAME') {
- throw new Error("User names may only contain alphanumeric characters, underscores or dots!");
- } else if (error.httpStatus >= 400 && error.httpStatus < 500) {
- let msg = null;
- if (error.message) {
- msg = error.message;
- } else if (error.errcode) {
- msg = error.errcode;
- }
- if (msg) {
- throw new Error(`Registration failed! (${error.httpStatus}) - ${msg}`);
- } else {
- throw new Error(`Registration failed! (${error.httpStatus}) - That's all we know.`);
- }
- } else if (error.httpStatus >= 500 && error.httpStatus < 600) {
- throw new Error(
- `Server error during registration! (${error.httpStatus})`
- );
- } else if (error.name == "M_MISSING_PARAM") {
- // The HS hasn't remembered the login params from
- // the first try when the login email was sent.
- throw new Error(
- "This home server does not support resuming registration."
- );
- }
- }
- });
- }
-
- firstUncompletedStage(flow) {
- for (var i = 0; i < flow.stages.length; ++i) {
- if (!this.hasCompletedStage(flow.stages[i])) {
- return flow.stages[i];
- }
- }
- }
-
- hasCompletedStage(stageType) {
- var completed = (this.data || {}).completed || [];
- return completed.indexOf(stageType) !== -1;
- }
-
- _startStage(client, stageName) {
- var self = this;
- this.setStep(`STEP_${stageName}`);
- var StageClass = SignupStages[stageName];
- if (!StageClass) {
- // no idea how to handle this!
- throw new Error("Unknown stage: " + stageName);
- }
-
- var stage = new StageClass(client, this);
- this.activeStage = stage;
- return stage.complete().then(function(request) {
- if (request.auth) {
- console.log("Stage %s is returning an auth dict", stageName);
- return self._tryRegister(client, request.auth, request.poll_for_success);
- }
- else {
- // never resolve the promise chain. This is for things like email auth
- // which display a "check your email" message and relies on the
- // link in the email to actually register you.
- console.log("Waiting for external action.");
- return q.defer().promise;
- }
- });
- }
-
- chooseFlow(flows) {
- // If the user gave us an email then we want to pick an email
- // flow we can do, else any other flow.
- var emailFlow = null;
- var otherFlow = null;
- flows.forEach(function(flow) {
- var flowHasEmail = false;
- for (var stageI = 0; stageI < flow.stages.length; ++stageI) {
- var stage = flow.stages[stageI];
-
- if (!SignupStages[stage]) {
- // we can't do this flow, don't have a Stage impl.
- return;
- }
-
- if (stage === EMAIL_STAGE_TYPE) {
- flowHasEmail = true;
- }
- }
-
- if (flowHasEmail) {
- emailFlow = flow;
- } else {
- otherFlow = flow;
- }
- });
-
- if (this.email || this.hasCompletedStage(EMAIL_STAGE_TYPE)) {
- // we've been given an email or we've already done an email part
- return emailFlow;
- } else {
- return otherFlow;
- }
- }
-
- recheckState() {
- // We've been given a bunch of data from a previous register step,
- // this only happens for email auth currently. It's kinda ming we need
- // to know this though. A better solution would be to ask the stages if
- // they are ready to do something rather than accepting that we know about
- // email auth and its internals.
- this.params.hasEmailInfo = (
- this.params.clientSecret && this.params.sessionId && this.params.idSid
- );
-
- if (this.params.hasEmailInfo) {
- const client = this._createTemporaryClient();
- this.registrationPromise = this._startStage(client, EMAIL_STAGE_TYPE);
- }
- return this.registrationPromise;
- }
-
- tellStage(stageName, data) {
- if (this.activeStage && this.activeStage.type === stageName) {
- console.log("Telling stage %s about something..", stageName);
- this.activeStage.onReceiveData(data);
- }
- }
-}
-
-
-class Login extends Signup {
- constructor(hsUrl, isUrl, fallbackHsUrl, opts) {
- super(hsUrl, isUrl, opts);
- this._fallbackHsUrl = fallbackHsUrl;
- this._currentFlowIndex = 0;
- this._flows = [];
- }
-
- getFlows() {
- var self = this;
- var client = this._createTemporaryClient();
- return client.loginFlows().then(function(result) {
- self._flows = result.flows;
- self._currentFlowIndex = 0;
- // technically the UI should display options for all flows for the
- // user to then choose one, so return all the flows here.
- return self._flows;
- });
- }
-
- chooseFlow(flowIndex) {
- this._currentFlowIndex = flowIndex;
- }
-
- getCurrentFlowStep() {
- // technically the flow can have multiple steps, but no one does this
- // for login so we can ignore it.
- var flowStep = this._flows[this._currentFlowIndex];
- return flowStep ? flowStep.type : null;
- }
-
- loginAsGuest() {
- var client = this._createTemporaryClient();
- return client.registerGuest({
- body: {
- initial_device_display_name: this._defaultDeviceDisplayName,
- },
- }).then((creds) => {
- return {
- userId: creds.user_id,
- deviceId: creds.device_id,
- accessToken: creds.access_token,
- homeserverUrl: this._hsUrl,
- identityServerUrl: this._isUrl,
- guest: true
- };
- }, (error) => {
- if (error.httpStatus === 403) {
- error.friendlyText = "Guest access is disabled on this Home Server.";
- } else {
- error.friendlyText = "Failed to register as guest: " + error.data;
- }
- throw error;
- });
- }
-
- 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;
- } else {
- loginParams.user = username;
- }
-
- var client = this._createTemporaryClient();
- return client.login('m.login.password', loginParams).then(function(data) {
- return q({
- homeserverUrl: self._hsUrl,
- identityServerUrl: self._isUrl,
- userId: data.user_id,
- deviceId: data.device_id,
- accessToken: data.access_token
- });
- }, function(error) {
- if (error.httpStatus == 400 && loginParams.medium) {
- error.friendlyText = (
- 'This Home Server does not support login using email address.'
- );
- }
- else if (error.httpStatus === 403) {
- error.friendlyText = (
- 'Incorrect username and/or password.'
- );
- if (self._fallbackHsUrl) {
- var fbClient = Matrix.createClient({
- baseUrl: self._fallbackHsUrl,
- idBaseUrl: this._isUrl,
- });
-
- return fbClient.login('m.login.password', loginParams).then(function(data) {
- return q({
- homeserverUrl: self._fallbackHsUrl,
- identityServerUrl: self._isUrl,
- userId: data.user_id,
- deviceId: data.device_id,
- accessToken: data.access_token
- });
- }, function(fallback_error) {
- // throw the original error
- throw error;
- });
- }
- }
- else {
- error.friendlyText = (
- 'There was a problem logging in. (HTTP ' + error.httpStatus + ")"
- );
- }
- throw error;
- });
- }
-
- redirectToCas() {
- var client = this._createTemporaryClient();
- var parsedUrl = url.parse(window.location.href, true);
- parsedUrl.query["homeserver"] = client.getHomeserverUrl();
- parsedUrl.query["identityServer"] = client.getIdentityServerUrl();
- var casUrl = client.getCasLoginUrl(url.format(parsedUrl));
- window.location.href = casUrl;
- }
-}
-
-module.exports.Register = Register;
-module.exports.Login = Login;
diff --git a/src/SignupStages.js b/src/SignupStages.js
deleted file mode 100644
index 6bdc331566c..00000000000
--- a/src/SignupStages.js
+++ /dev/null
@@ -1,171 +0,0 @@
-"use strict";
-var q = require("q");
-
-/**
- * An interface class which login types should abide by.
- */
-class Stage {
- constructor(type, matrixClient, signupInstance) {
- this.type = type;
- this.client = matrixClient;
- this.signupInstance = signupInstance;
- }
-
- complete() {
- // Return a promise which is:
- // RESOLVED => With an Object which has an 'auth' key which is the auth dict
- // to submit.
- // REJECTED => With an Error if there was a problem with this stage.
- // Has a "message" string and an "isFatal" flag.
- return q.reject("NOT IMPLEMENTED");
- }
-
- onReceiveData() {
- // NOP
- }
-}
-Stage.TYPE = "NOT IMPLEMENTED";
-
-
-/**
- * This stage requires no auth.
- */
-class DummyStage extends Stage {
- constructor(matrixClient, signupInstance) {
- super(DummyStage.TYPE, matrixClient, signupInstance);
- }
-
- complete() {
- return q({
- auth: {
- type: DummyStage.TYPE
- }
- });
- }
-}
-DummyStage.TYPE = "m.login.dummy";
-
-
-/**
- * This stage uses Google's Recaptcha to do auth.
- */
-class RecaptchaStage extends Stage {
- constructor(matrixClient, signupInstance) {
- super(RecaptchaStage.TYPE, matrixClient, signupInstance);
- this.authDict = {
- auth: {
- type: 'm.login.recaptcha',
- // we'll add in the response param if we get one from the local user.
- },
- poll_for_success: true,
- };
- }
-
- // called when the recaptcha has been completed.
- onReceiveData(data) {
- if (!data || !data.response) {
- return;
- }
- this.authDict.auth.response = data.response;
- }
-
- complete() {
- // we return the authDict with no response, telling Signup to keep polling
- // the server in case the captcha is filled in on another window (e.g. by
- // following a nextlink from an email signup). If the user completes the
- // captcha locally, then we return at the next poll.
- return q(this.authDict);
- }
-}
-RecaptchaStage.TYPE = "m.login.recaptcha";
-
-
-/**
- * This state uses the IS to verify email addresses.
- */
-class EmailIdentityStage extends Stage {
- constructor(matrixClient, signupInstance) {
- super(EmailIdentityStage.TYPE, matrixClient, signupInstance);
- }
-
- _completeVerify() {
- // pull out the host of the IS URL by creating an anchor element
- var isLocation = document.createElement('a');
- isLocation.href = this.signupInstance.getIdentityServerUrl();
-
- var clientSecret = this.clientSecret || this.signupInstance.params.clientSecret;
- var sid = this.sid || this.signupInstance.params.idSid;
-
- return q({
- auth: {
- type: 'm.login.email.identity',
- threepid_creds: {
- sid: sid,
- client_secret: clientSecret,
- id_server: isLocation.host
- }
- }
- });
- }
-
- /**
- * Complete the email stage.
- *
- * This is called twice under different circumstances:
- * 1) When requesting an email token from the IS
- * 2) When validating query parameters received from the link in the email
- */
- complete() {
- // TODO: The Registration class shouldn't really know this info.
- if (this.signupInstance.params.hasEmailInfo) {
- return this._completeVerify();
- }
-
- this.clientSecret = this.signupInstance.params.clientSecret;
- if (!this.clientSecret) {
- return q.reject(new Error("No client secret specified by Signup class!"));
- }
-
- var nextLink = this.signupInstance.params.registrationUrl +
- '?client_secret=' +
- encodeURIComponent(this.clientSecret) +
- "&hs_url=" +
- encodeURIComponent(this.signupInstance.getHomeserverUrl()) +
- "&is_url=" +
- encodeURIComponent(this.signupInstance.getIdentityServerUrl()) +
- "&session_id=" +
- encodeURIComponent(this.signupInstance.getServerData().session);
-
- var self = this;
- return this.client.requestRegisterEmailToken(
- this.signupInstance.email,
- this.clientSecret,
- 1, // TODO: Multiple send attempts?
- nextLink
- ).then(function(response) {
- self.sid = response.sid;
- return self._completeVerify();
- }).then(function(request) {
- request.poll_for_success = true;
- return request;
- }, function(error) {
- console.error(error);
- var e = {
- isFatal: true
- };
- if (error.errcode == 'M_THREEPID_IN_USE') {
- e.message = "This email address is already registered";
- } else {
- e.message = 'Unable to contact the given identity server';
- }
- throw e;
- });
- }
-}
-EmailIdentityStage.TYPE = "m.login.email.identity";
-
-module.exports = {
- [DummyStage.TYPE]: DummyStage,
- [RecaptchaStage.TYPE]: RecaptchaStage,
- [EmailIdentityStage.TYPE]: EmailIdentityStage
-};
diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index 3f772e9cfb8..3e1659f3922 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -116,7 +116,6 @@ function textForRoomNameEvent(ev) {
function textForMessageEvent(ev) {
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
-
var message = senderDisplayName + ': ' + ev.getContent().body;
if (ev.getContent().msgtype === "m.emote") {
message = "* " + senderDisplayName + " " + message;
diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js
new file mode 100644
index 00000000000..d842cc3a6e5
--- /dev/null
+++ b/src/UnknownDeviceErrorHandler.js
@@ -0,0 +1,47 @@
+/*
+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 dis from './dispatcher';
+import sdk from './index';
+import Modal from './Modal';
+
+const onAction = function(payload) {
+ if (payload.action === 'unknown_device_error') {
+ var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
+ Modal.createDialog(UnknownDeviceDialog, {
+ devices: payload.err.devices,
+ room: payload.room,
+ onFinished: (r) => {
+ // XXX: temporary logging to try to diagnose
+ // https://github.com/vector-im/riot-web/issues/3148
+ console.log('UnknownDeviceDialog closed with '+r);
+ },
+ }, "mx_Dialog_unknownDevice");
+ }
+}
+
+let ref = null;
+
+export function startListening () {
+ ref = dis.register(onAction);
+}
+
+export function stopListening () {
+ if (ref) {
+ dis.unregister(ref);
+ ref = null;
+ }
+}
diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js
index d7d3e7bc7a2..66a872958c8 100644
--- a/src/UserSettingsStore.js
+++ b/src/UserSettingsStore.js
@@ -26,7 +26,7 @@ var Notifier = require("./Notifier");
module.exports = {
LABS_FEATURES: [
{
- name: 'Rich Text Editor',
+ name: 'New Composer & Autocomplete',
id: 'rich_text_editor',
default: false,
},
diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js
index ecd7c495f93..4502b0ccd99 100644
--- a/src/WhoIsTyping.js
+++ b/src/WhoIsTyping.js
@@ -48,10 +48,11 @@ module.exports = {
return whoIsTyping;
},
- whoIsTypingString: function(room, limit) {
- const whoIsTyping = this.usersTypingApartFromMe(room);
- const othersCount = limit === undefined ?
- 0 : Math.max(whoIsTyping.length - limit, 0);
+ whoIsTypingString: function(whoIsTyping, limit) {
+ let othersCount = 0;
+ if (whoIsTyping.length > limit) {
+ othersCount = whoIsTyping.length - limit + 1;
+ }
if (whoIsTyping.length == 0) {
return '';
} else if (whoIsTyping.length == 1) {
@@ -62,7 +63,7 @@ module.exports = {
});
if (othersCount) {
const other = ' other' + (othersCount > 1 ? 's' : '');
- return names.slice(0, limit).join(', ') + ' and ' +
+ return names.slice(0, limit - 1).join(', ') + ' and ' +
othersCount + other + ' are typing';
} else {
const lastPerson = names.pop();
diff --git a/src/component-index.js b/src/component-index.js
index 5b28be0627c..59d3ad53e48 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -31,6 +31,8 @@ import structures$CreateRoom from './components/structures/CreateRoom';
structures$CreateRoom && (module.exports.components['structures.CreateRoom'] = structures$CreateRoom);
import structures$FilePanel from './components/structures/FilePanel';
structures$FilePanel && (module.exports.components['structures.FilePanel'] = structures$FilePanel);
+import structures$InteractiveAuth from './components/structures/InteractiveAuth';
+structures$InteractiveAuth && (module.exports.components['structures.InteractiveAuth'] = structures$InteractiveAuth);
import structures$LoggedInView from './components/structures/LoggedInView';
structures$LoggedInView && (module.exports.components['structures.LoggedInView'] = structures$LoggedInView);
import structures$MatrixChat from './components/structures/MatrixChat';
@@ -73,8 +75,12 @@ import views$create_room$RoomAlias from './components/views/create_room/RoomAlia
views$create_room$RoomAlias && (module.exports.components['views.create_room.RoomAlias'] = views$create_room$RoomAlias);
import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog';
views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog);
+import views$dialogs$ChatCreateOrReuseDialog from './components/views/dialogs/ChatCreateOrReuseDialog';
+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$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';
views$dialogs$DeactivateAccountDialog && (module.exports.components['views.dialogs.DeactivateAccountDialog'] = views$dialogs$DeactivateAccountDialog);
import views$dialogs$ErrorDialog from './components/views/dialogs/ErrorDialog';
@@ -85,6 +91,8 @@ import views$dialogs$NeedToRegisterDialog from './components/views/dialogs/NeedT
views$dialogs$NeedToRegisterDialog && (module.exports.components['views.dialogs.NeedToRegisterDialog'] = views$dialogs$NeedToRegisterDialog);
import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDialog';
views$dialogs$QuestionDialog && (module.exports.components['views.dialogs.QuestionDialog'] = views$dialogs$QuestionDialog);
+import views$dialogs$SessionRestoreErrorDialog from './components/views/dialogs/SessionRestoreErrorDialog';
+views$dialogs$SessionRestoreErrorDialog && (module.exports.components['views.dialogs.SessionRestoreErrorDialog'] = views$dialogs$SessionRestoreErrorDialog);
import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDisplayNameDialog';
views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog);
import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog';
@@ -101,6 +109,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';
@@ -123,6 +133,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';
diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js
new file mode 100644
index 00000000000..71fee883bea
--- /dev/null
+++ b/src/components/structures/InteractiveAuth.js
@@ -0,0 +1,219 @@
+/*
+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 Matrix from 'matrix-js-sdk';
+const InteractiveAuth = Matrix.InteractiveAuth;
+
+import React from 'react';
+
+import sdk from '../../index';
+
+import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents';
+
+export default React.createClass({
+ displayName: 'InteractiveAuth',
+
+ propTypes: {
+ // matrix client to use for UI auth requests
+ matrixClient: React.PropTypes.object.isRequired,
+
+ // response from initial request. If not supplied, will do a request on
+ // mount.
+ authData: React.PropTypes.shape({
+ flows: React.PropTypes.array,
+ params: React.PropTypes.object,
+ session: React.PropTypes.string,
+ }),
+
+ // callback
+ makeRequest: React.PropTypes.func.isRequired,
+
+ // callback called when the auth process has finished,
+ // successfully or unsuccessfully.
+ // @param {bool} status True if the operation requiring
+ // auth was completed sucessfully, false if canceled.
+ // @param {object} result The result of the authenticated call
+ // if successful, otherwise the error object
+ // @param {object} extra Additional information about the UI Auth
+ // process:
+ // * emailSid {string} If email auth was performed, the sid of
+ // the auth session.
+ // * clientSecret {string} The client secret used in auth
+ // sessions with the ID server.
+ onAuthFinished: React.PropTypes.func.isRequired,
+
+ // Inputs provided by the user to the auth process
+ // and used by various stages. As passed to js-sdk
+ // interactive-auth
+ inputs: React.PropTypes.object,
+
+ // As js-sdk interactive-auth
+ makeRegistrationUrl: React.PropTypes.func,
+ sessionId: React.PropTypes.string,
+ clientSecret: React.PropTypes.string,
+ emailSid: React.PropTypes.string,
+
+ // If true, poll to see if the auth flow has been completed
+ // out-of-band
+ poll: React.PropTypes.bool,
+ },
+
+ getInitialState: function() {
+ return {
+ authStage: null,
+ busy: false,
+ errorText: null,
+ stageErrorText: null,
+ submitButtonEnabled: false,
+ };
+ },
+
+ componentWillMount: function() {
+ this._unmounted = false;
+ this._authLogic = new InteractiveAuth({
+ authData: this.props.authData,
+ doRequest: this._requestCallback,
+ inputs: this.props.inputs,
+ stateUpdated: this._authStateUpdated,
+ matrixClient: this.props.matrixClient,
+ sessionId: this.props.sessionId,
+ clientSecret: this.props.clientSecret,
+ emailSid: this.props.emailSid,
+ });
+
+ this._authLogic.attemptAuth().then((result) => {
+ const extra = {
+ emailSid: this._authLogic.getEmailSid(),
+ clientSecret: this._authLogic.getClientSecret(),
+ };
+ this.props.onAuthFinished(true, result, extra);
+ }).catch((error) => {
+ this.props.onAuthFinished(false, error);
+ console.error("Error during user-interactive auth:", error);
+ if (this._unmounted) {
+ return;
+ }
+
+ const msg = error.message || error.toString();
+ this.setState({
+ errorText: msg
+ });
+ }).done();
+
+ this._intervalId = null;
+ if (this.props.poll) {
+ this._intervalId = setInterval(() => {
+ this._authLogic.poll();
+ }, 2000);
+ }
+ },
+
+ componentWillUnmount: function() {
+ this._unmounted = true;
+
+ if (this._intervalId !== null) {
+ clearInterval(this._intervalId);
+ }
+ },
+
+ _authStateUpdated: function(stageType, stageState) {
+ const oldStage = this.state.authStage;
+ this.setState({
+ authStage: stageType,
+ stageState: stageState,
+ errorText: stageState.error,
+ }, () => {
+ if (oldStage != stageType) this._setFocus();
+ });
+ },
+
+ _requestCallback: function(auth) {
+ this.setState({
+ busy: true,
+ errorText: null,
+ stageErrorText: null,
+ });
+ return this.props.makeRequest(auth).finally(() => {
+ if (this._unmounted) {
+ return;
+ }
+ this.setState({
+ busy: false,
+ });
+ });
+ },
+
+ _setFocus: function() {
+ if (this.refs.stageComponent && this.refs.stageComponent.focus) {
+ this.refs.stageComponent.focus();
+ }
+ },
+
+ _submitAuthDict: function(authData) {
+ this._authLogic.submitAuthDict(authData);
+ },
+
+ _renderCurrentStage: function() {
+ const stage = this.state.authStage;
+ if (!stage) return null;
+
+ const StageComponent = getEntryComponentForLoginType(stage);
+ return (
+
End-to-end encryption is in beta and may not be reliable.
+You should not yet trust it to secure data.
+Devices will not yet be able to decrypt history from before they joined the room.
+Encrypted messages will not be visible on clients that do not yet implement encryption.
+{deviceId}{identityKey}This operation requires additional authentication.
- {this._renderCurrentStage()} - {error} -Otherwise, + click here to send a bug report. +
+ ); + } + + return ( +We encountered an error trying to restore your previous session. If + you continue, you will need to log in again, and encrypted chat + history will be unreadable.
+ +If you have previously used a more recent version of Riot, your session + may be incompatible with this version. Close this window and return + to the more recent version.
+ + {bugreport} +- This means there is no guarantee that the devices - belong to the users they claim to. -
We recommend you go through the verification process - for each device before continuing, but you can resend - the message without verifying if you prefer. + for each device to confirm they belong to their legitimate owner, + but you can resend the message without verifying if you prefer.
To continue, please enter your password.
Password:
- +An email has been sent to {this.props.inputs.emailAddress}
+Please check your email to continue registration.
+A text message has been sent to +{this._msisdn}
+Please enter the code it contains:
+