diff --git a/CHANGELOG.md b/CHANGELOG.md
index 488a9814e6..97dda666de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,270 @@
+Changes in [0.8.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8) (2017-04-25)
+===================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.2...v0.8.8)
+
+ * No changes
+
+
+Changes in [0.8.8-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8-rc.2) (2017-04-24)
+=============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.1...v0.8.8-rc.2)
+
+ * Fix bug where links to Riot would fail to open.
+
+
+Changes in [0.8.8-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8-rc.1) (2017-04-21)
+=============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7...v0.8.8-rc.1)
+
+ * Update js-sdk to fix registration without a captcha (https://github.com/vector-im/riot-web/issues/3621)
+
+
+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/karma.conf.js b/karma.conf.js
index 6d3047bb3b..3495a981be 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -135,17 +135,24 @@ module.exports = function (config) {
},
],
noParse: [
+ // for cross platform compatibility use [\\\/] as the path separator
+ // this ensures that the regex trips on both Windows and *nix
+
// don't parse the languages within highlight.js. They
// cause stack overflows
// (https://github.com/webpack/webpack/issues/1721), and
// there is no need for webpack to parse them - they can
// just be included as-is.
- /highlight\.js\/lib\/languages/,
+ /highlight\.js[\\\/]lib[\\\/]languages/,
+
+ // olm takes ages for webpack to process, and it's already heavily
+ // optimised, so there is little to gain by us uglifying it.
+ /olm[\\\/](javascript[\\\/])?olm\.js$/,
// also disable parsing for sinon, because it
// tries to do voodoo with 'require' which upsets
// webpack (https://github.com/webpack/webpack/issues/304)
- /sinon\/pkg\/sinon\.js$/,
+ /sinon[\\\/]pkg[\\\/]sinon\.js$/,
],
},
resolve: {
diff --git a/package.json b/package.json
index 1015eb3fe9..8a1baa6a0b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "0.8.6",
+ "version": "0.8.8",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@@ -32,8 +32,8 @@
},
"scripts": {
"reskindex": "scripts/reskindex.js -h header",
- "build": "node scripts/babelcheck.js && babel src -d lib --source-maps",
- "start": "node scripts/babelcheck.js && babel src -w -d lib --source-maps",
+ "build": "babel src -d lib --source-maps",
+ "start": "babel src -w -d lib --source-maps",
"lint": "eslint src/",
"lintall": "eslint src/ test/",
"clean": "rimraf lib",
@@ -53,7 +53,7 @@
"draft-js-export-markdown": "^0.2.0",
"emojione": "2.2.3",
"file-saver": "^1.3.3",
- "filesize": "^3.1.2",
+ "filesize": "3.5.6",
"flux": "^2.0.3",
"glob": "^5.0.14",
"highlight.js": "^8.9.1",
@@ -63,11 +63,12 @@
"lodash": "^4.13.1",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
"optimist": "^0.6.1",
+ "prop-types": "^15.5.8",
"q": "^1.4.1",
"react": "^15.4.0",
"react-addons-css-transition-group": "15.3.2",
"react-dom": "^15.4.0",
- "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
+ "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#39d858c",
"sanitize-html": "^1.11.1",
"text-encoding-utf-8": "^1.0.1",
"velocity-vector": "vector-im/velocity#059e3b2",
diff --git a/scripts/babelcheck.js b/scripts/babelcheck.js
deleted file mode 100644
index 14e4a28a70..0000000000
--- a/scripts/babelcheck.js
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env node
-
-var exec = require('child_process').exec;
-
-// Makes sure the babel executable in the path is babel 6 (or greater), not
-// babel 5, which it is if you upgrade from an older version of react-sdk and
-// run 'npm install' since the package has changed to babel-cli, so 'babel'
-// remains installed and the executable in node_modules/.bin remains as babel
-// 5.
-
-exec("babel -V", function (error, stdout, stderr) {
- if ((error && error.code) || parseInt(stdout.substr(0,1), 10) < 6) {
- console.log("\033[31m\033[1m"+
- '*****************************************\n'+
- '* matrix-react-sdk has moved to babel 6 *\n'+
- '* Please "rm -rf node_modules && npm i" *\n'+
- '* then restore links as appropriate *\n'+
- '*****************************************\n'+
- "\033[91m");
- process.exit(1);
- }
-});
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/Avatar.js b/src/Avatar.js
index 76f5e55ff0..c0127d49af 100644
--- a/src/Avatar.js
+++ b/src/Avatar.js
@@ -22,8 +22,8 @@ module.exports = {
avatarUrlForMember: function(member, width, height, resizeMethod) {
var url = member.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
- width,
- height,
+ Math.floor(width * window.devicePixelRatio),
+ Math.floor(height * window.devicePixelRatio),
resizeMethod,
false,
false
@@ -40,7 +40,9 @@ module.exports = {
avatarUrlForUser: function(user, width, height, resizeMethod) {
var url = ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
- width, height, resizeMethod
+ Math.floor(width * window.devicePixelRatio),
+ Math.floor(height * window.devicePixelRatio),
+ resizeMethod
);
if (!url || url.length === 0) {
return null;
@@ -57,4 +59,3 @@ module.exports = {
return 'img/' + images[total % images.length] + '.png';
}
};
-
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 bb46056d19..5199ef0a67 100644
--- a/src/CallHandler.js
+++ b/src/CallHandler.js
@@ -310,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. " + ((err && err.message) ? err.message : ""),
});
});
}
diff --git a/src/ConstantTimeDispatcher.js b/src/ConstantTimeDispatcher.js
new file mode 100644
index 0000000000..6c2c3266aa
--- /dev/null
+++ b/src/ConstantTimeDispatcher.js
@@ -0,0 +1,62 @@
+/*
+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.
+*/
+
+// singleton which dispatches invocations of a given type & argument
+// rather than just a type (as per EventEmitter and Flux's dispatcher etc)
+//
+// This means you can have a single point which listens for an EventEmitter event
+// and then dispatches out to one of thousands of RoomTiles (for instance) rather than
+// having each RoomTile register for the EventEmitter event and having to
+// iterate over all of them.
+class ConstantTimeDispatcher {
+ constructor() {
+ // type -> arg -> [ listener(arg, params) ]
+ this.listeners = {};
+ }
+
+ register(type, arg, listener) {
+ if (!this.listeners[type]) this.listeners[type] = {};
+ if (!this.listeners[type][arg]) this.listeners[type][arg] = [];
+ this.listeners[type][arg].push(listener);
+ }
+
+ unregister(type, arg, listener) {
+ if (this.listeners[type] && this.listeners[type][arg]) {
+ var i = this.listeners[type][arg].indexOf(listener);
+ if (i > -1) {
+ this.listeners[type][arg].splice(i, 1);
+ }
+ }
+ else {
+ console.warn("Unregistering unrecognised listener (type=" + type + ", arg=" + arg + ")");
+ }
+ }
+
+ dispatch(type, arg, params) {
+ if (!this.listeners[type] || !this.listeners[type][arg]) {
+ //console.warn("No registered listeners for dispatch (type=" + type + ", arg=" + arg + ")");
+ return;
+ }
+ this.listeners[type][arg].forEach(listener=>{
+ listener.call(arg, params);
+ });
+ }
+}
+
+if (!global.constantTimeDispatcher) {
+ global.constantTimeDispatcher = new ConstantTimeDispatcher();
+}
+module.exports = global.constantTimeDispatcher;
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 c500076783..a31601790f 100644
--- a/src/HtmlUtils.js
+++ b/src/HtmlUtils.js
@@ -25,6 +25,9 @@ import emojione from 'emojione';
import classNames from 'classnames';
emojione.imagePathSVG = 'emojione/svg/';
+// Store PNG path for displaying many flags at once (for increased performance over SVG)
+emojione.imagePathPNG = 'emojione/png/';
+// Use SVGs for emojis
emojione.imageType = 'svg';
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
@@ -58,6 +61,29 @@ 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 useSvg {boolean} Whether to use SVG image src. If False, PNG will be used.
+ * @param unicode {integer} One or more integers representing unicode characters
+ * @returns A img node with the corresponding emoji
+ */
+export function charactersToImageNode(alt, useSvg, ...unicode) {
+ const fileName = unicode.map((u) => {
+ return u.toString(16);
+ }).join('-');
+ const path = useSvg ? emojione.imagePathSVG : emojione.imagePathPNG;
+ const fileType = useSvg ? 'svg' : 'png';
+ return ;
+}
+
+
export function stripParagraphs(html: string): string {
const contentDiv = document.createElement('div');
contentDiv.innerHTML = html;
@@ -85,8 +111,7 @@ var sanitizeHtmlParams = {
allowedTags: [
'font', // custom to matrix for IRC-style font coloring
'del', // for markdown
- // deliberately no h1/h2 to stop people shouting.
- 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
+ 'h1', 'h2', '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', 'span',
],
@@ -98,6 +123,7 @@ var sanitizeHtmlParams = {
// 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'],
diff --git a/src/KeyCode.js b/src/KeyCode.js
index c9cac01239..f164dbc15c 100644
--- a/src/KeyCode.js
+++ b/src/KeyCode.js
@@ -32,4 +32,5 @@ module.exports = {
DELETE: 46,
KEY_D: 68,
KEY_E: 69,
+ KEY_K: 75,
};
diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index fc8087e12d..f34aeae0e5 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -49,7 +49,7 @@ import sdk from './index';
* If any of steps 1-4 are successful, it will call {setLoggedIn}, which in
* turn will raise on_logged_in and will_start_client events.
*
- * It returns a promise which resolves when the above process completes.
+ * @param {object} opts
*
* @param {object} opts.realQueryParams: string->string map of the
* query-parameters extracted from the real query-string of the starting
@@ -67,6 +67,7 @@ import sdk from './index';
* @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is
* true; defines the IS to use.
*
+ * @returns {Promise} a promise which resolves when the above process completes.
*/
export function loadSession(opts) {
const realQueryParams = opts.realQueryParams || {};
@@ -127,7 +128,7 @@ export function loadSession(opts) {
function _loginWithToken(queryParams, defaultDeviceDisplayName) {
// create a temporary MatrixClient to do the login
- var client = Matrix.createClient({
+ const client = Matrix.createClient({
baseUrl: queryParams.homeserver,
});
@@ -159,7 +160,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
// Not really sure where the right home for it is.
// create a temporary MatrixClient to do the login
- var client = Matrix.createClient({
+ const client = Matrix.createClient({
baseUrl: hsUrl,
});
@@ -188,30 +189,30 @@ function _restoreFromLocalStorage() {
if (!localStorage) {
return q(false);
}
- const hs_url = localStorage.getItem("mx_hs_url");
- const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
- const access_token = localStorage.getItem("mx_access_token");
- const user_id = localStorage.getItem("mx_user_id");
- const device_id = localStorage.getItem("mx_device_id");
+ const hsUrl = localStorage.getItem("mx_hs_url");
+ const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
+ const accessToken = localStorage.getItem("mx_access_token");
+ const userId = localStorage.getItem("mx_user_id");
+ const deviceId = localStorage.getItem("mx_device_id");
- let is_guest;
+ let isGuest;
if (localStorage.getItem("mx_is_guest") !== null) {
- is_guest = localStorage.getItem("mx_is_guest") === "true";
+ isGuest = localStorage.getItem("mx_is_guest") === "true";
} else {
// legacy key name
- is_guest = localStorage.getItem("matrix-is-guest") === "true";
+ isGuest = localStorage.getItem("matrix-is-guest") === "true";
}
- if (access_token && user_id && hs_url) {
- console.log("Restoring session for %s", user_id);
+ if (accessToken && userId && hsUrl) {
+ console.log("Restoring session for %s", userId);
try {
setLoggedIn({
- userId: user_id,
- deviceId: device_id,
- accessToken: access_token,
- homeserverUrl: hs_url,
- identityServerUrl: is_url,
- guest: is_guest,
+ userId: userId,
+ deviceId: deviceId,
+ accessToken: accessToken,
+ homeserverUrl: hsUrl,
+ identityServerUrl: isUrl,
+ guest: isGuest,
});
return q(true);
} catch (e) {
@@ -273,9 +274,18 @@ export function initRtsClient(url) {
*/
export function setLoggedIn(credentials) {
credentials.guest = Boolean(credentials.guest);
- console.log("setLoggedIn => %s (guest=%s) hs=%s",
- credentials.userId, credentials.guest,
- credentials.homeserverUrl);
+
+ console.log(
+ "setLoggedIn: mxid:", credentials.userId,
+ "deviceId:", credentials.deviceId,
+ "guest:", credentials.guest,
+ "hs:", 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);
@@ -347,7 +357,7 @@ export function logout() {
return;
}
- return MatrixClientPeg.get().logout().then(onLoggedOut,
+ MatrixClientPeg.get().logout().then(onLoggedOut,
(err) => {
// Just throwing an error here is going to be very unhelpful
// if you're trying to log out because your server's down and
@@ -358,8 +368,8 @@ export function logout() {
// change your password).
console.log("Failed to call logout API: token will not be invalidated");
onLoggedOut();
- }
- );
+ },
+ ).done();
}
/**
@@ -415,7 +425,7 @@ export function stopMatrixClient() {
UserActivity.stop();
Presence.stop();
if (DMRoomMap.shared()) DMRoomMap.shared().stop();
- var cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.get();
if (cli) {
cli.stopClient();
cli.removeAllListeners();
diff --git a/src/Login.js b/src/Login.js
index 96f953c130..107a8825e9 100644
--- a/src/Login.js
+++ b/src/Login.js
@@ -105,21 +105,48 @@ export default class Login {
});
}
- loginViaPassword(username, pass) {
- var self = this;
- var isEmail = username.indexOf("@") > 0;
- var loginParams = {
- password: pass,
- initial_device_display_name: this._defaultDeviceDisplayName,
- };
- if (isEmail) {
- loginParams.medium = 'email';
- loginParams.address = username;
+ loginViaPassword(username, phoneCountry, phoneNumber, pass) {
+ const self = this;
+
+ const isEmail = username.indexOf("@") > 0;
+
+ let identifier;
+ let legacyParams; // parameters added to support old HSes
+ if (phoneCountry && phoneNumber) {
+ identifier = {
+ type: 'm.id.phone',
+ country: phoneCountry,
+ number: phoneNumber,
+ };
+ // No legacy support for phone number login
+ } else if (isEmail) {
+ identifier = {
+ type: 'm.id.thirdparty',
+ medium: 'email',
+ address: username,
+ };
+ legacyParams = {
+ medium: 'email',
+ address: username,
+ };
} else {
- loginParams.user = username;
+ identifier = {
+ type: 'm.id.user',
+ user: username,
+ };
+ legacyParams = {
+ user: username,
+ };
}
- var client = this._createTemporaryClient();
+ const loginParams = {
+ password: pass,
+ identifier: identifier,
+ initial_device_display_name: this._defaultDeviceDisplayName,
+ };
+ Object.assign(loginParams, legacyParams);
+
+ const client = this._createTemporaryClient();
return client.login('m.login.password', loginParams).then(function(data) {
return q({
homeserverUrl: self._hsUrl,
diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js
index baa3293073..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 {
@@ -125,12 +137,12 @@ class MatrixClientPeg {
// FIXME: bodge to remove old database. Remove this after a few weeks.
window.indexedDB.deleteDatabase("matrix-js-sdk:default");
- opts.store = new Matrix.IndexedDBStore(
- new Matrix.IndexedDBStoreBackend(window.indexedDB, "riot-web-sync"),
- new Matrix.SyncAccumulator(), {
- localStorage: localStorage,
- }
- );
+ 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..6473ab4d9c 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,13 +15,13 @@ 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');
-var Avatar = require('./Avatar');
-var dis = require("./dispatcher");
+import MatrixClientPeg from './MatrixClientPeg';
+import PlatformPeg from './PlatformPeg';
+import TextForEvent from './TextForEvent';
+import Avatar from './Avatar';
+import dis from './dispatcher';
+import sdk from './index';
+import Modal from './Modal';
/*
* Dispatches:
@@ -30,7 +31,7 @@ var dis = require("./dispatcher");
* }
*/
-var Notifier = {
+const Notifier = {
notifsByRoom: {},
notificationMessageForEvent: function(ev) {
@@ -49,16 +50,16 @@ var Notifier = {
return;
}
- var msg = this.notificationMessageForEvent(ev);
+ let msg = this.notificationMessageForEvent(ev);
if (!msg) return;
- var title;
- if (!ev.sender || room.name == ev.sender.name) {
+ let title;
+ if (!ev.sender || room.name === ev.sender.name) {
title = room.name;
// notificationMessageForEvent includes sender,
// but we already have the sender here
if (ev.getContent().body) msg = ev.getContent().body;
- } else if (ev.getType() == 'm.room.member') {
+ } else if (ev.getType() === 'm.room.member') {
// context is all in the message here, we don't need
// to display sender info
title = room.name;
@@ -69,7 +70,7 @@ var Notifier = {
if (ev.getContent().body) msg = ev.getContent().body;
}
- var avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
+ const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
ev.sender, 40, 40, 'crop'
) : null;
@@ -84,7 +85,7 @@ var Notifier = {
},
_playAudioNotification: function(ev, room) {
- var e = document.getElementById("messageAudio");
+ const e = document.getElementById("messageAudio");
if (e) {
e.load();
e.play();
@@ -96,19 +97,19 @@ var Notifier = {
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
- MatrixClientPeg.get().on("Room.receipt", this.boundOnRoomReceipt);
+ 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('Room.receipt', this.boundOnRoomReceipt);
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
}
- this.isPrepared = false;
+ this.isSyncing = false;
},
supportsDesktopNotifications: function() {
@@ -122,7 +123,7 @@ var Notifier = {
// make sure that we persist the current setting audio_enabled setting
// before changing anything
if (global.localStorage) {
- if(global.localStorage.getItem('audio_notifications_enabled') == null) {
+ if (global.localStorage.getItem('audio_notifications_enabled') === null) {
this.setAudioEnabled(this.isEnabled());
}
}
@@ -132,6 +133,16 @@ var Notifier = {
plaf.requestNotificationPermission().done((result) => {
if (result !== 'granted') {
// The permission request was dismissed or denied
+ const description = result === 'denied'
+ ? 'Riot does not have permission to send you notifications'
+ + ' - please check your browser settings'
+ : 'Riot was not given permission to send notifications'
+ + ' - please try again';
+ const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
+ Modal.createDialog(ErrorDialog, {
+ title: 'Unable to enable Notifications',
+ description,
+ });
return;
}
@@ -142,7 +153,7 @@ var Notifier = {
if (callback) callback();
dis.dispatch({
action: "notifier_enabled",
- value: true
+ value: true,
});
});
// clear the notifications_hidden flag, so that if notifications are
@@ -153,7 +164,7 @@ var Notifier = {
global.localStorage.setItem('notifications_enabled', 'false');
dis.dispatch({
action: "notifier_enabled",
- value: false
+ value: false,
});
}
},
@@ -166,7 +177,7 @@ var Notifier = {
if (!global.localStorage) return true;
- var enabled = global.localStorage.getItem('notifications_enabled');
+ const enabled = global.localStorage.getItem('notifications_enabled');
if (enabled === null) return true;
return enabled === 'true';
},
@@ -174,12 +185,12 @@ var Notifier = {
setAudioEnabled: function(enable) {
if (!global.localStorage) return;
global.localStorage.setItem('audio_notifications_enabled',
- enable ? 'true' : 'false');
+ enable ? 'true' : 'false');
},
isAudioEnabled: function(enable) {
if (!global.localStorage) return true;
- var enabled = global.localStorage.getItem(
+ const enabled = global.localStorage.getItem(
'audio_notifications_enabled');
// default to true if the popups are enabled
if (enabled === null) return this.isEnabled();
@@ -193,7 +204,7 @@ var Notifier = {
// this is nothing to do with notifier_enabled
dis.dispatch({
action: "notifier_enabled",
- value: this.isEnabled()
+ value: this.isEnabled(),
});
// update the info to localStorage for persistent settings
@@ -214,22 +225,21 @@ var Notifier = {
},
onSyncStateChange: function(state) {
- if (state === "PREPARED" || state === "SYNCING") {
- this.isPrepared = true;
- }
- else if (state === "STOPPED" || state === "ERROR") {
- this.isPrepared = false;
+ if (state === "SYNCING") {
+ this.isSyncing = true;
+ } else if (state === "STOPPED" || state === "ERROR") {
+ 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 (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return;
+ 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;
- var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
+ const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
if (actions && actions.notify) {
if (this.isEnabled()) {
this._displayPopupNotification(ev, room);
@@ -241,7 +251,7 @@ var Notifier = {
},
onRoomReceipt: function(ev, room) {
- if (room.getUnreadNotificationCount() == 0) {
+ if (room.getUnreadNotificationCount() === 0) {
// ideally we would clear each notification when it was read,
// but we have no way, given a read receipt, to know whether
// the receipt comes before or after an event, so we can't
@@ -256,7 +266,7 @@ var Notifier = {
}
delete this.notifsByRoom[room.roomId];
}
- }
+ },
};
if (!global.mxNotifier) {
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/TextForEvent.js b/src/TextForEvent.js
index 3f772e9cfb..3f200a089d 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();
@@ -63,8 +65,8 @@ function textForMemberEvent(ev) {
} else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
return senderName + " set a profile picture";
} else {
- // hacky hack for https://github.com/vector-im/vector-web/issues/2020
- return senderName + " rejoined the room.";
+ // suppress null rejoins
+ return '';
}
} else {
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
@@ -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
index 88f4f57fe4..2aa0573e22 100644
--- a/src/UnknownDeviceErrorHandler.js
+++ b/src/UnknownDeviceErrorHandler.js
@@ -1,14 +1,34 @@
+/*
+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') {
+ 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);
diff --git a/src/UserActivity.js b/src/UserActivity.js
index e7338e17e9..1ae272f5df 100644
--- a/src/UserActivity.js
+++ b/src/UserActivity.js
@@ -32,7 +32,7 @@ class UserActivity {
start() {
document.onmousedown = this._onUserActivity.bind(this);
document.onmousemove = this._onUserActivity.bind(this);
- document.onkeypress = this._onUserActivity.bind(this);
+ document.onkeydown = this._onUserActivity.bind(this);
// can't use document.scroll here because that's only the document
// itself being scrolled. Need to use addEventListener's useCapture.
// also this needs to be the wheel event, not scroll, as scroll is
@@ -50,7 +50,7 @@ class UserActivity {
stop() {
document.onmousedown = undefined;
document.onmousemove = undefined;
- document.onkeypress = undefined;
+ document.onkeydown = undefined;
window.removeEventListener('wheel', this._onUserActivity.bind(this),
{ passive: true, capture: true });
}
diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js
index 0ee78b4f2e..9de291249f 100644
--- a/src/UserSettingsStore.js
+++ b/src/UserSettingsStore.js
@@ -15,9 +15,9 @@ limitations under the License.
*/
'use strict';
-var q = require("q");
-var MatrixClientPeg = require("./MatrixClientPeg");
-var Notifier = require("./Notifier");
+import q from 'q';
+import MatrixClientPeg from './MatrixClientPeg';
+import Notifier from './Notifier';
/*
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
@@ -33,7 +33,7 @@ module.exports = {
],
loadProfileInfo: function() {
- var cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.get();
return cli.getProfileInfo(cli.credentials.userId);
},
@@ -44,7 +44,7 @@ module.exports = {
loadThreePids: function() {
if (MatrixClientPeg.get().isGuest()) {
return q({
- threepids: []
+ threepids: [],
}); // guests can't poke 3pid endpoint
}
return MatrixClientPeg.get().getThreePids();
@@ -73,19 +73,19 @@ module.exports = {
Notifier.setAudioEnabled(enable);
},
- changePassword: function(old_password, new_password) {
- var cli = MatrixClientPeg.get();
+ changePassword: function(oldPassword, newPassword) {
+ const cli = MatrixClientPeg.get();
- var authDict = {
+ const authDict = {
type: 'm.login.password',
user: cli.credentials.userId,
- password: old_password
+ password: oldPassword,
};
- return cli.setPassword(authDict, new_password);
+ return cli.setPassword(authDict, newPassword);
},
- /**
+ /*
* Returns the email pusher (pusher of type 'email') for a given
* email address. Email pushers all have the same app ID, so since
* pushers are unique over (app ID, pushkey), there will be at most
@@ -95,8 +95,8 @@ module.exports = {
if (pushers === undefined) {
return undefined;
}
- for (var i = 0; i < pushers.length; ++i) {
- if (pushers[i].kind == 'email' && pushers[i].pushkey == address) {
+ for (let i = 0; i < pushers.length; ++i) {
+ if (pushers[i].kind === 'email' && pushers[i].pushkey === address) {
return pushers[i];
}
}
@@ -110,7 +110,7 @@ module.exports = {
addEmailPusher: function(address, data) {
return MatrixClientPeg.get().setPusher({
kind: 'email',
- app_id: "m.email",
+ app_id: 'm.email',
pushkey: address,
app_display_name: 'Email Notifications',
device_display_name: address,
@@ -121,46 +121,46 @@ module.exports = {
},
getUrlPreviewsDisabled: function() {
- var event = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls");
+ const event = MatrixClientPeg.get().getAccountData('org.matrix.preview_urls');
return (event && event.getContent().disable);
},
setUrlPreviewsDisabled: function(disabled) {
// FIXME: handle errors
- return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", {
- disable: disabled
+ return MatrixClientPeg.get().setAccountData('org.matrix.preview_urls', {
+ disable: disabled,
});
},
getSyncedSettings: function() {
- var event = MatrixClientPeg.get().getAccountData("im.vector.web.settings");
+ const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings');
return event ? event.getContent() : {};
},
getSyncedSetting: function(type, defaultValue = null) {
- var settings = this.getSyncedSettings();
+ const settings = this.getSyncedSettings();
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
},
setSyncedSetting: function(type, value) {
- var settings = this.getSyncedSettings();
+ const settings = this.getSyncedSettings();
settings[type] = value;
// FIXME: handle errors
- return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings);
+ return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings);
},
getLocalSettings: function() {
- var localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
+ const localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
return JSON.parse(localSettingsString);
},
getLocalSetting: function(type, defaultValue = null) {
- var settings = this.getLocalSettings();
+ const settings = this.getLocalSettings();
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
},
setLocalSetting: function(type, value) {
- var settings = this.getLocalSettings();
+ const settings = this.getLocalSettings();
settings[type] = value;
// FIXME: handle errors
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
@@ -171,8 +171,8 @@ module.exports = {
if (MatrixClientPeg.get().isGuest()) return false;
if (localStorage.getItem(`mx_labs_feature_${feature}`) === null) {
- for (var i = 0; i < this.LABS_FEATURES.length; i++) {
- var f = this.LABS_FEATURES[i];
+ for (let i = 0; i < this.LABS_FEATURES.length; i++) {
+ const f = this.LABS_FEATURES[i];
if (f.id === feature) {
return f.default;
}
@@ -183,5 +183,5 @@ module.exports = {
setFeatureEnabled: function(feature: string, enabled: boolean) {
localStorage.setItem(`mx_labs_feature_${feature}`, enabled);
- }
+ },
};
diff --git a/src/component-index.js b/src/component-index.js
index c705150e12..090a27d5ed 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';
@@ -99,26 +103,40 @@ import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/Unknow
views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog);
import views$elements$AccessibleButton from './components/views/elements/AccessibleButton';
views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton);
+import views$elements$ActionButton from './components/views/elements/ActionButton';
+views$elements$ActionButton && (module.exports.components['views.elements.ActionButton'] = views$elements$ActionButton);
import views$elements$AddressSelector from './components/views/elements/AddressSelector';
views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector);
import views$elements$AddressTile from './components/views/elements/AddressTile';
views$elements$AddressTile && (module.exports.components['views.elements.AddressTile'] = views$elements$AddressTile);
+import views$elements$CreateRoomButton from './components/views/elements/CreateRoomButton';
+views$elements$CreateRoomButton && (module.exports.components['views.elements.CreateRoomButton'] = views$elements$CreateRoomButton);
import views$elements$DeviceVerifyButtons from './components/views/elements/DeviceVerifyButtons';
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';
views$elements$EditableTextContainer && (module.exports.components['views.elements.EditableTextContainer'] = views$elements$EditableTextContainer);
import views$elements$EmojiText from './components/views/elements/EmojiText';
views$elements$EmojiText && (module.exports.components['views.elements.EmojiText'] = views$elements$EmojiText);
+import views$elements$HomeButton from './components/views/elements/HomeButton';
+views$elements$HomeButton && (module.exports.components['views.elements.HomeButton'] = views$elements$HomeButton);
import views$elements$MemberEventListSummary from './components/views/elements/MemberEventListSummary';
views$elements$MemberEventListSummary && (module.exports.components['views.elements.MemberEventListSummary'] = views$elements$MemberEventListSummary);
import views$elements$PowerSelector from './components/views/elements/PowerSelector';
views$elements$PowerSelector && (module.exports.components['views.elements.PowerSelector'] = views$elements$PowerSelector);
import views$elements$ProgressBar from './components/views/elements/ProgressBar';
views$elements$ProgressBar && (module.exports.components['views.elements.ProgressBar'] = views$elements$ProgressBar);
+import views$elements$RoomDirectoryButton from './components/views/elements/RoomDirectoryButton';
+views$elements$RoomDirectoryButton && (module.exports.components['views.elements.RoomDirectoryButton'] = views$elements$RoomDirectoryButton);
+import views$elements$SettingsButton from './components/views/elements/SettingsButton';
+views$elements$SettingsButton && (module.exports.components['views.elements.SettingsButton'] = views$elements$SettingsButton);
+import views$elements$StartChatButton from './components/views/elements/StartChatButton';
+views$elements$StartChatButton && (module.exports.components['views.elements.StartChatButton'] = views$elements$StartChatButton);
import views$elements$TintableSvg from './components/views/elements/TintableSvg';
views$elements$TintableSvg && (module.exports.components['views.elements.TintableSvg'] = views$elements$TintableSvg);
import views$elements$TruncatedList from './components/views/elements/TruncatedList';
@@ -129,6 +147,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 +241,8 @@ import views$rooms$TopUnreadMessagesBar from './components/views/rooms/TopUnread
views$rooms$TopUnreadMessagesBar && (module.exports.components['views.rooms.TopUnreadMessagesBar'] = views$rooms$TopUnreadMessagesBar);
import views$rooms$UserTile from './components/views/rooms/UserTile';
views$rooms$UserTile && (module.exports.components['views.rooms.UserTile'] = views$rooms$UserTile);
+import views$settings$AddPhoneNumber from './components/views/settings/AddPhoneNumber';
+views$settings$AddPhoneNumber && (module.exports.components['views.settings.AddPhoneNumber'] = views$settings$AddPhoneNumber);
import views$settings$ChangeAvatar from './components/views/settings/ChangeAvatar';
views$settings$ChangeAvatar && (module.exports.components['views.settings.ChangeAvatar'] = views$settings$ChangeAvatar);
import views$settings$ChangeDisplayName from './components/views/settings/ChangeDisplayName';
diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js
index 71fee883be..7c8a5b8065 100644
--- a/src/components/structures/InteractiveAuth.js
+++ b/src/components/structures/InteractiveAuth.js
@@ -140,13 +140,20 @@ export default React.createClass({
});
},
- _requestCallback: function(auth) {
+ _requestCallback: function(auth, background) {
+ const makeRequestPromise = this.props.makeRequest(auth);
+
+ // if it's a background request, just do it: we don't want
+ // it to affect the state of our UI.
+ if (background) return makeRequestPromise;
+
+ // otherwise, manage the state of the spinner and error messages
this.setState({
busy: true,
errorText: null,
stageErrorText: null,
});
- return this.props.makeRequest(auth).finally(() => {
+ return makeRequestPromise.finally(() => {
if (this._unmounted) {
return;
}
diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js
index c2243820cd..c4eeb03d5f 100644
--- a/src/components/structures/LoggedInView.js
+++ b/src/components/structures/LoggedInView.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.
@@ -81,6 +82,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
@@ -99,9 +107,21 @@ 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) {
+ if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
var action = ev.keyCode == KeyCode.UP ?
'view_prev_room' : 'view_next_room';
dis.dispatch({action: action});
@@ -111,13 +131,15 @@ export default React.createClass({
case KeyCode.PAGE_UP:
case KeyCode.PAGE_DOWN:
- this._onScrollKeyPressed(ev);
- handled = true;
+ if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
+ this._onScrollKeyPressed(ev);
+ handled = true;
+ }
break;
case KeyCode.HOME:
case KeyCode.END:
- if (ev.ctrlKey) {
+ if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this._onScrollKeyPressed(ev);
handled = true;
}
@@ -135,22 +157,25 @@ export default React.createClass({
if (this.refs.roomView) {
this.refs.roomView.handleScrollKey(ev);
}
+ else if (this.refs.roomDirectory) {
+ this.refs.roomDirectory.handleScrollKey(ev);
+ }
},
render: function() {
- var LeftPanel = sdk.getComponent('structures.LeftPanel');
- var RightPanel = sdk.getComponent('structures.RightPanel');
- var RoomView = sdk.getComponent('structures.RoomView');
- var UserSettings = sdk.getComponent('structures.UserSettings');
- var CreateRoom = sdk.getComponent('structures.CreateRoom');
- var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
- var HomePage = sdk.getComponent('structures.HomePage');
- var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
- var GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
- var NewVersionBar = sdk.getComponent('globals.NewVersionBar');
+ const LeftPanel = sdk.getComponent('structures.LeftPanel');
+ const RightPanel = sdk.getComponent('structures.RightPanel');
+ const RoomView = sdk.getComponent('structures.RoomView');
+ const UserSettings = sdk.getComponent('structures.UserSettings');
+ const CreateRoom = sdk.getComponent('structures.CreateRoom');
+ const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
+ const HomePage = sdk.getComponent('structures.HomePage');
+ const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
+ const GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
+ const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
- var page_element;
- var right_panel = '';
+ let page_element;
+ let right_panel = '';
switch (this.props.page_type) {
case PageTypes.RoomView:
@@ -195,10 +220,9 @@ export default React.createClass({
case PageTypes.RoomDirectory:
page_element = ;
- if (!this.props.collapse_rhs) right_panel = ;
break;
case PageTypes.HomePage:
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 44fdfcf23e..9b8aa3426a 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -29,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');
@@ -63,6 +59,13 @@ 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,
@@ -89,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,
@@ -104,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,
@@ -184,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;
@@ -280,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.
@@ -307,7 +312,7 @@ module.exports = React.createClass({
const newState = {
screen: undefined,
viewUserId: null,
- logged_in: false,
+ loggedIn: false,
ready: false,
upgradeUsername: null,
guestAccessToken: null,
@@ -317,14 +322,13 @@ 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':
@@ -344,14 +348,20 @@ module.exports = React.createClass({
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;
@@ -359,8 +369,8 @@ module.exports = React.createClass({
// 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.guestCreds = MatrixClientPeg.getCredentials();
this.setStateForNewScreen({
+ guestCreds: MatrixClientPeg.getCredentials(),
screen: "register",
upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(),
guestAccessToken: MatrixClientPeg.get().getAccessToken(),
@@ -375,35 +385,60 @@ module.exports = React.createClass({
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: (err && err.message ? err.message : "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()
});
});
@@ -530,6 +565,9 @@ 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(payload.teamToken);
break;
@@ -603,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() {
@@ -658,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();
+ }
},
/**
@@ -708,18 +756,46 @@ module.exports = React.createClass({
* Called when a new logged in session has started
*/
_onLoggedIn: function(teamToken) {
- this.guestCreds = null;
- this.notifyNewScreen('');
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;
- this._setPage(PageTypes.HomePage);
+ dis.dispatch({action: 'view_home_page'});
} else if (this._is_registered) {
- this._setPage(PageTypes.UserSettings);
+ // 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'});
}
},
@@ -729,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,
@@ -745,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") {
@@ -755,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({
@@ -903,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) {
@@ -1002,9 +1052,9 @@ 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});
}
},
@@ -1086,14 +1136,12 @@ module.exports = React.createClass({
},
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 (
@@ -1102,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 0981b7b706..d4bf147ad5 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -279,23 +279,25 @@ module.exports = React.createClass({
this.currentGhostEventId = null;
}
- var isMembershipChange = (e) =>
- e.getType() === 'm.room.member'
- && (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership);
+ var isMembershipChange = (e) => e.getType() === 'm.room.member';
for (i = 0; i < this.props.events.length; i++) {
- var mxEv = this.props.events[i];
- var wantTile = true;
- var eventId = mxEv.getId();
+ let mxEv = this.props.events[i];
+ let wantTile = true;
+ let eventId = mxEv.getId();
+ let readMarkerInMels = false;
if (!EventTile.haveTileForEvent(mxEv)) {
wantTile = false;
}
- var last = (i == lastShownEventIndex);
+ let 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
@@ -331,6 +333,9 @@ module.exports = React.createClass({
let eventTiles = summarisedEvents.map(
(e) => {
+ if (e.getId() === this.props.readMarkerEventId) {
+ readMarkerInMels = true;
+ }
// In order to prevent DateSeparators from appearing in the expanded form
// of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
@@ -349,12 +354,16 @@ module.exports = React.createClass({
{eventTiles}
);
+
+ if (readMarkerInMels) {
+ ret.push(this._getReadMarkerTile(visible));
+ }
+
continue;
}
@@ -385,6 +394,8 @@ module.exports = React.createClass({
isVisibleReadMarker = visible;
}
+ // XXX: there should be no need for a ghost tile - we should just use a
+ // a dispatch (user_activity_end) to start the RM animation.
if (eventId == this.currentGhostEventId) {
// if we're showing an animation, continue to show it.
ret.push(this._getReadMarkerGhostTile());
@@ -408,7 +419,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;
@@ -459,8 +472,9 @@ module.exports = React.createClass({
ret.push(
+ data-scroll-tokens={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 626c376d9f..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,33 +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 ||
- props.tabComplete.isTabCompleting()
+ _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
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 936d88c0ee..9f84657912 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -26,6 +26,7 @@ var q = require("q");
var classNames = require("classnames");
var Matrix = require("matrix-js-sdk");
+var UserSettingsStore = require('../../UserSettingsStore');
var MatrixClientPeg = require("../../MatrixClientPeg");
var ContentMessages = require("../../ContentMessages");
var Modal = require("../../Modal");
@@ -270,6 +271,7 @@ module.exports = React.createClass({
this._updateConfCallNotification();
+ window.addEventListener('beforeunload', this.onPageUnload);
window.addEventListener('resize', this.onResize);
this.onResize();
@@ -352,6 +354,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
}
+ window.removeEventListener('beforeunload', this.onPageUnload);
window.removeEventListener('resize', this.onResize);
document.removeEventListener("keydown", this.onKeyDown);
@@ -364,6 +367,17 @@ module.exports = React.createClass({
// Tinter.tint(); // reset colourscheme
},
+ onPageUnload(event) {
+ if (ContentMessages.getCurrentUploads().length > 0) {
+ return event.returnValue =
+ 'You seem to be uploading files, are you sure you want to quit?';
+ } else if (this._getCallForRoom() && this.state.callState !== 'ended') {
+ return event.returnValue =
+ 'You seem to be in a call, are you sure you want to quit?';
+ }
+ },
+
+
onKeyDown: function(ev) {
let handled = false;
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
@@ -489,6 +503,13 @@ module.exports = React.createClass({
}
},
+ canResetTimeline: function() {
+ if (!this.refs.messagePanel) {
+ return true;
+ }
+ return this.refs.messagePanel.canResetTimeline();
+ },
+
// called when state.room is first initialised (either at initial load,
// after a successful peek, or after we join the room).
_onRoomLoaded: function(room) {
@@ -914,8 +935,6 @@ module.exports = React.createClass({
},
uploadFile: function(file) {
- var self = this;
-
if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
@@ -927,11 +946,20 @@ module.exports = React.createClass({
ContentMessages.sendContentToRoom(
file, this.state.room.roomId, MatrixClientPeg.get()
- ).done(undefined, function(error) {
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ ).done(undefined, (error) => {
+ if (error.name === "UnknownDeviceError") {
+ dis.dispatch({
+ action: 'unknown_device_error',
+ err: error,
+ room: this.state.room,
+ });
+ return;
+ }
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ console.error("Failed to upload file " + file + " " + error);
Modal.createDialog(ErrorDialog, {
title: "Failed to upload file",
- description: error.toString()
+ description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or the file too big"),
});
});
},
@@ -1015,9 +1043,10 @@ module.exports = React.createClass({
});
}, function(error) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ console.error("Search failed: " + error);
Modal.createDialog(ErrorDialog, {
title: "Search failed",
- description: error.toString()
+ description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or search timed out :("),
});
}).finally(function() {
self.setState({
@@ -1165,6 +1194,7 @@ module.exports = React.createClass({
console.log("updateTint from onCancelClick");
this.updateTint();
this.setState({editingRoomSettings: false});
+ dis.dispatch({action: 'focus_composer'});
},
onLeaveClick: function() {
@@ -1238,6 +1268,7 @@ module.exports = React.createClass({
// jump down to the bottom of this room, where new events are arriving
jumpToLiveTimeline: function() {
this.refs.messagePanel.jumpToLiveTimeline();
+ dis.dispatch({action: 'focus_composer'});
},
// jump up to wherever our read marker is
@@ -1257,12 +1288,7 @@ module.exports = React.createClass({
return;
}
- var pos = this.refs.messagePanel.getReadMarkerPosition();
-
- // we want to show the bar if the read-marker is off the top of the
- // screen.
- var showBar = (pos < 0);
-
+ const showBar = this.refs.messagePanel.canJumpToReadMarker();
if (this.state.showTopUnreadMessagesBar != showBar) {
this.setState({showTopUnreadMessagesBar: showBar},
this.onChildResize);
@@ -1701,7 +1727,7 @@ module.exports = React.createClass({
var messagePanel = (
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.scrollTokens) {
+ markerScrollToken = tile.dataset.scrollTokens.split(',')[0];
}
- } 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;
}
}
@@ -425,7 +423,8 @@ module.exports = React.createClass({
* scroll. false if we are tracking a particular child.
*
* string trackedScrollToken: undefined if stuckAtBottom is true; if it is
- * false, the data-scroll-token of the child which we are tracking.
+ * false, the first token in data-scroll-tokens of the child which we are
+ * tracking.
*
* number pixelOffset: undefined if stuckAtBottom is true; if it is false,
* the number of pixels the bottom of the tracked child is above the
@@ -489,21 +488,25 @@ module.exports = React.createClass({
handleScrollKey: function(ev) {
switch (ev.keyCode) {
case KeyCode.PAGE_UP:
- this.scrollRelative(-1);
+ if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
+ this.scrollRelative(-1);
+ }
break;
case KeyCode.PAGE_DOWN:
- this.scrollRelative(1);
+ if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
+ this.scrollRelative(1);
+ }
break;
case KeyCode.HOME:
- if (ev.ctrlKey) {
+ if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollToTop();
}
break;
case KeyCode.END:
- if (ev.ctrlKey) {
+ if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
this.scrollToBottom();
}
break;
@@ -553,8 +556,10 @@ module.exports = React.createClass({
var messages = this.refs.itemlist.children;
for (var i = messages.length-1; i >= 0; --i) {
var m = messages[i];
- if (!m.dataset.scrollToken) continue;
- if (m.dataset.scrollToken == scrollToken) {
+ // 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
+ // There might only be one scroll token
+ if (m.dataset.scrollTokens &&
+ m.dataset.scrollTokens.split(',').indexOf(scrollToken) !== -1) {
node = m;
break;
}
@@ -570,7 +575,7 @@ module.exports = React.createClass({
var boundingRect = node.getBoundingClientRect();
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
- debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" +
+ debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
pixelOffset + " (delta: "+scrollDelta+")");
if(scrollDelta != 0) {
@@ -589,24 +594,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;
+ if (!node.dataset.scrollTokens) 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.scrollTokens.split(',')[0],
+ 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.scrollTokens` 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..7c89694a29 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.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.
@@ -102,9 +103,6 @@ var TimelinePanel = React.createClass({
},
statics: {
- // a map from room id to read marker event ID
- roomReadMarkerMap: {},
-
// a map from room id to read marker event timestamp
roomReadMarkerTsMap: {},
},
@@ -121,10 +119,14 @@ var TimelinePanel = React.createClass({
getInitialState: function() {
// XXX: we could track RM per TimelineSet rather than per Room.
// but for now we just do it per room for simplicity.
+ let initialReadMarker = null;
if (this.props.manageReadMarkers) {
- var initialReadMarker =
- TimelinePanel.roomReadMarkerMap[this.props.timelineSet.room.roomId]
- || this._getCurrentReadReceipt();
+ const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read');
+ if (readmarker){
+ initialReadMarker = readmarker.getContent().event_id;
+ } else {
+ initialReadMarker = this._getCurrentReadReceipt();
+ }
}
return {
@@ -166,6 +168,9 @@ var TimelinePanel = React.createClass({
backPaginating: false,
forwardPaginating: false,
+
+ // cache of matrixClient.getSyncState() (but from the 'sync' event)
+ clientSyncState: MatrixClientPeg.get().getSyncState(),
};
},
@@ -173,6 +178,7 @@ var TimelinePanel = React.createClass({
debuglog("TimelinePanel: mounting");
this.last_rr_sent_event_id = undefined;
+ this.last_rm_sent_event_id = undefined;
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
@@ -180,6 +186,8 @@ var TimelinePanel = React.createClass({
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
+ MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
+ MatrixClientPeg.get().on("sync", this.onSync);
this._initTimeline(this.props);
},
@@ -247,14 +255,18 @@ var TimelinePanel = React.createClass({
client.removeListener("Room.redaction", this.onRoomRedaction);
client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
+ client.removeListener("Room.accountData", this.onAccountData);
+ client.removeListener("sync", this.onSync);
}
},
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(
@@ -412,6 +424,7 @@ var TimelinePanel = React.createClass({
} else if(lastEv && this.getReadMarkerPosition() === 0) {
// we know we're stuckAtBottom, so we can advance the RM
// immediately, to save a later render cycle
+
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
updatedState.readMarkerVisible = false;
updatedState.readMarkerEventId = lastEv.getId();
@@ -431,6 +444,10 @@ var TimelinePanel = React.createClass({
}
},
+ canResetTimeline: function() {
+ return this.refs.messagePanel && this.refs.messagePanel.isAtBottom();
+ },
+
onRoomRedaction: function(ev, room) {
if (this.unmounted) return;
@@ -460,6 +477,25 @@ var TimelinePanel = React.createClass({
this._reloadEvents();
},
+ onAccountData: function(ev, room) {
+ if (this.unmounted) return;
+
+ // ignore events for other rooms
+ if (room !== this.props.timelineSet.room) return;
+
+ if (ev.getType() !== "m.fully_read") return;
+
+ // XXX: roomReadMarkerTsMap not updated here so it is now inconsistent. Replace
+ // this mechanism of determining where the RM is relative to the view-port with
+ // one supported by the server (the client needs more than an event ID).
+ this.setState({
+ readMarkerEventId: ev.getContent().event_id,
+ }, this.props.onReadMarkerUpdated);
+ },
+
+ onSync: function(state, prevState, data) {
+ this.setState({clientSyncState: state});
+ },
sendReadReceipt: function() {
if (!this.refs.messagePanel) return;
@@ -467,15 +503,9 @@ var TimelinePanel = React.createClass({
// This happens on user_activity_end which is delayed, and it's
// very possible have logged out within that timeframe, so check
// 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
- }
+ const cli = MatrixClientPeg.get();
+ // if no client or client is guest don't send RR
+ if (!cli || cli.isGuest()) return;
var currentReadUpToEventId = this._getCurrentReadReceipt(true);
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
@@ -507,13 +537,44 @@ var TimelinePanel = React.createClass({
// we also remember the last read receipt we sent to avoid spamming the
// same one at the server repeatedly
- if (lastReadEventIndex > currentReadUpToEventIndex
- && this.last_rr_sent_event_id != lastReadEvent.getId()) {
+ if ((lastReadEventIndex > currentReadUpToEventIndex &&
+ this.last_rr_sent_event_id != lastReadEvent.getId()) ||
+ this.last_rm_sent_event_id != this.state.readMarkerEventId) {
+
this.last_rr_sent_event_id = lastReadEvent.getId();
- MatrixClientPeg.get().sendReadReceipt(lastReadEvent).catch(() => {
+ this.last_rm_sent_event_id = this.state.readMarkerEventId;
+
+ MatrixClientPeg.get().setRoomReadMarkers(
+ this.props.timelineSet.room.roomId,
+ this.state.readMarkerEventId,
+ lastReadEvent
+ ).catch((e) => {
+ // /read_markers API is not implemented on this HS, fallback to just RR
+ if (e.errcode === 'M_UNRECOGNIZED') {
+ return MatrixClientPeg.get().sendReadReceipt(
+ lastReadEvent
+ ).catch(() => {
+ this.last_rr_sent_event_id = undefined;
+ });
+ }
// it failed, so allow retries next time the user is active
this.last_rr_sent_event_id = undefined;
+ this.last_rm_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',
+ room: this.props.timelineSet.room,
+ });
+ }
}
},
@@ -695,7 +756,7 @@ var TimelinePanel = React.createClass({
// the messagePanel doesn't know where the read marker is.
// if we know the timestamp of the read marker, make a guess based on that.
- var rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.roomId];
+ const rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.room.roomId];
if (rmTs && this.state.events.length > 0) {
if (rmTs < this.state.events[0].getTs()) {
return -1;
@@ -707,6 +768,19 @@ var TimelinePanel = React.createClass({
return null;
},
+ canJumpToReadMarker: function() {
+ // 1. Do not show jump bar if neither the RM nor the RR are set.
+ // 2. Only show jump bar if RR !== RM. If they are the same, there are only fully
+ // read messages and unread messages. We already have a badge count and the bottom
+ // bar to jump to "live" when we have unread messages.
+ // 3. We want to show the bar if the read-marker is off the top of the screen.
+ // 4. Also, if pos === null, the event might not be paginated - show the unread bar
+ const pos = this.getReadMarkerPosition();
+ return this.state.readMarkerEventId !== null && // 1.
+ this.state.readMarkerEventId !== this._getCurrentReadReceipt() && // 2.
+ (pos < 0 || pos === null); // 3., 4.
+ },
+
/**
* called by the parent component when PageUp/Down/etc is pressed.
*
@@ -717,7 +791,9 @@ var TimelinePanel = React.createClass({
// jump to the live timeline on ctrl-end, rather than the end of the
// timeline window.
- if (ev.ctrlKey && ev.keyCode == KeyCode.END) {
+ if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
+ ev.keyCode == KeyCode.END)
+ {
this.jumpToLiveTimeline();
} else {
this.refs.messagePanel.handleScrollKey(ev);
@@ -810,7 +886,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,
});
};
}
@@ -945,16 +1021,12 @@ var TimelinePanel = React.createClass({
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
var roomId = this.props.timelineSet.room.roomId;
- if (TimelinePanel.roomReadMarkerMap[roomId] == eventId) {
- // don't update the state (and cause a re-render) if there is
- // no change to the RM.
+ // don't update the state (and cause a re-render) if there is
+ // no change to the RM.
+ if (eventId === this.state.readMarkerEventId) {
return;
}
- // ideally we'd sync these via the server, but for now just stash them
- // in a map.
- TimelinePanel.roomReadMarkerMap[roomId] = eventId;
-
// in order to later figure out if the read marker is
// above or below the visible timeline, we stash the timestamp.
TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs;
@@ -963,6 +1035,7 @@ var TimelinePanel = React.createClass({
return;
}
+ // Do the local echo of the RM
// run the render cycle before calling the callback, so that
// getReadMarkerPosition() returns the right thing.
this.setState({
@@ -1011,11 +1084,17 @@ var TimelinePanel = React.createClass({
// of paginating our way through the entire history of the room.
var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
+ // If the state is PREPARED, we're still waiting for the js-sdk to sync with
+ // the HS and fetch the latest events, so we are effectively forward paginating.
+ const forwardPaginating = (
+ this.state.forwardPaginating || this.state.clientSyncState == 'PREPARED'
+ );
+
return (
";
+// the git sha. Prepend version with v, to look like riot-web version
+const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || '';
+// Simple method to help prettify GH Release Tags and Commit Hashes.
+const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i;
+const gHVersionLabel = function(repo, token) {
+ const match = token.match(semVerRegex);
+ let url;
+ if (match && match[1]) { // basic semVer string possibly with commit hash
+ url = (match.length > 1 && match[2])
+ ? `https://github.com/${repo}/commit/${match[2]}`
+ : `https://github.com/${repo}/releases/tag/v${match[1]}`;
+ } else {
+ url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
+ }
+ return {token};
+};
// Enumerate some simple 'flip a bit' UI settings (if any).
// 'id' gives the key name in the im.vector.web.settings account data event
@@ -43,6 +57,14 @@ const SETTINGS_LABELS = [
id: 'autoplayGifsAndVideos',
label: 'Autoplay GIFs and videos',
},
+ {
+ id: 'hideReadReceipts',
+ label: 'Hide read receipts',
+ },
+ {
+ id: 'dontSendTypingNotifications',
+ label: "Don't send typing notifications",
+ },
/*
{
id: 'alwaysShowTimestamps',
@@ -93,7 +115,7 @@ const THEMES = [
id: 'theme',
label: 'Dark theme',
value: 'dark',
- }
+ },
];
@@ -139,6 +161,7 @@ module.exports = React.createClass({
componentWillMount: function() {
this._unmounted = false;
+ this._addThreepid = null;
if (PlatformPeg.get()) {
q().then(() => {
@@ -166,7 +189,7 @@ module.exports = React.createClass({
});
this._refreshFromServer();
- var syncedSettings = UserSettingsStore.getSyncedSettings();
+ const syncedSettings = UserSettingsStore.getSyncedSettings();
if (!syncedSettings.theme) {
syncedSettings.theme = 'light';
}
@@ -188,16 +211,16 @@ module.exports = React.createClass({
middleOpacity: 1.0,
});
dis.unregister(this.dispatcherRef);
- let cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomMember.membership", this._onInviteStateChange);
}
},
_refreshFromServer: function() {
- var self = this;
+ const self = this;
q.all([
- UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids()
+ UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids(),
]).done(function(resps) {
self.setState({
avatarUrl: resps[0].avatar_url,
@@ -205,10 +228,11 @@ module.exports = React.createClass({
phase: "UserSettings.DISPLAY",
});
}, function(error) {
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const 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: ((error && error.message) ? error.message : "Server may be unavailable or overloaded"),
});
});
},
@@ -221,7 +245,7 @@ module.exports = React.createClass({
onAvatarPickerClick: function(ev) {
if (MatrixClientPeg.get().isGuest()) {
- var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
+ const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guests can't set avatars. Please register.",
@@ -235,8 +259,8 @@ module.exports = React.createClass({
},
onAvatarSelected: function(ev) {
- var self = this;
- var changeAvatar = this.refs.changeAvatar;
+ const self = this;
+ const changeAvatar = this.refs.changeAvatar;
if (!changeAvatar) {
console.error("No ChangeAvatar found to upload image to!");
return;
@@ -245,27 +269,34 @@ module.exports = React.createClass({
// dunno if the avatar changed, re-check it.
self._refreshFromServer();
}, function(err) {
- var errMsg = (typeof err === "string") ? err : (err.error || "");
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ // const errMsg = (typeof err === "string") ? err : (err.error || "");
+ console.error("Failed to set avatar: " + err);
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
- title: "Error",
- description: "Failed to set avatar. " + errMsg
+ title: "Failed to set avatar",
+ description: ((err && err.message) ? err.message : "Operation failed"),
});
});
},
onLogoutClicked: function(ev) {
- var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+ const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
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'});
@@ -278,33 +309,33 @@ module.exports = React.createClass({
},
onPasswordChangeError: function(err) {
- var errMsg = err.error || "";
+ let errMsg = err.error || "";
if (err.httpStatus === 403) {
errMsg = "Failed to change password. Is your password correct?";
- }
- else if (err.httpStatus) {
+ } else if (err.httpStatus) {
errMsg += ` (HTTP status ${err.httpStatus})`;
}
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ console.error("Failed to change password: " + errMsg);
Modal.createDialog(ErrorDialog, {
title: "Error",
- description: errMsg
+ description: errMsg,
});
},
onPasswordChanged: function() {
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Success",
description: `Your password was successfully changed. You will not
receive push notifications on other devices until you
- log back in to them.`
+ log back in to them.`,
});
},
onUpgradeClicked: function() {
dis.dispatch({
- action: "start_upgrade_registration"
+ action: "start_upgrade_registration",
});
},
@@ -312,23 +343,27 @@ module.exports = React.createClass({
UserSettingsStore.setEnableNotifications(event.target.checked);
},
- onAddThreepidClicked: function(value, shouldSubmit) {
+ _onAddEmailEditFinished: function(value, shouldSubmit) {
if (!shouldSubmit) return;
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
- var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+ this._addEmail();
+ },
- var email_address = this.refs.add_threepid_input.value;
- if (!Email.looksValid(email_address)) {
+ _addEmail: function() {
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+
+ const emailAddress = this.refs.add_email_input.value;
+ if (!Email.looksValid(emailAddress)) {
Modal.createDialog(ErrorDialog, {
title: "Invalid Email Address",
description: "This doesn't appear to be a valid email address",
});
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(emailAddress, 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.",
@@ -337,12 +372,13 @@ module.exports = React.createClass({
});
}, (err) => {
this.setState({email_add_pending: false});
+ console.error("Unable to add email address " + emailAddress + " " + err);
Modal.createDialog(ErrorDialog, {
title: "Unable to add email address",
- description: err.message
+ description: ((err && err.message) ? err.message : "Operation failed"),
});
});
- ReactDOM.findDOMNode(this.refs.add_threepid_input).blur();
+ ReactDOM.findDOMNode(this.refs.add_email_input).blur();
this.setState({email_add_pending: true});
},
@@ -361,9 +397,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(),
+ description: ((err && err.message) ? err.message : "Operation failed"),
});
}).done();
}
@@ -380,8 +417,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",
});
@@ -389,9 +426,9 @@ module.exports = React.createClass({
this.setState({email_add_pending: false});
}, (err) => {
this.setState({email_add_pending: false});
- if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
- var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
- var message = "Unable to verify email address. ";
+ if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
+ const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+ let message = "Unable to verify email address. ";
message += "Please check your email and click on the link it contains. Once this is done, click continue.";
Modal.createDialog(QuestionDialog, {
title: "Verification Pending",
@@ -400,10 +437,11 @@ module.exports = React.createClass({
onFinished: this.onEmailDialogFinished,
});
} else {
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const 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(),
+ description: ((err && err.message) ? err.message : "Operation failed"),
});
}
});
@@ -423,10 +461,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();
});
},
@@ -438,17 +477,17 @@ module.exports = React.createClass({
_onRejectAllInvitesClicked: function(rooms, ev) {
this.setState({
- rejectingInvites: true
+ rejectingInvites: true,
});
// reject the invites
- let promises = rooms.map((room) => {
+ const promises = rooms.map((room) => {
return MatrixClientPeg.get().leave(room.roomId);
});
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI
// after trying to reject all the invites.
q.allSettled(promises).then(() => {
this.setState({
- rejectingInvites: false
+ rejectingInvites: false,
});
}).done();
},
@@ -461,7 +500,7 @@ module.exports = React.createClass({
}, "e2e-export");
}, {
matrixClient: MatrixClientPeg.get(),
- }
+ },
);
},
@@ -473,7 +512,7 @@ module.exports = React.createClass({
}, "e2e-export");
}, {
matrixClient: MatrixClientPeg.get(),
- }
+ },
);
},
@@ -499,8 +538,6 @@ module.exports = React.createClass({
},
_renderUserInterfaceSettings: function() {
- var client = MatrixClientPeg.get();
-
return (