diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000..880331a09e
--- /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/CHANGELOG.md b/CHANGELOG.md
index 488a9814e6..292e60607d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,249 @@
+Changes in [0.8.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7) (2017-04-12)
+===================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.4...v0.8.7)
+
+ * No changes
+
+Changes in [0.8.7-rc.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7-rc.4) (2017-04-11)
+=============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.3...v0.8.7-rc.4)
+
+ * Fix people section vanishing on 'clear cache'
+ [\#799](https://github.com/matrix-org/matrix-react-sdk/pull/799)
+ * Make the clear cache button work on desktop
+ [\#798](https://github.com/matrix-org/matrix-react-sdk/pull/798)
+
+Changes in [0.8.7-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7-rc.3) (2017-04-10)
+=============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.2...v0.8.7-rc.3)
+
+ * Use matrix-js-sdk v0.7.6-rc.2
+
+
+Changes in [0.8.7-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7-rc.2) (2017-04-10)
+=============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.1...v0.8.7-rc.2)
+
+ * fix the warning shown to users about needing to export e2e keys
+ [\#797](https://github.com/matrix-org/matrix-react-sdk/pull/797)
+
+Changes in [0.8.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7-rc.1) (2017-04-07)
+=============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.6...v0.8.7-rc.1)
+
+ * Add support for using indexeddb in a webworker
+ [\#792](https://github.com/matrix-org/matrix-react-sdk/pull/792)
+ * Fix infinite pagination/glitches with pagination
+ [\#795](https://github.com/matrix-org/matrix-react-sdk/pull/795)
+ * Fix issue where teamTokenMap was ignored for guests
+ [\#793](https://github.com/matrix-org/matrix-react-sdk/pull/793)
+ * Click emote sender -> insert display name into composer
+ [\#791](https://github.com/matrix-org/matrix-react-sdk/pull/791)
+ * Fix scroll token selection logic
+ [\#785](https://github.com/matrix-org/matrix-react-sdk/pull/785)
+ * Replace sdkReady with firstSyncPromise, add mx_last_room_id
+ [\#790](https://github.com/matrix-org/matrix-react-sdk/pull/790)
+ * Change "Unread messages." to "Jump to first unread message."
+ [\#789](https://github.com/matrix-org/matrix-react-sdk/pull/789)
+ * Update for new IndexedDBStore interface
+ [\#786](https://github.com/matrix-org/matrix-react-sdk/pull/786)
+ * Add
to allowed attributes list
+ [\#787](https://github.com/matrix-org/matrix-react-sdk/pull/787)
+ * Fix the onFinished for timeline pos dialog
+ [\#784](https://github.com/matrix-org/matrix-react-sdk/pull/784)
+ * Only join a room when enter is hit if the join button is shown
+ [\#776](https://github.com/matrix-org/matrix-react-sdk/pull/776)
+ * Remove non-functional session load error
+ [\#783](https://github.com/matrix-org/matrix-react-sdk/pull/783)
+ * Use Login & Register via component interface
+ [\#782](https://github.com/matrix-org/matrix-react-sdk/pull/782)
+ * Attempt to fix the flakyness seen with tests
+ [\#781](https://github.com/matrix-org/matrix-react-sdk/pull/781)
+ * Remove React warning
+ [\#780](https://github.com/matrix-org/matrix-react-sdk/pull/780)
+ * Only clear the local notification count if needed
+ [\#779](https://github.com/matrix-org/matrix-react-sdk/pull/779)
+ * Don't re-notify about messages on browser refresh
+ [\#777](https://github.com/matrix-org/matrix-react-sdk/pull/777)
+ * Improve zeroing of RoomList notification badges
+ [\#775](https://github.com/matrix-org/matrix-react-sdk/pull/775)
+ * Fix VOIP bar hidden on first render of RoomStatusBar
+ [\#774](https://github.com/matrix-org/matrix-react-sdk/pull/774)
+ * Correct confirm prompt for disinvite
+ [\#772](https://github.com/matrix-org/matrix-react-sdk/pull/772)
+ * Add state loggingIn to MatrixChat to fix flashing login
+ [\#773](https://github.com/matrix-org/matrix-react-sdk/pull/773)
+ * Fix bug where you can't invite a valid address
+ [\#771](https://github.com/matrix-org/matrix-react-sdk/pull/771)
+ * Fix people section DropTarget and refactor Rooms
+ [\#761](https://github.com/matrix-org/matrix-react-sdk/pull/761)
+ * Read Receipt offset
+ [\#770](https://github.com/matrix-org/matrix-react-sdk/pull/770)
+ * Support adding phone numbers in UserSettings
+ [\#756](https://github.com/matrix-org/matrix-react-sdk/pull/756)
+ * Prevent crash on login of no guest session
+ [\#769](https://github.com/matrix-org/matrix-react-sdk/pull/769)
+ * Add canResetTimeline callback and thread it through to TimelinePanel
+ [\#768](https://github.com/matrix-org/matrix-react-sdk/pull/768)
+ * Show spinner whilst processing recaptcha response
+ [\#767](https://github.com/matrix-org/matrix-react-sdk/pull/767)
+ * Login / registration with phone number, mark 2
+ [\#750](https://github.com/matrix-org/matrix-react-sdk/pull/750)
+ * Display threepids slightly prettier
+ [\#758](https://github.com/matrix-org/matrix-react-sdk/pull/758)
+ * Fix extraneous leading space in sent emotes
+ [\#764](https://github.com/matrix-org/matrix-react-sdk/pull/764)
+ * Add ConfirmRedactDialog component
+ [\#763](https://github.com/matrix-org/matrix-react-sdk/pull/763)
+ * Fix password UI auth test
+ [\#760](https://github.com/matrix-org/matrix-react-sdk/pull/760)
+ * Display timestamps and profiles for redacted events
+ [\#759](https://github.com/matrix-org/matrix-react-sdk/pull/759)
+ * Fix UDD for voip in e2e rooms
+ [\#757](https://github.com/matrix-org/matrix-react-sdk/pull/757)
+ * Add "Export E2E keys" option to logout dialog
+ [\#755](https://github.com/matrix-org/matrix-react-sdk/pull/755)
+ * Fix People section a bit
+ [\#754](https://github.com/matrix-org/matrix-react-sdk/pull/754)
+ * Do routing to /register _onLoadCompleted
+ [\#753](https://github.com/matrix-org/matrix-react-sdk/pull/753)
+ * Double UNPAGINATION_PADDING again
+ [\#747](https://github.com/matrix-org/matrix-react-sdk/pull/747)
+ * Add null check to start_login
+ [\#751](https://github.com/matrix-org/matrix-react-sdk/pull/751)
+ * Merge the two RoomTile context menus into one
+ [\#746](https://github.com/matrix-org/matrix-react-sdk/pull/746)
+ * Fix import for Lifecycle
+ [\#748](https://github.com/matrix-org/matrix-react-sdk/pull/748)
+ * Make UDD appear when UDE on uploading a file
+ [\#745](https://github.com/matrix-org/matrix-react-sdk/pull/745)
+ * Decide on which screen to show after login in one place
+ [\#743](https://github.com/matrix-org/matrix-react-sdk/pull/743)
+ * Add onClick to permalinks to route within Riot
+ [\#744](https://github.com/matrix-org/matrix-react-sdk/pull/744)
+ * Add support for pasting files into the text box
+ [\#605](https://github.com/matrix-org/matrix-react-sdk/pull/605)
+ * Show message redactions as black event tiles
+ [\#739](https://github.com/matrix-org/matrix-react-sdk/pull/739)
+ * Allow user to choose from existing DMs on new chat
+ [\#736](https://github.com/matrix-org/matrix-react-sdk/pull/736)
+ * Fix the team server registration
+ [\#741](https://github.com/matrix-org/matrix-react-sdk/pull/741)
+ * Clarify "No devices" message
+ [\#740](https://github.com/matrix-org/matrix-react-sdk/pull/740)
+ * Change timestamp permalinks to matrix.to
+ [\#735](https://github.com/matrix-org/matrix-react-sdk/pull/735)
+ * Fix resend bar and "send anyway" in UDD
+ [\#734](https://github.com/matrix-org/matrix-react-sdk/pull/734)
+ * Make COLOR_REGEX stricter
+ [\#737](https://github.com/matrix-org/matrix-react-sdk/pull/737)
+ * Port registration over to use InteractiveAuth
+ [\#729](https://github.com/matrix-org/matrix-react-sdk/pull/729)
+ * Test to see how fuse feels
+ [\#732](https://github.com/matrix-org/matrix-react-sdk/pull/732)
+ * Submit a new display name on blur of input field
+ [\#733](https://github.com/matrix-org/matrix-react-sdk/pull/733)
+ * Allow [bf]g colors for style attrib
+ [\#610](https://github.com/matrix-org/matrix-react-sdk/pull/610)
+ * MELS: either expanded or summary, not both
+ [\#683](https://github.com/matrix-org/matrix-react-sdk/pull/683)
+ * Autoplay videos and GIFs if enabled by the user.
+ [\#730](https://github.com/matrix-org/matrix-react-sdk/pull/730)
+ * Warn users about using e2e for the first time
+ [\#731](https://github.com/matrix-org/matrix-react-sdk/pull/731)
+ * Show UDDialog on UDE during VoIP calls
+ [\#721](https://github.com/matrix-org/matrix-react-sdk/pull/721)
+ * Notify MatrixChat of teamToken after login
+ [\#726](https://github.com/matrix-org/matrix-react-sdk/pull/726)
+ * Fix a couple of issues with RRs
+ [\#727](https://github.com/matrix-org/matrix-react-sdk/pull/727)
+ * Do not push a dummy element with a scroll token for invisible events
+ [\#718](https://github.com/matrix-org/matrix-react-sdk/pull/718)
+ * MELS: check scroll on load + use mels-1,-2,... key
+ [\#715](https://github.com/matrix-org/matrix-react-sdk/pull/715)
+ * Fix message composer placeholders
+ [\#723](https://github.com/matrix-org/matrix-react-sdk/pull/723)
+ * Clarify non-e2e vs. e2e /w composers placeholder
+ [\#720](https://github.com/matrix-org/matrix-react-sdk/pull/720)
+ * Fix status bar expanded on tab-complete
+ [\#722](https://github.com/matrix-org/matrix-react-sdk/pull/722)
+ * add .editorconfig
+ [\#713](https://github.com/matrix-org/matrix-react-sdk/pull/713)
+ * Change the name of the database
+ [\#719](https://github.com/matrix-org/matrix-react-sdk/pull/719)
+ * Allow setting the default HS from the query parameter
+ [\#716](https://github.com/matrix-org/matrix-react-sdk/pull/716)
+ * first cut of improving UX for deleting devices.
+ [\#717](https://github.com/matrix-org/matrix-react-sdk/pull/717)
+ * Fix block quotes all being on a single line
+ [\#711](https://github.com/matrix-org/matrix-react-sdk/pull/711)
+ * Support reasons for kick / ban
+ [\#710](https://github.com/matrix-org/matrix-react-sdk/pull/710)
+ * Show when you've been kicked or banned
+ [\#709](https://github.com/matrix-org/matrix-react-sdk/pull/709)
+ * Add a 'Clear Cache' button
+ [\#708](https://github.com/matrix-org/matrix-react-sdk/pull/708)
+ * Update the room view on room name change
+ [\#707](https://github.com/matrix-org/matrix-react-sdk/pull/707)
+ * Add a button to un-ban users in RoomSettings
+ [\#698](https://github.com/matrix-org/matrix-react-sdk/pull/698)
+ * Use IndexedDBStore from the JS-SDK
+ [\#687](https://github.com/matrix-org/matrix-react-sdk/pull/687)
+ * Make UserSettings use the right teamToken
+ [\#706](https://github.com/matrix-org/matrix-react-sdk/pull/706)
+ * If the home page is somehow accessed, goto directory
+ [\#705](https://github.com/matrix-org/matrix-react-sdk/pull/705)
+ * Display avatar initials in typing notifications
+ [\#699](https://github.com/matrix-org/matrix-react-sdk/pull/699)
+ * fix eslint's no-invalid-this rule for class properties
+ [\#703](https://github.com/matrix-org/matrix-react-sdk/pull/703)
+ * If a referrer hasn't been specified, use empty string
+ [\#701](https://github.com/matrix-org/matrix-react-sdk/pull/701)
+ * Don't force-logout the user if reading localstorage fails
+ [\#700](https://github.com/matrix-org/matrix-react-sdk/pull/700)
+ * Convert some missed buttons to AccessibleButton
+ [\#697](https://github.com/matrix-org/matrix-react-sdk/pull/697)
+ * Make ban either ban or unban
+ [\#696](https://github.com/matrix-org/matrix-react-sdk/pull/696)
+ * Add confirmation dialog to kick/ban buttons
+ [\#694](https://github.com/matrix-org/matrix-react-sdk/pull/694)
+ * Fix typo with Scalar popup
+ [\#695](https://github.com/matrix-org/matrix-react-sdk/pull/695)
+ * Treat the literal team token string "undefined" as undefined
+ [\#693](https://github.com/matrix-org/matrix-react-sdk/pull/693)
+ * Store retrieved sid in the signupInstance of EmailIdentityStage
+ [\#692](https://github.com/matrix-org/matrix-react-sdk/pull/692)
+ * Split out InterActiveAuthDialog
+ [\#691](https://github.com/matrix-org/matrix-react-sdk/pull/691)
+ * View /home on registered /w team
+ [\#689](https://github.com/matrix-org/matrix-react-sdk/pull/689)
+ * Instead of sending userId, userEmail, send sid, client_secret
+ [\#688](https://github.com/matrix-org/matrix-react-sdk/pull/688)
+ * Enable branded URLs again by parsing the path client-side
+ [\#686](https://github.com/matrix-org/matrix-react-sdk/pull/686)
+ * Use new method of getting team icon
+ [\#680](https://github.com/matrix-org/matrix-react-sdk/pull/680)
+ * Persist query parameter team token across refreshes
+ [\#685](https://github.com/matrix-org/matrix-react-sdk/pull/685)
+ * Thread teamToken through to LeftPanel for "Home" button
+ [\#684](https://github.com/matrix-org/matrix-react-sdk/pull/684)
+ * Fix typing notif and status bar
+ [\#682](https://github.com/matrix-org/matrix-react-sdk/pull/682)
+ * Consider emails ending in matrix.org as a uni email
+ [\#681](https://github.com/matrix-org/matrix-react-sdk/pull/681)
+ * Set referrer qp in nextLink
+ [\#679](https://github.com/matrix-org/matrix-react-sdk/pull/679)
+ * Do not set team_token if not returned by RTS on login
+ [\#678](https://github.com/matrix-org/matrix-react-sdk/pull/678)
+ * Get team_token from the RTS on login
+ [\#676](https://github.com/matrix-org/matrix-react-sdk/pull/676)
+ * Quick and dirty support for custom welcome pages
+ [\#550](https://github.com/matrix-org/matrix-react-sdk/pull/550)
+ * RTS Welcome Pages
+ [\#666](https://github.com/matrix-org/matrix-react-sdk/pull/666)
+ * Logging to try to track down riot-web#3148
+ [\#677](https://github.com/matrix-org/matrix-react-sdk/pull/677)
+
Changes in [0.8.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.6) (2017-02-04)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.6-rc.3...v0.8.6)
diff --git a/package.json b/package.json
index 9b260e341a..30eed9a59d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "0.8.6",
+ "version": "0.8.7",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
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/BasePlatform.js b/src/BasePlatform.js
index 8bdf7d0391..6eed22f436 100644
--- a/src/BasePlatform.js
+++ b/src/BasePlatform.js
@@ -82,4 +82,12 @@ export default class BasePlatform {
screenCaptureErrorString() {
return "Not implemented";
}
+
+ /**
+ * Restarts the application, without neccessarily reloading
+ * any application code
+ */
+ reload() {
+ throw new Error("reload not implemented!");
+ }
}
diff --git a/src/CallHandler.js b/src/CallHandler.js
index 268a599d8e..42cc681d08 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 17c8155c1b..4ab982c98f 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 8ae2c0a4a8..5fe3fb890e 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,15 +104,17 @@ 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', 'img',
+ 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img',
],
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
img: ['src'],
+ ol: ['start'],
},
// Lots of these won't come up by default because we don't allow them
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
@@ -136,6 +155,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 };
+ },
},
};
diff --git a/src/Invite.js b/src/Invite.js
index d1f03fe211..0e8aca2cb5 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 295ffe44f4..f20716cae6 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -155,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
@@ -276,6 +276,14 @@ export function setLoggedIn(credentials) {
console.log("setLoggedIn => %s (guest=%s) hs=%s",
credentials.userId, credentials.guest,
credentials.homeserverUrl);
+ // This is dispatched to indicate that the user is still in the process of logging in
+ // because `teamPromise` may take some time to resolve, breaking the assumption that
+ // `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms
+ // later than MatrixChat might assume.
+ dis.dispatch({action: 'on_logging_in'});
+
+ // Resolves by default
+ let teamPromise = Promise.resolve(null);
// persist the session
if (localStorage) {
@@ -300,25 +308,29 @@ export function setLoggedIn(credentials) {
console.warn("Error using local storage: can't persist session!", e);
}
- if (rtsClient) {
- rtsClient.login(credentials.userId).then((body) => {
+ if (rtsClient && !credentials.guest) {
+ teamPromise = rtsClient.login(credentials.userId).then((body) => {
if (body.team_token) {
localStorage.setItem("mx_team_token", body.team_token);
}
- }, (err) =>{
- console.error(
- "Failed to get team token on login, not persisting to localStorage",
- err
- );
+ 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();
}
diff --git a/src/Login.js b/src/Login.js
new file mode 100644
index 0000000000..107a8825e9
--- /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 d6dc979a5a..4a46ce4f24 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 3759aa72c1..452b67c4ee 100644
--- a/src/MatrixClientPeg.js
+++ b/src/MatrixClientPeg.js
@@ -50,6 +50,18 @@ class MatrixClientPeg {
this.opts = {
initialSyncLimit: 20,
};
+ this.indexedDbWorkerScript = null;
+ }
+
+ /**
+ * Sets the script href passed to the IndexedDB web worker
+ * If set, a separate web worker will be started to run the IndexedDB
+ * queries on.
+ *
+ * @param {string} script href to the script to be passed to the web worker
+ */
+ setIndexedDbWorkerScript(script) {
+ this.indexedDbWorkerScript = script;
}
get(): MatrixClient {
@@ -122,12 +134,15 @@ class MatrixClientPeg {
opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
}
if (window.indexedDB && localStorage) {
- opts.store = new Matrix.IndexedDBStore(
- new Matrix.IndexedDBStoreBackend(window.indexedDB),
- new Matrix.SyncAccumulator(), {
- localStorage: 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({
+ indexedDB: window.indexedDB,
+ dbName: "riot-web-sync",
+ localStorage: localStorage,
+ workerScript: this.indexedDbWorkerScript,
+ });
}
this.matrixClient = Matrix.createClient(opts);
diff --git a/src/Notifier.js b/src/Notifier.js
index 67642e734a..92770877b7 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');
@@ -99,16 +98,16 @@ var Notifier = {
MatrixClientPeg.get().on("Room.receipt", this.boundOnRoomReceipt);
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
this.toolbarHidden = false;
- this.isPrepared = false;
+ this.isSyncing = false;
},
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);
}
- this.isPrepared = false;
+ this.isSyncing = false;
},
supportsDesktopNotifications: function() {
@@ -214,18 +213,18 @@ var Notifier = {
},
onSyncStateChange: function(state) {
- if (state === "PREPARED" || state === "SYNCING") {
- this.isPrepared = true;
+ if (state === "SYNCING") {
+ this.isSyncing = true;
}
else if (state === "STOPPED" || state === "ERROR") {
- this.isPrepared = false;
+ this.isSyncing = false;
}
},
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
if (toStartOfTimeline) return;
if (!room) return;
- if (!this.isPrepared) return; // don't alert for any messages initially
+ if (!this.isSyncing) return; // don't alert for any messages initially
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return;
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
diff --git a/src/Resend.js b/src/Resend.js
index e2f0c5a1ee..bbd980ea7f 100644
--- a/src/Resend.js
+++ b/src/Resend.js
@@ -18,11 +18,27 @@ 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',
@@ -33,16 +49,11 @@ module.exports = {
// 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()),
- 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");
+ dis.dispatch({
+ action: 'unknown_device_error',
+ err: err,
+ room: room,
+ });
}
dis.dispatch({
@@ -51,7 +62,6 @@ module.exports = {
});
});
},
-
removeFromQueue: function(event) {
MatrixClientPeg.get().cancelPendingEvent(event);
dis.dispatch({
diff --git a/src/Roles.js b/src/Roles.js
new file mode 100644
index 0000000000..cef8670aad
--- /dev/null
+++ b/src/Roles.js
@@ -0,0 +1,29 @@
+/*
+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.
+*/
+export const LEVEL_ROLE_MAP = {
+ undefined: 'Default',
+ 0: 'User',
+ 50: 'Moderator',
+ 100: 'Admin',
+};
+
+export function textualPowerLevel(level, userDefault) {
+ if (LEVEL_ROLE_MAP[level]) {
+ return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`);
+ } else {
+ return level;
+ }
+}
diff --git a/src/Rooms.js b/src/Rooms.js
index fbcc843ad2..08fa7f797f 100644
--- a/src/Rooms.js
+++ b/src/Rooms.js
@@ -79,6 +79,20 @@ export function looksLikeDirectMessageRoom(room, me) {
return false;
}
+export function guessAndSetDMRoom(room, isDirect) {
+ let newTarget;
+ if (isDirect) {
+ const guessedTarget = guessDMRoomTarget(
+ room, room.getMember(MatrixClientPeg.get().credentials.userId),
+ );
+ newTarget = guessedTarget.userId;
+ } else {
+ newTarget = null;
+ }
+
+ return setDMRoom(room.roomId, newTarget);
+}
+
/**
* Marks or unmarks the given room as being as a DM room.
* @param {string} roomId The ID of the room to modify
diff --git a/src/Signup.js b/src/Signup.js
deleted file mode 100644
index 022a93524c..0000000000
--- a/src/Signup.js
+++ /dev/null
@@ -1,465 +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;
- }
-
- setReferrer(referrer) {
- this.params.referrer = referrer;
- }
-
- 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 1441682c85..0000000000
--- a/src/SignupStages.js
+++ /dev/null
@@ -1,177 +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);
-
- // Add the user ID of the referring user, if set
- if (this.signupInstance.params.referrer) {
- nextLink += "&referrer=" + encodeURIComponent(this.signupInstance.params.referrer);
- }
-
- 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;
- self.signupInstance.setIdSid(self.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 3f772e9cfb..40d6a49998 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -17,6 +17,8 @@ limitations under the License.
var MatrixClientPeg = require("./MatrixClientPeg");
var CallHandler = require("./CallHandler");
+import * as Roles from './Roles';
+
function textForMemberEvent(ev) {
// XXX: SYJS-16 "sender is sometimes null for join messages"
var senderName = ev.sender ? ev.sender.name : ev.getSender();
@@ -116,7 +118,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;
@@ -183,6 +184,45 @@ function textForEncryptionEvent(event) {
return senderName + " turned on end-to-end encryption (algorithm " + event.getContent().algorithm + ")";
}
+// Currently will only display a change if a user's power level is changed
+function textForPowerEvent(event) {
+ const senderName = event.sender ? event.sender.name : event.getSender();
+ if (!event.getPrevContent() || !event.getPrevContent().users) {
+ return '';
+ }
+ const userDefault = event.getContent().users_default || 0;
+ // Construct set of userIds
+ let users = [];
+ Object.keys(event.getContent().users).forEach(
+ (userId) => {
+ if (users.indexOf(userId) === -1) users.push(userId);
+ }
+ );
+ Object.keys(event.getPrevContent().users).forEach(
+ (userId) => {
+ if (users.indexOf(userId) === -1) users.push(userId);
+ }
+ );
+ let diff = [];
+ users.forEach((userId) => {
+ // Previous power level
+ const from = event.getPrevContent().users[userId];
+ // Current power level
+ const to = event.getContent().users[userId];
+ if (to !== from) {
+ diff.push(
+ userId +
+ ' from ' + Roles.textualPowerLevel(from, userDefault) +
+ ' to ' + Roles.textualPowerLevel(to, userDefault)
+ );
+ }
+ });
+ if (!diff.length) {
+ return '';
+ }
+ return senderName + ' changed the power level of ' + diff.join(', ');
+}
+
var handlers = {
'm.room.message': textForMessageEvent,
'm.room.name': textForRoomNameEvent,
@@ -194,6 +234,7 @@ var handlers = {
'm.room.third_party_invite': textForThreePidInviteEvent,
'm.room.history_visibility': textForHistoryVisibilityEvent,
'm.room.encryption': textForEncryptionEvent,
+ 'm.room.power_levels': textForPowerEvent,
};
module.exports = {
diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js
new file mode 100644
index 0000000000..2aa0573e22
--- /dev/null
+++ b/src/UnknownDeviceErrorHandler.js
@@ -0,0 +1,51 @@
+/*
+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';
+
+let isDialogOpen = false;
+
+const onAction = function(payload) {
+ if (payload.action === 'unknown_device_error' && !isDialogOpen) {
+ var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
+ isDialogOpen = true;
+ Modal.createDialog(UnknownDeviceDialog, {
+ devices: payload.err.devices,
+ room: payload.room,
+ onFinished: (r) => {
+ isDialogOpen = false;
+ // 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/component-index.js b/src/component-index.js
index c705150e12..d6873c6dfd 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -75,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$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';
@@ -107,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';
@@ -129,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';
@@ -221,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 70b3c2e306..7c8a5b8065 100644
--- a/src/components/structures/InteractiveAuth.js
+++ b/src/components/structures/InteractiveAuth.js
@@ -27,6 +27,9 @@ 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({
@@ -38,11 +41,34 @@ export default React.createClass({
// callback
makeRequest: React.PropTypes.func.isRequired,
- // callback called when the auth process has finished
+ // 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 result The result of the authenticated call
- onFinished: React.PropTypes.func.isRequired,
+ // @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() {
@@ -60,12 +86,22 @@ export default React.createClass({
this._authLogic = new InteractiveAuth({
authData: this.props.authData,
doRequest: this._requestCallback,
- startAuthStage: this._startAuthStage,
+ 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) => {
- this.props.onFinished(true, 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;
@@ -76,26 +112,48 @@ export default React.createClass({
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);
+ }
},
- _startAuthStage: function(stageType, error) {
+ _authStateUpdated: function(stageType, stageState) {
+ const oldStage = this.state.authStage;
this.setState({
authStage: stageType,
- errorText: error ? error.error : null,
- }, this._setFocus);
+ stageState: stageState,
+ errorText: stageState.error,
+ }, () => {
+ if (oldStage != stageType) this._setFocus();
+ });
},
- _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;
}
@@ -117,19 +175,35 @@ export default React.createClass({
_renderCurrentStage: function() {
const stage = this.state.authStage;
- var StageComponent = getEntryComponentForLoginType(stage);
+ if (!stage) return null;
+
+ const StageComponent = getEntryComponentForLoginType(stage);
return (
);
},
+ _onAuthStageFailed: function(e) {
+ this.props.onAuthFinished(false, e);
+ },
+ _setEmailSid: function(sid) {
+ this._authLogic.setEmailSid(sid);
+ },
+
render: function() {
let error = null;
if (this.state.errorText) {
diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js
index aa9470f126..ef9d8d112a 100644
--- a/src/components/structures/LoggedInView.js
+++ b/src/components/structures/LoggedInView.js
@@ -49,11 +49,16 @@ export default React.createClass({
childContextTypes: {
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient),
+ authCache: React.PropTypes.object,
},
getChildContext: function() {
return {
matrixClient: this._matrixClient,
+ authCache: {
+ auth: {},
+ lastUpdate: 0,
+ },
};
},
@@ -76,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
@@ -94,6 +106,17 @@ export default React.createClass({
var handled = false;
switch (ev.keyCode) {
+ case KeyCode.ESCAPE:
+
+ // Implemented this way so possible handling for other pages is neater
+ switch (this.props.page_type) {
+ case PageTypes.UserSettings:
+ this.props.onUserSettingsClose();
+ handled = true;
+ break;
+ }
+
+ break;
case KeyCode.UP:
case KeyCode.DOWN:
if (ev.altKey) {
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 3265249105..b449ff3094 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.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.
@@ -28,10 +29,6 @@ var UserActivity = require("../../UserActivity");
var Presence = require("../../Presence");
var dis = require("../../dispatcher");
-var Login = require("./login/Login");
-var Registration = require("./login/Registration");
-var PostRegistration = require("./login/PostRegistration");
-
var Modal = require("../../Modal");
var Tinter = require("../../Tinter");
var sdk = require('../../index');
@@ -41,6 +38,7 @@ var Lifecycle = require('../../Lifecycle');
var PageTypes = require('../../PageTypes');
var createRoom = require("../../createRoom");
+import * as UDEHandler from '../../UnknownDeviceErrorHandler';
module.exports = React.createClass({
displayName: 'MatrixChat',
@@ -61,9 +59,19 @@ module.exports = React.createClass({
// called when the session load completes
onLoadCompleted: React.PropTypes.func,
+ // Represents the screen to display as a result of parsing the initial
+ // window.location
+ initialScreenAfterLogin: React.PropTypes.shape({
+ screen: React.PropTypes.string.isRequired,
+ params: React.PropTypes.object,
+ }),
+
// displayname, if any, to set on the device when logging
// in/registering.
defaultDeviceDisplayName: React.PropTypes.string,
+
+ // A function that makes a registration URL
+ makeRegistrationUrl: React.PropTypes.func.isRequired,
},
childContextTypes: {
@@ -84,6 +92,12 @@ module.exports = React.createClass({
var s = {
loading: true,
screen: undefined,
+ screenAfterLogin: this.props.initialScreenAfterLogin,
+
+ // Stashed guest credentials if the user logs out
+ // whilst logged in as a guest user (so they can change
+ // their mind & log back in)
+ guestCreds: null,
// What the LoggedInView would be showing if visible
page_type: null,
@@ -99,7 +113,8 @@ module.exports = React.createClass({
// If we're trying to just view a user ID (i.e. /user URL), this is it
viewUserId: null,
- logged_in: false,
+ loggedIn: false,
+ loggingIn: false,
collapse_lhs: false,
collapse_rhs: false,
ready: false,
@@ -179,13 +194,9 @@ module.exports = React.createClass({
componentWillMount: function() {
SdkConfig.put(this.props.config);
- // Stashed guest credentials if the user logs out
- // whilst logged in as a guest user (so they can change
- // their mind & log back in)
- this.guestCreds = null;
-
- // if the automatic session load failed, the error
- this.sessionLoadError = null;
+ // Used by _viewRoom before getting state from sync
+ this.firstSyncComplete = false;
+ this.firstSyncPromise = q.defer();
if (this.props.config.sync_timeline_limit) {
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
@@ -226,10 +237,20 @@ module.exports = React.createClass({
if (this._teamToken) {
console.info(`Team token set to ${this._teamToken}`);
}
+
+ // Set a default HS with query param `hs_url`
+ const paramHs = this.props.startingFragmentQueryParams.hs_url;
+ if (paramHs) {
+ console.log('Setting register_hs_url ', paramHs);
+ this.setState({
+ register_hs_url: paramHs,
+ });
+ }
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
+ UDEHandler.startListening();
this.focusComposer = false;
window.addEventListener("focus", this.onFocus);
@@ -265,7 +286,6 @@ module.exports = React.createClass({
});
}).catch((e) => {
console.error("Unable to load session", e);
- this.sessionLoadError = e.message;
}).done(()=>{
// stuff this through the dispatcher so that it happens
// after the on_logged_in action.
@@ -276,6 +296,7 @@ module.exports = React.createClass({
componentWillUnmount: function() {
Lifecycle.stopMatrixClient();
dis.unregister(this.dispatcherRef);
+ UDEHandler.stopListening();
window.removeEventListener("focus", this.onFocus);
window.removeEventListener('resize', this.handleResize);
},
@@ -291,7 +312,7 @@ module.exports = React.createClass({
const newState = {
screen: undefined,
viewUserId: null,
- logged_in: false,
+ loggedIn: false,
ready: false,
upgradeUsername: null,
guestAccessToken: null,
@@ -301,88 +322,123 @@ module.exports = React.createClass({
},
onAction: function(payload) {
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var roomIndexDelta = 1;
var self = this;
switch (payload.action) {
case 'logout':
- if (MatrixClientPeg.get().isGuest()) {
- this.guestCreds = MatrixClientPeg.getCredentials();
- }
Lifecycle.logout();
break;
case 'start_registration':
- var newState = payload.params || {};
- newState.screen = 'register';
- if (
- payload.params &&
- payload.params.client_secret &&
- payload.params.session_id &&
- payload.params.hs_url &&
- payload.params.is_url &&
- payload.params.sid
- ) {
- newState.register_client_secret = payload.params.client_secret;
- newState.register_session_id = payload.params.session_id;
- newState.register_hs_url = payload.params.hs_url;
- newState.register_is_url = payload.params.is_url;
- newState.register_id_sid = payload.params.sid;
- }
- this.setStateForNewScreen(newState);
+ const params = payload.params || {};
+ this.setStateForNewScreen({
+ screen: 'register',
+ // these params may be undefined, but if they are,
+ // unset them from our state: we don't want to
+ // resume a previous registration session if the
+ // user just clicked 'register'
+ register_client_secret: params.client_secret,
+ register_session_id: params.session_id,
+ register_hs_url: params.hs_url,
+ register_is_url: params.is_url,
+ register_id_sid: params.sid,
+ });
this.notifyNewScreen('register');
break;
case 'start_login':
- if (this.state.logged_in) return;
+ if (MatrixClientPeg.get() &&
+ MatrixClientPeg.get().isGuest()
+ ) {
+ this.setState({
+ guestCreds: MatrixClientPeg.getCredentials(),
+ });
+ }
this.setStateForNewScreen({
screen: 'login',
});
this.notifyNewScreen('login');
break;
case 'start_post_registration':
- this.setState({ // don't clobber logged_in status
+ this.setState({ // don't clobber loggedIn status
screen: 'post_registration'
});
break;
case 'start_upgrade_registration':
- // stash our guest creds so we can backout if needed
- this.guestCreds = MatrixClientPeg.getCredentials();
+ // also stash our credentials, then if we restore the session,
+ // we can just do it the same way whether we started upgrade
+ // registration or explicitly logged out
this.setStateForNewScreen({
+ guestCreds: MatrixClientPeg.getCredentials(),
screen: "register",
upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(),
guestAccessToken: MatrixClientPeg.get().getAccessToken(),
});
+
+ // stop the client: if we are syncing whilst the registration
+ // is completed in another browser, we'll be 401ed for using
+ // a guest access token for a non-guest account.
+ // It will be restarted in onReturnToGuestClick
+ Lifecycle.stopMatrixClient();
+
this.notifyNewScreen('register');
break;
case 'start_password_recovery':
- if (this.state.logged_in) return;
+ if (this.state.loggedIn) return;
this.setStateForNewScreen({
screen: 'forgot_password',
});
this.notifyNewScreen('forgot_password');
break;
case 'leave_room':
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
- var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-
- var roomId = payload.room_id;
Modal.createDialog(QuestionDialog, {
title: "Leave room",
description: "Are you sure you want to leave the room?",
- onFinished: function(should_leave) {
+ onFinished: (should_leave) => {
if (should_leave) {
- var d = MatrixClientPeg.get().leave(roomId);
+ const d = MatrixClientPeg.get().leave(payload.room_id);
// FIXME: controller shouldn't be loading a view :(
- var Loader = sdk.getComponent("elements.Spinner");
- var modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
+ const Loader = sdk.getComponent("elements.Spinner");
+ const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
- d.then(function() {
+ d.then(() => {
modal.close();
- dis.dispatch({action: 'view_next_room'});
- }, function(err) {
+ if (this.currentRoomId === payload.room_id) {
+ dis.dispatch({action: 'view_next_room'});
+ }
+ }, (err) => {
modal.close();
+ console.error("Failed to leave room " + payload.room_id + " " + err);
Modal.createDialog(ErrorDialog, {
title: "Failed to leave room",
+ description: "Server may be unavailable, overloaded, or you hit a bug."
+ });
+ });
+ }
+ }
+ });
+ break;
+ case 'reject_invite':
+ Modal.createDialog(QuestionDialog, {
+ title: "Reject invitation",
+ description: "Are you sure you want to reject the invitation?",
+ onFinished: (confirm) => {
+ if (confirm) {
+ // FIXME: controller shouldn't be loading a view :(
+ const Loader = sdk.getComponent("elements.Spinner");
+ const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
+
+ MatrixClientPeg.get().leave(payload.room_id).done(() => {
+ modal.close();
+ if (this.currentRoomId === payload.room_id) {
+ dis.dispatch({action: 'view_next_room'});
+ }
+ }, (err) => {
+ modal.close();
+ Modal.createDialog(ErrorDialog, {
+ title: "Failed to reject invitation",
description: err.toString()
});
});
@@ -509,8 +565,11 @@ module.exports = React.createClass({
case 'set_theme':
this._onSetTheme(payload.value);
break;
+ case 'on_logging_in':
+ this.setState({loggingIn: true});
+ break;
case 'on_logged_in':
- this._onLoggedIn();
+ this._onLoggedIn(payload.teamToken);
break;
case 'on_logged_out':
this._onLoggedOut();
@@ -582,36 +641,38 @@ module.exports = React.createClass({
}
}
- if (this.sdkReady) {
- // if the SDK is not ready yet, remember what room
- // we're supposed to be on but don't notify about
- // the new screen yet (we won't be showing it yet)
- // The normal case where this happens is navigating
- // to the room in the URL bar on page load.
- var presentedId = room_info.room_alias || room_info.room_id;
- var room = MatrixClientPeg.get().getRoom(room_info.room_id);
+ // Wait for the first sync to complete so that if a room does have an alias,
+ // it would have been retrieved.
+ let waitFor = q(null);
+ if (!this.firstSyncComplete) {
+ if (!this.firstSyncPromise) {
+ console.warn('Cannot view a room before first sync. room_id:', room_info.room_id);
+ return;
+ }
+ waitFor = this.firstSyncPromise.promise;
+ }
+
+ waitFor.done(() => {
+ let presentedId = room_info.room_alias || room_info.room_id;
+ const room = MatrixClientPeg.get().getRoom(room_info.room_id);
if (room) {
- var theAlias = Rooms.getDisplayAliasForRoom(room);
+ const theAlias = Rooms.getDisplayAliasForRoom(room);
if (theAlias) presentedId = theAlias;
- // No need to do this given RoomView triggers it itself...
- // var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
- // var color_scheme = {};
- // if (color_scheme_event) {
- // color_scheme = color_scheme_event.getContent();
- // // XXX: we should validate the event
- // }
- // console.log("Tinter.tint from _viewRoom");
- // Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
+ // Store this as the ID of the last room accessed. This is so that we can
+ // persist which room is being stored across refreshes and browser quits.
+ if (localStorage) {
+ localStorage.setItem('mx_last_room_id', room.roomId);
+ }
}
if (room_info.event_id) {
- presentedId += "/"+room_info.event_id;
+ presentedId += "/" + room_info.event_id;
}
- this.notifyNewScreen('room/'+presentedId);
+ this.notifyNewScreen('room/' + presentedId);
newState.ready = true;
- }
- this.setState(newState);
+ this.setState(newState);
+ });
},
_createChat: function() {
@@ -637,6 +698,14 @@ module.exports = React.createClass({
_onLoadCompleted: function() {
this.props.onLoadCompleted();
this.setState({loading: false});
+
+ // Show screens (like 'register') that need to be shown without _onLoggedIn
+ // being called. 'register' needs to be routed here when the email confirmation
+ // link is clicked on.
+ if (this.state.screenAfterLogin &&
+ ['register'].indexOf(this.state.screenAfterLogin.screen) !== -1) {
+ this._showScreenAfterLogin();
+ }
},
/**
@@ -686,13 +755,48 @@ module.exports = React.createClass({
/**
* Called when a new logged in session has started
*/
- _onLoggedIn: function(credentials) {
- this.guestCreds = null;
- this.notifyNewScreen('');
+ _onLoggedIn: function(teamToken) {
this.setState({
- screen: undefined,
- logged_in: true,
+ guestCreds: null,
+ loggedIn: true,
+ loggingIn: false,
});
+
+ if (teamToken) {
+ // A team member has logged in, not a guest
+ this._teamToken = teamToken;
+ dis.dispatch({action: 'view_home_page'});
+ } else if (this._is_registered) {
+ // The user has just logged in after registering
+ dis.dispatch({action: 'view_user_settings'});
+ } else {
+ this._showScreenAfterLogin();
+ }
+ },
+
+ _showScreenAfterLogin: function() {
+ // If screenAfterLogin is set, use that, then null it so that a second login will
+ // result in view_home_page, _user_settings or _room_directory
+ if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) {
+ this.showScreen(
+ this.state.screenAfterLogin.screen,
+ this.state.screenAfterLogin.params
+ );
+ this.notifyNewScreen(this.state.screenAfterLogin.screen);
+ this.setState({screenAfterLogin: null});
+ } else if (localStorage && localStorage.getItem('mx_last_room_id')) {
+ // Before defaulting to directory, show the last viewed room
+ dis.dispatch({
+ action: 'view_room',
+ room_id: localStorage.getItem('mx_last_room_id'),
+ });
+ } else if (this._teamToken) {
+ // Team token might be set if we're a guest.
+ // Guests do not call _onLoggedIn with a teamToken
+ dis.dispatch({action: 'view_home_page'});
+ } else {
+ dis.dispatch({action: 'view_room_directory'});
+ }
},
/**
@@ -701,7 +805,7 @@ module.exports = React.createClass({
_onLoggedOut: function() {
this.notifyNewScreen('login');
this.setStateForNewScreen({
- logged_in: false,
+ loggedIn: false,
ready: false,
collapse_lhs: false,
collapse_rhs: false,
@@ -709,6 +813,7 @@ module.exports = React.createClass({
currentRoomId: null,
page_type: PageTypes.RoomDirectory,
});
+ this._teamToken = null;
},
/**
@@ -716,9 +821,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") {
@@ -726,55 +853,12 @@ module.exports = React.createClass({
}
console.log("MatrixClient sync state => %s", state);
if (state !== "PREPARED") { return; }
- self.sdkReady = true;
- if (self.starting_room_alias_payload) {
- dis.dispatch(self.starting_room_alias_payload);
- delete self.starting_room_alias_payload;
- } else if (!self.state.page_type) {
- if (!self.state.currentRoomId) {
- var firstRoom = null;
- if (cli.getRooms() && cli.getRooms().length) {
- firstRoom = RoomListSorter.mostRecentActivityFirst(
- cli.getRooms()
- )[0].roomId;
- self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView});
- } else {
- if (self._teamToken) {
- self.setState({ready: true, page_type: PageTypes.HomePage});
- } else {
- self.setState({ready: true, page_type: PageTypes.RoomDirectory});
- }
- }
- } else {
- self.setState({ready: true, page_type: PageTypes.RoomView});
- }
+ self.firstSyncComplete = true;
+ self.firstSyncPromise.resolve();
- // we notifyNewScreen now because now the room will actually be displayed,
- // and (mostly) now we can get the correct alias.
- var presentedId = self.state.currentRoomId;
- var room = MatrixClientPeg.get().getRoom(self.state.currentRoomId);
- if (room) {
- var theAlias = Rooms.getDisplayAliasForRoom(room);
- if (theAlias) presentedId = theAlias;
- }
-
- if (presentedId != undefined) {
- self.notifyNewScreen('room/'+presentedId);
- } else {
- // There is no information on presentedId
- // so point user to fallback like /directory
- if (self._teamToken) {
- self.notifyNewScreen('home');
- } else {
- self.notifyNewScreen('directory');
- }
- }
-
- dis.dispatch({action: 'focus_composer'});
- } else {
- self.setState({ready: true});
- }
+ dis.dispatch({action: 'focus_composer'});
+ self.setState({ready: true});
});
cli.on('Call.incoming', function(call) {
dis.dispatch({
@@ -874,12 +958,7 @@ module.exports = React.createClass({
// we can't view a room unless we're logged in
// (a guest account is fine)
- if (!this.state.logged_in) {
- // we may still be loading (ie, trying to register a guest
- // session); otherwise we're (probably) already showing a login
- // screen. Either way, we'll show the room once the client starts.
- this.starting_room_alias_payload = payload;
- } else {
+ if (this.state.loggedIn) {
dis.dispatch(payload);
}
} else if (screen.indexOf('user/') == 0) {
@@ -973,29 +1052,17 @@ module.exports = React.createClass({
onReturnToGuestClick: function() {
// reanimate our guest login
- if (this.guestCreds) {
- Lifecycle.setLoggedIn(this.guestCreds);
- this.guestCreds = null;
+ if (this.state.guestCreds) {
+ Lifecycle.setLoggedIn(this.state.guestCreds);
+ this.setState({guestCreds: null});
}
},
- onRegistered: function(credentials) {
- Lifecycle.setLoggedIn(credentials);
- // do post-registration stuff
- // This now goes straight to user settings
- // We use _setPage since if we wait for
- // showScreen to do the dispatch loop,
- // the showScreen dispatch will race with the
- // sdk sync finishing and we'll probably see
- // the page type still unset when the MatrixClient
- // is started and show the Room Directory instead.
- //this.showScreen("view_user_settings");
- this._setPage(PageTypes.UserSettings);
- },
-
- onTeamMemberRegistered: function(teamToken) {
+ onRegistered: function(credentials, teamToken) {
+ // teamToken may not be truthy
this._teamToken = teamToken;
- this._setPage(PageTypes.HomePage);
+ this._is_registered = true;
+ Lifecycle.setLoggedIn(credentials);
},
onFinishPostRegistration: function() {
@@ -1061,15 +1128,20 @@ module.exports = React.createClass({
this.setState({currentRoomId: room_id});
},
+ _makeRegistrationUrl: function(params) {
+ if (this.props.startingFragmentQueryParams.referrer) {
+ params.referrer = this.props.startingFragmentQueryParams.referrer;
+ }
+ return this.props.makeRegistrationUrl(params);
+ },
+
render: function() {
- var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
- var LoggedInView = sdk.getComponent('structures.LoggedInView');
-
- // console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen +
- // "; logged_in="+this.state.logged_in+"; ready="+this.state.ready);
-
- if (this.state.loading) {
- var Spinner = sdk.getComponent('elements.Spinner');
+ // `loading` might be set to false before `loggedIn = true`, causing the default
+ // (``) to be visible for a few MS (say, whilst a request is in-flight to
+ // the RTS). So in the meantime, use `loggingIn`, which is true between
+ // actions `on_logging_in` and `on_logged_in`.
+ if (this.state.loading || this.state.loggingIn) {
+ const Spinner = sdk.getComponent('elements.Spinner');
return (
@@ -1078,15 +1150,17 @@ module.exports = React.createClass({
}
// needs to be before normal PageTypes as you are logged in technically
else if (this.state.screen == 'post_registration') {
+ const PostRegistration = sdk.getComponent('structures.login.PostRegistration');
return (
);
- } else if (this.state.logged_in && this.state.ready) {
+ } else if (this.state.loggedIn && this.state.ready) {
/* for now, we stuff the entirety of our props and state into the LoggedInView.
* we should go through and figure out what we actually need to pass down, as well
* as using something like redux to avoid having a billion bits of state kicking around.
*/
+ const LoggedInView = sdk.getComponent('structures.LoggedInView');
return (
);
- } else if (this.state.logged_in) {
+ } else if (this.state.loggedIn) {
// we think we are logged in, but are still waiting for the /sync to complete
- var Spinner = sdk.getComponent('elements.Spinner');
+ const Spinner = sdk.getComponent('elements.Spinner');
return (
);
} else if (this.state.screen == 'register') {
+ const Registration = sdk.getComponent('structures.login.Registration');
return (
);
} else if (this.state.screen == 'forgot_password') {
+ const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
return (
);
} else {
- var r = (
+ const Login = sdk.getComponent('structures.login.Login');
+ return (
);
-
- // we only want to show the session load error the first time the
- // Login component is rendered. This is pretty hacky but I can't
- // think of another way to achieve it.
- this.sessionLoadError = null;
-
- return r;
}
}
});
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index dcebe38fa4..0f8d35f525 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -295,7 +295,10 @@ module.exports = React.createClass({
var last = (i == lastShownEventIndex);
// Wrap consecutive member events in a ListSummary, ignore if redacted
- if (isMembershipChange(mxEv) && EventTile.haveTileForEvent(mxEv)) {
+ if (isMembershipChange(mxEv) &&
+ EventTile.haveTileForEvent(mxEv) &&
+ !mxEv.isRedacted()
+ ) {
let ts1 = mxEv.getTs();
// Ensure that the key of the MemberEventListSummary does not change with new
// member events. This will prevent it from being re-created unnecessarily, and
@@ -349,7 +352,9 @@ module.exports = React.createClass({
+ data-scroll-token={eventId}
+ onToggle={this._onWidgetLoad} // Update scroll state
+ >
{eventTiles}
);
@@ -362,10 +367,6 @@ module.exports = React.createClass({
// replacing all of the DOM elements every time we paginate.
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last));
prevEvent = mxEv;
- } else if (!mxEv.status) {
- // if we aren't showing the event, put in a dummy scroll token anyway, so
- // that we can scroll to the right place.
- ret.push(
);
}
var isVisibleReadMarker = false;
@@ -410,7 +411,9 @@ module.exports = React.createClass({
// is this a continuation of the previous message?
var continuation = false;
- if (prevEvent !== null && prevEvent.sender && mxEv.sender
+
+ if (prevEvent !== null
+ && prevEvent.sender && mxEv.sender
&& mxEv.sender.userId === prevEvent.sender.userId
&& mxEv.getType() == prevEvent.getType()) {
continuation = true;
@@ -463,6 +466,7 @@ module.exports = React.createClass({
ref={this._collectEventNode.bind(this, eventId)}
data-scroll-token={scrollToken}>
24h apart
if (Math.abs(prevEvent.getTs() - nextEventDate.getTime()) > MILLIS_IN_DAY) {
return true;
}
// Compare weekdays
- return prevEvent.getDate().getDay() !== nextEventDate.getDay();
+ return prevEventDate.getDay() !== nextEventDate.getDay();
},
// get a list of read receipts that should be shown next to this event
diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js
index ca50e1071a..0389b606aa 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.js
@@ -96,26 +96,12 @@ module.exports = React.createClass({
componentWillMount: function() {
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
+
+ this._checkSize();
},
- componentDidUpdate: function(prevProps, prevState) {
- if(this.props.onResize && this._checkForResize(prevProps, prevState)) {
- this.props.onResize();
- }
-
- const size = this._getSize(this.props, this.state);
- if (size > 0) {
- this.props.onVisible();
- } else {
- if (this.hideDebouncer) {
- clearTimeout(this.hideDebouncer);
- }
- this.hideDebouncer = setTimeout(() => {
- // temporarily stop hiding the statusbar as per
- // https://github.com/vector-im/riot-web/issues/1991#issuecomment-276953915
- // this.props.onHidden();
- }, HIDE_DEBOUNCE_MS);
- }
+ componentDidUpdate: function() {
+ this._checkSize();
},
componentWillUnmount: function() {
@@ -142,31 +128,33 @@ module.exports = React.createClass({
});
},
+ // Check whether current size is greater than 0, if yes call props.onVisible
+ _checkSize: function () {
+ if (this.props.onVisible && this._getSize()) {
+ this.props.onVisible();
+ }
+ },
+
// We don't need the actual height - just whether it is likely to have
// changed - so we use '0' to indicate normal size, and other values to
// indicate other sizes.
- _getSize: function(props, state) {
- if (state.syncState === "ERROR" ||
- (state.usersTyping.length > 0) ||
- props.numUnreadMessages ||
- !props.atEndOfLiveTimeline ||
- props.hasActiveCall) {
+ _getSize: function() {
+ if (this.state.syncState === "ERROR" ||
+ (this.state.usersTyping.length > 0) ||
+ this.props.numUnreadMessages ||
+ !this.props.atEndOfLiveTimeline ||
+ this.props.hasActiveCall ||
+ this.props.tabComplete.isTabCompleting()
+ ) {
return STATUS_BAR_EXPANDED;
- } else if (props.tabCompleteEntries) {
+ } else if (this.props.tabCompleteEntries) {
return STATUS_BAR_HIDDEN;
- } else if (props.unsentMessageError) {
+ } else if (this.props.unsentMessageError) {
return STATUS_BAR_EXPANDED_LARGE;
}
return STATUS_BAR_HIDDEN;
},
- // determine if we need to call onResize
- _checkForResize: function(prevProps, prevState) {
- // figure out the old height and the new height of the status bar.
- return this._getSize(prevProps, prevState)
- !== this._getSize(this.props, this.state);
- },
-
// return suitable content for the image on the left of the status bar.
//
// if wantPlaceholder is true, we include a "..." placeholder if
@@ -194,8 +182,9 @@ module.exports = React.createClass({
}
if (this.props.hasActiveCall) {
+ var TintableSvg = sdk.getComponent("elements.TintableSvg");
return (
-
+
);
}
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 5bf192dfc6..b22d867acf 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -490,13 +490,49 @@ 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) {
+ this._warnAboutEncryption(room);
this._calculatePeekRules(room);
this._updatePreviewUrlVisibility(room);
},
+ _warnAboutEncryption: function (room) {
+ if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) {
+ return;
+ }
+ let userHasUsedEncryption = false;
+ if (localStorage) {
+ userHasUsedEncryption = localStorage.getItem('mx_user_has_used_encryption');
+ }
+ if (!userHasUsedEncryption) {
+ const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+ Modal.createDialog(QuestionDialog, {
+ title: "Warning!",
+ hasCancelButton: false,
+ description: (
+
+
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.
;
diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js
index 4a0faae9db..83bec03e9e 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.js
@@ -25,7 +25,7 @@ var DEBUG_SCROLL = false;
// The amount of extra scroll distance to allow prior to unfilling.
// See _getExcessHeight.
-const UNPAGINATION_PADDING = 3000;
+const UNPAGINATION_PADDING = 6000;
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
// many scroll events causing many unfilling requests.
const UNFILL_REQUEST_DEBOUNCE_MS = 200;
@@ -333,33 +333,27 @@ module.exports = React.createClass({
if (excessHeight <= 0) {
return;
}
- var itemlist = this.refs.itemlist;
- var tiles = itemlist.children;
+ const tiles = this.refs.itemlist.children;
// The scroll token of the first/last tile to be unpaginated
let markerScrollToken = null;
- // Subtract clientHeights to simulate the events being unpaginated whilst counting
- // the events to be unpaginated.
- if (backwards) {
- // Iterate forwards from start of tiles, subtracting event tile height
- let i = 0;
- while (i < tiles.length && excessHeight > tiles[i].clientHeight) {
- excessHeight -= tiles[i].clientHeight;
- if (tiles[i].dataset.scrollToken) {
- markerScrollToken = tiles[i].dataset.scrollToken;
- }
- i++;
+ // Subtract heights of tiles to simulate the tiles being unpaginated until the
+ // excess height is less than the height of the next tile to subtract. This
+ // prevents excessHeight becoming negative, which could lead to future
+ // pagination.
+ //
+ // If backwards is true, we unpaginate (remove) tiles from the back (top).
+ for (let i = 0; i < tiles.length; i++) {
+ const tile = tiles[backwards ? i : tiles.length - 1 - i];
+ // Subtract height of tile as if it were unpaginated
+ excessHeight -= tile.clientHeight;
+ // The tile may not have a scroll token, so guard it
+ if (tile.dataset.scrollToken) {
+ markerScrollToken = tile.dataset.scrollToken;
}
- } else {
- // Iterate backwards from end of tiles, subtracting event tile height
- let i = tiles.length - 1;
- while (i > 0 && excessHeight > tiles[i].clientHeight) {
- excessHeight -= tiles[i].clientHeight;
- if (tiles[i].dataset.scrollToken) {
- markerScrollToken = tiles[i].dataset.scrollToken;
- }
- i--;
+ if (tile.clientHeight > excessHeight) {
+ break;
}
}
@@ -589,24 +583,34 @@ module.exports = React.createClass({
var itemlist = this.refs.itemlist;
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
var messages = itemlist.children;
+ let newScrollState = null;
for (var i = messages.length-1; i >= 0; --i) {
var node = messages[i];
if (!node.dataset.scrollToken) continue;
var boundingRect = node.getBoundingClientRect();
- if (boundingRect.bottom < wrapperRect.bottom) {
- this.scrollState = {
- stuckAtBottom: false,
- trackedScrollToken: node.dataset.scrollToken,
- pixelOffset: wrapperRect.bottom - boundingRect.bottom,
- };
- debuglog("ScrollPanel: saved scroll state", this.scrollState);
- return;
+ newScrollState = {
+ stuckAtBottom: false,
+ trackedScrollToken: node.dataset.scrollToken,
+ pixelOffset: wrapperRect.bottom - boundingRect.bottom,
+ };
+ // If the bottom of the panel intersects the ClientRect of node, use this node
+ // as the scrollToken.
+ // If this is false for the entire for-loop, we default to the last node
+ // (which is why newScrollState is set on every iteration).
+ if (boundingRect.top < wrapperRect.bottom) {
+ // Use this node as the scrollToken
+ break;
}
}
-
- debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport");
+ // This is only false if there were no nodes with `node.dataset.scrollToken` set.
+ if (newScrollState) {
+ this.scrollState = newScrollState;
+ debuglog("ScrollPanel: saved scroll state", this.scrollState);
+ } else {
+ debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport");
+ }
},
_restoreSavedScrollState: function() {
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index cb42f701a3..8cd820c284 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -251,10 +251,12 @@ var TimelinePanel = React.createClass({
},
onMessageListUnfillRequest: function(backwards, scrollToken) {
+ // If backwards, unpaginate from the back (i.e. the start of the timeline)
let dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
debuglog("TimelinePanel: unpaginating events in direction", dir);
- // All tiles are inserted by MessagePanel to have a scrollToken === eventId
+ // All tiles are inserted by MessagePanel to have a scrollToken === eventId, and
+ // this particular event should be the first or last to be unpaginated.
let eventId = scrollToken;
let marker = this.state.events.findIndex(
@@ -431,6 +433,10 @@ var TimelinePanel = React.createClass({
}
},
+ canResetTimeline: function() {
+ return this.refs.messagePanel && this.refs.messagePanel.isAtBottom();
+ },
+
onRoomRedaction: function(ev, room) {
if (this.unmounted) return;
@@ -469,14 +475,6 @@ var TimelinePanel = React.createClass({
// we still have a client.
if (!MatrixClientPeg.get()) return;
- // if we are scrolled to the bottom, do a quick-reset of our unreadNotificationCount
- // to avoid having to wait from the remote echo from the homeserver.
- if (this.isAtEndOfLiveTimeline()) {
- this.props.timelineSet.room.setUnreadNotificationCount('total', 0);
- this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
- // XXX: i'm a bit surprised we don't have to emit an event or dispatch to get this picked up
- }
-
var currentReadUpToEventId = this._getCurrentReadReceipt(true);
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
@@ -514,6 +512,19 @@ var TimelinePanel = React.createClass({
// it failed, so allow retries next time the user is active
this.last_rr_sent_event_id = undefined;
});
+
+ // do a quick-reset of our unreadNotificationCount to avoid having
+ // to wait from the remote echo from the homeserver.
+ // we only do this if we're right at the end, because we're just assuming
+ // that sending an RR for the latest message will set our notif counter
+ // to zero: it may not do this if we send an RR for somewhere before the end.
+ if (this.isAtEndOfLiveTimeline()) {
+ this.props.timelineSet.room.setUnreadNotificationCount('total', 0);
+ this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
+ dis.dispatch({
+ action: 'on_room_read',
+ });
+ }
}
},
@@ -810,7 +821,7 @@ var TimelinePanel = React.createClass({
// go via the dispatcher so that the URL is updated
dis.dispatch({
action: 'view_room',
- room_id: this.props.timelineSet.roomId,
+ room_id: this.props.timelineSet.room.roomId,
});
};
}
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 faa8a56894..892865fdf9 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.
@@ -39,6 +40,10 @@ const REACT_SDK_VERSION =
// 'id' gives the key name in the im.vector.web.settings account data event
// 'label' is how we describe it in the UI.
const SETTINGS_LABELS = [
+ {
+ id: 'autoplayGifsAndVideos',
+ label: 'Autoplay GIFs and videos',
+ },
/*
{
id: 'alwaysShowTimestamps',
@@ -135,6 +140,7 @@ module.exports = React.createClass({
componentWillMount: function() {
this._unmounted = false;
+ this._addThreepid = null;
if (PlatformPeg.get()) {
q().then(() => {
@@ -202,9 +208,10 @@ module.exports = React.createClass({
});
}, function(error) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ console.error("Failed to load user settings: " + error);
Modal.createDialog(ErrorDialog, {
title: "Can't load user settings",
- description: error.toString()
+ description: "Server may be unavailable or overloaded",
});
});
},
@@ -242,10 +249,11 @@ module.exports = React.createClass({
self._refreshFromServer();
}, function(err) {
var errMsg = (typeof err === "string") ? err : (err.error || "");
+ console.error("Failed to set avatar: " + err);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Error",
- description: "Failed to set avatar. " + errMsg
+ description: "Failed to set avatar."
});
});
},
@@ -256,12 +264,18 @@ module.exports = React.createClass({
title: "Sign out?",
description:
- For security, logging out will delete any end-to-end encryption keys from this browser,
- making previous encrypted chat history unreadable if you log back in.
- In future this will be improved,
- but for now be warned.
+ For security, logging out will delete any end-to-end encryption keys from this browser.
+
+ If you want to be able to decrypt your conversation history from future Riot sessions,
+ please export your room keys for safe-keeping.
,
button: "Sign out",
+ extraButtons: [
+
+ ],
onFinished: (confirmed) => {
if (confirmed) {
dis.dispatch({action: 'logout'});
@@ -282,6 +296,7 @@ module.exports = React.createClass({
errMsg += ` (HTTP status ${err.httpStatus})`;
}
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ console.error("Failed to change password: " + errMsg);
Modal.createDialog(ErrorDialog, {
title: "Error",
description: errMsg
@@ -308,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",
@@ -321,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.",
@@ -333,12 +352,13 @@ module.exports = React.createClass({
});
}, (err) => {
this.setState({email_add_pending: false});
+ console.error("Unable to add email address " + email_address + " " + err);
Modal.createDialog(ErrorDialog, {
- title: "Unable to add email address",
- description: err.message
+ title: "Error",
+ 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});
},
@@ -357,9 +377,10 @@ module.exports = React.createClass({
return this._refreshFromServer();
}).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ console.error("Unable to remove contact information: " + err);
Modal.createDialog(ErrorDialog, {
- title: "Unable to remove contact information",
- description: err.toString(),
+ title: "Error",
+ description: "Unable to remove contact information",
});
}).done();
}
@@ -376,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",
});
@@ -397,9 +418,10 @@ module.exports = React.createClass({
});
} else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ console.error("Unable to verify email address: " + err);
Modal.createDialog(ErrorDialog, {
- title: "Unable to verify email address",
- description: err.toString(),
+ title: "Error",
+ description: "Unable to verify email address",
});
}
});
@@ -419,10 +441,11 @@ module.exports = React.createClass({
},
_onClearCacheClicked: function() {
+ if (!PlatformPeg.get()) return;
+
+ MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().store.deleteAllData().done(() => {
- // forceReload=false since we don't really need new HTML/JS files
- // we just need to restart the JS runtime.
- window.location.reload(false);
+ PlatformPeg.get().reload();
});
},
@@ -745,6 +768,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) {
@@ -777,7 +808,9 @@ module.exports = React.createClass({
Resetting password will currently reset any end-to-end encryption keys on all devices,
- making encrypted chat history unreadable.
- In future this may be improved,
- but for now be warned.
+ making encrypted chat history unreadable, unless you first export your room keys
+ and re-import them afterwards.
+ In future this will be improved.
,
button: "Continue",
+ extraButtons: [
+
+ ],
onFinished: (confirmed) => {
if (confirmed) {
this.submitPasswordReset(
@@ -110,6 +116,18 @@ module.exports = React.createClass({
}
},
+ _onExportE2eKeysClicked: function() {
+ Modal.createDialogAsync(
+ (cb) => {
+ require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
+ cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog'));
+ }, "e2e-export");
+ }, {
+ matrixClient: MatrixClientPeg.get(),
+ }
+ );
+ },
+
onInputChanged: function(stateKey, ev) {
this.setState({
[stateKey]: ev.target.value
diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js
index fe9b544751..7e1a5f9d35 100644
--- a/src/components/structures/login/Login.js
+++ b/src/components/structures/login/Login.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.
@@ -19,13 +20,13 @@ limitations under the License.
var React = require('react');
var ReactDOM = require('react-dom');
var sdk = require('../../../index');
-var Signup = require("../../../Signup");
+var Login = require("../../../Login");
var PasswordLogin = require("../../views/login/PasswordLogin");
var CasLogin = require("../../views/login/CasLogin");
var ServerConfig = require("../../views/login/ServerConfig");
/**
- * A wire component which glues together login UI components and Signup logic
+ * A wire component which glues together login UI components and Login logic
*/
module.exports = React.createClass({
displayName: 'Login',
@@ -52,20 +53,20 @@ module.exports = React.createClass({
// login shouldn't care how password recovery is done.
onForgotPasswordClick: React.PropTypes.func,
onCancelClick: React.PropTypes.func,
-
- initialErrorText: React.PropTypes.string,
},
getInitialState: function() {
return {
busy: false,
- errorText: this.props.initialErrorText,
+ errorText: null,
loginIncorrect: false,
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
- // used for preserving username when changing homeserver
+ // used for preserving form values when changing homeserver
username: "",
+ phoneCountry: null,
+ phoneNumber: "",
};
},
@@ -73,20 +74,21 @@ module.exports = React.createClass({
this._initLoginLogic();
},
- onPasswordLogin: function(username, password) {
- var self = this;
- self.setState({
+ onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
+ this.setState({
busy: true,
errorText: null,
loginIncorrect: false,
});
- this._loginLogic.loginViaPassword(username, password).then(function(data) {
- self.props.onLoggedIn(data);
- }, function(error) {
- self._setStateFromError(error, true);
- }).finally(function() {
- self.setState({
+ this._loginLogic.loginViaPassword(
+ username, phoneCountry, phoneNumber, password,
+ ).then((data) => {
+ this.props.onLoggedIn(data);
+ }, (error) => {
+ this._setStateFromError(error, true);
+ }).finally(() => {
+ this.setState({
busy: false
});
}).done();
@@ -119,6 +121,14 @@ module.exports = React.createClass({
this.setState({ username: username });
},
+ onPhoneCountryChanged: function(phoneCountry) {
+ this.setState({ phoneCountry: phoneCountry });
+ },
+
+ onPhoneNumberChanged: function(phoneNumber) {
+ this.setState({ phoneNumber: phoneNumber });
+ },
+
onHsUrlChanged: function(newHsUrl) {
var self = this;
this.setState({
@@ -146,7 +156,7 @@ module.exports = React.createClass({
var fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
- var loginLogic = new Signup.Login(hsUrl, isUrl, fallbackHsUrl, {
+ var loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
});
this._loginLogic = loginLogic;
@@ -225,7 +235,11 @@ module.exports = React.createClass({
diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js
index db1147a5d2..4e0d61e716 100644
--- a/src/components/structures/login/Registration.js
+++ b/src/components/structures/login/Registration.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,25 +15,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-'use strict';
+import Matrix from 'matrix-js-sdk';
-var React = require('react');
+import q from 'q';
+import React from 'react';
-var sdk = require('../../../index');
-var dis = require('../../../dispatcher');
-var Signup = require("../../../Signup");
-var ServerConfig = require("../../views/login/ServerConfig");
-var MatrixClientPeg = require("../../../MatrixClientPeg");
-var RegistrationForm = require("../../views/login/RegistrationForm");
-var CaptchaForm = require("../../views/login/CaptchaForm");
-var RtsClient = require("../../../RtsClient");
+import sdk from '../../../index';
+import dis from '../../../dispatcher';
+import ServerConfig from '../../views/login/ServerConfig';
+import MatrixClientPeg from '../../../MatrixClientPeg';
+import RegistrationForm from '../../views/login/RegistrationForm';
+import CaptchaForm from '../../views/login/CaptchaForm';
+import RtsClient from '../../../RtsClient';
-var MIN_PASSWORD_LENGTH = 6;
+const MIN_PASSWORD_LENGTH = 6;
-/**
- * TODO: It would be nice to make use of the InteractiveAuthEntryComponents
- * here, rather than inventing our own.
- */
module.exports = React.createClass({
displayName: 'Registration',
@@ -40,7 +37,7 @@ module.exports = React.createClass({
onLoggedIn: React.PropTypes.func.isRequired,
clientSecret: React.PropTypes.string,
sessionId: React.PropTypes.string,
- registrationUrl: React.PropTypes.string,
+ makeRegistrationUrl: React.PropTypes.func.isRequired,
idSid: React.PropTypes.string,
customHsUrl: React.PropTypes.string,
customIsUrl: React.PropTypes.string,
@@ -58,7 +55,6 @@ module.exports = React.createClass({
teamServerURL: React.PropTypes.string.isRequired,
}),
teamSelected: React.PropTypes.object,
- onTeamMemberRegistered: React.PropTypes.func.isRequired,
defaultDeviceDisplayName: React.PropTypes.string,
@@ -82,27 +78,20 @@ module.exports = React.createClass({
formVals: {
email: this.props.email,
},
+ // true if we're waiting for the user to complete
+ // user-interactive auth
+ // If we've been given a session ID, we're resuming
+ // straight back into UI auth
+ doingUIAuth: Boolean(this.props.sessionId),
+ hsUrl: this.props.customHsUrl,
+ isUrl: this.props.customIsUrl,
};
},
componentWillMount: function() {
this._unmounted = false;
- this.dispatcherRef = dis.register(this.onAction);
- // attach this to the instance rather than this.state since it isn't UI
- this.registerLogic = new Signup.Register(
- this.props.customHsUrl, this.props.customIsUrl, {
- defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
- }
- );
- this.registerLogic.setClientSecret(this.props.clientSecret);
- this.registerLogic.setSessionId(this.props.sessionId);
- this.registerLogic.setRegistrationUrl(this.props.registrationUrl);
- this.registerLogic.setIdSid(this.props.idSid);
- this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
- if (this.props.referrer) {
- this.registerLogic.setReferrer(this.props.referrer);
- }
- this.registerLogic.recheckState();
+
+ this._replaceClient();
if (
this.props.teamServerConfig &&
@@ -134,154 +123,137 @@ module.exports = React.createClass({
}
},
- componentWillUnmount: function() {
- dis.unregister(this.dispatcherRef);
- this._unmounted = true;
- },
-
- componentDidMount: function() {
- // may have already done an HTTP hit (e.g. redirect from an email) so
- // check for any pending response
- var promise = this.registerLogic.getPromise();
- if (promise) {
- this.onProcessingRegistration(promise);
- }
- },
-
onHsUrlChanged: function(newHsUrl) {
- this.registerLogic.setHomeserverUrl(newHsUrl);
+ this.setState({
+ hsUrl: newHsUrl,
+ });
+ this._replaceClient();
},
onIsUrlChanged: function(newIsUrl) {
- this.registerLogic.setIdentityServerUrl(newIsUrl);
+ this.setState({
+ isUrl: newIsUrl,
+ });
+ this._replaceClient();
},
- onAction: function(payload) {
- if (payload.action !== "registration_step_update") {
- return;
- }
- // If the registration state has changed, this means the
- // user now needs to do something. It would be better
- // to expose the explicitly in the register logic.
- this.setState({
- busy: false
+ _replaceClient: function() {
+ this._matrixClient = Matrix.createClient({
+ baseUrl: this.state.hsUrl,
+ idBaseUrl: this.state.isUrl,
});
},
onFormSubmit: function(formVals) {
- var self = this;
this.setState({
errorText: "",
busy: true,
formVals: formVals,
+ doingUIAuth: true,
});
-
- if (formVals.username !== this.props.username) {
- // don't try to upgrade if we changed our username
- this.registerLogic.setGuestAccessToken(null);
- }
-
- this.onProcessingRegistration(this.registerLogic.register(formVals));
},
- // Promise is resolved when the registration process is FULLY COMPLETE
- onProcessingRegistration: function(promise) {
- var self = this;
- promise.done(function(response) {
- self.setState({
- busy: false
- });
- if (!response || !response.access_token) {
- console.warn(
- "FIXME: Register fulfilled without a final response, " +
- "did you break the promise chain?"
- );
- // no matter, we'll grab it direct
- response = self.registerLogic.getCredentials();
+ _onUIAuthFinished: function(success, response, extra) {
+ 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";
+ }
}
- if (!response || !response.user_id || !response.access_token) {
- console.error("Final response is missing keys.");
- self.setState({
- errorText: "Registration failed on server"
- });
- return;
- }
- self.props.onLoggedIn({
- userId: response.user_id,
- deviceId: response.device_id,
- homeserverUrl: self.registerLogic.getHomeserverUrl(),
- identityServerUrl: self.registerLogic.getIdentityServerUrl(),
- accessToken: response.access_token
+ this.setState({
+ busy: false,
+ doingUIAuth: false,
+ errorText: msg,
});
+ return;
+ }
- // Done regardless of `teamSelected`. People registering with non-team emails
- // will just nop. The point of this being we might not have the email address
- // that the user registered with at this stage (depending on whether this
- // is the client they initiated registration).
- if (self._rtsClient) {
- // Track referral if self.props.referrer set, get team_token in order to
- // retrieve team config and see welcome page etc.
- self._rtsClient.trackReferral(
- self.props.referrer || '', // Default to empty string = not referred
- self.registerLogic.params.idSid,
- self.registerLogic.params.clientSecret
- ).then((data) => {
- const teamToken = data.team_token;
- // Store for use /w welcome pages
- window.localStorage.setItem('mx_team_token', teamToken);
- self.props.onTeamMemberRegistered(teamToken);
+ this.setState({
+ // we're still busy until we get unmounted: don't show the registration form again
+ busy: true,
+ doingUIAuth: false,
+ });
- self._rtsClient.getTeam(teamToken).then((team) => {
- console.log(
- `User successfully registered with team ${team.name}`
- );
- if (!team.rooms) {
- return;
+ // Done regardless of `teamSelected`. People registering with non-team emails
+ // will just nop. The point of this being we might not have the email address
+ // that the user registered with at this stage (depending on whether this
+ // is the client they initiated registration).
+ let trackPromise = q(null);
+ if (this._rtsClient && extra.emailSid) {
+ // Track referral if this.props.referrer set, get team_token in order to
+ // retrieve team config and see welcome page etc.
+ trackPromise = this._rtsClient.trackReferral(
+ this.props.referrer || '', // Default to empty string = not referred
+ extra.emailSid,
+ extra.clientSecret,
+ ).then((data) => {
+ const teamToken = data.team_token;
+ // Store for use /w welcome pages
+ window.localStorage.setItem('mx_team_token', teamToken);
+
+ this._rtsClient.getTeam(teamToken).then((team) => {
+ console.log(
+ `User successfully registered with team ${team.name}`
+ );
+ if (!team.rooms) {
+ return;
+ }
+ // Auto-join rooms
+ team.rooms.forEach((room) => {
+ if (room.auto_join && room.room_id) {
+ console.log(`Auto-joining ${room.room_id}`);
+ MatrixClientPeg.get().joinRoom(room.room_id);
}
- // Auto-join rooms
- team.rooms.forEach((room) => {
- if (room.auto_join && room.room_id) {
- console.log(`Auto-joining ${room.room_id}`);
- MatrixClientPeg.get().joinRoom(room.room_id);
- }
- });
- }, (err) => {
- console.error('Error getting team config', err);
});
}, (err) => {
- console.error('Error tracking referral', err);
+ console.error('Error getting team config', err);
});
- }
- if (self.props.brand) {
- MatrixClientPeg.get().getPushers().done((resp)=>{
- var pushers = resp.pushers;
- for (var i = 0; i < pushers.length; ++i) {
- if (pushers[i].kind == 'email') {
- var emailPusher = pushers[i];
- emailPusher.data = { brand: self.props.brand };
- MatrixClientPeg.get().setPusher(emailPusher).done(() => {
- console.log("Set email branding to " + self.props.brand);
- }, (error) => {
- console.error("Couldn't set email branding: " + error);
- });
- }
- }
- }, (error) => {
- console.error("Couldn't get pushers: " + error);
- });
- }
-
- }, function(err) {
- if (err.message) {
- self.setState({
- errorText: err.message
- });
- }
- self.setState({
- busy: false
+ return teamToken;
+ }, (err) => {
+ console.error('Error tracking referral', err);
});
- console.log(err);
+ }
+
+ trackPromise.then((teamToken) => {
+ console.info('Team token promise',teamToken);
+ this.props.onLoggedIn({
+ userId: response.user_id,
+ deviceId: response.device_id,
+ homeserverUrl: this._matrixClient.getHomeserverUrl(),
+ identityServerUrl: this._matrixClient.getIdentityServerUrl(),
+ accessToken: response.access_token
+ }, teamToken);
+ }).then(() => {
+ return this._setupPushers();
+ });
+ },
+
+ _setupPushers: function() {
+ if (!this.props.brand) {
+ return q();
+ }
+ return MatrixClientPeg.get().getPushers().then((resp)=>{
+ const pushers = resp.pushers;
+ for (let i = 0; i < pushers.length; ++i) {
+ if (pushers[i].kind == 'email') {
+ const emailPusher = pushers[i];
+ emailPusher.data = { brand: this.props.brand };
+ MatrixClientPeg.get().setPusher(emailPusher).done(() => {
+ console.log("Set email branding to " + this.props.brand);
+ }, (error) => {
+ console.error("Couldn't set email branding: " + error);
+ });
+ }
+ }
+ }, (error) => {
+ console.error("Couldn't get pushers: " + error);
});
},
@@ -300,6 +272,9 @@ module.exports = React.createClass({
case "RegistrationForm.ERR_EMAIL_INVALID":
errMsg = "This doesn't look like a valid email address";
break;
+ case "RegistrationForm.ERR_PHONE_NUMBER_INVALID":
+ errMsg = "This doesn't look like a valid phone number";
+ break;
case "RegistrationForm.ERR_USERNAME_INVALID":
errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores.";
break;
@@ -316,116 +291,121 @@ module.exports = React.createClass({
});
},
- onCaptchaResponse: function(response) {
- this.registerLogic.tellStage("m.login.recaptcha", {
- response: response
- });
- },
-
onTeamSelected: function(teamSelected) {
if (!this._unmounted) {
this.setState({ teamSelected });
}
},
- _getRegisterContentJsx: function() {
- const Spinner = sdk.getComponent("elements.Spinner");
+ _makeRegisterRequest: function(auth) {
+ let guestAccessToken = this.props.guestAccessToken;
- var currStep = this.registerLogic.getStep();
- var registerStep;
- switch (currStep) {
- case "Register.COMPLETE":
- break; // NOP
- case "Register.START":
- case "Register.STEP_m.login.dummy":
- // NB. Our 'username' prop is specifically for upgrading
- // a guest account
- if (this.state.teamServerBusy) {
- registerStep = ;
- break;
- }
- registerStep = (
+ if (
+ this.state.formVals.username !== this.props.username ||
+ this.state.hsUrl != this.props.defaultHsUrl
+ ) {
+ // don't try to upgrade if we changed our username
+ // or are registering on a different HS
+ 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(
+ this.state.formVals.username,
+ this.state.formVals.password,
+ undefined, // session id: included in the auth dict already
+ auth,
+ bindThreepids,
+ guestAccessToken,
+ );
+ },
+
+ _getUIAuthInputs: function() {
+ return {
+ emailAddress: this.state.formVals.email,
+ phoneCountry: this.state.formVals.phoneCountry,
+ phoneNumber: this.state.formVals.phoneNumber,
+ }
+ },
+
+ render: function() {
+ const LoginHeader = sdk.getComponent('login.LoginHeader');
+ const LoginFooter = sdk.getComponent('login.LoginFooter');
+ const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
+ const Spinner = sdk.getComponent("elements.Spinner");
+ const ServerConfig = sdk.getComponent('views.login.ServerConfig');
+
+ let registerBody;
+ if (this.state.doingUIAuth) {
+ registerBody = (
+
+ );
+ } else if (this.state.busy || this.state.teamServerBusy) {
+ registerBody = ;
+ } else {
+ let guestUsername = this.props.username;
+ if (this.state.hsUrl != this.props.defaultHsUrl) {
+ guestUsername = null;
+ }
+ let errorSection;
+ if (this.state.errorText) {
+ errorSection =
);
diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js
index 3bebb8fdda..da9c8e8f65 100644
--- a/src/components/views/dialogs/UnknownDeviceDialog.js
+++ b/src/components/views/dialogs/UnknownDeviceDialog.js
@@ -16,8 +16,10 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
+import dis from '../../../dispatcher';
import MatrixClientPeg from '../../../MatrixClientPeg';
import GeminiScrollbar from 'react-gemini-scrollbar';
+import Resend from '../../../Resend';
function DeviceListEntry(props) {
const {userId, device} = props;
@@ -85,7 +87,7 @@ UnknownDeviceList.propTypes = {
export default React.createClass({
- displayName: 'UnknownEventDialog',
+ displayName: 'UnknownDeviceDialog',
propTypes: {
room: React.PropTypes.object.isRequired,
@@ -125,14 +127,10 @@ export default React.createClass({
} else {
warning = (
-
- 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.