diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index f1b63d7367..ffd492d491 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -22,7 +22,6 @@ src/components/structures/login/ForgotPassword.js src/components/structures/login/Login.js src/components/structures/login/PostRegistration.js src/components/structures/login/Registration.js -src/components/structures/MatrixChat.js src/components/structures/MessagePanel.js src/components/structures/NotificationPanel.js src/components/structures/RoomStatusBar.js @@ -30,7 +29,6 @@ src/components/structures/RoomView.js src/components/structures/ScrollPanel.js src/components/structures/TimelinePanel.js src/components/structures/UploadBar.js -src/components/structures/UserSettings.js src/components/views/avatars/BaseAvatar.js src/components/views/avatars/MemberAvatar.js src/components/views/avatars/RoomAvatar.js @@ -148,7 +146,6 @@ src/Roles.js src/RoomListSorter.js src/RoomNotifs.js src/Rooms.js -src/RtsClient.js src/ScalarAuthClient.js src/ScalarMessaging.js src/SdkConfig.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 66e4627afd..22049b6af6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,278 @@ +Changes in [0.9.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.6) (2017-06-20) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.5...v0.9.6) + + * Fix infinite spinner on email registration + [\#1120](https://github.com/matrix-org/matrix-react-sdk/pull/1120) + * Translate help promots in room list + [\#1121](https://github.com/matrix-org/matrix-react-sdk/pull/1121) + * Internationalise the drop targets + [\#1122](https://github.com/matrix-org/matrix-react-sdk/pull/1122) + * Fix another infinite spin on register + [\#1124](https://github.com/matrix-org/matrix-react-sdk/pull/1124) + + +Changes in [0.9.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.5) (2017-06-19) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.5-rc.2...v0.9.5) + + * Don't peek when creating a room + [\#1113](https://github.com/matrix-org/matrix-react-sdk/pull/1113) + * More translations & translation fixes + + +Changes in [0.9.5-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.5-rc.2) (2017-06-16) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.5-rc.1...v0.9.5-rc.2) + + * Avoid getting stuck in a loop in CAS login + [\#1109](https://github.com/matrix-org/matrix-react-sdk/pull/1109) + * Update from Weblate. + [\#1101](https://github.com/matrix-org/matrix-react-sdk/pull/1101) + * Correctly inspect state when rejecting invite + [\#1108](https://github.com/matrix-org/matrix-react-sdk/pull/1108) + * Make sure to pass the roomAlias to the preview header if we have it + [\#1107](https://github.com/matrix-org/matrix-react-sdk/pull/1107) + * Make sure captcha disappears when container does + [\#1106](https://github.com/matrix-org/matrix-react-sdk/pull/1106) + * Fix URL previews + [\#1105](https://github.com/matrix-org/matrix-react-sdk/pull/1105) + +Changes in [0.9.5-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.5-rc.1) (2017-06-15) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.4...v0.9.5-rc.1) + + * Groundwork for tests including a teamserver login + [\#1098](https://github.com/matrix-org/matrix-react-sdk/pull/1098) + * Show a spinner when accepting an invite and waitingForRoom + [\#1100](https://github.com/matrix-org/matrix-react-sdk/pull/1100) + * Display a spinner until new room object after join success + [\#1099](https://github.com/matrix-org/matrix-react-sdk/pull/1099) + * Luke/attempt fix peeking regression + [\#1097](https://github.com/matrix-org/matrix-react-sdk/pull/1097) + * Show correct text in set email password dialog (2) + [\#1096](https://github.com/matrix-org/matrix-react-sdk/pull/1096) + * Don't create a guest login if user went to /login + [\#1092](https://github.com/matrix-org/matrix-react-sdk/pull/1092) + * Give password confirmation correct title, description + [\#1095](https://github.com/matrix-org/matrix-react-sdk/pull/1095) + * Make enter submit change password form + [\#1094](https://github.com/matrix-org/matrix-react-sdk/pull/1094) + * When not specified, remove roomAlias state in RoomViewStore + [\#1093](https://github.com/matrix-org/matrix-react-sdk/pull/1093) + * Update from Weblate. + [\#1091](https://github.com/matrix-org/matrix-react-sdk/pull/1091) + * Fixed pagination infinite loop caused by long messages + [\#1045](https://github.com/matrix-org/matrix-react-sdk/pull/1045) + * Clear persistent storage on login and logout + [\#1085](https://github.com/matrix-org/matrix-react-sdk/pull/1085) + * DM guessing: prefer oldest joined member + [\#1087](https://github.com/matrix-org/matrix-react-sdk/pull/1087) + * Ask for email address after setting password for the first time + [\#1090](https://github.com/matrix-org/matrix-react-sdk/pull/1090) + * i18n for setting password flow + [\#1089](https://github.com/matrix-org/matrix-react-sdk/pull/1089) + * remove mx_filterFlipColor from verified e2e icon so its not purple :/ + [\#1088](https://github.com/matrix-org/matrix-react-sdk/pull/1088) + * width and height must be int otherwise synapse cries + [\#1083](https://github.com/matrix-org/matrix-react-sdk/pull/1083) + * remove RoomViewStore listener from MatrixChat on unmount + [\#1084](https://github.com/matrix-org/matrix-react-sdk/pull/1084) + * Add script to copy translations between files + [\#1082](https://github.com/matrix-org/matrix-react-sdk/pull/1082) + * Only process user_directory response if it's for the current query + [\#1081](https://github.com/matrix-org/matrix-react-sdk/pull/1081) + * Fix regressions with starting a 1-1. + [\#1080](https://github.com/matrix-org/matrix-react-sdk/pull/1080) + * allow forcing of TURN + [\#1079](https://github.com/matrix-org/matrix-react-sdk/pull/1079) + * Remove a bunch of dead code from react-sdk + [\#1077](https://github.com/matrix-org/matrix-react-sdk/pull/1077) + * Improve error logging/reporting in megolm import/export + [\#1061](https://github.com/matrix-org/matrix-react-sdk/pull/1061) + * Delinting + [\#1064](https://github.com/matrix-org/matrix-react-sdk/pull/1064) + * Show reason for a call hanging up unexpectedly. + [\#1071](https://github.com/matrix-org/matrix-react-sdk/pull/1071) + * Add reason for ban in room settings + [\#1072](https://github.com/matrix-org/matrix-react-sdk/pull/1072) + * adds mx_filterFlipColor so that the dark theme will invert this image + [\#1070](https://github.com/matrix-org/matrix-react-sdk/pull/1070) + +Changes in [0.9.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.4) (2017-06-14) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.3...v0.9.4) + + * Ask for email address after setting password for the first time + [\#1090](https://github.com/matrix-org/matrix-react-sdk/pull/1090) + * DM guessing: prefer oldest joined member + [\#1087](https://github.com/matrix-org/matrix-react-sdk/pull/1087) + * More translations + +Changes in [0.9.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.3) (2017-06-12) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.3-rc.2...v0.9.3) + + * Add more translations & fix some existing ones + +Changes in [0.9.3-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.3-rc.2) (2017-06-09) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.3-rc.1...v0.9.3-rc.2) + + * Fix flux dependency + * Fix translations on conference call bar + +Changes in [0.9.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.3-rc.1) (2017-06-09) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.2...v0.9.3-rc.1) + + * When ChatCreateOrReuseDialog is cancelled by a guest, go home + [\#1069](https://github.com/matrix-org/matrix-react-sdk/pull/1069) + * Update from Weblate. + [\#1065](https://github.com/matrix-org/matrix-react-sdk/pull/1065) + * Goto /home when forgetting the last room + [\#1067](https://github.com/matrix-org/matrix-react-sdk/pull/1067) + * Default to home page when settings is closed + [\#1066](https://github.com/matrix-org/matrix-react-sdk/pull/1066) + * Update from Weblate. + [\#1063](https://github.com/matrix-org/matrix-react-sdk/pull/1063) + * When joining, use a roomAlias if we have it + [\#1062](https://github.com/matrix-org/matrix-react-sdk/pull/1062) + * Control currently viewed event via RoomViewStore + [\#1058](https://github.com/matrix-org/matrix-react-sdk/pull/1058) + * Better error messages for login + [\#1060](https://github.com/matrix-org/matrix-react-sdk/pull/1060) + * Add remaining translations + [\#1056](https://github.com/matrix-org/matrix-react-sdk/pull/1056) + * Added button that copies code to clipboard + [\#1040](https://github.com/matrix-org/matrix-react-sdk/pull/1040) + * de-lint MegolmExportEncryption + test + [\#1059](https://github.com/matrix-org/matrix-react-sdk/pull/1059) + * Better RTL support + [\#1021](https://github.com/matrix-org/matrix-react-sdk/pull/1021) + * make mels emoji capable + [\#1057](https://github.com/matrix-org/matrix-react-sdk/pull/1057) + * Make travis check for lint on files which are clean to start with + [\#1055](https://github.com/matrix-org/matrix-react-sdk/pull/1055) + * Update from Weblate. + [\#1053](https://github.com/matrix-org/matrix-react-sdk/pull/1053) + * Add some logging around switching rooms + [\#1054](https://github.com/matrix-org/matrix-react-sdk/pull/1054) + * Update from Weblate. + [\#1052](https://github.com/matrix-org/matrix-react-sdk/pull/1052) + * Use user_directory endpoint to populate ChatInviteDialog + [\#1050](https://github.com/matrix-org/matrix-react-sdk/pull/1050) + * Various Analytics changes/fixes/improvements + [\#1046](https://github.com/matrix-org/matrix-react-sdk/pull/1046) + * Use an arrow function to allow `this` + [\#1051](https://github.com/matrix-org/matrix-react-sdk/pull/1051) + * New guest access + [\#937](https://github.com/matrix-org/matrix-react-sdk/pull/937) + * Translate src/components/structures + [\#1048](https://github.com/matrix-org/matrix-react-sdk/pull/1048) + * Cancel 'join room' action if 'log in' is clicked + [\#1049](https://github.com/matrix-org/matrix-react-sdk/pull/1049) + * fix copy and paste derp and rip out unused imports + [\#1015](https://github.com/matrix-org/matrix-react-sdk/pull/1015) + * Update from Weblate. + [\#1042](https://github.com/matrix-org/matrix-react-sdk/pull/1042) + * Reset 'first sync' flag / promise on log in + [\#1041](https://github.com/matrix-org/matrix-react-sdk/pull/1041) + * Remove DM-guessing code (again) + [\#1036](https://github.com/matrix-org/matrix-react-sdk/pull/1036) + * Cancel deferred actions + [\#1039](https://github.com/matrix-org/matrix-react-sdk/pull/1039) + * Merge develop, add i18n for SetMxIdDialog + [\#1034](https://github.com/matrix-org/matrix-react-sdk/pull/1034) + * Defer an intention for creating a room + [\#1038](https://github.com/matrix-org/matrix-react-sdk/pull/1038) + * Fix 'create room' button + [\#1037](https://github.com/matrix-org/matrix-react-sdk/pull/1037) + * Always show the spinner during the first sync + [\#1033](https://github.com/matrix-org/matrix-react-sdk/pull/1033) + * Only view welcome user if we are not looking at a room + [\#1032](https://github.com/matrix-org/matrix-react-sdk/pull/1032) + * Update from Weblate. + [\#1030](https://github.com/matrix-org/matrix-react-sdk/pull/1030) + * Keep deferred actions for view_user_settings and view_create_chat + [\#1031](https://github.com/matrix-org/matrix-react-sdk/pull/1031) + * Don't do a deferred start chat if user is welcome user + [\#1029](https://github.com/matrix-org/matrix-react-sdk/pull/1029) + * Introduce state `peekLoading` to avoid collision with `roomLoading` + [\#1028](https://github.com/matrix-org/matrix-react-sdk/pull/1028) + * Update from Weblate. + [\#1016](https://github.com/matrix-org/matrix-react-sdk/pull/1016) + * Fix accepting a 3pid invite + [\#1013](https://github.com/matrix-org/matrix-react-sdk/pull/1013) + * Propagate room join errors to the UI + [\#1007](https://github.com/matrix-org/matrix-react-sdk/pull/1007) + * Implement /user/@userid:domain?action=chat + [\#1006](https://github.com/matrix-org/matrix-react-sdk/pull/1006) + * Show People/Rooms emptySubListTip even when total rooms !== 0 + [\#967](https://github.com/matrix-org/matrix-react-sdk/pull/967) + * Fix to show the correct room + [\#995](https://github.com/matrix-org/matrix-react-sdk/pull/995) + * Remove cachedPassword from localStorage on_logged_out + [\#977](https://github.com/matrix-org/matrix-react-sdk/pull/977) + * Add /start to show the setMxId above HomePage + [\#964](https://github.com/matrix-org/matrix-react-sdk/pull/964) + * Allow pressing Enter to submit setMxId + [\#961](https://github.com/matrix-org/matrix-react-sdk/pull/961) + * add login link to SetMxIdDialog + [\#954](https://github.com/matrix-org/matrix-react-sdk/pull/954) + * Block user settings with view_set_mxid + [\#936](https://github.com/matrix-org/matrix-react-sdk/pull/936) + * Show "Something went wrong!" when errcode undefined + [\#935](https://github.com/matrix-org/matrix-react-sdk/pull/935) + * Reset store state when logging out + [\#930](https://github.com/matrix-org/matrix-react-sdk/pull/930) + * Set the displayname to the mxid once PWLU + [\#933](https://github.com/matrix-org/matrix-react-sdk/pull/933) + * Fix view_next_room, view_previous_room and view_indexed_room + [\#929](https://github.com/matrix-org/matrix-react-sdk/pull/929) + * Use RVS to indicate "joining" when setting a mxid + [\#928](https://github.com/matrix-org/matrix-react-sdk/pull/928) + * Don't show notif nag bar if guest + [\#932](https://github.com/matrix-org/matrix-react-sdk/pull/932) + * Show "Password" instead of "New Password" + [\#927](https://github.com/matrix-org/matrix-react-sdk/pull/927) + * Remove warm-fuzzy after setting mxid + [\#926](https://github.com/matrix-org/matrix-react-sdk/pull/926) + * Allow teamServerConfig to be missing + [\#925](https://github.com/matrix-org/matrix-react-sdk/pull/925) + * Remove GuestWarningBar + [\#923](https://github.com/matrix-org/matrix-react-sdk/pull/923) + * Make left panel better for new users (mk III) + [\#924](https://github.com/matrix-org/matrix-react-sdk/pull/924) + * Implement default welcome page and allow custom URL /w config + [\#922](https://github.com/matrix-org/matrix-react-sdk/pull/922) + * Implement a store for RoomView + [\#921](https://github.com/matrix-org/matrix-react-sdk/pull/921) + * Add prop to toggle whether new password input is autoFocused + [\#915](https://github.com/matrix-org/matrix-react-sdk/pull/915) + * Implement warm-fuzzy success dialog for SetMxIdDialog + [\#905](https://github.com/matrix-org/matrix-react-sdk/pull/905) + * Write some tests for the RTS UI + [\#893](https://github.com/matrix-org/matrix-react-sdk/pull/893) + * Make confirmation optional on ChangePassword + [\#890](https://github.com/matrix-org/matrix-react-sdk/pull/890) + * Remove "Current Password" input if mx_pass exists + [\#881](https://github.com/matrix-org/matrix-react-sdk/pull/881) + * Replace NeedToRegisterDialog /w SetMxIdDialog + [\#889](https://github.com/matrix-org/matrix-react-sdk/pull/889) + * Invite the welcome user after registration if configured + [\#882](https://github.com/matrix-org/matrix-react-sdk/pull/882) + * Prevent ROUs from creating new chats/new rooms + [\#879](https://github.com/matrix-org/matrix-react-sdk/pull/879) + * Redesign mxID chooser, add availability checking + [\#877](https://github.com/matrix-org/matrix-react-sdk/pull/877) + * Show password nag bar when user is PWLU + [\#864](https://github.com/matrix-org/matrix-react-sdk/pull/864) + * fix typo + [\#858](https://github.com/matrix-org/matrix-react-sdk/pull/858) + * Initial implementation: SetDisplayName -> SetMxIdDialog + [\#849](https://github.com/matrix-org/matrix-react-sdk/pull/849) + Changes in [0.9.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.9.2) (2017-06-06) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.9.1...v0.9.2) diff --git a/package.json b/package.json index 15a903c25a..f49fc04661 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.9.2", + "version": "0.9.6", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -57,14 +57,14 @@ "emojione": "2.2.3", "file-saver": "^1.3.3", "filesize": "3.5.6", - "flux": "^2.0.3", + "flux": "2.1.1", "fuse.js": "^2.2.0", "glob": "^5.0.14", "highlight.js": "^8.9.1", "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "0.7.10", + "matrix-js-sdk": "0.7.12", "optimist": "^0.6.1", "prop-types": "^15.5.8", "q": "^1.4.1", diff --git a/scripts/copy-i18n.py b/scripts/copy-i18n.py new file mode 100755 index 0000000000..07b1271239 --- /dev/null +++ b/scripts/copy-i18n.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python + +import json +import sys +import os + +if len(sys.argv) < 3: + print "Usage: %s " % (sys.argv[0],) + print "eg. %s pt_BR.json pt.json" % (sys.argv[0],) + print + print "Adds any translations to that exist in but not " + sys.exit(1) + +srcpath = sys.argv[1] +dstpath = sys.argv[2] +tmppath = dstpath + ".tmp" + +with open(srcpath) as f: + src = json.load(f) + +with open(dstpath) as f: + dst = json.load(f) + +toAdd = {} +for k,v in src.iteritems(): + if k not in dst: + print "Adding %s" % (k,) + toAdd[k] = v + +# don't just json.dumps as we'll probably re-order all the keys (and they're +# not in any given order so we can't just sort_keys). Append them to the end. +with open(dstpath) as ifp: + with open(tmppath, 'w') as ofp: + for line in ifp: + strippedline = line.strip() + if strippedline in ('{', '}'): + ofp.write(line) + elif strippedline.endswith(','): + ofp.write(line) + else: + ofp.write(' '+strippedline+',') + toAddStr = json.dumps(toAdd, indent=4, separators=(',', ': '), ensure_ascii=False, encoding="utf8").strip("{}\n") + ofp.write("\n") + ofp.write(toAddStr.encode('utf8')) + ofp.write("\n") + +os.rename(tmppath, dstpath) diff --git a/src/CallHandler.js b/src/CallHandler.js index b2ccf65df7..e3fbe9e5e3 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -51,13 +51,14 @@ limitations under the License. * } */ -var MatrixClientPeg = require('./MatrixClientPeg'); -var PlatformPeg = require("./PlatformPeg"); -var Modal = require('./Modal'); -var sdk = require('./index'); +import MatrixClientPeg from './MatrixClientPeg'; +import UserSettingsStore from './UserSettingsStore'; +import PlatformPeg from './PlatformPeg'; +import Modal from './Modal'; +import sdk from './index'; import { _t } from './languageHandler'; -var Matrix = require("matrix-js-sdk"); -var dis = require("./dispatcher"); +import Matrix from 'matrix-js-sdk'; +import dis from './dispatcher'; global.mxCalls = { //room_id: MatrixCall @@ -257,9 +258,9 @@ function _onAction(payload) { } else if (members.length === 2) { console.log("Place %s call in %s", payload.type, payload.room_id); - var call = Matrix.createNewMatrixCall( - MatrixClientPeg.get(), payload.room_id - ); + const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, { + forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false), + }); placeCall(call); } else { // > 2 diff --git a/src/KeyRequestHandler.js b/src/KeyRequestHandler.js new file mode 100644 index 0000000000..1da4922153 --- /dev/null +++ b/src/KeyRequestHandler.js @@ -0,0 +1,138 @@ +/* +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 sdk from './index'; +import Modal from './Modal'; + +export default class KeyRequestHandler { + constructor(matrixClient) { + this._matrixClient = matrixClient; + + // the user/device for which we currently have a dialog open + this._currentUser = null; + this._currentDevice = null; + + // userId -> deviceId -> [keyRequest] + this._pendingKeyRequests = Object.create(null); + } + + handleKeyRequest(keyRequest) { + const userId = keyRequest.userId; + const deviceId = keyRequest.deviceId; + const requestId = keyRequest.requestId; + + if (!this._pendingKeyRequests[userId]) { + this._pendingKeyRequests[userId] = Object.create(null); + } + if (!this._pendingKeyRequests[userId][deviceId]) { + this._pendingKeyRequests[userId][deviceId] = []; + } + + // check if we already have this request + const requests = this._pendingKeyRequests[userId][deviceId]; + if (requests.find((r) => r.requestId === requestId)) { + console.log("Already have this key request, ignoring"); + return; + } + + requests.push(keyRequest); + + if (this._currentUser) { + // ignore for now + console.log("Key request, but we already have a dialog open"); + return; + } + + this._processNextRequest(); + } + + handleKeyRequestCancellation(cancellation) { + // see if we can find the request in the queue + const userId = cancellation.userId; + const deviceId = cancellation.deviceId; + const requestId = cancellation.requestId; + + if (userId === this._currentUser && deviceId === this._currentDevice) { + console.log( + "room key request cancellation for the user we currently have a" + + " dialog open for", + ); + // TODO: update the dialog. For now, we just ignore the + // cancellation. + return; + } + + if (!this._pendingKeyRequests[userId]) { + return; + } + const requests = this._pendingKeyRequests[userId][deviceId]; + if (!requests) { + return; + } + const idx = requests.findIndex((r) => r.requestId === requestId); + if (idx < 0) { + return; + } + console.log("Forgetting room key request"); + requests.splice(idx, 1); + if (requests.length === 0) { + delete this._pendingKeyRequests[userId][deviceId]; + if (Object.keys(this._pendingKeyRequests[userId]).length === 0) { + delete this._pendingKeyRequests[userId]; + } + } + } + + _processNextRequest() { + const userId = Object.keys(this._pendingKeyRequests)[0]; + if (!userId) { + return; + } + const deviceId = Object.keys(this._pendingKeyRequests[userId])[0]; + if (!deviceId) { + return; + } + console.log(`Starting KeyShareDialog for ${userId}:${deviceId}`); + + const finished = (r) => { + this._currentUser = null; + this._currentDevice = null; + + if (r) { + for (const req of this._pendingKeyRequests[userId][deviceId]) { + req.share(); + } + } + delete this._pendingKeyRequests[userId][deviceId]; + if (Object.keys(this._pendingKeyRequests[userId]).length === 0) { + delete this._pendingKeyRequests[userId]; + } + + this._processNextRequest(); + }; + + const KeyShareDialog = sdk.getComponent("dialogs.KeyShareDialog"); + Modal.createDialog(KeyShareDialog, { + matrixClient: this._matrixClient, + userId: userId, + deviceId: deviceId, + onFinished: finished, + }); + this._currentUser = userId; + this._currentDevice = deviceId; + } +} + diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 54014a0166..59580e7cb6 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -19,6 +19,7 @@ import q from 'q'; import Matrix from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; +import createMatrixClient from './utils/createMatrixClient'; import Analytics from './Analytics'; import Notifier from './Notifier'; import UserActivity from './UserActivity'; @@ -34,29 +35,20 @@ import { _t } from './languageHandler'; * Called at startup, to attempt to build a logged-in Matrix session. It tries * a number of things: * - * 0. if it looks like we are in the middle of a registration process, it does - * nothing. * - * 1. if we have a loginToken in the (real) query params, it uses that to log - * in. - * - * 2. if we have a guest access token in the fragment query params, it uses + * 1. if we have a guest access token in the fragment query params, it uses * that. * - * 3. if an access token is stored in local storage (from a previous session), + * 2. if an access token is stored in local storage (from a previous session), * it uses that. * - * 4. it attempts to auto-register as a guest user. + * 3. it attempts to auto-register as a guest user. * - * If any of steps 1-4 are successful, it will call {setLoggedIn}, which in + * If any of steps 1-4 are successful, it will call {_doSetLoggedIn}, which in * turn will raise on_logged_in and will_start_client events. * * @param {object} opts * - * @param {object} opts.realQueryParams: string->string map of the - * query-parameters extracted from the real query-string of the starting - * URI. - * * @param {object} opts.fragmentQueryParams: string->string map of the * query-parameters extracted from the #-fragment of the starting URI. * @@ -70,54 +62,38 @@ import { _t } from './languageHandler'; * true; defines the IS to use. * * @returns {Promise} a promise which resolves when the above process completes. + * Resolves to `true` if we ended up starting a session, or `false` if we + * failed. */ export function loadSession(opts) { - const realQueryParams = opts.realQueryParams || {}; const fragmentQueryParams = opts.fragmentQueryParams || {}; let enableGuest = opts.enableGuest || false; const guestHsUrl = opts.guestHsUrl; const guestIsUrl = opts.guestIsUrl; const defaultDeviceDisplayName = opts.defaultDeviceDisplayName; - if (fragmentQueryParams.client_secret && fragmentQueryParams.sid) { - // this happens during email validation: the email contains a link to the - // IS, which in turn redirects back to vector. We let MatrixChat create a - // Registration component which completes the next stage of registration. - console.log("Not registering as guest: registration already in progress."); - return q(); - } - if (!guestHsUrl) { console.warn("Cannot enable guest access: can't determine HS URL to use"); enableGuest = false; } - if (realQueryParams.loginToken) { - if (!realQueryParams.homeserver) { - console.warn("Cannot log in with token: can't determine HS URL to use"); - } else { - return _loginWithToken(realQueryParams, defaultDeviceDisplayName); - } - } - if (enableGuest && fragmentQueryParams.guest_user_id && fragmentQueryParams.guest_access_token ) { console.log("Using guest access credentials"); - setLoggedIn({ + return _doSetLoggedIn({ userId: fragmentQueryParams.guest_user_id, accessToken: fragmentQueryParams.guest_access_token, homeserverUrl: guestHsUrl, identityServerUrl: guestIsUrl, guest: true, - }); - return q(); + }, true).then(() => true); } return _restoreFromLocalStorage().then((success) => { if (success) { - return; + return true; } if (enableGuest) { @@ -125,10 +101,30 @@ export function loadSession(opts) { } // fall back to login screen + return false; }); } -function _loginWithToken(queryParams, defaultDeviceDisplayName) { +/** + * @param {Object} queryParams string->string map of the + * query-parameters extracted from the real query-string of the starting + * URI. + * + * @param {String} defaultDeviceDisplayName + * + * @returns {Promise} promise which resolves to true if we completed the token + * login, else false + */ +export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) { + if (!queryParams.loginToken) { + return q(false); + } + + if (!queryParams.homeserver) { + console.warn("Cannot log in with token: can't determine HS URL to use"); + return q(false); + } + // create a temporary MatrixClient to do the login const client = Matrix.createClient({ baseUrl: queryParams.homeserver, @@ -141,17 +137,21 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) { }, ).then(function(data) { console.log("Logged in with token"); - setLoggedIn({ - userId: data.user_id, - deviceId: data.device_id, - accessToken: data.access_token, - homeserverUrl: queryParams.homeserver, - identityServerUrl: queryParams.identityServer, - guest: false, + return _clearStorage().then(() => { + _persistCredentialsToLocalStorage({ + userId: data.user_id, + deviceId: data.device_id, + accessToken: data.access_token, + homeserverUrl: queryParams.homeserver, + identityServerUrl: queryParams.identityServer, + guest: false, + }); + return true; }); - }, (err) => { + }).catch((err) => { console.error("Failed to log in with login token: " + err + " " + err.data); + return false; }); } @@ -172,16 +172,17 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { }, }).then((creds) => { console.log("Registered as guest: %s", creds.user_id); - setLoggedIn({ + return _doSetLoggedIn({ userId: creds.user_id, deviceId: creds.device_id, accessToken: creds.access_token, homeserverUrl: hsUrl, identityServerUrl: isUrl, guest: true, - }); + }, true).then(() => true); }, (err) => { console.error("Failed to register as guest: " + err + " " + err.data); + return false; }); } @@ -216,15 +217,14 @@ function _restoreFromLocalStorage() { if (accessToken && userId && hsUrl) { console.log("Restoring session for %s", userId); try { - setLoggedIn({ + return _doSetLoggedIn({ userId: userId, deviceId: deviceId, accessToken: accessToken, homeserverUrl: hsUrl, identityServerUrl: isUrl, guest: isGuest, - }); - return q(true); + }, false).then(() => true); } catch (e) { return _handleRestoreFailure(e); } @@ -245,7 +245,7 @@ function _handleRestoreFailure(e) { + ' This is a once off; sorry for the inconvenience.', ); - _clearLocalStorage(); + _clearStorage(); return q.reject(new Error( _t('Unable to restore previous session') + ': ' + msg, @@ -266,7 +266,7 @@ function _handleRestoreFailure(e) { return def.promise.then((success) => { if (success) { // user clicked continue. - _clearLocalStorage(); + _clearStorage(); return false; } @@ -277,17 +277,42 @@ function _handleRestoreFailure(e) { let rtsClient = null; export function initRtsClient(url) { - rtsClient = new RtsClient(url); + if (url) { + rtsClient = new RtsClient(url); + } else { + rtsClient = null; + } } /** - * Transitions to a logged-in state using the given credentials + * Transitions to a logged-in state using the given credentials. + * + * Starts the matrix client and all other react-sdk services that + * listen for events while a session is logged in. + * + * Also stops the old MatrixClient and clears old credentials/etc out of + * storage before starting the new client. + * * @param {MatrixClientCreds} credentials The credentials to use + * + * @returns {Promise} promise which resolves to the new MatrixClient once it has been started */ export function setLoggedIn(credentials) { - credentials.guest = Boolean(credentials.guest); + stopMatrixClient(); + return _doSetLoggedIn(credentials, true); +} - Analytics.setGuest(credentials.guest); +/** + * fires on_logging_in, optionally clears localstorage, persists new credentials + * to localstorage, starts the new client. + * + * @param {MatrixClientCreds} credentials + * @param {Boolean} clearStorage + * + * @returns {Promise} promise which resolves to the new MatrixClient once it has been started + */ +async function _doSetLoggedIn(credentials, clearStorage) { + credentials.guest = Boolean(credentials.guest); console.log( "setLoggedIn: mxid:", credentials.userId, @@ -295,32 +320,26 @@ export function setLoggedIn(credentials) { "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'}); + if (clearStorage) { + await _clearStorage(); + } + + Analytics.setGuest(credentials.guest); + // Resolves by default let teamPromise = Promise.resolve(null); - // persist the session + if (localStorage) { try { - localStorage.setItem("mx_hs_url", credentials.homeserverUrl); - localStorage.setItem("mx_is_url", credentials.identityServerUrl); - localStorage.setItem("mx_user_id", credentials.userId); - localStorage.setItem("mx_access_token", credentials.accessToken); - localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest)); - - // if we didn't get a deviceId from the login, leave mx_device_id unset, - // rather than setting it to "undefined". - // - // (in this case MatrixClient doesn't bother with the crypto stuff - // - that's fine for us). - if (credentials.deviceId) { - localStorage.setItem("mx_device_id", credentials.deviceId); - } + _persistCredentialsToLocalStorage(credentials); // The user registered as a PWLU (PassWord-Less User), the generated password // is cached here such that the user can change it at a later time. @@ -331,8 +350,6 @@ export function setLoggedIn(credentials) { cachedPassword: credentials.password, }); } - - console.log("Session persisted for %s", credentials.userId); } catch (e) { console.warn("Error using local storage: can't persist session!", e); } @@ -349,9 +366,6 @@ export function setLoggedIn(credentials) { console.warn("No local storage available: can't persist session!"); } - // stop any running clients before we create a new one with these new credentials - stopMatrixClient(); - MatrixClientPeg.replaceUsingCreds(credentials); teamPromise.then((teamToken) => { @@ -362,6 +376,26 @@ export function setLoggedIn(credentials) { }); startMatrixClient(); + return MatrixClientPeg.get(); +} + +function _persistCredentialsToLocalStorage(credentials) { + localStorage.setItem("mx_hs_url", credentials.homeserverUrl); + localStorage.setItem("mx_is_url", credentials.identityServerUrl); + localStorage.setItem("mx_user_id", credentials.userId); + localStorage.setItem("mx_access_token", credentials.accessToken); + localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest)); + + // if we didn't get a deviceId from the login, leave mx_device_id unset, + // rather than setting it to "undefined". + // + // (in this case MatrixClient doesn't bother with the crypto stuff + // - that's fine for us). + if (credentials.deviceId) { + localStorage.setItem("mx_device_id", credentials.deviceId); + } + + console.log("Session persisted for %s", credentials.userId); } /** @@ -400,7 +434,7 @@ export function logout() { * Starts the matrix client and all other react-sdk services that * listen for events while a session is logged in. */ -export function startMatrixClient() { +function startMatrixClient() { // dispatch this before starting the matrix client: it's used // to add listeners for the 'sync' event so otherwise we'd have // a race condition (and we need to dispatch synchronously for this @@ -416,34 +450,44 @@ export function startMatrixClient() { } /* - * Stops a running client and all related services, used after - * a session has been logged out / ended. + * Stops a running client and all related services, and clears persistent + * storage. Used after a session has been logged out. */ export function onLoggedOut() { - _clearLocalStorage(); stopMatrixClient(); + _clearStorage().done(); dis.dispatch({action: 'on_logged_out'}); } -function _clearLocalStorage() { +/** + * @returns {Promise} promise which resolves once the stores have been cleared + */ +function _clearStorage() { Analytics.logout(); - if (!window.localStorage) { - return; - } - const hsUrl = window.localStorage.getItem("mx_hs_url"); - const isUrl = window.localStorage.getItem("mx_is_url"); - window.localStorage.clear(); - // preserve our HS & IS URLs for convenience - // N.B. we cache them in hsUrl/isUrl and can't really inline them - // as getCurrentHsUrl() may call through to localStorage. - // NB. We do clear the device ID (as well as all the settings) - if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl); - if (isUrl) window.localStorage.setItem("mx_is_url", isUrl); + if (window.localStorage) { + const hsUrl = window.localStorage.getItem("mx_hs_url"); + const isUrl = window.localStorage.getItem("mx_is_url"); + window.localStorage.clear(); + + // preserve our HS & IS URLs for convenience + // N.B. we cache them in hsUrl/isUrl and can't really inline them + // as getCurrentHsUrl() may call through to localStorage. + // NB. We do clear the device ID (as well as all the settings) + if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl); + if (isUrl) window.localStorage.setItem("mx_is_url", isUrl); + } + + // create a temporary client to clear out the persistent stores. + const cli = createMatrixClient({ + // we'll never make any requests, so can pass a bogus HS URL + baseUrl: "", + }); + return cli.clearStores(); } /** - * Stop all the background processes related to the current client + * Stop all the background processes related to the current client. */ export function stopMatrixClient() { Notifier.stop(); @@ -454,7 +498,6 @@ export function stopMatrixClient() { if (cli) { cli.stopClient(); cli.removeAllListeners(); - cli.store.deleteAllData(); MatrixClientPeg.unset(); } } diff --git a/src/Login.js b/src/Login.js index 8db6e99b89..8225509919 100644 --- a/src/Login.js +++ b/src/Login.js @@ -178,11 +178,18 @@ export default class Login { } redirectToCas() { - var client = this._createTemporaryClient(); - var parsedUrl = url.parse(window.location.href, true); + const client = this._createTemporaryClient(); + const parsedUrl = url.parse(window.location.href, true); + + // XXX: at this point, the fragment will always be #/login, which is no + // use to anyone. Ideally, we would get the intended fragment from + // MatrixChat.screenAfterLogin so that you could follow #/room links etc + // through a CAS login. + parsedUrl.hash = ""; + parsedUrl.query["homeserver"] = client.getHomeserverUrl(); parsedUrl.query["identityServer"] = client.getIdentityServerUrl(); - var casUrl = client.getCasLoginUrl(url.format(parsedUrl)); + const casUrl = client.getCasLoginUrl(url.format(parsedUrl)); window.location.href = casUrl; } } diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 94e55a8d8a..47370e2142 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -16,12 +16,10 @@ limitations under the License. 'use strict'; -import Matrix from 'matrix-js-sdk'; import utils from 'matrix-js-sdk/lib/utils'; import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline'; import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set'; - -const localStorage = window.localStorage; +import createMatrixClient from './utils/createMatrixClient'; interface MatrixClientCreds { homeserverUrl: string, @@ -129,22 +127,7 @@ class MatrixClientPeg { timelineSupport: true, }; - if (localStorage) { - opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); - } - if (window.indexedDB && localStorage) { - // FIXME: bodge to remove old database. Remove this after a few weeks. - window.indexedDB.deleteDatabase("matrix-js-sdk:default"); - - opts.store = new Matrix.IndexedDBStore({ - indexedDB: window.indexedDB, - dbName: "riot-web-sync", - localStorage: localStorage, - workerScript: this.indexedDbWorkerScript, - }); - } - - this.matrixClient = Matrix.createClient(opts); + this.matrixClient = createMatrixClient(opts); // we're going to add eventlisteners for each matrix event tile, so the // potential number of event listeners is quite high. diff --git a/src/Modal.js b/src/Modal.js index 8d53b2da7d..e100105a88 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -64,7 +64,6 @@ const AsyncWrapper = React.createClass({ render: function() { const {loader, ...otherProps} = this.props; - if (this.state.component) { const Component = this.state.component; return ; @@ -199,4 +198,7 @@ class ModalManager { } } -export default new ModalManager(); +if (!global.singletonModalManager) { + global.singletonModalManager = new ModalManager(); +} +export default global.singletonModalManager; diff --git a/src/Rooms.js b/src/Rooms.js index 16b5ab9ee2..3ac7c68533 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -144,7 +144,18 @@ export function guessDMRoomTarget(room, me) { let oldestTs; let oldestUser; - // Pick the user who's been here longest (and isn't us) + // Pick the joined user who's been here longest (and isn't us), + for (const user of room.getJoinedMembers()) { + if (user.userId == me.userId) continue; + + if (oldestTs === undefined || user.events.member.getTs() < oldestTs) { + oldestUser = user; + oldestTs = user.events.member.getTs(); + } + } + if (oldestUser) return oldestUser; + + // if there are no joined members other than us, use the oldest member for (const user of room.currentState.getMembers()) { if (user.userId == me.userId) continue; diff --git a/src/RtsClient.js b/src/RtsClient.js index 8c3ce54b37..493b19599c 100644 --- a/src/RtsClient.js +++ b/src/RtsClient.js @@ -1,5 +1,7 @@ import 'whatwg-fetch'; +let fetchFunction = fetch; + function checkStatus(response) { if (!response.ok) { return response.text().then((text) => { @@ -31,7 +33,7 @@ const request = (url, opts) => { opts.body = JSON.stringify(opts.body); opts.headers['Content-Type'] = 'application/json'; } - return fetch(url, opts) + return fetchFunction(url, opts) .then(checkStatus) .then(parseJson); }; @@ -64,7 +66,7 @@ export default class RtsClient { client_secret: clientSecret, }, method: 'POST', - } + }, ); } @@ -74,7 +76,7 @@ export default class RtsClient { qs: { team_token: teamToken, }, - } + }, ); } @@ -91,7 +93,12 @@ export default class RtsClient { qs: { user_id: userId, }, - } + }, ); } + + // allow fetch to be replaced, for testing. + static setFetch(fn) { + fetchFunction = fn; + } } diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index 045ea63c34..8f113353d9 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -81,11 +81,13 @@ export default React.createClass({ FileSaver.saveAs(blob, 'riot-keys.txt'); this.props.onFinished(true); }).catch((e) => { + console.error("Error exporting e2e keys:", e); if (this._unmounted) { return; } + const msg = e.friendlyText || _t('Unknown error'); this.setState({ - errStr: e.message, + errStr: msg, phase: PHASE_EDIT, }); }); diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js index 91010d33b9..9eac7f78b2 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -89,11 +89,13 @@ export default React.createClass({ // TODO: it would probably be nice to give some feedback about what we've imported here. this.props.onFinished(true); }).catch((e) => { + console.error("Error importing e2e keys:", e); if (this._unmounted) { return; } + const msg = e.friendlyText || _t('Unknown error'); this.setState({ - errStr: e.message, + errStr: msg, phase: PHASE_EDIT, }); }); diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 0a8a02f327..051e49acda 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -240,7 +240,6 @@ export default React.createClass({ page_element = {}, + onTokenLoginCompleted: () => {}, }; }, @@ -192,7 +217,7 @@ module.exports = React.createClass({ componentWillMount: function() { SdkConfig.put(this.props.config); - RoomViewStore.addListener(this._onRoomViewStoreUpdated); + this._roomViewStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdated); this._onRoomViewStoreUpdated(); if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable(); @@ -269,30 +294,52 @@ module.exports = React.createClass({ window.addEventListener('resize', this.handleResize); this.handleResize(); - if (this.props.config.teamServerConfig && - this.props.config.teamServerConfig.teamServerURL - ) { - Lifecycle.initRtsClient(this.props.config.teamServerConfig.teamServerURL); - } + const teamServerConfig = this.props.config.teamServerConfig || {}; + Lifecycle.initRtsClient(teamServerConfig.teamServerURL); - // the extra q() ensures that synchronous exceptions hit the same codepath as - // asynchronous ones. - q().then(() => { - return Lifecycle.loadSession({ - realQueryParams: this.props.realQueryParams, - fragmentQueryParams: this.props.startingFragmentQueryParams, - enableGuest: this.props.enableGuest, - guestHsUrl: this.getCurrentHsUrl(), - guestIsUrl: this.getCurrentIsUrl(), - defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, + // the first thing to do is to try the token params in the query-string + Lifecycle.attemptTokenLogin(this.props.realQueryParams).then((loggedIn) => { + if(loggedIn) { + this.props.onTokenLoginCompleted(); + + // don't do anything else until the page reloads - just stay in + // the 'loading' state. + return; + } + + // if the user has followed a login or register link, don't reanimate + // the old creds, but rather go straight to the relevant page + const firstScreen = this.state.screenAfterLogin ? + this.state.screenAfterLogin.screen : null; + + if (firstScreen === 'login' || + firstScreen === 'register' || + firstScreen === 'forgot_password') { + this.setState({loading: false}); + this._showScreenAfterLogin(); + return; + } + + // the extra q() ensures that synchronous exceptions hit the same codepath as + // asynchronous ones. + return q().then(() => { + return Lifecycle.loadSession({ + fragmentQueryParams: this.props.startingFragmentQueryParams, + enableGuest: this.props.enableGuest, + guestHsUrl: this.getCurrentHsUrl(), + guestIsUrl: this.getCurrentIsUrl(), + defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, + }); + }).catch((e) => { + console.error("Unable to load session", e); + return false; + }).then((loadedSession) => { + if (!loadedSession) { + // fall back to showing the login screen + dis.dispatch({action: "start_login"}); + } }); - }).catch((e) => { - console.error("Unable to load session", e); - }).done(()=>{ - // stuff this through the dispatcher so that it happens - // after the on_logged_in action. - dis.dispatch({action: 'load_completed'}); - }); + }).done(); }, componentWillUnmount: function() { @@ -301,6 +348,7 @@ module.exports = React.createClass({ UDEHandler.stopListening(); window.removeEventListener("focus", this.onFocus); window.removeEventListener('resize', this.handleResize); + this._roomViewStoreToken.remove(); }, componentDidUpdate: function() { @@ -310,20 +358,19 @@ module.exports = React.createClass({ } }, - setStateForNewScreen: function(state) { + setStateForNewView: function(state) { + if (state.view === undefined) { + throw new Error("setStateForNewView with no view!"); + } const newState = { - screen: undefined, viewUserId: null, - loggedIn: false, - ready: false, - upgradeUsername: null, - guestAccessToken: null, }; Object.assign(newState, state); this.setState(newState); }, onAction: function(payload) { + // console.log(`MatrixClientPeg.onAction: ${payload.action}`); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); @@ -335,45 +382,19 @@ module.exports = React.createClass({ this._startRegistration(payload.params || {}); break; case 'start_login': - if (MatrixClientPeg.get() && - MatrixClientPeg.get().isGuest() - ) { - this.setState({ - guestCreds: MatrixClientPeg.getCredentials(), - }); - } - this.setStateForNewScreen({ - screen: 'login', + this.setStateForNewView({ + view: VIEWS.LOGIN, }); this.notifyNewScreen('login'); break; case 'start_post_registration': - this.setState({ // don't clobber loggedIn status - screen: 'post_registration', + this.setState({ + view: VIEWS.POST_REGISTRATION, }); break; - case 'start_upgrade_registration': - // also stash our credentials, then if we restore the session, - // we can just do it the same way whether we started upgrade - // registration or explicitly logged out - this.setStateForNewScreen({ - guestCreds: MatrixClientPeg.getCredentials(), - screen: "register", - upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(), - guestAccessToken: MatrixClientPeg.get().getAccessToken(), - }); - - // stop the client: if we are syncing whilst the registration - // is completed in another browser, we'll be 401ed for using - // a guest access token for a non-guest account. - // It will be restarted in onReturnToGuestClick - Lifecycle.stopMatrixClient(); - - this.notifyNewScreen('register'); - break; case 'start_password_recovery': - this.setStateForNewScreen({ - screen: 'forgot_password', + this.setStateForNewView({ + view: VIEWS.FORGOT_PASSWORD, }); this.notifyNewScreen('forgot_password'); break; @@ -397,7 +418,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().leave(payload.room_id).done(() => { modal.close(); - if (this.currentRoomId === payload.room_id) { + if (this.state.currentRoomId === payload.room_id) { dis.dispatch({action: 'view_next_room'}); } }, (err) => { @@ -514,10 +535,11 @@ module.exports = React.createClass({ break; case 'on_logging_in': // We are now logging in, so set the state to reflect that - // and also that we're not ready (we'll be marked as logged - // in once the login completes, then ready once the sync - // completes). - this.setState({loggingIn: true, ready: false}); + // NB. This does not touch 'ready' since if our dispatches + // are delayed, the sync could already have completed + this.setStateForNewView({ + view: VIEWS.LOGGING_IN, + }); break; case 'on_logged_in': this._onLoggedIn(payload.teamToken); @@ -528,9 +550,6 @@ module.exports = React.createClass({ case 'will_start_client': this._onWillStartClient(); break; - case 'load_completed': - this._onLoadCompleted(); - break; case 'new_version': this.onVersion( payload.currentVersion, payload.newVersion, @@ -540,6 +559,9 @@ module.exports = React.createClass({ case 'check_updates': this.setState({ checkingForUpdate: payload.value }); break; + case 'send_event': + this.onSendEvent(payload.room_id, payload.event); + break; } }, @@ -554,8 +576,8 @@ module.exports = React.createClass({ }, _startRegistration: function(params) { - this.setStateForNewScreen({ - screen: 'register', + this.setStateForNewView({ + view: VIEWS.REGISTER, // these params may be undefined, but if they are, // unset them from our state: we don't want to // resume a previous registration session if the @@ -749,8 +771,8 @@ module.exports = React.createClass({ title: _t('Create Room'), description: _t('Room name (optional)'), button: _t('Create Room'), - onFinished: (should_create, name) => { - if (should_create) { + onFinished: (shouldCreate, name) => { + if (shouldCreate) { const createOpts = {}; if (name) createOpts.name = name; createRoom({createOpts}).done(); @@ -863,22 +885,6 @@ module.exports = React.createClass({ }); }, - /** - * Called when the sessionloader has finished - */ - _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(); - } - }, - /** * Called whenever someone changes the theme * @@ -931,9 +937,7 @@ module.exports = React.createClass({ */ _onLoggedIn: function(teamToken) { this.setState({ - guestCreds: null, - loggedIn: true, - loggingIn: false, + view: VIEWS.LOGGED_IN, }); if (teamToken) { @@ -975,6 +979,7 @@ module.exports = React.createClass({ this.state.screenAfterLogin.screen, this.state.screenAfterLogin.params, ); + // XXX: is this necessary? `showScreen` should do it for us. this.notifyNewScreen(this.state.screenAfterLogin.screen); this.setState({screenAfterLogin: null}); } else if (localStorage && localStorage.getItem('mx_last_room_id')) { @@ -993,8 +998,8 @@ module.exports = React.createClass({ */ _onLoggedOut: function() { this.notifyNewScreen('login'); - this.setStateForNewScreen({ - loggedIn: false, + this.setStateForNewView({ + view: VIEWS.LOGIN, ready: false, collapse_lhs: false, collapse_rhs: false, @@ -1010,6 +1015,10 @@ module.exports = React.createClass({ */ _onWillStartClient() { const self = this; + // if the client is about to start, we are, by definition, not ready. + // Set ready to false now, then it'll be set to true when the sync + // listener we set below fires. + this.setState({ready: false}); const cli = MatrixClientPeg.get(); // Allow the JS SDK to reap timeline events. This reduces the amount of @@ -1080,6 +1089,14 @@ module.exports = React.createClass({ } } }); + + const krh = new KeyRequestHandler(cli); + cli.on("crypto.roomKeyRequest", (req) => { + krh.handleKeyRequest(req); + }); + cli.on("crypto.roomKeyRequestCancellation", (req) => { + krh.handleKeyRequestCancellation(req); + }); }, showScreen: function(screen, params) { @@ -1157,7 +1174,7 @@ module.exports = React.createClass({ // we can't view a room unless we're logged in // (a guest account is fine) - if (this.state.loggedIn) { + if (this.state.view === VIEWS.LOGGED_IN) { dis.dispatch(payload); } } else if (screen.indexOf('user/') == 0) { @@ -1255,27 +1272,25 @@ module.exports = React.createClass({ this.showScreen("forgot_password"); }, - onReturnToGuestClick: function() { - // reanimate our guest login - if (this.state.guestCreds) { - Lifecycle.setLoggedIn(this.state.guestCreds); - this.setState({guestCreds: null}); - } + onReturnToAppClick: function() { + // treat it the same as if the user had completed the login + this._onLoggedIn(null); }, + // returns a promise which resolves to the new MatrixClient onRegistered: function(credentials, teamToken) { // XXX: These both should be in state or ideally store(s) because we risk not // rendering the most up-to-date view of state otherwise. // teamToken may not be truthy this._teamToken = teamToken; this._is_registered = true; - Lifecycle.setLoggedIn(credentials); + return Lifecycle.setLoggedIn(credentials); }, onFinishPostRegistration: function() { // Don't confuse this with "PageType" which is the middle window to show this.setState({ - screen: undefined, + view: VIEWS.LOGGED_IN, }); this.showScreen("settings"); }, @@ -1290,6 +1305,27 @@ module.exports = React.createClass({ }); }, + onSendEvent: function(roomId, event) { + const cli = MatrixClientPeg.get(); + if (!cli) { + dis.dispatch({action: 'message_send_failed'}); + return; + } + + cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => { + dis.dispatch({action: 'message_sent'}); + }, (err) => { + if (err.name === 'UnknownDeviceError') { + dis.dispatch({ + action: 'unknown_device_error', + err: err, + room: cli.getRoom(roomId), + }); + } + dis.dispatch({action: 'message_send_failed'}); + }); + }, + updateStatusIndicator: function(state, prevState) { let notifCount = 0; @@ -1344,11 +1380,9 @@ module.exports = React.createClass({ }, render: function() { - // `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) { + // console.log(`Rendering MatrixChat with view ${this.state.view}`); + + if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN) { const Spinner = sdk.getComponent('elements.Spinner'); return (
@@ -1358,7 +1392,7 @@ module.exports = React.createClass({ } // needs to be before normal PageTypes as you are logged in technically - if (this.state.screen == 'post_registration') { + if (this.state.view === VIEWS.POST_REGISTRATION) { const PostRegistration = sdk.getComponent('structures.login.PostRegistration'); return ( - ); - } else if (this.state.loggedIn) { - // we think we are logged in, but are still waiting for the /sync to complete - const Spinner = sdk.getComponent('elements.Spinner'); - return ( - - ); - } else if (this.state.screen == 'register') { + if (this.state.view === VIEWS.LOGGED_IN) { + // `ready` and `view==LOGGED_IN` may be set before `page_type` (because the + // latter is set via the dispatcher). If we don't yet have a `page_type`, + // keep showing the spinner for now. + if (this.state.ready && this.state.page_type) { + /* 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 { + // we think we are logged in, but are still waiting for the /sync to complete + const Spinner = sdk.getComponent('elements.Spinner'); + return ( + + ); + } + } + + if (this.state.view === VIEWS.REGISTER) { const Registration = sdk.getComponent('structures.login.Registration'); return ( ); - } else if (this.state.screen == 'forgot_password') { + } + + + if (this.state.view === VIEWS.FORGOT_PASSWORD) { const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); return ( ); - } else { + } + + if (this.state.view === VIEWS.LOGIN) { const Login = sdk.getComponent('structures.login.Login'); return ( ); } + + console.error(`Unknown view ${this.state.view}`); }, }); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 79420e776a..b29b3579f0 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -93,6 +93,7 @@ module.exports = React.createClass({ roomId: null, roomLoading: true, peekLoading: false, + shouldPeek: true, // The event to be scrolled to initially initialEventId: null, @@ -168,8 +169,32 @@ module.exports = React.createClass({ initialEventId: RoomViewStore.getInitialEventId(), initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), + forwardingEvent: RoomViewStore.getForwardingEvent(), + shouldPeek: RoomViewStore.shouldPeek(), }; + // finished joining, start waiting for a room and show a spinner. See onRoom. + newState.waitingForRoom = this.state.joining && !newState.joining && + !RoomViewStore.getJoinError(); + + // Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307 + console.log( + 'RVS update:', + newState.roomId, + newState.roomAlias, + 'loading?', newState.roomLoading, + 'joining?', newState.joining, + 'initial?', initial, + 'waiting?', newState.waitingForRoom, + 'shouldPeek?', newState.shouldPeek, + ); + + // NB: This does assume that the roomID will not change for the lifetime of + // the RoomView instance + if (initial) { + newState.room = MatrixClientPeg.get().getRoom(newState.roomId); + } + // Clear the search results when clicking a search result (which changes the // currently scrolled to event, this.state.initialEventId). if (this.state.initialEventId !== newState.initialEventId) { @@ -185,8 +210,9 @@ module.exports = React.createClass({ this.setState(newState, () => { // At this point, this.state.roomId could be null (e.g. the alias might not // have been resolved yet) so anything called here must handle this case. - this._onHaveRoom(); - this.onRoom(MatrixClientPeg.get().getRoom(this.state.roomId)); + if (initial) { + this._onHaveRoom(); + } }); }, @@ -202,25 +228,24 @@ module.exports = React.createClass({ // which must be by alias or invite wherever possible (peeking currently does // not work over federation). - // NB. We peek if we are not in the room, although if we try to peek into - // a room in which we have a member event (ie. we've left) synapse will just - // send us the same data as we get in the sync (ie. the last events we saw). - const room = MatrixClientPeg.get().getRoom(this.state.roomId); - let isUserJoined = null; + // NB. We peek if we have never seen the room before (i.e. js-sdk does not know + // about it). We don't peek in the historical case where we were joined but are + // now not joined because the js-sdk peeking API will clobber our historical room, + // making it impossible to indicate a newly joined room. + const room = this.state.room; if (room) { - isUserJoined = room.hasMembershipState( - MatrixClientPeg.get().credentials.userId, 'join', - ); - this._updateAutoComplete(room); this.tabComplete.loadEntries(room); + this.setState({ + unsentMessageError: this._getUnsentMessageError(room), + }); + this._onRoomLoaded(room); } - if (!isUserJoined && !this.state.joining && this.state.roomId) { + if (!this.state.joining && this.state.roomId) { if (this.props.autoJoin) { this.onJoinButtonClicked(); - } else if (this.state.roomId) { + } else if (!room && this.state.shouldPeek) { console.log("Attempting to peek into room %s", this.state.roomId); - this.setState({ peekLoading: true, }); @@ -244,12 +269,9 @@ module.exports = React.createClass({ } }).done(); } - } else if (isUserJoined) { + } else if (room) { + // Stop peeking because we have joined this room previously MatrixClientPeg.get().stopPeeking(); - this.setState({ - unsentMessageError: this._getUnsentMessageError(room), - }); - this._onRoomLoaded(room); } }, @@ -436,11 +458,6 @@ module.exports = React.createClass({ callState: callState }); - break; - case 'forward_event': - this.setState({ - forwardingEvent: payload.content, - }); break; } }, @@ -607,6 +624,7 @@ module.exports = React.createClass({ } this.setState({ room: room, + waitingForRoom: false, }, () => { this._onRoomLoaded(room); }); @@ -662,7 +680,14 @@ module.exports = React.createClass({ onRoomMemberMembership: function(ev, member, oldMembership) { if (member.userId == MatrixClientPeg.get().credentials.userId) { - this.forceUpdate(); + + if (member.membership === 'join') { + this.setState({ + waitingForRoom: false, + }); + } else { + this.forceUpdate(); + } } }, @@ -1140,8 +1165,13 @@ module.exports = React.createClass({ this.updateTint(); this.setState({ editingRoomSettings: false, - forwardingEvent: null, }); + if (this.state.forwardingEvent) { + dis.dispatch({ + action: 'forward_event', + event: null, + }); + } dis.dispatch({action: 'focus_composer'}); }, @@ -1416,6 +1446,10 @@ module.exports = React.createClass({ const Loader = sdk.getComponent("elements.Spinner"); const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); + // Whether the preview bar spinner should be shown. We do this when joining or + // when waiting for a room to be returned by js-sdk when joining + const previewBarSpinner = this.state.joining || this.state.waitingForRoom; + if (!this.state.room) { if (this.state.roomLoading || this.state.peekLoading) { return ( @@ -1435,7 +1469,7 @@ module.exports = React.createClass({ // We have no room object for this room, only the ID. // We've got to this room by following a link, possibly a third party invite. - var room_alias = this.state.room_alias; + const roomAlias = this.state.roomAlias; return (
@@ -1548,7 +1582,7 @@ module.exports = React.createClass({ } else if (this.state.uploadingRoomSettings) { aux = ; } else if (this.state.forwardingEvent !== null) { - aux = ; + aux = ; } else if (this.state.searching) { hideCancel = true; // has own cancel aux = ; @@ -1568,7 +1602,7 @@ module.exports = React.createClass({ excessHeight) { + break; + } // The tile may not have a scroll token, so guard it if (tile.dataset.scrollTokens) { markerScrollToken = tile.dataset.scrollTokens.split(',')[0]; } - if (tile.clientHeight > excessHeight) { - break; - } } if (markerScrollToken) { diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 7edeafe889..4cf81ee375 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -110,6 +110,13 @@ const ANALYTICS_SETTINGS_LABELS = [ }, ]; +const WEBRTC_SETTINGS_LABELS = [ + { + id: 'webRtcForceTURN', + label: 'Disable Peer-to-Peer for 1:1 calls', + }, +]; + // Warning: Each "label" string below must be added to i18n/strings/en_EN.json, // since they will be translated when rendered. const CRYPTO_SETTINGS_LABELS = [ @@ -162,9 +169,6 @@ module.exports = React.createClass({ // The base URL to use in the referral link. Defaults to window.location.origin. referralBaseUrl: React.PropTypes.string, - // true if RightPanel is collapsed - collapsedRhs: React.PropTypes.bool, - // Team token for the referral link. If falsy, the referral section will // not appear teamToken: React.PropTypes.string, @@ -310,11 +314,6 @@ module.exports = React.createClass({ }, onAvatarPickerClick: function(ev) { - if (MatrixClientPeg.get().isGuest()) { - dis.dispatch({action: 'view_set_mxid'}); - return; - } - if (this.refs.file_label) { this.refs.file_label.click(); } @@ -389,17 +388,14 @@ module.exports = React.createClass({ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: _t("Success"), - description: _t("Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them") + ".", + description: _t( + "Your password was successfully changed. You will not receive " + + "push notifications on other devices until you log back in to them", + ) + ".", }); dis.dispatch({action: 'password_changed'}); }, - onUpgradeClicked: function() { - dis.dispatch({ - action: "start_upgrade_registration", - }); - }, - onEnableNotificationsChange: function(event) { UserSettingsStore.setEnableNotifications(event.target.checked); }, @@ -427,7 +423,10 @@ module.exports = React.createClass({ this._addThreepid.addEmailAddress(emailAddress, true).done(() => { Modal.createDialog(QuestionDialog, { title: _t("Verification Pending"), - description: _t("Please check your email and click on the link it contains. Once this is done, click continue."), + description: _t( + "Please check your email and click on the link it contains. Once this " + + "is done, click continue.", + ), button: _t('Continue'), onFinished: this.onEmailDialogFinished, }); @@ -447,7 +446,7 @@ module.exports = React.createClass({ const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createDialog(QuestionDialog, { title: _t("Remove Contact Information?"), - description: _t("Remove %(threePid)s?", { threePid : threepid.address }), + description: _t("Remove %(threePid)s?", { threePid: threepid.address }), button: _t('Remove'), onFinished: (submit) => { if (submit) { @@ -489,8 +488,8 @@ module.exports = React.createClass({ this.setState({email_add_pending: false}); if (err.errcode == 'M_THREEPID_AUTH_FAILED') { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - let message = _t("Unable to verify email address.") + " " + - _t("Please check your email and click on the link it contains. Once this is done, click continue."); + const message = _t("Unable to verify email address.") + " " + + _t("Please check your email and click on the link it contains. Once this is done, click continue."); Modal.createDialog(QuestionDialog, { title: _t("Verification Pending"), description: message, @@ -608,7 +607,7 @@ module.exports = React.createClass({ } }, - _renderLanguageSetting: function () { + _renderLanguageSetting: function() { const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown'); return
@@ -639,7 +638,7 @@ module.exports = React.createClass({ UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) } + onChange={ this._onPreviewsDisabledChanged } />
; }, + _onPreviewsDisabledChanged: function(e) { + UserSettingsStore.setUrlPreviewsDisabled(e.target.checked); + }, + _renderSyncedSetting: function(setting) { + // TODO: this ought to be a separate component so that we don't need + // to rebind the onChange each time we render + + const onChange = (e) => { + UserSettingsStore.setSyncedSetting(setting.id, e.target.checked); + if (setting.fn) setting.fn(e.target.checked); + }; + return
{ - UserSettingsStore.setSyncedSetting(setting.id, e.target.checked); - if (setting.fn) setting.fn(e.target.checked); - } - } + onChange={ onChange } />