From a850f19cd40486a6328ac93c49fa1be3fe7d1945 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 26 Oct 2015 13:54:54 +0000 Subject: [PATCH 001/152] Separate out the activity watcher from presence code so I can hook read receipts into it without tangling it into the presence code. --- src/Presence.js | 66 +++++++++++++++-------------- src/UserActivity.js | 57 +++++++++++++++++++++++++ src/controllers/pages/MatrixChat.js | 3 ++ 3 files changed, 95 insertions(+), 31 deletions(-) create mode 100644 src/UserActivity.js diff --git a/src/Presence.js b/src/Presence.js index d77058abd8..1f5617514a 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -15,58 +15,54 @@ limitations under the License. */ var MatrixClientPeg = require("./MatrixClientPeg"); +var dis = require("./dispatcher"); // Time in ms after that a user is considered as unavailable/away var UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins var PRESENCE_STATES = ["online", "offline", "unavailable"]; -// The current presence state -var state, timer; - -module.exports = { +class Presence { /** * Start listening the user activity to evaluate his presence state. * Any state change will be sent to the Home Server. */ - start: function() { - var self = this; + start() { this.running = true; - if (undefined === state) { - // The user is online if they move the mouse or press a key - document.onmousemove = function() { self._resetTimer(); }; - document.onkeypress = function() { self._resetTimer(); }; + if (undefined === this.state) { this._resetTimer(); + this.dispatcherRef = dis.register(this._onUserActivity.bind(this)); } - }, + } /** * Stop tracking user activity */ - stop: function() { + stop() { this.running = false; - if (timer) { - clearTimeout(timer); - timer = undefined; + if (this.timer) { + clearInterval(this.timer); + this.timer = undefined; + dis.unregister(this.dispatcherRef); } - state = undefined; - }, + this.state = undefined; + } /** * Get the current presence state. * @returns {string} the presence state (see PRESENCE enum) */ - getState: function() { - return state; - }, + getState() { + return this.state; + } /** * Set the presence state. * If the state has changed, the Home Server will be notified. * @param {string} newState the new presence state (see PRESENCE enum) */ - setState: function(newState) { - if (newState === state) { + setState(newState) { + if (newState === this.state) { return; } if (PRESENCE_STATES.indexOf(newState) === -1) { @@ -75,33 +71,41 @@ module.exports = { if (!this.running) { return; } - state = newState; - MatrixClientPeg.get().setPresence(state).done(function() { + var old_state = this.state; + this.state = newState; + MatrixClientPeg.get().setPresence(this.state).done(function() { console.log("Presence: %s", newState); }, function(err) { console.error("Failed to set presence: %s", err); + this.state = old_state; }); - }, + } /** * Callback called when the user made no action on the page for UNAVAILABLE_TIME ms. * @private */ - _onUnavailableTimerFire: function() { + _onUnavailableTimerFire() { this.setState("unavailable"); - }, + } + + _onUserActivity() { + this._resetTimer(); + } /** * Callback called when the user made an action on the page * @private */ - _resetTimer: function() { + _resetTimer() { var self = this; this.setState("online"); // Re-arm the timer - clearTimeout(timer); - timer = setTimeout(function() { + clearTimeout(this.timer); + this.timer = setTimeout(function() { self._onUnavailableTimerFire(); }, UNAVAILABLE_TIME_MS); } -}; +} + +module.exports = new Presence(); diff --git a/src/UserActivity.js b/src/UserActivity.js new file mode 100644 index 0000000000..46a46f0b0e --- /dev/null +++ b/src/UserActivity.js @@ -0,0 +1,57 @@ +/* +Copyright 2015 OpenMarket 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. +*/ + +var dis = require("./dispatcher"); + +var MIN_DISPATCH_INTERVAL = 1 * 1000; + +/** + * This class watches for user activity (moving the mouse or pressing a key) + * and dispatches the user_activity action at times when the user is interacting + * with the app (but at a much lower frequency than mouse move events) + */ +class UserActivity { + + /** + * Start listening to user activity + */ + start() { + document.onmousemove = this._onUserActivity.bind(this); + document.onkeypress = this._onUserActivity.bind(this); + this.lastActivityAt = (new Date).getTime(); + this.lastDispatchAt = 0; + } + + /** + * Stop tracking user activity + */ + stop() { + document.onmousemove = undefined; + document.onkeypress = undefined; + } + + _onUserActivity() { + this.lastActivityAt = (new Date).getTime(); + if (this.lastDispatchAt < this.lastActivityAt - MIN_DISPATCH_INTERVAL) { + this.lastDispatchAt = this.lastActivityAt; + dis.dispatch({ + action: 'user_activity' + }); + } + } +} + +module.exports = new UserActivity(); diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index 2a712b22f6..97fc3a1fe1 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/controllers/pages/MatrixChat.js @@ -16,6 +16,7 @@ limitations under the License. var MatrixClientPeg = require("../../MatrixClientPeg"); var RoomListSorter = require("../../RoomListSorter"); +var UserActivity = require("../../UserActivity"); var Presence = require("../../Presence"); var dis = require("../../dispatcher"); @@ -92,6 +93,7 @@ module.exports = { window.localStorage.clear(); } Notifier.stop(); + UserActivity.stop(); Presence.stop(); MatrixClientPeg.get().stopClient(); MatrixClientPeg.get().removeAllListeners(); @@ -316,6 +318,7 @@ module.exports = { }); }); Notifier.start(); + UserActivity.start(); Presence.start(); cli.startClient(); }, From 2365fe8ceb92d908e3050019cfb9f25ce7d3b002 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 28 Oct 2015 15:15:35 +0000 Subject: [PATCH 002/152] Refresh room & member avatars when a roommember.name event comes in --- src/controllers/atoms/MemberAvatar.js | 20 ++++++++++++++++-- src/controllers/atoms/RoomAvatar.js | 27 +++++++++++++++++++++---- src/controllers/organisms/MemberList.js | 5 +++++ src/controllers/organisms/RoomList.js | 5 +++++ src/controllers/pages/MatrixChat.js | 2 +- 5 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/controllers/atoms/MemberAvatar.js b/src/controllers/atoms/MemberAvatar.js index a94b4291a0..e170d2e04c 100644 --- a/src/controllers/atoms/MemberAvatar.js +++ b/src/controllers/atoms/MemberAvatar.js @@ -35,6 +35,10 @@ module.exports = { } }, + componentWillReceiveProps: function(nextProps) { + this.refreshUrl(); + }, + defaultAvatarUrl: function(member, width, height, resizeMethod) { if (this.skinnedDefaultAvatarUrl) { return this.skinnedDefaultAvatarUrl(member, width, height, resizeMethod); @@ -52,7 +56,7 @@ module.exports = { }); }, - getInitialState: function() { + _computeUrl: function() { var url = this.props.member.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), this.props.width, @@ -68,8 +72,20 @@ module.exports = { this.props.resizeMethod ); } + return url; + }, + + refreshUrl: function() { + var newUrl = this._computeUrl(); + if (newUrl != this.currentUrl) { + this.currentUrl = newUrl; + this.setState({imageUrl: newUrl}); + } + }, + + getInitialState: function() { return { - imageUrl: url + imageUrl: this._computeUrl() }; } }; diff --git a/src/controllers/atoms/RoomAvatar.js b/src/controllers/atoms/RoomAvatar.js index 6c55345ead..061a12eb14 100644 --- a/src/controllers/atoms/RoomAvatar.js +++ b/src/controllers/atoms/RoomAvatar.js @@ -41,10 +41,29 @@ module.exports = { }, componentWillReceiveProps: function(nextProps) { - this._update(); - this.setState({ - imageUrl: this._nextUrl() - }); + this.refreshImageUrl(); + }, + + refreshImageUrl: function(nextProps) { + // If the list has changed, we start from scratch and re-check, but + // don't do so unless the list has changed or we'd re-try fetching + // images each time we re-rendered + var newList = this.getUrlList(); + var differs = false; + for (var i = 0; i < newList.length && i < this.urlList.length; ++i) { + if (this.urlList[i] != newList[i]) differs = true; + } + if (this.urlList.length != newList.length) differs = true; + + if (differs) { + console.log("list differs"); + this._update(); + this.setState({ + imageUrl: this._nextUrl() + }); + } else { + console.log("list is the same"); + } }, _update: function() { diff --git a/src/controllers/organisms/MemberList.js b/src/controllers/organisms/MemberList.js index 48fef531bb..a2298a20ba 100644 --- a/src/controllers/organisms/MemberList.js +++ b/src/controllers/organisms/MemberList.js @@ -38,6 +38,7 @@ module.exports = { componentWillMount: function() { var cli = MatrixClientPeg.get(); cli.on("RoomState.members", this.onRoomStateMember); + cli.on("RoomMember.name", this.onRoomMemberName); cli.on("Room", this.onRoom); // invites }, @@ -97,6 +98,10 @@ module.exports = { this._updateList(); }, + onRoomMemberName: function(ev, member) { + this._updateList(); + }, + _updateList: function() { this.memberDict = this.getMemberDict(); diff --git a/src/controllers/organisms/RoomList.js b/src/controllers/organisms/RoomList.js index ff3522d9c3..6b5a4c4722 100644 --- a/src/controllers/organisms/RoomList.js +++ b/src/controllers/organisms/RoomList.js @@ -29,6 +29,7 @@ module.exports = { cli.on("Room.timeline", this.onRoomTimeline); cli.on("Room.name", this.onRoomName); cli.on("RoomState.events", this.onRoomStateEvents); + cli.on("RoomMember.name", this.onRoomMemberName); var rooms = this.getRoomList(); this.setState({ @@ -89,6 +90,10 @@ module.exports = { this.refreshRoomList(); }, + onRoomMemberName: function(ev, member) { + this.refreshRoomList(); + }, + refreshRoomList: function() { var rooms = this.getRoomList(); this.setState({ diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index 97fc3a1fe1..edb55eb1b0 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/controllers/pages/MatrixChat.js @@ -320,7 +320,7 @@ module.exports = { Notifier.start(); UserActivity.start(); Presence.start(); - cli.startClient(); + cli.startClient({resolveInvitesToProfiles: true}); }, onKeyDown: function(ev) { From c46f40c816ff63cdaee7ca3839583ac6b78aca51 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 28 Oct 2015 18:02:50 +0000 Subject: [PATCH 003/152] bump js-sdk -> 0.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f28cfdf215..ff7d75e712 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "flux": "^2.0.3", "glob": "^5.0.14", "linkifyjs": "^2.0.0-beta.4", - "matrix-js-sdk": "^0.2.2", + "matrix-js-sdk": "^0.3.0", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^0.13.3", From 7c9b773bf8bb162d3e16c662d626d5c7ddf3e9e5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 3 Nov 2015 11:22:18 +0000 Subject: [PATCH 004/152] unintentionally comitted logging --- src/controllers/atoms/RoomAvatar.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/controllers/atoms/RoomAvatar.js b/src/controllers/atoms/RoomAvatar.js index 061a12eb14..57c9a71842 100644 --- a/src/controllers/atoms/RoomAvatar.js +++ b/src/controllers/atoms/RoomAvatar.js @@ -56,13 +56,10 @@ module.exports = { if (this.urlList.length != newList.length) differs = true; if (differs) { - console.log("list differs"); this._update(); this.setState({ imageUrl: this._nextUrl() }); - } else { - console.log("list is the same"); } }, From 5a72f199e1b69f0acfb6f6a50f593849e4a3e319 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 3 Nov 2015 11:41:18 +0000 Subject: [PATCH 005/152] listen for read receipts --- src/controllers/organisms/RoomView.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index 931dbb5bcb..925f896556 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -43,6 +43,7 @@ module.exports = { this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.name", this.onRoomName); + MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping); this.atBottom = true; }, @@ -59,6 +60,7 @@ module.exports = { if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); + MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping); } }, @@ -149,6 +151,12 @@ module.exports = { } }, + onRoomReceipt: function(receiptEvent, room) { + if (room.roomId == this.props.roomId) { + this.forceUpdate(); + } + }, + onRoomMemberTyping: function(ev, member) { this.forceUpdate(); }, From 86ef0e762ed50773d307e6f66d598bef4df311f5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 3 Nov 2015 14:08:51 +0000 Subject: [PATCH 006/152] Merge code to send read receipts into react-sdk RoomView controller --- src/controllers/organisms/RoomView.js | 55 ++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index 925f896556..2dc9e1bf26 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -89,6 +89,9 @@ module.exports = { messageWrapper.scrollTop = messageWrapper.scrollHeight; } break; + case 'user_activity': + this.sendReadReceipt(); + break; } }, @@ -172,6 +175,8 @@ module.exports = { messageWrapper.scrollTop = messageWrapper.scrollHeight; + this.sendReadReceipt(); + this.fillSpace(); } }, @@ -354,7 +359,7 @@ module.exports = { } } ret.unshift( -
  • +
  • ); ++count; } @@ -446,5 +451,53 @@ module.exports = { uploadingRoomSettings: false, }); } + }, + + _collectEventNode: function(eventId, node) { + if (this.eventNodes == undefined) this.eventNodes = {}; + this.eventNodes[eventId] = node; + }, + + _indexForEventId(evId) { + for (var i = 0; i < this.state.room.timeline.length; ++i) { + if (evId == this.state.room.timeline[i].getId()) { + return i; + } + } + return null; + }, + + sendReadReceipt: function() { + var currentReadUpToEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId); + var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId); + + var lastReadEventIndex = this._getLastDisplayedEventIndex(); + if (lastReadEventIndex === null) return; + + if (lastReadEventIndex > currentReadUpToEventIndex) { + MatrixClientPeg.get().sendReadReceipt(this.state.room.timeline[lastReadEventIndex]); + } + }, + + _getLastDisplayedEventIndex: function() { + if (this.eventNodes === undefined) return null; + + var messageWrapper = this.refs.messageWrapper; + if (messageWrapper === undefined) return null; + var wrapperRect = messageWrapper.getDOMNode().getBoundingClientRect(); + + for (var i = this.state.room.timeline.length-1; i >= 0; --i) { + var ev = this.state.room.timeline[i]; + var node = this.eventNodes[ev.getId()]; + if (node === undefined) continue; + + var domNode = node.getDOMNode(); + var boundingRect = domNode.getBoundingClientRect(); + + if (boundingRect.bottom < wrapperRect.bottom) { + return i; + } + } + return null; } }; From f9385b455a1be6598dca926935fc00211cf0df36 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 5 Nov 2015 13:27:03 +0000 Subject: [PATCH 007/152] Don't try to send read receipts if the room is null --- src/controllers/organisms/RoomView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index 2dc9e1bf26..7ab59b497a 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -468,6 +468,7 @@ module.exports = { }, sendReadReceipt: function() { + if (!this.state.room) return; var currentReadUpToEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId); var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId); From d8edbd2e3c79984a01c7ae6dec0bc9b45e60af90 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 5 Nov 2015 14:45:48 +0000 Subject: [PATCH 008/152] Requires js-sdk develop --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ff7d75e712..c1c7f5fbe5 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "flux": "^2.0.3", "glob": "^5.0.14", "linkifyjs": "^2.0.0-beta.4", - "matrix-js-sdk": "^0.3.0", + "matrix-js-sdk": "https://github.com/matrix-org/matrix-js-sdk.git#develop", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^0.13.3", From f4e65f8e17524994321d62c52a65dbc084dcd720 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 5 Nov 2015 15:07:46 +0000 Subject: [PATCH 009/152] Remove name event listener --- src/controllers/organisms/MemberList.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/organisms/MemberList.js b/src/controllers/organisms/MemberList.js index a2298a20ba..4dfe5330e1 100644 --- a/src/controllers/organisms/MemberList.js +++ b/src/controllers/organisms/MemberList.js @@ -46,6 +46,7 @@ module.exports = { if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room", this.onRoom); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); + MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName); MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn); } }, From f4dd88ed642fcae8316e12c75685eeffb0508624 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 12 Nov 2015 11:54:35 +0000 Subject: [PATCH 010/152] Remove ServerConfig; Add Signup logic class - ServerConfig seems too specific to Vector, but we can always add it back later. - Signup.js contains all the logic for determining what to show which was previously in UI components. --- src/Signup.js | 49 +++++++++++++++++ src/controllers/molecules/ServerConfig.js | 67 ----------------------- 2 files changed, 49 insertions(+), 67 deletions(-) create mode 100644 src/Signup.js delete mode 100644 src/controllers/molecules/ServerConfig.js diff --git a/src/Signup.js b/src/Signup.js new file mode 100644 index 0000000000..06b70fb7f5 --- /dev/null +++ b/src/Signup.js @@ -0,0 +1,49 @@ +"use strict"; +var MatrixClientPeg = require("./MatrixClientPeg"); +var dis = require("./dispatcher"); + +class Register { + +} + +class Login { + constructor(hsUrl, isUrl) { + this._hsUrl = hsUrl; + this._isUrl = isUrl; + this._currentFlowIndex = 0; + this._flows = []; + } + + getFlows() { + var self = this; + // feels a bit wrong to be clobbering the global client for something we + // don't even know if it'll work, but we'll leave this here for now to + // not complicate matters further. It would be nicer to isolate this + // logic entirely from the rest of the app though. + MatrixClientPeg.replaceUsingUrls( + this._hsUrl, + this._isUrl + ); + return MatrixClientPeg.get().loginFlows().then(function(result) { + self._flows = result.flows; + self._currentFlowIndex = 0; + // technically the UI should display options for all flows for the + // user to then choose one, so return all the flows here. + return self._flows; + }); + } + + chooseFlow(flowIndex) { + this._currentFlowIndex = flowIndex; + } + + getCurrentFlowStep() { + // technically the flow can have multiple steps, but no one does this + // for login so we can ignore it. + var flowStep = this._flows[this._currentFlowIndex]; + return flowStep ? flowStep.type : null; + } +} + +module.exports.Register = Register; +module.exports.Login = Login; diff --git a/src/controllers/molecules/ServerConfig.js b/src/controllers/molecules/ServerConfig.js deleted file mode 100644 index 77fcb1d8c5..0000000000 --- a/src/controllers/molecules/ServerConfig.js +++ /dev/null @@ -1,67 +0,0 @@ -/* -Copyright 2015 OpenMarket 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. -*/ - -'use strict'; - -var React = require("react"); - -module.exports = { - propTypes: { - onHsUrlChanged: React.PropTypes.func, - onIsUrlChanged: React.PropTypes.func, - defaultHsUrl: React.PropTypes.string, - defaultIsUrl: React.PropTypes.string - }, - - getDefaultProps: function() { - return { - onHsUrlChanged: function() {}, - onIsUrlChanged: function() {}, - defaultHsUrl: 'https://matrix.org/', - defaultIsUrl: 'https://matrix.org/' - }; - }, - - getInitialState: function() { - return { - hs_url: this.props.defaultHsUrl, - is_url: this.props.defaultIsUrl, - original_hs_url: this.props.defaultHsUrl, - original_is_url: this.props.defaultIsUrl, - } - }, - - hsChanged: function(ev) { - this.setState({hs_url: ev.target.value}, function() { - this.props.onHsUrlChanged(this.state.hs_url); - }); - }, - - // XXX: horrible naming due to potential confusion between the word 'is' and the acronym 'IS' - isChanged: function(ev) { - this.setState({is_url: ev.target.value}, function() { - this.props.onIsUrlChanged(this.state.is_url); - }); - }, - - getHsUrl: function() { - return this.state.hs_url; - }, - - getIsUrl: function() { - return this.state.is_url; - }, -}; From b127c3043601c5a76ed98f52829439f305a619c1 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 12 Nov 2015 15:15:00 +0000 Subject: [PATCH 011/152] Implement logging in via password --- src/Signup.js | 41 +++++++++++++++++++++++++++++ src/controllers/pages/MatrixChat.js | 10 +++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/Signup.js b/src/Signup.js index 06b70fb7f5..ba91ff60b0 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -1,6 +1,7 @@ "use strict"; var MatrixClientPeg = require("./MatrixClientPeg"); var dis = require("./dispatcher"); +var q = require("q"); class Register { @@ -43,6 +44,46 @@ class Login { var flowStep = this._flows[this._currentFlowIndex]; return flowStep ? flowStep.type : null; } + + loginViaPassword(username, pass) { + var self = this; + var isEmail = username.indexOf("@") > 0; + var loginParams = { + password: pass + }; + if (isEmail) { + loginParams.medium = 'email'; + loginParams.address = username; + } else { + loginParams.user = username; + } + + return MatrixClientPeg.get().login('m.login.password', loginParams).then(function(data) { + return q({ + homeserverUrl: self._hsUrl, + identityServerUrl: self._isUrl, + userId: data.user_id, + accessToken: data.access_token + }); + }, function(error) { + if (error.httpStatus == 400 && loginParams.medium) { + error.friendlyText = ( + 'This Home Server does not support login using email address.' + ); + } + else if (error.httpStatus === 403) { + error.friendlyText = ( + 'Incorrect username and/or password.' + ); + } + else { + error.friendlyText = ( + 'There was a problem logging in. (HTTP ' + error.httpStatus + ")" + ); + } + throw error; + }); + } } module.exports.Register = Register; diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index 2992d0fe3e..6be223e578 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/controllers/pages/MatrixChat.js @@ -293,7 +293,12 @@ module.exports = { } }, - onLoggedIn: function() { + onLoggedIn: function(credentials) { + console.log("onLoggedIn => %s", credentials.userId); + MatrixClientPeg.replaceUsingAccessToken( + credentials.homeserverUrl, credentials.identityServerUrl, + credentials.userId, credentials.accessToken + ); this.setState({ screen: undefined, logged_in: true @@ -307,7 +312,8 @@ module.exports = { var cli = MatrixClientPeg.get(); var self = this; cli.on('sync', function(state) { - if (self.sdkReady || state !== "PREPARED") { return; } + console.log("MatrixClient sync state => %s", state); + if (state !== "PREPARED") { return; } self.sdkReady = true; if (self.starting_room_alias) { From ccd24dd3ea600da8d1d2b3263ad2a3dd53f5476a Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 12 Nov 2015 15:28:57 +0000 Subject: [PATCH 012/152] Move Cas/PasswordLogin to a new directory so it isn't confused with existing stuff --- .../login}/CasLogin.js | 16 ++++- src/components/login/PasswordLogin.js | 65 +++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) rename src/{controllers/organisms => components/login}/CasLogin.js (75%) create mode 100644 src/components/login/PasswordLogin.js diff --git a/src/controllers/organisms/CasLogin.js b/src/components/login/CasLogin.js similarity index 75% rename from src/controllers/organisms/CasLogin.js rename to src/components/login/CasLogin.js index d84306e587..32116a3f76 100644 --- a/src/controllers/organisms/CasLogin.js +++ b/src/components/login/CasLogin.js @@ -16,10 +16,12 @@ limitations under the License. 'use strict'; -var MatrixClientPeg = require("../../MatrixClientPeg"); +var MatrixClientPeg = require("../MatrixClientPeg"); +var React = require('react'); var url = require("url"); -module.exports = { +module.exports = React.createClass({ + displayName: 'CasLogin', onCasClicked: function(ev) { var cli = MatrixClientPeg.get(); @@ -30,4 +32,12 @@ module.exports = { window.location.href = casUrl; }, -}; + render: function() { + return ( +
    + +
    + ); + } + +}); diff --git a/src/components/login/PasswordLogin.js b/src/components/login/PasswordLogin.js new file mode 100644 index 0000000000..fabd71d67e --- /dev/null +++ b/src/components/login/PasswordLogin.js @@ -0,0 +1,65 @@ +/* +Copyright 2015 OpenMarket 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. +*/ + +var React = require('react'); +var ReactDOM = require('react-dom'); + +/** + * A pure UI component which displays a username/password form. + */ +module.exports = React.createClass({displayName: 'PasswordLogin', + propTypes: { + onSubmit: React.PropTypes.func.isRequired // fn(username, password) + }, + + getInitialState: function() { + return { + username: "", + password: "" + }; + }, + + onSubmitForm: function(ev) { + ev.preventDefault(); + this.props.onSubmit(this.state.username, this.state.password); + }, + + onUsernameChanged: function(ev) { + this.setState({username: ev.target.value}); + }, + + onPasswordChanged: function(ev) { + this.setState({password: ev.target.value}); + }, + + render: function() { + return ( +
    +
    + +
    + +
    + +
    +
    + ); + } +}); \ No newline at end of file From b8d579ac5ca1a78152d9f49263ab0ee49715570a Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 12 Nov 2015 15:53:50 +0000 Subject: [PATCH 013/152] Remove old login code --- src/controllers/templates/Login.js | 116 ----------------------------- 1 file changed, 116 deletions(-) delete mode 100644 src/controllers/templates/Login.js diff --git a/src/controllers/templates/Login.js b/src/controllers/templates/Login.js deleted file mode 100644 index 3c3a40e53a..0000000000 --- a/src/controllers/templates/Login.js +++ /dev/null @@ -1,116 +0,0 @@ -/* -Copyright 2015 OpenMarket 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. -*/ - -'use strict'; - -var MatrixClientPeg = require("../../MatrixClientPeg"); -var dis = require("../../dispatcher"); - -module.exports = { - getInitialState: function() { - return { - step: 'choose_hs', - busy: false, - currentStep: 0, - totalSteps: 1 - }; - }, - - setStep: function(step) { - this.setState({ step: step, busy: false }); - }, - - onHSChosen: function() { - MatrixClientPeg.replaceUsingUrls( - // XXX: why is the controller invoking methods from the view? :( -matthew - this.getHsUrl(), - this.getIsUrl() - ); - this.setState({ - hs_url: this.getHsUrl(), - is_url: this.getIsUrl(), - }); - this.setStep("fetch_stages"); - var cli = MatrixClientPeg.get(); - this.setState({ - busy: true, - errorText: "", - }); - var self = this; - cli.loginFlows().done(function(result) { - self.setState({ - flows: result.flows, - currentStep: 1, - totalSteps: result.flows.length+1 - }); - self.setStep('stage_'+result.flows[0].type); - }, function(error) { - self.setStep("choose_hs"); - self.setState({errorText: 'Unable to contact the given home server'}); - }); - }, - - onUserPassEntered: function(ev) { - ev.preventDefault(); - this.setState({ - busy: true, - errorText: "", - }); - var self = this; - - var formVals = this.getFormVals(); - - var loginParams = { - password: formVals.password - }; - if (formVals.username.indexOf('@') > 0) { - loginParams.medium = 'email'; - loginParams.address = formVals.username; - } else { - loginParams.user = formVals.username; - } - - MatrixClientPeg.get().login('m.login.password', loginParams).done(function(data) { - MatrixClientPeg.replaceUsingAccessToken( - self.state.hs_url, self.state.is_url, - data.user_id, data.access_token - ); - if (self.props.onLoggedIn) { - self.props.onLoggedIn(); - } - }, function(error) { - self.setStep("stage_m.login.password"); - if (error.httpStatus == 400 && loginParams.medium) { - self.setState({errorText: 'This Home Server does not support login using email address.'}); - } - else if (error.httpStatus === 403) { - self.setState({errorText: 'Incorrect username and/or password.'}); - } - else { - self.setState({ - errorText: 'There was a problem logging in. (HTTP ' + error.httpStatus + ")" - }); - } - }); - }, - - showRegister: function(ev) { - ev.preventDefault(); - dis.dispatch({ - action: 'start_registration' - }); - } -}; From 900b7dd94aeeeb1207c4dc22f9446c75e7310867 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 12 Nov 2015 16:14:01 +0000 Subject: [PATCH 014/152] Guard onLoggedIn since registration uses it too and that isn't done yet --- src/controllers/pages/MatrixChat.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index 6be223e578..c96da15522 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/controllers/pages/MatrixChat.js @@ -294,11 +294,13 @@ module.exports = { }, onLoggedIn: function(credentials) { - console.log("onLoggedIn => %s", credentials.userId); - MatrixClientPeg.replaceUsingAccessToken( - credentials.homeserverUrl, credentials.identityServerUrl, - credentials.userId, credentials.accessToken - ); + if (credentials) { // registration doesn't do this yet + console.log("onLoggedIn => %s", credentials.userId); + MatrixClientPeg.replaceUsingAccessToken( + credentials.homeserverUrl, credentials.identityServerUrl, + credentials.userId, credentials.accessToken + ); + } this.setState({ screen: undefined, logged_in: true From 257a65de142f56dc0117ab5548135077b6e6a7bb Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 17 Nov 2015 13:26:07 +0000 Subject: [PATCH 015/152] Fix path resolution --- src/components/login/CasLogin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/login/CasLogin.js b/src/components/login/CasLogin.js index 32116a3f76..8a45fa0643 100644 --- a/src/components/login/CasLogin.js +++ b/src/components/login/CasLogin.js @@ -16,7 +16,7 @@ limitations under the License. 'use strict'; -var MatrixClientPeg = require("../MatrixClientPeg"); +var MatrixClientPeg = require("../../MatrixClientPeg"); var React = require('react'); var url = require("url"); From 0f34f8b494fc98da4fdfd765408b91e4d18b2ad8 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 17 Nov 2015 17:25:14 +0000 Subject: [PATCH 016/152] Extend from a Signup class to keep hs/is URL logic together --- src/Signup.js | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Signup.js b/src/Signup.js index ba91ff60b0..8caf868fb4 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -3,14 +3,43 @@ var MatrixClientPeg = require("./MatrixClientPeg"); var dis = require("./dispatcher"); var q = require("q"); -class Register { - -} - -class Login { +class Signup { constructor(hsUrl, isUrl) { this._hsUrl = hsUrl; this._isUrl = isUrl; + } + + getHomeserverUrl() { + return this._hsUrl; + } + + getIdentityServerUrl() { + return this._isUrl; + } + + setHomeserverUrl(hsUrl) { + this._hsUrl = hsUrl; + } + + setIdentityServerUrl(isUrl) { + this._isUrl = isUrl; + } +} + +class Register extends Signup { + constructor(hsUrl, isUrl) { + super(hsUrl, isUrl); + this._state = "start"; + } + + getState() { + return this._state; + } +} + +class Login extends Signup { + constructor(hsUrl, isUrl) { + super(hsUrl, isUrl); this._currentFlowIndex = 0; this._flows = []; } From 95cdbe3a48b081c988e87c32918e424cab6370ee Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 17 Nov 2015 17:36:15 +0000 Subject: [PATCH 017/152] stop launch from wedging solid for 5 minutes >:( --- src/controllers/pages/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index 2fdb9eb8ef..ea34ec13b3 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/controllers/pages/MatrixChat.js @@ -351,7 +351,7 @@ module.exports = { Notifier.start(); UserActivity.start(); Presence.start(); - cli.startClient({resolveInvitesToProfiles: true}); + cli.startClient(); }, onKeyDown: function(ev) { From 1fca3f66066ef65fe8cf3a8602deb195121eff11 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 17 Nov 2015 17:38:37 +0000 Subject: [PATCH 018/152] Better const name --- src/Signup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Signup.js b/src/Signup.js index 8caf868fb4..2446130405 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -29,7 +29,7 @@ class Signup { class Register extends Signup { constructor(hsUrl, isUrl) { super(hsUrl, isUrl); - this._state = "start"; + this._state = "Register.START"; } getState() { From 0df0935b9c4079c8d75e749de59cc4cafb5900db Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 18 Nov 2015 09:57:14 +0000 Subject: [PATCH 019/152] Fix presence exception. Yay, javascript. --- src/Presence.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Presence.js b/src/Presence.js index 1f5617514a..e776cca078 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -73,11 +73,12 @@ class Presence { } var old_state = this.state; this.state = newState; + var self = this; MatrixClientPeg.get().setPresence(this.state).done(function() { console.log("Presence: %s", newState); }, function(err) { console.error("Failed to set presence: %s", err); - this.state = old_state; + self.state = old_state; }); } From 31b083d93ec3870ba62bb9e43c5e01979500bc6a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 18 Nov 2015 14:51:06 +0000 Subject: [PATCH 020/152] new Date() syntax & units on var name --- src/UserActivity.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/UserActivity.js b/src/UserActivity.js index 46a46f0b0e..cee1b4efe2 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.js @@ -31,8 +31,8 @@ class UserActivity { start() { document.onmousemove = this._onUserActivity.bind(this); document.onkeypress = this._onUserActivity.bind(this); - this.lastActivityAt = (new Date).getTime(); - this.lastDispatchAt = 0; + this.lastActivityAtTs = new Date().getTime(); + this.lastDispatchAtTs = 0; } /** @@ -44,9 +44,9 @@ class UserActivity { } _onUserActivity() { - this.lastActivityAt = (new Date).getTime(); - if (this.lastDispatchAt < this.lastActivityAt - MIN_DISPATCH_INTERVAL) { - this.lastDispatchAt = this.lastActivityAt; + this.lastActivityAtTs = (new Date).getTime(); + if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL) { + this.lastDispatchAtTs = this.lastActivityAtTs; dis.dispatch({ action: 'user_activity' }); From 991a96cfc5b4e3875b0bcf21006e582eea26e9ad Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 18 Nov 2015 17:13:43 +0000 Subject: [PATCH 021/152] Get dummy registrations working This means you can now register on localhost without needing an email. Email and Recaptcha are still broken. --- src/Signup.js | 192 +++++++++++++++++++++++++++++++++++++++++++- src/SignupStages.js | 125 ++++++++++++++++++++++++++++ 2 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 src/SignupStages.js diff --git a/src/Signup.js b/src/Signup.js index 2446130405..b679df4e8a 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -1,8 +1,11 @@ "use strict"; var MatrixClientPeg = require("./MatrixClientPeg"); +var SignupStages = require("./SignupStages"); var dis = require("./dispatcher"); var q = require("q"); +const EMAIL_STAGE_TYPE = "m.login.email.identity"; + class Signup { constructor(hsUrl, isUrl) { this._hsUrl = hsUrl; @@ -26,17 +29,200 @@ class Signup { } } + class Register extends Signup { constructor(hsUrl, isUrl) { super(hsUrl, isUrl); - this._state = "Register.START"; + this.setStep("START"); + this.data = null; // from the server + this.username = null; // desired + this.email = null; // desired + this.password = null; // desired + this.params = {}; // random other stuff (e.g. query params) + this.credentials = null; } - getState() { - return this._state; + setClientSecret(secret) { + this.params.clientSecret = secret; + } + + setSessionId(sessionId) { + this.params.sessionId = sessionId; + } + + setRegistrationUrl(regUrl) { + this.params.registrationUrl = regUrl; + } + + setIdSid(idSid) { + this.params.idSid = idSid; + } + + getStep() { + return this._step; + } + + getCredentials() { + return this.credentials; + } + + setStep(step) { + this._step = 'Register.' + step; + // TODO: + // It's a shame this is going to the global dispatcher, we only really + // want things which have an instance of this class to be able to add + // listeners... + console.log("Dispatching 'registration_step_update' for step %s", this._step); + dis.dispatch({ + action: "registration_step_update" + }); + } + + register(formVals) { + var {username, password, email} = formVals; + this.email = email; + this.username = username; + this.password = password; + + // feels a bit wrong to be clobbering the global client for something we + // don't even know if it'll work, but we'll leave this here for now to + // not complicate matters further. It would be nicer to isolate this + // logic entirely from the rest of the app though. + MatrixClientPeg.replaceUsingUrls( + this._hsUrl, + this._isUrl + ); + return this._tryRegister(); + } + + _tryRegister(authDict) { + console.log("_tryRegister %s", JSON.stringify(authDict)); + var self = this; + return MatrixClientPeg.get().register( + this.username, this.password, this._sessionId, authDict + ).then(function(result) { + console.log("Got a final response"); + self.credentials = result; + self.setStep("COMPLETE"); + return result; // contains the credentials + }, function(error) { + console.error(error); + if (error.httpStatus === 401 && error.data && error.data.flows) { + self.data = error.data || {}; + var flow = self.chooseFlow(error.data.flows); + + if (flow) { + var flowStage = self.firstUncompletedStageIndex(flow); + return self.startStage(flow.stages[flowStage]); + } + else { + throw new Error("Unable to register - missing email address?"); + } + } else { + if (error.errcode === 'M_USER_IN_USE') { + throw new Error("Username in use"); + } else if (error.httpStatus == 401) { + throw new Error("Authorisation failed!"); + } else if (error.httpStatus >= 400 && error.httpStatus < 500) { + throw new Error(`Registration failed! (${error.httpStatus})`); + } else if (error.httpStatus >= 500 && error.httpStatus < 600) { + throw new Error( + `Server error during registration! (${error.httpStatus})` + ); + } else if (error.name == "M_MISSING_PARAM") { + // The HS hasn't remembered the login params from + // the first try when the login email was sent. + throw new Error( + "This home server does not support resuming registration." + ); + } + } + }); + } + + firstUncompletedStageIndex(flow) { + if (!this.completedStages) { + return 0; + } + for (var i = 0; i < flow.stages.length; ++i) { + if (this.completedStages.indexOf(flow.stages[i]) == -1) { + return i; + } + } + } + + numCompletedStages(flow) { + if (!this.completedStages) { + return 0; + } + var nCompleted = 0; + for (var i = 0; i < flow.stages.length; ++i) { + if (this.completedStages.indexOf(flow.stages[i]) > -1) { + ++nCompleted; + } + } + return nCompleted; + } + + startStage(stageName) { + var self = this; + this.setStep(`STEP_${stageName}`); + var StageClass = SignupStages[stageName]; + if (!StageClass) { + // no idea how to handle this! + throw new Error("Unknown stage: " + stageName); + } + + var stage = new StageClass(MatrixClientPeg.get(), this); + return stage.complete().then(function(request) { + if (request.auth) { + return self._tryRegister(request.auth); + } + }); + } + + hasCompletedStage(stageType) { + var completed = (this.data || {}).completed || []; + return completed.indexOf(stageType) !== -1; + } + + chooseFlow(flows) { + // If the user gave us an email then we want to pick an email + // flow we can do, else any other flow. + var emailFlow = null; + var otherFlow = null; + flows.forEach(function(flow) { + var flowHasEmail = false; + for (var stageI = 0; stageI < flow.stages.length; ++stageI) { + var stage = flow.stages[stageI]; + + if (!SignupStages[stage]) { + // we can't do this flow, don't have a Stage impl. + return; + } + + if (stage === EMAIL_STAGE_TYPE) { + flowHasEmail = true; + } + } + + if (flowHasEmail) { + emailFlow = flow; + } else { + otherFlow = flow; + } + }); + + if (this.email || this.hasCompletedStage(EMAIL_STAGE_TYPE)) { + // we've been given an email or we've already done an email part + return emailFlow; + } else { + return otherFlow; + } } } + class Login extends Signup { constructor(hsUrl, isUrl) { super(hsUrl, isUrl); diff --git a/src/SignupStages.js b/src/SignupStages.js new file mode 100644 index 0000000000..a4b51f0abb --- /dev/null +++ b/src/SignupStages.js @@ -0,0 +1,125 @@ +"use strict"; +var q = require("q"); + +class Stage { + constructor(type, matrixClient, signupInstance) { + this.type = type; + this.client = matrixClient; + this.signupInstance = signupInstance; + } + + complete() { + // Return a promise which is: + // RESOLVED => With an Object which has an 'auth' key which is the auth dict + // to submit. + // REJECTED => With an Error if there was a problem with this stage. + // Has a "message" string and an "isFatal" flag. + return q.reject("NOT IMPLEMENTED"); + } +} +Stage.TYPE = "NOT IMPLEMENTED"; + + +class DummyStage extends Stage { + constructor(matrixClient, signupInstance) { + super(DummyStage.TYPE, matrixClient, signupInstance); + } + + complete() { + return q({ + auth: { + type: DummyStage.TYPE + } + }); + } +} +DummyStage.TYPE = "m.login.dummy"; + + +class RecaptchaStage extends Stage { + constructor(matrixClient, signupInstance) { + super(RecaptchaStage.TYPE, matrixClient, signupInstance); + } + + complete() { + var publicKey; + if (this.signupInstance.params['m.login.recaptcha']) { + publicKey = this.signupInstance.params['m.login.recaptcha'].public_key; + } + if (!publicKey) { + return q.reject({ + message: "This server has not supplied enough information for Recaptcha " + + "authentication", + isFatal: true + }); + } + + var defer = q.defer(); + global.grecaptcha.render('mx_recaptcha', { + sitekey: publicKey, + callback: function(response) { + return defer.resolve({ + auth: { + type: 'm.login.recaptcha', + response: response + } + }); + } + }); + + return defer.promise; + } +} +RecaptchaStage.TYPE = "m.login.recaptcha"; + + +class EmailIdentityStage extends Stage { + constructor(matrixClient, signupInstance) { + super(EmailIdentityStage.TYPE, matrixClient, signupInstance); + } + + complete() { + var config = { + clientSecret: this.client.generateClientSecret(), + sendAttempt: 1 + }; + this.signupInstance.params[EmailIdentityStage.TYPE] = config; + + var nextLink = this.signupInstance.params.registrationUrl + + '?client_secret=' + + encodeURIComponent(config.clientSecret) + + "&hs_url=" + + encodeURIComponent(this.signupInstance.getHomeserverUrl()) + + "&is_url=" + + encodeURIComponent(this.signupInstance.getIdentityServerUrl()) + + "&session_id=" + + encodeURIComponent(this.signupInstance.getSessionId()); + + return this.client.requestEmailToken( + this.signupInstance.email, + config.clientSecret, + config.sendAttempt, + nextLink + ).then(function(response) { + return {}; // don't want to make a request + }, function(error) { + console.error(error); + var e = { + isFatal: true + }; + if (error.errcode == 'THREEPID_IN_USE') { + e.message = "Email in use"; + } else { + e.message = 'Unable to contact the given identity server'; + } + return e; + }); + } +} +EmailIdentityStage.TYPE = "m.login.email.identity"; + +module.exports = { + [DummyStage.TYPE]: DummyStage, + [RecaptchaStage.TYPE]: RecaptchaStage, + [EmailIdentityStage.TYPE]: EmailIdentityStage +}; \ No newline at end of file From 3e903be73dd3652435833fe1a43539775ce052ec Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 18 Nov 2015 17:43:38 +0000 Subject: [PATCH 022/152] Get Recaptcha working again. Add a backchannel for stage prodding. Recaptcha is a special snowflake because it dynamically loads the script and THEN renders with info from the registration request. This means we need a back-channel for the UI component to 'tell' the stage that everything is loaded. This Just Works which is nice. --- src/Signup.js | 16 +++++++++++++++- src/SignupStages.js | 40 +++++++++++++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/Signup.js b/src/Signup.js index b679df4e8a..79686b7abf 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -38,8 +38,9 @@ class Register extends Signup { this.username = null; // desired this.email = null; // desired this.password = null; // desired - this.params = {}; // random other stuff (e.g. query params) + this.params = {}; // random other stuff (e.g. query params, NOT params from the server) this.credentials = null; + this.activeStage = null; } setClientSecret(secret) { @@ -66,6 +67,10 @@ class Register extends Signup { return this.credentials; } + getServerData() { + return this.data || {}; + } + setStep(step) { this._step = 'Register.' + step; // TODO: @@ -112,6 +117,7 @@ class Register extends Signup { var flow = self.chooseFlow(error.data.flows); if (flow) { + console.log("Active flow => %s", JSON.stringify(flow)); var flowStage = self.firstUncompletedStageIndex(flow); return self.startStage(flow.stages[flowStage]); } @@ -174,6 +180,7 @@ class Register extends Signup { } var stage = new StageClass(MatrixClientPeg.get(), this); + this.activeStage = stage; return stage.complete().then(function(request) { if (request.auth) { return self._tryRegister(request.auth); @@ -220,6 +227,13 @@ class Register extends Signup { return otherFlow; } } + + tellStage(stageName, data) { + if (this.activeStage && this.activeStage.type === stageName) { + console.log("Telling stage %s about something..", stageName); + this.activeStage.onReceiveData(data); + } + } } diff --git a/src/SignupStages.js b/src/SignupStages.js index a4b51f0abb..a4d7ac9d17 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -16,6 +16,10 @@ class Stage { // Has a "message" string and an "isFatal" flag. return q.reject("NOT IMPLEMENTED"); } + + onReceiveData() { + // NOP + } } Stage.TYPE = "NOT IMPLEMENTED"; @@ -39,12 +43,22 @@ DummyStage.TYPE = "m.login.dummy"; class RecaptchaStage extends Stage { constructor(matrixClient, signupInstance) { super(RecaptchaStage.TYPE, matrixClient, signupInstance); + this.defer = q.defer(); + this.publicKey = null; + } + + onReceiveData(data) { + if (data !== "loaded") { + return; + } + this._attemptRender(); } complete() { var publicKey; - if (this.signupInstance.params['m.login.recaptcha']) { - publicKey = this.signupInstance.params['m.login.recaptcha'].public_key; + var serverParams = this.signupInstance.getServerData().params; + if (serverParams && serverParams["m.login.recaptcha"]) { + publicKey = serverParams["m.login.recaptcha"].public_key; } if (!publicKey) { return q.reject({ @@ -53,12 +67,26 @@ class RecaptchaStage extends Stage { isFatal: true }); } + this.publicKey = publicKey; + this._attemptRender(); - var defer = q.defer(); + return this.defer.promise; + } + + _attemptRender() { + if (!global.grecaptcha) { + console.error("grecaptcha not loaded!"); + return; + } + if (!this.publicKey) { + console.error("No public key for recaptcha!"); + return; + } + var self = this; global.grecaptcha.render('mx_recaptcha', { - sitekey: publicKey, + sitekey: this.publicKey, callback: function(response) { - return defer.resolve({ + return self.defer.resolve({ auth: { type: 'm.login.recaptcha', response: response @@ -66,8 +94,6 @@ class RecaptchaStage extends Stage { }); } }); - - return defer.promise; } } RecaptchaStage.TYPE = "m.login.recaptcha"; From f2f5496b78f54c098c79e83a9e8d33d6ebeb35b8 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 11:41:32 +0000 Subject: [PATCH 023/152] Get email auth sending working (not the link back though) --- src/Signup.js | 8 +++++++- src/SignupStages.js | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Signup.js b/src/Signup.js index 79686b7abf..db69441d6f 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -184,7 +184,13 @@ class Register extends Signup { return stage.complete().then(function(request) { if (request.auth) { return self._tryRegister(request.auth); - } + } + else { + // never resolve the promise chain. This is for things like email auth + // which display a "check your email" message and relies on the + // link in the email to actually register you. + return q.defer().promise; + } }); } diff --git a/src/SignupStages.js b/src/SignupStages.js index a4d7ac9d17..3521b4ba39 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -119,7 +119,7 @@ class EmailIdentityStage extends Stage { "&is_url=" + encodeURIComponent(this.signupInstance.getIdentityServerUrl()) + "&session_id=" + - encodeURIComponent(this.signupInstance.getSessionId()); + encodeURIComponent(this.signupInstance.params.sessionId); return this.client.requestEmailToken( this.signupInstance.email, From 8d7d338f44f6296ddb39f2c4c6d26befd768f9f4 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 13:58:34 +0000 Subject: [PATCH 024/152] Pass the right session ID --- src/SignupStages.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SignupStages.js b/src/SignupStages.js index 3521b4ba39..d49a488ec8 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -83,6 +83,7 @@ class RecaptchaStage extends Stage { return; } var self = this; + // FIXME: Tight coupling here and in CaptchaForm.js global.grecaptcha.render('mx_recaptcha', { sitekey: this.publicKey, callback: function(response) { @@ -119,7 +120,7 @@ class EmailIdentityStage extends Stage { "&is_url=" + encodeURIComponent(this.signupInstance.getIdentityServerUrl()) + "&session_id=" + - encodeURIComponent(this.signupInstance.params.sessionId); + encodeURIComponent(this.signupInstance.getServerData().session); return this.client.requestEmailToken( this.signupInstance.email, From 7568a3b2d346236447206271795484095de28e9f Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 14:16:49 +0000 Subject: [PATCH 025/152] Hookup 2nd stage email registration; not finished as we aren't storing u/p --- src/Signup.js | 23 +++++++++++++++++++++++ src/SignupStages.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/Signup.js b/src/Signup.js index db69441d6f..7e3b3335ff 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -234,6 +234,29 @@ class Register extends Signup { } } + recheckState() { + // feels a bit wrong to be clobbering the global client for something we + // don't even know if it'll work, but we'll leave this here for now to + // not complicate matters further. It would be nicer to isolate this + // logic entirely from the rest of the app though. + MatrixClientPeg.replaceUsingUrls( + this._hsUrl, + this._isUrl + ); + // We've been given a bunch of data from a previous register step, + // this only happens for email auth currently. It's kinda ming we need + // to know this though. A better solution would be to ask the stages if + // they are ready to do something rather than accepting that we know about + // email auth and its internals. + this.params.hasEmailInfo = ( + this.params.clientSecret && this.params.sessionId && this.params.idSid + ); + + if (this.params.hasEmailInfo) { + this.startStage(EMAIL_STAGE_TYPE); + } + } + tellStage(stageName, data) { if (this.activeStage && this.activeStage.type === stageName) { console.log("Telling stage %s about something..", stageName); diff --git a/src/SignupStages.js b/src/SignupStages.js index d49a488ec8..53e75e39ea 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -105,7 +105,36 @@ class EmailIdentityStage extends Stage { super(EmailIdentityStage.TYPE, matrixClient, signupInstance); } + _completeVerify() { + console.log("_completeVerify"); + var isLocation = document.createElement('a'); + isLocation.href = this.signupInstance.getIdentityServerUrl(); + + return q({ + auth: { + type: 'm.login.email.identity', + threepid_creds: { + sid: this.signupInstance.params.idSid, + client_secret: this.signupInstance.params.clientSecret, + id_server: isLocation.host + } + } + }); + } + + /** + * Complete the email stage. + * + * This is called twice under different circumstances: + * 1) When requesting an email token from the IS + * 2) When validating query parameters received from the link in the email + */ complete() { + console.log("Email complete()"); + if (this.signupInstance.params.hasEmailInfo) { + return this._completeVerify(); + } + var config = { clientSecret: this.client.generateClientSecret(), sendAttempt: 1 From cc746767187cdcc1721b236b9d133160761d9ca0 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 15:19:30 +0000 Subject: [PATCH 026/152] Mostly fix 2nd step email registration - Don't send u/p: null - Remove unused functions - Moar logging Still doesn't work yet though. --- src/Signup.js | 49 +++++++++++++++++++++------------------------ src/SignupStages.js | 4 ++-- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/Signup.js b/src/Signup.js index 7e3b3335ff..0107075f9c 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -35,12 +35,15 @@ class Register extends Signup { super(hsUrl, isUrl); this.setStep("START"); this.data = null; // from the server - this.username = null; // desired - this.email = null; // desired - this.password = null; // desired this.params = {}; // random other stuff (e.g. query params, NOT params from the server) this.credentials = null; this.activeStage = null; + this.registrationPromise = null; + // These values MUST be undefined else we'll send "username: null" which + // will error on Synapse rather than having the key absent. + this.username = undefined; // desired + this.email = undefined; // desired + this.password = undefined; // desired } setClientSecret(secret) { @@ -71,6 +74,10 @@ class Register extends Signup { return this.data || {}; } + getPromise() { + return this.registrationPromise; + } + setStep(step) { this._step = 'Register.' + step; // TODO: @@ -97,6 +104,7 @@ class Register extends Signup { this._hsUrl, this._isUrl ); + console.log("register(formVals)"); return this._tryRegister(); } @@ -104,7 +112,7 @@ class Register extends Signup { console.log("_tryRegister %s", JSON.stringify(authDict)); var self = this; return MatrixClientPeg.get().register( - this.username, this.password, this._sessionId, authDict + this.username, this.password, this.params.sessionId, authDict ).then(function(result) { console.log("Got a final response"); self.credentials = result; @@ -114,12 +122,13 @@ class Register extends Signup { console.error(error); if (error.httpStatus === 401 && error.data && error.data.flows) { self.data = error.data || {}; + console.log("RAW: %s", JSON.stringify(error.data)); var flow = self.chooseFlow(error.data.flows); if (flow) { console.log("Active flow => %s", JSON.stringify(flow)); - var flowStage = self.firstUncompletedStageIndex(flow); - return self.startStage(flow.stages[flowStage]); + var flowStage = self.firstUncompletedStage(flow); + return self.startStage(flowStage); } else { throw new Error("Unable to register - missing email address?"); @@ -146,30 +155,14 @@ class Register extends Signup { }); } - firstUncompletedStageIndex(flow) { - if (!this.completedStages) { - return 0; - } + firstUncompletedStage(flow) { for (var i = 0; i < flow.stages.length; ++i) { - if (this.completedStages.indexOf(flow.stages[i]) == -1) { - return i; + if (!this.hasCompletedStage(flow.stages[i])) { + return flow.stages[i]; } } } - numCompletedStages(flow) { - if (!this.completedStages) { - return 0; - } - var nCompleted = 0; - for (var i = 0; i < flow.stages.length; ++i) { - if (this.completedStages.indexOf(flow.stages[i]) > -1) { - ++nCompleted; - } - } - return nCompleted; - } - startStage(stageName) { var self = this; this.setStep(`STEP_${stageName}`); @@ -182,6 +175,7 @@ class Register extends Signup { var stage = new StageClass(MatrixClientPeg.get(), this); this.activeStage = stage; return stage.complete().then(function(request) { + console.log("Stage %s completed with %s", stageName, JSON.stringify(request)); if (request.auth) { return self._tryRegister(request.auth); } @@ -189,6 +183,7 @@ class Register extends Signup { // never resolve the promise chain. This is for things like email auth // which display a "check your email" message and relies on the // link in the email to actually register you. + console.log("Waiting for external action."); return q.defer().promise; } }); @@ -253,8 +248,10 @@ class Register extends Signup { ); if (this.params.hasEmailInfo) { - this.startStage(EMAIL_STAGE_TYPE); + console.log("recheckState has email info.. starting email info.."); + this.registrationPromise = this.startStage(EMAIL_STAGE_TYPE); } + return this.registrationPromise; } tellStage(stageName, data) { diff --git a/src/SignupStages.js b/src/SignupStages.js index 53e75e39ea..2a63ae6058 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -69,7 +69,6 @@ class RecaptchaStage extends Stage { } this.publicKey = publicKey; this._attemptRender(); - return this.defer.promise; } @@ -87,7 +86,8 @@ class RecaptchaStage extends Stage { global.grecaptcha.render('mx_recaptcha', { sitekey: this.publicKey, callback: function(response) { - return self.defer.resolve({ + console.log("Received captcha response"); + self.defer.resolve({ auth: { type: 'm.login.recaptcha', response: response From b12f0f1df710820db16ffbf69d42213bd47a1e4e Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 16:07:58 +0000 Subject: [PATCH 027/152] Minor refactoring; remove debug logging; add comments --- src/Signup.js | 28 ++++++++++++++++----------- src/SignupStages.js | 47 ++++++++++++++++++++++++++++++--------------- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/src/Signup.js b/src/Signup.js index 0107075f9c..02a59ebe6e 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -6,6 +6,10 @@ var q = require("q"); const EMAIL_STAGE_TYPE = "m.login.email.identity"; +/** + * A base class for common functionality between Registration and Login e.g. + * storage of HS/IS URLs. + */ class Signup { constructor(hsUrl, isUrl) { this._hsUrl = hsUrl; @@ -29,13 +33,16 @@ class Signup { } } - +/** + * Registration logic class + */ class Register extends Signup { constructor(hsUrl, isUrl) { super(hsUrl, isUrl); this.setStep("START"); this.data = null; // from the server - this.params = {}; // random other stuff (e.g. query params, NOT params from the server) + // random other stuff (e.g. query params, NOT params from the server) + this.params = {}; this.credentials = null; this.activeStage = null; this.registrationPromise = null; @@ -104,12 +111,12 @@ class Register extends Signup { this._hsUrl, this._isUrl ); - console.log("register(formVals)"); + console.log("Starting registration process (form submission)"); return this._tryRegister(); } _tryRegister(authDict) { - console.log("_tryRegister %s", JSON.stringify(authDict)); + console.log("Trying to register with auth dict: %s", JSON.stringify(authDict)); var self = this; return MatrixClientPeg.get().register( this.username, this.password, this.params.sessionId, authDict @@ -163,6 +170,11 @@ class Register extends Signup { } } + hasCompletedStage(stageType) { + var completed = (this.data || {}).completed || []; + return completed.indexOf(stageType) !== -1; + } + startStage(stageName) { var self = this; this.setStep(`STEP_${stageName}`); @@ -175,8 +187,8 @@ class Register extends Signup { var stage = new StageClass(MatrixClientPeg.get(), this); this.activeStage = stage; return stage.complete().then(function(request) { - console.log("Stage %s completed with %s", stageName, JSON.stringify(request)); if (request.auth) { + console.log("Stage %s is returning an auth dict", stageName); return self._tryRegister(request.auth); } else { @@ -189,11 +201,6 @@ class Register extends Signup { }); } - hasCompletedStage(stageType) { - var completed = (this.data || {}).completed || []; - return completed.indexOf(stageType) !== -1; - } - chooseFlow(flows) { // If the user gave us an email then we want to pick an email // flow we can do, else any other flow. @@ -248,7 +255,6 @@ class Register extends Signup { ); if (this.params.hasEmailInfo) { - console.log("recheckState has email info.. starting email info.."); this.registrationPromise = this.startStage(EMAIL_STAGE_TYPE); } return this.registrationPromise; diff --git a/src/SignupStages.js b/src/SignupStages.js index 2a63ae6058..272a955d95 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -1,6 +1,9 @@ "use strict"; var q = require("q"); +/** + * An interface class which login types should abide by. + */ class Stage { constructor(type, matrixClient, signupInstance) { this.type = type; @@ -24,6 +27,9 @@ class Stage { Stage.TYPE = "NOT IMPLEMENTED"; +/** + * This stage requires no auth. + */ class DummyStage extends Stage { constructor(matrixClient, signupInstance) { super(DummyStage.TYPE, matrixClient, signupInstance); @@ -40,17 +46,24 @@ class DummyStage extends Stage { DummyStage.TYPE = "m.login.dummy"; +/** + * This stage uses Google's Recaptcha to do auth. + */ class RecaptchaStage extends Stage { constructor(matrixClient, signupInstance) { super(RecaptchaStage.TYPE, matrixClient, signupInstance); - this.defer = q.defer(); - this.publicKey = null; + this.defer = q.defer(); // resolved with the captcha response + this.publicKey = null; // from the HS + this.divId = null; // from the UI component } + // called when the UI component has loaded the recaptcha
    so we can + // render to it. onReceiveData(data) { - if (data !== "loaded") { + if (!data || !data.divId) { return; } + this.divId = data.divId; this._attemptRender(); } @@ -81,9 +94,13 @@ class RecaptchaStage extends Stage { console.error("No public key for recaptcha!"); return; } + if (!this.divId) { + console.error("No div ID specified!"); + return; + } + console.log("Rendering to %s", this.divId); var self = this; - // FIXME: Tight coupling here and in CaptchaForm.js - global.grecaptcha.render('mx_recaptcha', { + global.grecaptcha.render(this.divId, { sitekey: this.publicKey, callback: function(response) { console.log("Received captcha response"); @@ -100,13 +117,16 @@ class RecaptchaStage extends Stage { RecaptchaStage.TYPE = "m.login.recaptcha"; +/** + * This state uses the IS to verify email addresses. + */ class EmailIdentityStage extends Stage { constructor(matrixClient, signupInstance) { super(EmailIdentityStage.TYPE, matrixClient, signupInstance); } _completeVerify() { - console.log("_completeVerify"); + // pull out the host of the IS URL by creating an anchor element var isLocation = document.createElement('a'); isLocation.href = this.signupInstance.getIdentityServerUrl(); @@ -130,20 +150,15 @@ class EmailIdentityStage extends Stage { * 2) When validating query parameters received from the link in the email */ complete() { - console.log("Email complete()"); + // TODO: The Registration class shouldn't really know this info. if (this.signupInstance.params.hasEmailInfo) { return this._completeVerify(); } - var config = { - clientSecret: this.client.generateClientSecret(), - sendAttempt: 1 - }; - this.signupInstance.params[EmailIdentityStage.TYPE] = config; - + var clientSecret = this.client.generateClientSecret(); var nextLink = this.signupInstance.params.registrationUrl + '?client_secret=' + - encodeURIComponent(config.clientSecret) + + encodeURIComponent(clientSecret) + "&hs_url=" + encodeURIComponent(this.signupInstance.getHomeserverUrl()) + "&is_url=" + @@ -153,8 +168,8 @@ class EmailIdentityStage extends Stage { return this.client.requestEmailToken( this.signupInstance.email, - config.clientSecret, - config.sendAttempt, + clientSecret, + 1, // TODO: Multiple send attempts? nextLink ).then(function(response) { return {}; // don't want to make a request From 23467de016463aa0e94c69862f71c75985fca62b Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 16:47:28 +0000 Subject: [PATCH 028/152] Remove missed debug log --- src/Signup.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Signup.js b/src/Signup.js index 02a59ebe6e..bb74c58ac2 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -129,7 +129,6 @@ class Register extends Signup { console.error(error); if (error.httpStatus === 401 && error.data && error.data.flows) { self.data = error.data || {}; - console.log("RAW: %s", JSON.stringify(error.data)); var flow = self.chooseFlow(error.data.flows); if (flow) { From cad3afc7a47160046f6d8b6d3090bd863f41397d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 20 Nov 2015 10:11:51 +0000 Subject: [PATCH 029/152] Remove unhelpful log lines --- src/Signup.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Signup.js b/src/Signup.js index bb74c58ac2..02ddaacc6d 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -111,22 +111,18 @@ class Register extends Signup { this._hsUrl, this._isUrl ); - console.log("Starting registration process (form submission)"); return this._tryRegister(); } _tryRegister(authDict) { - console.log("Trying to register with auth dict: %s", JSON.stringify(authDict)); var self = this; return MatrixClientPeg.get().register( this.username, this.password, this.params.sessionId, authDict ).then(function(result) { - console.log("Got a final response"); self.credentials = result; self.setStep("COMPLETE"); return result; // contains the credentials }, function(error) { - console.error(error); if (error.httpStatus === 401 && error.data && error.data.flows) { self.data = error.data || {}; var flow = self.chooseFlow(error.data.flows); From 030e2f0979e73de98d136fb6598309b5694bda98 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 20 Nov 2015 10:14:00 +0000 Subject: [PATCH 030/152] Move CaptchaForm from Vector to React SDK --- src/components/login/CaptchaForm.js | 67 +++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/components/login/CaptchaForm.js diff --git a/src/components/login/CaptchaForm.js b/src/components/login/CaptchaForm.js new file mode 100644 index 0000000000..9b722f463b --- /dev/null +++ b/src/components/login/CaptchaForm.js @@ -0,0 +1,67 @@ +/* +Copyright 2015 OpenMarket 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. +*/ + +'use strict'; + +var React = require('react'); +var DIV_ID = 'mx_recaptcha'; + +/** + * A pure UI component which displays a captcha form. + */ +module.exports = React.createClass({ + displayName: 'CaptchaForm', + + propTypes: { + onCaptchaLoaded: React.PropTypes.func.isRequired // called with div id name + }, + + getDefaultProps: function() { + return { + onCaptchaLoaded: function() { + console.error("Unhandled onCaptchaLoaded"); + } + }; + }, + + componentDidMount: function() { + // Just putting a script tag into the returned jsx doesn't work, annoyingly, + // so we do this instead. + var self = this; + if (this.refs.recaptchaContainer) { + console.log("Loading recaptcha script..."); + var scriptTag = document.createElement('script'); + window.mx_on_recaptcha_loaded = function() { + console.log("Loaded recaptcha script."); + self.props.onCaptchaLoaded(DIV_ID); + }; + scriptTag.setAttribute( + 'src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit" + ); + this.refs.recaptchaContainer.appendChild(scriptTag); + } + }, + + render: function() { + // FIXME: Tight coupling with the div id and SignupStages.js + return ( +
    + This Home Server would like to make sure you are not a robot +
    +
    + ); + } +}); \ No newline at end of file From 05a7d76785c029577e506e87dde1f0bb982e2920 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 20 Nov 2015 10:15:18 +0000 Subject: [PATCH 031/152] Remove old Register files --- src/controllers/templates/Register.js | 352 -------------------------- 1 file changed, 352 deletions(-) delete mode 100644 src/controllers/templates/Register.js diff --git a/src/controllers/templates/Register.js b/src/controllers/templates/Register.js deleted file mode 100644 index a3dac5b9d0..0000000000 --- a/src/controllers/templates/Register.js +++ /dev/null @@ -1,352 +0,0 @@ -/* -Copyright 2015 OpenMarket 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. -*/ - -var MatrixClientPeg = require("../../MatrixClientPeg"); -var dis = require("../../dispatcher"); - -module.exports = { - FieldErrors: { - PasswordMismatch: 'PasswordMismatch', - TooShort: 'TooShort', - Missing: 'Missing', - InUse: 'InUse', - Length: 'Length' - }, - - getInitialState: function() { - return { - step: 'initial', - busy: false, - currentStep: 0, - totalSteps: 1 - }; - }, - - componentWillMount: function() { - this.savedParams = { - email: '', - username: '', - password: '', - confirmPassword: '' - }; - this.readNewProps(); - }, - - componentWillReceiveProps: function() { - this.readNewProps(); - }, - - readNewProps: function() { - if (this.props.clientSecret && this.props.hsUrl && - this.props.isUrl && this.props.sessionId && - this.props.idSid) { - this.authSessionId = this.props.sessionId; - MatrixClientPeg.replaceUsingUrls( - this.props.hsUrl, - this.props.isUrl - ); - this.setState({ - hs_url: this.props.hsUrl, - is_url: this.props.isUrl - }); - this.savedParams = {client_secret: this.props.clientSecret}; - this.setState({busy: true}); - - var isLocation = document.createElement('a'); - isLocation.href = this.props.isUrl; - - var auth = { - type: 'm.login.email.identity', - threepid_creds: { - sid: this.props.idSid, - client_secret: this.savedParams.client_secret, - id_server: isLocation.host - } - }; - this.tryRegister(auth); - } - }, - - componentDidUpdate: function() { - // Just putting a script tag into the returned jsx doesn't work, annoyingly, - // so we do this instead. - if (this.refs.recaptchaContainer) { - var scriptTag = document.createElement('script'); - window.mx_on_recaptcha_loaded = this.onCaptchaLoaded; - scriptTag.setAttribute('src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit"); - this.refs.recaptchaContainer.appendChild(scriptTag); - } - }, - - setStep: function(step) { - this.setState({ step: step, errorText: '', busy: false }); - }, - - getSupportedStageTypes: function() { - return ['m.login.email.identity', 'm.login.recaptcha']; - }, - - chooseFlow: function(flows) { - // this is fairly simple right now - var supportedTypes = this.getSupportedStageTypes(); - - var emailFlow = null; - var otherFlow = null; - for (var flowI = 0; flowI < flows.length; ++flowI) { - var flow = flows[flowI]; - var flowHasEmail = false; - var flowSupported = true; - for (var stageI = 0; stageI < flow.stages.length; ++stageI) { - var stage = flow.stages[stageI]; - - if (supportedTypes.indexOf(stage) == -1) { - flowSupported = false; - } - - if (stage == 'm.login.email.identity') { - flowHasEmail = true; - } - } - if (flowSupported) { - if (flowHasEmail) { - emailFlow = flow; - } else { - otherFlow = flow; - } - } - } - - if ( - this.savedParams.email != '' || - this.completedStages.indexOf('m.login.email.identity') > -1 - ) { - return emailFlow; - } else { - return otherFlow; - } - }, - - firstUncompletedStageIndex: function(flow) { - if (this.completedStages === undefined) return 0; - for (var i = 0; i < flow.stages.length; ++i) { - if (this.completedStages.indexOf(flow.stages[i]) == -1) { - return i; - } - } - }, - - numCompletedStages: function(flow) { - if (this.completedStages === undefined) return 0; - var nCompleted = 0; - for (var i = 0; i < flow.stages.length; ++i) { - if (this.completedStages.indexOf(flow.stages[i]) > -1) { - ++nCompleted; - } - } - return nCompleted; - }, - - onInitialStageSubmit: function(ev) { - ev.preventDefault(); - - var formVals = this.getRegFormVals(); - this.savedParams = formVals; - - var badFields = {}; - if (formVals.password != formVals.confirmPassword) { - badFields.confirmPassword = this.FieldErrors.PasswordMismatch; - } - if (formVals.password == '') { - badFields.password = this.FieldErrors.Missing; - } else if (formVals.password.length < 6) { - badFields.password = this.FieldErrors.Length; - } - if (formVals.username == '') { - badFields.username = this.FieldErrors.Missing; - } - if (formVals.email == '') { - badFields.email = this.FieldErrors.Missing; - } - if (Object.keys(badFields).length > 0) { - this.onBadFields(badFields); - return; - } - - MatrixClientPeg.replaceUsingUrls( - this.getHsUrl(), - this.getIsUrl() - ); - this.setState({ - hs_url: this.getHsUrl(), - is_url: this.getIsUrl() - }); - this.setState({busy: true}); - - this.tryRegister(); - }, - - startStage: function(stageName) { - var self = this; - this.setStep('stage_'+stageName); - switch(stageName) { - case 'm.login.email.identity': - self.setState({ - busy: true - }); - var cli = MatrixClientPeg.get(); - this.savedParams.client_secret = cli.generateClientSecret(); - this.savedParams.send_attempt = 1; - - var nextLink = this.props.registrationUrl + - '?client_secret=' + - encodeURIComponent(this.savedParams.client_secret) + - "&hs_url=" + - encodeURIComponent(this.state.hs_url) + - "&is_url=" + - encodeURIComponent(this.state.is_url) + - "&session_id=" + - encodeURIComponent(this.authSessionId); - - cli.requestEmailToken( - this.savedParams.email, - this.savedParams.client_secret, - this.savedParams.send_attempt, - nextLink - ).done(function(response) { - self.setState({ - busy: false, - }); - self.setStep('stage_m.login.email.identity'); - }, function(error) { - console.error(error); - self.setStep('initial'); - var newState = {busy: false}; - if (error.errcode == 'THREEPID_IN_USE') { - self.onBadFields({email: self.FieldErrors.InUse}); - } else { - newState.errorText = 'Unable to contact the given identity server'; - } - self.setState(newState); - }); - break; - case 'm.login.recaptcha': - if (!this.authParams || !this.authParams['m.login.recaptcha'].public_key) { - this.setState({ - errorText: "This server has not supplied enough information for Recaptcha authentication" - }); - } - break; - } - }, - - onRegistered: function(user_id, access_token) { - MatrixClientPeg.replaceUsingAccessToken( - this.state.hs_url, this.state.is_url, user_id, access_token - ); - if (this.props.onLoggedIn) { - this.props.onLoggedIn(); - } - }, - - onCaptchaLoaded: function() { - if (this.refs.recaptchaContainer) { - var sitekey = this.authParams['m.login.recaptcha'].public_key; - global.grecaptcha.render('mx_recaptcha', { - 'sitekey': sitekey, - 'callback': this.onCaptchaDone - }); - } - }, - - onCaptchaDone: function(captcha_response) { - this.tryRegister({ - type: 'm.login.recaptcha', - response: captcha_response - }); - }, - - tryRegister: function(auth) { - var self = this; - MatrixClientPeg.get().register( - this.savedParams.username, - this.savedParams.password, - this.authSessionId, - auth - ).done(function(result) { - self.onRegistered(result.user_id, result.access_token); - }, function(error) { - if (error.httpStatus == 401 && error.data.flows) { - self.authParams = error.data.params; - self.authSessionId = error.data.session; - - self.completedStages = error.data.completed || []; - - var flow = self.chooseFlow(error.data.flows); - - if (flow) { - var flowStage = self.firstUncompletedStageIndex(flow); - var numDone = self.numCompletedStages(flow); - - self.setState({ - busy: false, - flows: flow, - currentStep: 1+numDone, - totalSteps: flow.stages.length+1, - flowStage: flowStage - }); - self.startStage(flow.stages[flowStage]); - } - else { - self.setState({ - busy: false, - errorText: "Unable to register - missing email address?" - }); - } - } else { - console.log(error); - self.setStep("initial"); - var newState = { - busy: false, - errorText: "Unable to contact the given Home Server" - }; - if (error.name == 'M_USER_IN_USE') { - delete newState.errorText; - self.onBadFields({ - username: self.FieldErrors.InUse - }); - } else if (error.httpStatus == 401) { - newState.errorText = "Authorisation failed!"; - } else if (error.httpStatus >= 400 && error.httpStatus < 500) { - newState.errorText = "Registration failed!"; - } else if (error.httpStatus >= 500 && error.httpStatus < 600) { - newState.errorText = "Server error during registration!"; - } else if (error.name == "M_MISSING_PARAM") { - // The HS hasn't remembered the login params from - // the first try when the login email was sent. - newState.errorText = "This home server does not support resuming registration."; - } - self.setState(newState); - } - }); - }, - - showLogin: function(ev) { - ev.preventDefault(); - dis.dispatch({ - action: 'start_login' - }); - } -}; From ad60e234590b495e7598064c75609de968707c53 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 20 Nov 2015 10:37:46 +0000 Subject: [PATCH 032/152] Correctly display an error if a bad IS URL is entered. --- src/SignupStages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SignupStages.js b/src/SignupStages.js index 272a955d95..738732b9e2 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -183,7 +183,7 @@ class EmailIdentityStage extends Stage { } else { e.message = 'Unable to contact the given identity server'; } - return e; + throw e; }); } } From d46e42f8e77e76386ee36c805bcdd08537a5ad55 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 20 Nov 2015 11:57:04 +0000 Subject: [PATCH 033/152] Have a post-registration screen. Fix race in ChangeAvatar where if you hadn't got an initial avatar downloaded yet you couldn't update it after the component loaded. --- src/controllers/molecules/ChangeAvatar.js | 10 ++++++++ .../molecules/ChangeDisplayName.js | 6 ++++- src/controllers/pages/MatrixChat.js | 23 ++++++++++++------- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/controllers/molecules/ChangeAvatar.js b/src/controllers/molecules/ChangeAvatar.js index 093badfe8f..7e8f959ebf 100644 --- a/src/controllers/molecules/ChangeAvatar.js +++ b/src/controllers/molecules/ChangeAvatar.js @@ -36,6 +36,16 @@ module.exports = { } }, + componentWillReceiveProps: function(newProps) { + if (this.avatarSet) { + // don't clobber what the user has just set + return; + } + this.setState({ + avatarUrl: newProps.initialAvatarUrl + }); + }, + setAvatarFromFile: function(file) { var newUrl = null; diff --git a/src/controllers/molecules/ChangeDisplayName.js b/src/controllers/molecules/ChangeDisplayName.js index 7e49b8f725..afef82772c 100644 --- a/src/controllers/molecules/ChangeDisplayName.js +++ b/src/controllers/molecules/ChangeDisplayName.js @@ -15,10 +15,14 @@ limitations under the License. */ 'use strict'; - +var React = require('react'); var MatrixClientPeg = require("../../MatrixClientPeg"); module.exports = { + propTypes: { + onFinished: React.PropTypes.func + }, + getDefaultProps: function() { return { onFinished: function() {}, diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index 4655011a45..e31496afc0 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/controllers/pages/MatrixChat.js @@ -31,7 +31,7 @@ module.exports = { RoomView: "room_view", UserSettings: "user_settings", CreateRoom: "create_room", - RoomDirectory: "room_directory", + RoomDirectory: "room_directory" }, AuxPanel: { @@ -144,6 +144,11 @@ module.exports = { }); this.notifyNewScreen('login'); break; + case 'start_post_registration': + this.setState({ // don't clobber logged_in status + screen: 'post_registration' + }); + break; case 'token_login': if (this.state.logged_in) return; @@ -298,13 +303,11 @@ module.exports = { }, onLoggedIn: function(credentials) { - if (credentials) { // registration doesn't do this yet - console.log("onLoggedIn => %s", credentials.userId); - MatrixClientPeg.replaceUsingAccessToken( - credentials.homeserverUrl, credentials.identityServerUrl, - credentials.userId, credentials.accessToken - ); - } + console.log("onLoggedIn => %s", credentials.userId); + MatrixClientPeg.replaceUsingAccessToken( + credentials.homeserverUrl, credentials.identityServerUrl, + credentials.userId, credentials.accessToken + ); this.setState({ screen: undefined, logged_in: true @@ -431,6 +434,10 @@ module.exports = { dis.dispatch({ action: 'view_room_directory', }); + } else if (screen == 'post_registration') { + dis.dispatch({ + action: 'start_post_registration', + }); } else if (screen.indexOf('room/') == 0) { var roomString = screen.split('/')[1]; if (roomString[0] == '#') { From 1a72cb56c670d82804045a552a866205b5829be5 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 20 Nov 2015 14:26:49 +0000 Subject: [PATCH 034/152] Log an error for unknown screens --- src/controllers/pages/MatrixChat.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index e31496afc0..66e359a711 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/controllers/pages/MatrixChat.js @@ -459,6 +459,9 @@ module.exports = { }); } } + else { + console.error("Unknown screen : %s", screen); + } }, notifyNewScreen: function(screen) { From 032fdc0abc76e38ad976e864258764b0bc0eeed8 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 20 Nov 2015 14:32:00 +0000 Subject: [PATCH 035/152] Remove diff clutter --- src/controllers/pages/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index 66e359a711..af2c78ff26 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/controllers/pages/MatrixChat.js @@ -31,7 +31,7 @@ module.exports = { RoomView: "room_view", UserSettings: "user_settings", CreateRoom: "create_room", - RoomDirectory: "room_directory" + RoomDirectory: "room_directory", }, AuxPanel: { From b12fc67a63ade616cb9383ad90724e4663a800f3 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 20 Nov 2015 16:08:57 +0000 Subject: [PATCH 036/152] Add markdown support (enabled by default) --- package.json | 1 + src/controllers/molecules/MessageComposer.js | 76 ++++++++++++++++---- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index d51f3a05ba..2da0968345 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "flux": "^2.0.3", "glob": "^5.0.14", "linkifyjs": "^2.0.0-beta.4", + "marked": "^0.3.5", "matrix-js-sdk": "https://github.com/matrix-org/matrix-js-sdk.git#develop", "optimist": "^0.6.1", "q": "^1.4.1", diff --git a/src/controllers/molecules/MessageComposer.js b/src/controllers/molecules/MessageComposer.js index 5c8813dff6..64234558ab 100644 --- a/src/controllers/molecules/MessageComposer.js +++ b/src/controllers/molecules/MessageComposer.js @@ -14,6 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ +var marked = require("marked"); +marked.setOptions({ + renderer: new marked.Renderer(), + gfm: true, + tables: true, + breaks: true, + pedantic: false, + sanitize: true, + smartLists: true, + smartypants: false +}); var MatrixClientPeg = require("../../MatrixClientPeg"); var SlashCommands = require("../../SlashCommands"); var Modal = require("../../Modal"); @@ -32,11 +43,26 @@ var KeyCode = { var TYPING_USER_TIMEOUT = 10000; var TYPING_SERVER_TIMEOUT = 30000; +var MARKDOWN_ENABLED = true; + +function mdownToHtml(mdown) { + var html = marked(mdown) || ""; + html = html.trim(); + // strip start and end

    tags else you get 'orrible spacing + if (html.indexOf("

    ") === 0) { + html = html.substring("

    ".length); + } + if (html.lastIndexOf("

    ") === (html.length - "

    ".length)) { + html = html.substring(0, html.length - "

    ".length); + } + return html; +} module.exports = { oldScrollHeight: 0, componentWillMount: function() { + this.markdownEnabled = MARKDOWN_ENABLED; this.tabStruct = { completing: false, original: null, @@ -228,6 +254,27 @@ module.exports = { onEnter: function(ev) { var contentText = this.refs.textarea.value; + // bodge for now to set markdown state on/off. We probably want a separate + // area for "local" commands which don't hit out to the server. + if (contentText.indexOf("/markdown") === 0) { + ev.preventDefault(); + this.refs.textarea.value = ''; + if (contentText.indexOf("/markdown on") === 0) { + this.markdownEnabled = true; + } + else if (contentText.indexOf("/markdown off") === 0) { + this.markdownEnabled = false; + } + else { + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Unknown command", + description: "Usage: /markdown on|off" + }); + } + return; + } + var cmd = SlashCommands.processInput(this.props.room.roomId, contentText); if (cmd) { ev.preventDefault(); @@ -257,20 +304,25 @@ module.exports = { return; } - var content = null; - if (/^\/me /i.test(contentText)) { - content = { - msgtype: 'm.emote', - body: contentText.substring(4) - }; - } else { - content = { - msgtype: 'm.text', - body: contentText - }; + var isEmote = /^\/me /i.test(contentText); + var sendMessagePromise; + if (isEmote) { + sendMessagePromise = MatrixClientPeg.get().sendEmoteMessage( + this.props.room.roomId, contentText.substring(4) + ); + } + else if (this.markdownEnabled) { + sendMessagePromise = MatrixClientPeg.get().sendHtmlMessage( + this.props.room.roomId, contentText, mdownToHtml(contentText) + ); + } + else { + sendMessagePromise = MatrixClientPeg.get().sendTextMessage( + this.props.room.roomId, contentText + ); } - MatrixClientPeg.get().sendMessage(this.props.room.roomId, content).then(function() { + sendMessagePromise.then(function() { dis.dispatch({ action: 'message_sent' }); From 2e323835704e73c2c954d9ed763d8533c0a0c6a5 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 20 Nov 2015 17:09:28 +0000 Subject: [PATCH 037/152] fix up the textbox after hitting enter --- src/controllers/molecules/MessageComposer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/molecules/MessageComposer.js b/src/controllers/molecules/MessageComposer.js index 64234558ab..689841226d 100644 --- a/src/controllers/molecules/MessageComposer.js +++ b/src/controllers/molecules/MessageComposer.js @@ -332,6 +332,7 @@ module.exports = { }); }); this.refs.textarea.value = ''; + this.resizeInput(); ev.preventDefault(); }, From f5e2a54603ad06572a2d5be28c1917dbb4301cf8 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 20 Nov 2015 17:30:14 +0000 Subject: [PATCH 038/152] Only send HTML if we need to. --- src/controllers/molecules/MessageComposer.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/controllers/molecules/MessageComposer.js b/src/controllers/molecules/MessageComposer.js index 689841226d..237c710395 100644 --- a/src/controllers/molecules/MessageComposer.js +++ b/src/controllers/molecules/MessageComposer.js @@ -311,15 +311,18 @@ module.exports = { this.props.room.roomId, contentText.substring(4) ); } - else if (this.markdownEnabled) { - sendMessagePromise = MatrixClientPeg.get().sendHtmlMessage( - this.props.room.roomId, contentText, mdownToHtml(contentText) - ); - } else { - sendMessagePromise = MatrixClientPeg.get().sendTextMessage( - this.props.room.roomId, contentText - ); + var htmlText = mdownToHtml(contentText); + if (this.markdownEnabled && htmlText !== contentText) { + sendMessagePromise = MatrixClientPeg.get().sendHtmlMessage( + this.props.room.roomId, contentText, htmlText + ); + } + else { + sendMessagePromise = MatrixClientPeg.get().sendTextMessage( + this.props.room.roomId, contentText + ); + } } sendMessagePromise.then(function() { From b69fff5b01d54ab9230e0498661c5b9fef1c612c Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 12:02:31 +0000 Subject: [PATCH 039/152] Define component directories. Merge MemberAvatar and RoomAvatar to new-style components. Spoken to @ara4n about names/conventions. Settled on the following layout: src/components |_____________views | |____ tiles | | |___ MTextTile.js | | |___ MNoticeTile.js | | |___ ... | | | |____ avatars | | |____ RoomAvatar.js | | |____ MemberAvatar.js | | |____ ... | | | |____ ... | |_____________structures |____ RoomView.js |____ UserSettings.js |____ CreateRoom.js |____ ... Views are the "pure UI" components which can be reused. Structures are the wire components which give important contextual information to the views e.g. a view may be MemberList, but it's where it is in the structure that defines that it is a *Room* MemberList. --- .../views}/MemberAvatar.js | 55 ++++++++++++++- .../atoms => components/views}/RoomAvatar.js | 69 ++++++++++++++++--- src/controllers/atoms/UserSettingsButton.js | 27 -------- 3 files changed, 111 insertions(+), 40 deletions(-) rename src/{controllers/atoms => components/views}/MemberAvatar.js (57%) rename src/{controllers/atoms => components/views}/RoomAvatar.js (66%) delete mode 100644 src/controllers/atoms/UserSettingsButton.js diff --git a/src/controllers/atoms/MemberAvatar.js b/src/components/views/MemberAvatar.js similarity index 57% rename from src/controllers/atoms/MemberAvatar.js rename to src/components/views/MemberAvatar.js index e170d2e04c..9a7b171221 100644 --- a/src/controllers/atoms/MemberAvatar.js +++ b/src/components/views/MemberAvatar.js @@ -17,9 +17,12 @@ limitations under the License. 'use strict'; var React = require('react'); +var Avatar = require('../../../../Avatar'); var MatrixClientPeg = require('../../MatrixClientPeg'); -module.exports = { +module.exports = React.createClass({ + displayName: 'MemberAvatar', + propTypes: { member: React.PropTypes.object.isRequired, width: React.PropTypes.number, @@ -87,5 +90,53 @@ module.exports = { return { imageUrl: this._computeUrl() }; + }, + + + /////////////// + + + avatarUrlForMember: function(member) { + return Avatar.avatarUrlForMember( + member, + this.props.member, + this.props.width, + this.props.height, + this.props.resizeMethod + ); + }, + + skinnedDefaultAvatarUrl: function(member, width, height, resizeMethod) { + return Avatar.defaultAvatarUrlForString(member.userId); + }, + + render: function() { + // XXX: recalculates default avatar url constantly + if (this.state.imageUrl === this.defaultAvatarUrl(this.props.member)) { + var initial; + if (this.props.member.name[0]) + initial = this.props.member.name[0].toUpperCase(); + if (initial === '@' && this.props.member.name[1]) + initial = this.props.member.name[1].toUpperCase(); + + return ( + + + + + ); + } + return ( + + ); } -}; +}); diff --git a/src/controllers/atoms/RoomAvatar.js b/src/components/views/RoomAvatar.js similarity index 66% rename from src/controllers/atoms/RoomAvatar.js rename to src/components/views/RoomAvatar.js index 57c9a71842..086136fa1b 100644 --- a/src/controllers/atoms/RoomAvatar.js +++ b/src/components/views/RoomAvatar.js @@ -13,18 +13,12 @@ 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. */ - -'use strict'; - +var React = require('react'); var MatrixClientPeg = require('../../MatrixClientPeg'); -/* - * View class should provide: - * - getUrlList() returning an array of URLs to try for the room avatar - in order of preference from the most preferred at index 0. null entries - in the array will be skipped over. - */ -module.exports = { +module.exports = React.createClass({ + displayName: 'RoomAvatar', + getDefaultProps: function() { return { width: 36, @@ -124,5 +118,58 @@ module.exports = { this.setState({ imageUrl: this._nextUrl() }); + }, + + + + //////////// + + + getUrlList: function() { + return [ + this.roomAvatarUrl(), + this.getOneToOneAvatar(), + this.getFallbackAvatar() + ]; + }, + + getFallbackAvatar: function() { + var images = [ '76cfa6', '50e2c2', 'f4c371' ]; + var total = 0; + for (var i = 0; i < this.props.room.roomId.length; ++i) { + total += this.props.room.roomId.charCodeAt(i); + } + return 'img/' + images[total % images.length] + '.png'; + }, + + render: function() { + var style = { + width: this.props.width, + height: this.props.height, + }; + + // XXX: recalculates fallback avatar constantly + if (this.state.imageUrl === this.getFallbackAvatar()) { + var initial; + if (this.props.room.name[0]) + initial = this.props.room.name[0].toUpperCase(); + if ((initial === '@' || initial === '#') && this.props.room.name[1]) + initial = this.props.room.name[1].toUpperCase(); + + return ( + + + + + ); + } + else { + return + } } -}; +}); diff --git a/src/controllers/atoms/UserSettingsButton.js b/src/controllers/atoms/UserSettingsButton.js deleted file mode 100644 index 5138111ef8..0000000000 --- a/src/controllers/atoms/UserSettingsButton.js +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright 2015 OpenMarket 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. -*/ - -'use strict'; - -var dis = require("../../dispatcher"); - -module.exports = { - onClick: function() { - dis.dispatch({ - action: 'view_user_settings' - }); - }, -}; From 776369299d0a705a135162576fbb0cbb76c1acb2 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 13:27:52 +0000 Subject: [PATCH 040/152] Move login components to views --- src/components/{ => views}/login/CaptchaForm.js | 0 src/components/{ => views}/login/CasLogin.js | 0 src/components/{ => views}/login/PasswordLogin.js | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/components/{ => views}/login/CaptchaForm.js (100%) rename src/components/{ => views}/login/CasLogin.js (100%) rename src/components/{ => views}/login/PasswordLogin.js (100%) diff --git a/src/components/login/CaptchaForm.js b/src/components/views/login/CaptchaForm.js similarity index 100% rename from src/components/login/CaptchaForm.js rename to src/components/views/login/CaptchaForm.js diff --git a/src/components/login/CasLogin.js b/src/components/views/login/CasLogin.js similarity index 100% rename from src/components/login/CasLogin.js rename to src/components/views/login/CasLogin.js diff --git a/src/components/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js similarity index 100% rename from src/components/login/PasswordLogin.js rename to src/components/views/login/PasswordLogin.js From 7846d49403841dc374a055136d6fa4486a076d84 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 13:45:04 +0000 Subject: [PATCH 041/152] Add missing deps; Move stuff in 'views' to have functional descriptors --- src/Avatar.js | 53 +++++++++++++++++++ .../views/{ => avatars}/MemberAvatar.js | 2 +- .../views/{ => avatars}/RoomAvatar.js | 0 src/components/views/login/CasLogin.js | 2 +- 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 src/Avatar.js rename src/components/views/{ => avatars}/MemberAvatar.js (99%) rename src/components/views/{ => avatars}/RoomAvatar.js (100%) diff --git a/src/Avatar.js b/src/Avatar.js new file mode 100644 index 0000000000..afc5e9dd6d --- /dev/null +++ b/src/Avatar.js @@ -0,0 +1,53 @@ +/* +Copyright 2015 OpenMarket 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. +*/ + +'use strict'; + +var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); + +module.exports = { + avatarUrlForMember: function(member, width, height, resizeMethod) { + var url = member.getAvatarUrl( + MatrixClientPeg.get().getHomeserverUrl(), + width, + height, + resizeMethod + ); + if (!url) { + // member can be null here currently since on invites, the JS SDK + // does not have enough info to build a RoomMember object for + // the inviter. + url = this.defaultAvatarUrlForString(member ? member.userId : ''); + } + return url; + }, + + defaultAvatarUrlForString: function(s) { + var total = 0; + for (var i = 0; i < s.length; ++i) { + total += s.charCodeAt(i); + } + switch (total % 3) { + case 0: + return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9QjNbxSKP4eagAFnTseHFErFYLBaLxWKxWCwWi8Vi8cX4CzAABSwCRWJw31gAAAAASUVORK5CYII="; + case 1: + return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9chOaxgCP4eagAFk9seHFErFYLBaLxWKxWCwWi8Vi8cX4CzAAtKMCks/JG8MAAAAASUVORK5CYII="; + case 2: + return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9YzNayQCP4eagADldseHFErFYLBaLxWKxWCwWi8Vi8cX4CzAAyiACeHwPiu4AAAAASUVORK5CYII="; + } + } +} + diff --git a/src/components/views/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js similarity index 99% rename from src/components/views/MemberAvatar.js rename to src/components/views/avatars/MemberAvatar.js index 9a7b171221..8cfeaa98d2 100644 --- a/src/components/views/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.js @@ -17,7 +17,7 @@ limitations under the License. 'use strict'; var React = require('react'); -var Avatar = require('../../../../Avatar'); +var Avatar = require('../../Avatar'); var MatrixClientPeg = require('../../MatrixClientPeg'); module.exports = React.createClass({ diff --git a/src/components/views/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js similarity index 100% rename from src/components/views/RoomAvatar.js rename to src/components/views/avatars/RoomAvatar.js diff --git a/src/components/views/login/CasLogin.js b/src/components/views/login/CasLogin.js index 8a45fa0643..9380db9788 100644 --- a/src/components/views/login/CasLogin.js +++ b/src/components/views/login/CasLogin.js @@ -16,7 +16,7 @@ limitations under the License. 'use strict'; -var MatrixClientPeg = require("../../MatrixClientPeg"); +var MatrixClientPeg = require("../../../MatrixClientPeg"); var React = require('react'); var url = require("url"); From 1dc4e14606bf01865c4478bb0c3fc77a6f5ad164 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 13:49:39 +0000 Subject: [PATCH 042/152] Import things at the right levels --- src/Avatar.js | 2 +- src/components/views/avatars/MemberAvatar.js | 4 ++-- src/components/views/avatars/RoomAvatar.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avatar.js b/src/Avatar.js index afc5e9dd6d..02025a9384 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -16,7 +16,7 @@ limitations under the License. 'use strict'; -var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); +var MatrixClientPeg = require('./MatrixClientPeg'); module.exports = { avatarUrlForMember: function(member, width, height, resizeMethod) { diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js index 8cfeaa98d2..f65f11256b 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.js @@ -17,8 +17,8 @@ limitations under the License. 'use strict'; var React = require('react'); -var Avatar = require('../../Avatar'); -var MatrixClientPeg = require('../../MatrixClientPeg'); +var Avatar = require('../../../Avatar'); +var MatrixClientPeg = require('../../../MatrixClientPeg'); module.exports = React.createClass({ displayName: 'MemberAvatar', diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js index 086136fa1b..55f0e92cc1 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ var React = require('react'); -var MatrixClientPeg = require('../../MatrixClientPeg'); +var MatrixClientPeg = require('../../../MatrixClientPeg'); module.exports = React.createClass({ displayName: 'RoomAvatar', From 659fc8fcfb1073427f101884764fb5903744737f Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 14:24:21 +0000 Subject: [PATCH 043/152] Point to new Spinner location --- src/controllers/molecules/MemberInfo.js | 2 +- src/controllers/molecules/MemberTile.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/molecules/MemberInfo.js b/src/controllers/molecules/MemberInfo.js index d822a87f48..913e36a4ce 100644 --- a/src/controllers/molecules/MemberInfo.js +++ b/src/controllers/molecules/MemberInfo.js @@ -228,7 +228,7 @@ module.exports = { var d = MatrixClientPeg.get().leave(roomId); // FIXME: controller shouldn't be loading a view :( - var Loader = sdk.getComponent("atoms.Spinner"); + var Loader = sdk.getComponent("elements.Spinner"); var modal = Modal.createDialog(Loader); d.then(function() { diff --git a/src/controllers/molecules/MemberTile.js b/src/controllers/molecules/MemberTile.js index 222ebca145..057bc82497 100644 --- a/src/controllers/molecules/MemberTile.js +++ b/src/controllers/molecules/MemberTile.js @@ -39,7 +39,7 @@ module.exports = { var d = MatrixClientPeg.get().leave(roomId); // FIXME: controller shouldn't be loading a view :( - var Loader = sdk.getComponent("atoms.Spinner"); + var Loader = sdk.getComponent("elements.Spinner"); var modal = Modal.createDialog(Loader); d.then(function() { From c2ae6238b95e25da64f40e4bef03657fdfc977b1 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 14:48:02 +0000 Subject: [PATCH 044/152] Nuke LogoutButton; nothing used it. --- src/controllers/atoms/LogoutButton.js | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 src/controllers/atoms/LogoutButton.js diff --git a/src/controllers/atoms/LogoutButton.js b/src/controllers/atoms/LogoutButton.js deleted file mode 100644 index 87cf814801..0000000000 --- a/src/controllers/atoms/LogoutButton.js +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright 2015 OpenMarket 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. -*/ - -'use strict'; - -var dis = require("../../dispatcher"); - -module.exports = { - onClick: function() { - dis.dispatch({ - action: 'logout' - }); - }, -}; From 8bde761a8afe8d5c417bc8f0e73c4af04f085f74 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 15:11:08 +0000 Subject: [PATCH 045/152] Add EnableNotificationButton component --- .../settings}/EnableNotificationsButton.js | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) rename src/{controllers/atoms => components/views/settings}/EnableNotificationsButton.js (69%) diff --git a/src/controllers/atoms/EnableNotificationsButton.js b/src/components/views/settings/EnableNotificationsButton.js similarity index 69% rename from src/controllers/atoms/EnableNotificationsButton.js rename to src/components/views/settings/EnableNotificationsButton.js index 3c399484e8..de43a578f7 100644 --- a/src/controllers/atoms/EnableNotificationsButton.js +++ b/src/components/views/settings/EnableNotificationsButton.js @@ -15,10 +15,12 @@ limitations under the License. */ 'use strict'; -var sdk = require('../../index'); -var dis = require("../../dispatcher"); +var React = require("react"); +var sdk = require('../../../index'); +var dis = require("../../../dispatcher"); -module.exports = { +module.exports = React.createClass({ + displayName: 'EnableNotificationsButton', componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); @@ -55,4 +57,20 @@ module.exports = { } this.forceUpdate(); }, -}; + + render: function() { + if (this.enabled()) { + return ( + + ); + } else { + return ( + + ); + } + } +}); From 17d789eb971121b0a1ff2181b1d24b34b834adc6 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 15:16:50 +0000 Subject: [PATCH 046/152] Merge EditableText component --- .../views/elements}/EditableText.js | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) rename src/{controllers/atoms => components/views/elements}/EditableText.js (58%) diff --git a/src/controllers/atoms/EditableText.js b/src/components/views/elements/EditableText.js similarity index 58% rename from src/controllers/atoms/EditableText.js rename to src/components/views/elements/EditableText.js index 5ea4ce8c4a..63d8fe1877 100644 --- a/src/controllers/atoms/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -18,7 +18,8 @@ limitations under the License. var React = require('react'); -module.exports = { +module.exports = React.createClass({ + displayName: 'EditableText', propTypes: { onValueChanged: React.PropTypes.func, initialValue: React.PropTypes.string, @@ -85,4 +86,54 @@ module.exports = { onValueChanged: function(shouldSubmit) { this.props.onValueChanged(this.state.value, shouldSubmit); }, + + onKeyUp: function(ev) { + if (ev.key == "Enter") { + this.onFinish(ev); + } else if (ev.key == "Escape") { + this.cancelEdit(); + } + }, + + onClickDiv: function() { + this.setState({ + phase: this.Phases.Edit, + }) + }, + + onFocus: function(ev) { + ev.target.setSelectionRange(0, ev.target.value.length); + }, + + onFinish: function(ev) { + if (ev.target.value) { + this.setValue(ev.target.value, ev.key === "Enter"); + } else { + this.cancelEdit(); + } + }, + + render: function() { + var editable_el; + + if (this.state.phase == this.Phases.Display) { + if (this.state.value) { + editable_el =
    {this.state.value}
    ; + } else { + editable_el =
    {this.props.label}
    ; + } + } else if (this.state.phase == this.Phases.Edit) { + editable_el = ( +
    + +
    + ); + } + + return ( +
    + {editable_el} +
    + ); + } }; From 4fda0ce0c99c19a47be73161c5afc5ae63ba5ee7 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 15:17:34 +0000 Subject: [PATCH 047/152] Fix typo --- src/components/views/elements/EditableText.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index 63d8fe1877..0ed443fbae 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -136,4 +136,4 @@ module.exports = React.createClass({
    ); } -}; +}); From e55ecfeacb71937767499b9e1988d3eaf1176dfa Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 15:20:57 +0000 Subject: [PATCH 048/152] Add VideoFeed component --- src/components/views/voip/VideoFeed.js | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/components/views/voip/VideoFeed.js diff --git a/src/components/views/voip/VideoFeed.js b/src/components/views/voip/VideoFeed.js new file mode 100644 index 0000000000..9cf28d1ba4 --- /dev/null +++ b/src/components/views/voip/VideoFeed.js @@ -0,0 +1,31 @@ +/* +Copyright 2015 OpenMarket 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. +*/ + +'use strict'; + +var React = require('react'); + +module.exports = React.createClass({ + displayName: 'VideoFeed', + + render: function() { + return ( + + ); + }, +}); + From 172735a8379b817063cff477b0d4631f083fcff0 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 15:44:42 +0000 Subject: [PATCH 049/152] Move create_room atoms to components --- .../views}/create_room/CreateRoomButton.js | 11 +- .../views}/create_room/Presets.js | 19 +++- src/components/views/create_room/RoomAlias.js | 101 ++++++++++++++++++ .../atoms/create_room/RoomAlias.js | 47 -------- .../atoms/create_room/RoomNameTextbox.js | 41 ------- src/controllers/organisms/CreateRoom.js | 6 +- 6 files changed, 132 insertions(+), 93 deletions(-) rename src/{controllers/atoms => components/views}/create_room/CreateRoomButton.js (78%) rename src/{controllers/atoms => components/views}/create_room/Presets.js (62%) create mode 100644 src/components/views/create_room/RoomAlias.js delete mode 100644 src/controllers/atoms/create_room/RoomAlias.js delete mode 100644 src/controllers/atoms/create_room/RoomNameTextbox.js diff --git a/src/controllers/atoms/create_room/CreateRoomButton.js b/src/components/views/create_room/CreateRoomButton.js similarity index 78% rename from src/controllers/atoms/create_room/CreateRoomButton.js rename to src/components/views/create_room/CreateRoomButton.js index f03dd56c97..95ba4ac366 100644 --- a/src/controllers/atoms/create_room/CreateRoomButton.js +++ b/src/components/views/create_room/CreateRoomButton.js @@ -18,7 +18,8 @@ limitations under the License. var React = require('react'); -module.exports = { +module.exports = React.createClass({ + displayName: 'CreateRoomButton', propTypes: { onCreateRoom: React.PropTypes.func, }, @@ -32,4 +33,10 @@ module.exports = { onClick: function() { this.props.onCreateRoom(); }, -}; + + render: function() { + return ( + + ); + } +}); diff --git a/src/controllers/atoms/create_room/Presets.js b/src/components/views/create_room/Presets.js similarity index 62% rename from src/controllers/atoms/create_room/Presets.js rename to src/components/views/create_room/Presets.js index bcc2f51481..ee0d19c357 100644 --- a/src/controllers/atoms/create_room/Presets.js +++ b/src/components/views/create_room/Presets.js @@ -24,7 +24,8 @@ var Presets = { Custom: "custom", }; -module.exports = { +module.exports = React.createClass({ + displayName: 'CreateRoomPresets', propTypes: { onChange: React.PropTypes.func, preset: React.PropTypes.string @@ -37,4 +38,18 @@ module.exports = { onChange: function() {}, }; }, -}; + + onValueChanged: function(ev) { + this.props.onChange(ev.target.value) + }, + + render: function() { + return ( + + ); + } +}); diff --git a/src/components/views/create_room/RoomAlias.js b/src/components/views/create_room/RoomAlias.js new file mode 100644 index 0000000000..9a30d3fbff --- /dev/null +++ b/src/components/views/create_room/RoomAlias.js @@ -0,0 +1,101 @@ +/* +Copyright 2015 OpenMarket 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. +*/ + +var React = require('react'); + +module.exports = React.createClass({ + displayName: 'RoomAlias', + propTypes: { + // Specifying a homeserver will make magical things happen when you, + // e.g. start typing in the room alias box. + homeserver: React.PropTypes.string, + alias: React.PropTypes.string, + onChange: React.PropTypes.func, + }, + + getDefaultProps: function() { + return { + onChange: function() {}, + alias: '', + }; + }, + + getAliasLocalpart: function() { + var room_alias = this.props.alias; + + if (room_alias && this.props.homeserver) { + var suffix = ":" + this.props.homeserver; + if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) { + room_alias = room_alias.slice(1, -suffix.length); + } + } + + return room_alias; + }, + + onValueChanged: function(ev) { + this.props.onChange(ev.target.value); + }, + + onFocus: function(ev) { + var target = ev.target; + var curr_val = ev.target.value; + + if (this.props.homeserver) { + if (curr_val == "") { + setTimeout(function() { + target.value = "#:" + this.props.homeserver; + target.setSelectionRange(1, 1); + }, 0); + } else { + var suffix = ":" + this.props.homeserver; + setTimeout(function() { + target.setSelectionRange( + curr_val.startsWith("#") ? 1 : 0, + curr_val.endsWith(suffix) ? (target.value.length - suffix.length) : target.value.length + ); + }, 0); + } + } + }, + + onBlur: function(ev) { + var curr_val = ev.target.value; + + if (this.props.homeserver) { + if (curr_val == "#:" + this.props.homeserver) { + ev.target.value = ""; + return; + } + + if (curr_val != "") { + var new_val = ev.target.value; + var suffix = ":" + this.props.homeserver; + if (!curr_val.startsWith("#")) new_val = "#" + new_val; + if (!curr_val.endsWith(suffix)) new_val = new_val + suffix; + ev.target.value = new_val; + } + } + }, + + render: function() { + return ( + + ); + } +}); diff --git a/src/controllers/atoms/create_room/RoomAlias.js b/src/controllers/atoms/create_room/RoomAlias.js deleted file mode 100644 index b1176a2ab5..0000000000 --- a/src/controllers/atoms/create_room/RoomAlias.js +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2015 OpenMarket 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. -*/ - -var React = require('react'); - -module.exports = { - propTypes: { - // Specifying a homeserver will make magical things happen when you, - // e.g. start typing in the room alias box. - homeserver: React.PropTypes.string, - alias: React.PropTypes.string, - onChange: React.PropTypes.func, - }, - - getDefaultProps: function() { - return { - onChange: function() {}, - alias: '', - }; - }, - - getAliasLocalpart: function() { - var room_alias = this.props.alias; - - if (room_alias && this.props.homeserver) { - var suffix = ":" + this.props.homeserver; - if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) { - room_alias = room_alias.slice(1, -suffix.length); - } - } - - return room_alias; - }, -}; diff --git a/src/controllers/atoms/create_room/RoomNameTextbox.js b/src/controllers/atoms/create_room/RoomNameTextbox.js deleted file mode 100644 index e78692d992..0000000000 --- a/src/controllers/atoms/create_room/RoomNameTextbox.js +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright 2015 OpenMarket 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. -*/ - -'use strict'; - -var React = require('react'); - -module.exports = { - propTypes: { - default_name: React.PropTypes.string - }, - - getDefaultProps: function() { - return { - default_name: '', - }; - }, - - getInitialState: function() { - return { - room_name: this.props.default_name, - } - }, - - getName: function() { - return this.state.room_name; - }, -}; diff --git a/src/controllers/organisms/CreateRoom.js b/src/controllers/organisms/CreateRoom.js index 3c48e43f74..b39b734480 100644 --- a/src/controllers/organisms/CreateRoom.js +++ b/src/controllers/organisms/CreateRoom.js @@ -18,7 +18,11 @@ limitations under the License. var React = require("react"); var MatrixClientPeg = require("../../MatrixClientPeg"); -var PresetValues = require('../atoms/create_room/Presets').Presets; +var PresetValues = { + PrivateChat: "private_chat", + PublicChat: "public_chat", + Custom: "custom", +}; var q = require('q'); var encryption = require("../../encryption"); From 6c9f3303c6dfc84488a8605e66e7bb5fa85e7ad7 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 16:38:56 +0000 Subject: [PATCH 050/152] Convert voip molecules to components Don't pull in VectorConferenceHandler; instead accept a prop which meets a conference handler interface. --- .../views}/voip/CallView.js | 64 +++++++-- src/components/views/voip/IncomingCallBox.js | 122 ++++++++++++++++++ src/components/views/voip/VideoView.js | 93 +++++++++++++ .../molecules/voip/IncomingCallBox.js | 73 ----------- 4 files changed, 266 insertions(+), 86 deletions(-) rename src/{controllers/molecules => components/views}/voip/CallView.js (51%) create mode 100644 src/components/views/voip/IncomingCallBox.js create mode 100644 src/components/views/voip/VideoView.js delete mode 100644 src/controllers/molecules/voip/IncomingCallBox.js diff --git a/src/controllers/molecules/voip/CallView.js b/src/components/views/voip/CallView.js similarity index 51% rename from src/controllers/molecules/voip/CallView.js rename to src/components/views/voip/CallView.js index 4dd488c2dc..fbaed1dcd7 100644 --- a/src/controllers/molecules/voip/CallView.js +++ b/src/components/views/voip/CallView.js @@ -13,9 +13,11 @@ 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. */ - +var React = require("react"); var dis = require("../../../dispatcher"); var CallHandler = require("../../../CallHandler"); +var sdk = require('../../../index'); +var MatrixClientPeg = require("../../../MatrixClientPeg"); /* * State vars: @@ -23,14 +25,31 @@ var CallHandler = require("../../../CallHandler"); * * Props: * this.props.room = Room (JS SDK) + * this.props.ConferenceHandler = A Conference Handler implementation + * Must have a function signature: + * getConferenceCallForRoom(roomId: string): MatrixCall */ -module.exports = { +module.exports = React.createClass({ + displayName: 'CallView', componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); + this._trackedRoom = null; if (this.props.room) { - this.showCall(this.props.room.roomId); + this._trackedRoom = this.props.room; + this.showCall(this._trackedRoom.roomId); + } + else { + var call = CallHandler.getAnyActiveCall(); + if (call) { + console.log( + "Global CallView is now tracking active call in room %s", + call.roomId + ); + this._trackedRoom = MatrixClientPeg.get().getRoom(call.roomId); + this.showCall(call.roomId); + } } }, @@ -39,19 +58,22 @@ module.exports = { }, onAction: function(payload) { - // if we were given a room_id to track, don't handle anything else. - if (payload.room_id && this.props.room && - this.props.room.roomId !== payload.room_id) { - return; - } - if (payload.action !== 'call_state') { + // don't filter out payloads for room IDs other than props.room because + // we may be interested in the conf 1:1 room + if (payload.action !== 'call_state' || !payload.room_id) { return; } this.showCall(payload.room_id); }, showCall: function(roomId) { - var call = CallHandler.getCall(roomId); + var call = ( + CallHandler.getCallForRoom(roomId) || + (this.props.ConferenceHandler ? + this.props.ConferenceHandler.getConferenceCallForRoom(roomId) : + null + ) + ); if (call) { call.setLocalVideoElement(this.getVideoView().getLocalVideoElement()); call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement()); @@ -60,13 +82,29 @@ module.exports = { call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement()); } if (call && call.type === "video" && call.state !== 'ended') { - this.getVideoView().getLocalVideoElement().style.display = "initial"; + // if this call is a conf call, don't display local video as the + // conference will have us in it + this.getVideoView().getLocalVideoElement().style.display = ( + call.confUserId ? "none" : "initial" + ); this.getVideoView().getRemoteVideoElement().style.display = "initial"; } else { this.getVideoView().getLocalVideoElement().style.display = "none"; this.getVideoView().getRemoteVideoElement().style.display = "none"; + dis.dispatch({action: 'video_fullscreen', fullscreen: false}); } - } -}; + }, + + getVideoView: function() { + return this.refs.video; + }, + + render: function(){ + var VideoView = sdk.getComponent('voip.VideoView'); + return ( + + ); + } +}); diff --git a/src/components/views/voip/IncomingCallBox.js b/src/components/views/voip/IncomingCallBox.js new file mode 100644 index 0000000000..263bbf543c --- /dev/null +++ b/src/components/views/voip/IncomingCallBox.js @@ -0,0 +1,122 @@ +/* +Copyright 2015 OpenMarket 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. +*/ +var React = require('react'); +var MatrixClientPeg = require('../../../MatrixClientPeg'); +var dis = require("../../../dispatcher"); +var CallHandler = require("../../../CallHandler"); + +module.exports = React.createClass({ + displayName: 'IncomingCallBox', + + componentDidMount: function() { + this.dispatcherRef = dis.register(this.onAction); + }, + + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); + }, + + getInitialState: function() { + return { + incomingCall: null + } + }, + + onAction: function(payload) { + if (payload.action !== 'call_state') { + return; + } + var call = CallHandler.getCall(payload.room_id); + if (!call || call.call_state !== 'ringing') { + this.setState({ + incomingCall: null, + }); + this.getRingAudio().pause(); + return; + } + if (call.call_state === "ringing") { + this.getRingAudio().load(); + this.getRingAudio().play(); + } + else { + this.getRingAudio().pause(); + } + + this.setState({ + incomingCall: call + }); + }, + + onAnswerClick: function() { + dis.dispatch({ + action: 'answer', + room_id: this.state.incomingCall.roomId + }); + }, + + onRejectClick: function() { + dis.dispatch({ + action: 'hangup', + room_id: this.state.incomingCall.roomId + }); + }, + + getRingAudio: function() { + return this.refs.ringAudio; + }, + + render: function() { + // NB: This block MUST have a "key" so React doesn't clobber the elements + // between in-call / not-in-call. + var audioBlock = ( + + ); + + if (!this.state.incomingCall || !this.state.incomingCall.roomId) { + return ( +
    + {audioBlock} +
    + ); + } + var caller = MatrixClientPeg.get().getRoom(this.state.incomingCall.roomId).name; + return ( +
    + {audioBlock} + +
    + Incoming { this.state.incomingCall ? this.state.incomingCall.type : '' } call from { caller } +
    +
    +
    +
    + Decline +
    +
    +
    +
    + Accept +
    +
    +
    +
    + ); + } +}); + diff --git a/src/components/views/voip/VideoView.js b/src/components/views/voip/VideoView.js new file mode 100644 index 0000000000..0a95e0d0c8 --- /dev/null +++ b/src/components/views/voip/VideoView.js @@ -0,0 +1,93 @@ +/* +Copyright 2015 OpenMarket 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. +*/ + +'use strict'; + +var React = require('react'); +var ReactDOM = require('react-dom'); + +var sdk = require('../../../index'); +var dis = require('../../../dispatcher'); + +module.exports = React.createClass({ + displayName: 'VideoView', + + componentWillMount: function() { + dis.register(this.onAction); + }, + + getRemoteVideoElement: function() { + return ReactDOM.findDOMNode(this.refs.remote); + }, + + getRemoteAudioElement: function() { + return this.refs.remoteAudio; + }, + + getLocalVideoElement: function() { + return ReactDOM.findDOMNode(this.refs.local); + }, + + setContainer: function(c) { + this.container = c; + }, + + onAction: function(payload) { + switch (payload.action) { + case 'video_fullscreen': + if (!this.container) { + return; + } + var element = this.container; + if (payload.fullscreen) { + var requestMethod = ( + element.requestFullScreen || + element.webkitRequestFullScreen || + element.mozRequestFullScreen || + element.msRequestFullscreen + ); + requestMethod.call(element); + } + else { + var exitMethod = ( + document.exitFullscreen || + document.mozCancelFullScreen || + document.webkitExitFullscreen || + document.msExitFullscreen + ); + if (exitMethod) { + exitMethod.call(document); + } + } + break; + } + }, + + render: function() { + var VideoFeed = sdk.getComponent('voip.VideoFeed'); + return ( +
    +
    + +
    +
    + +
    +
    + ); + } +}); diff --git a/src/controllers/molecules/voip/IncomingCallBox.js b/src/controllers/molecules/voip/IncomingCallBox.js deleted file mode 100644 index 9ecced56c5..0000000000 --- a/src/controllers/molecules/voip/IncomingCallBox.js +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2015 OpenMarket 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. -*/ - -var dis = require("../../../dispatcher"); -var CallHandler = require("../../../CallHandler"); - -module.exports = { - componentDidMount: function() { - this.dispatcherRef = dis.register(this.onAction); - }, - - componentWillUnmount: function() { - dis.unregister(this.dispatcherRef); - }, - - getInitialState: function() { - return { - incomingCall: null - } - }, - - onAction: function(payload) { - if (payload.action !== 'call_state') { - return; - } - var call = CallHandler.getCall(payload.room_id); - if (!call || call.call_state !== 'ringing') { - this.setState({ - incomingCall: null, - }); - this.getRingAudio().pause(); - return; - } - if (call.call_state === "ringing") { - this.getRingAudio().load(); - this.getRingAudio().play(); - } - else { - this.getRingAudio().pause(); - } - - this.setState({ - incomingCall: call - }); - }, - - onAnswerClick: function() { - dis.dispatch({ - action: 'answer', - room_id: this.state.incomingCall.roomId - }); - }, - onRejectClick: function() { - dis.dispatch({ - action: 'hangup', - room_id: this.state.incomingCall.roomId - }); - } -}; - From fc7707971eae173f4fb1ec627de98e9cb82665ae Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 17:10:36 +0000 Subject: [PATCH 051/152] Move and merge Change Avatar|DisplayName|Password components --- .../views/settings}/ChangeAvatar.js | 57 +++++++- .../views/settings}/ChangeDisplayName.js | 36 ++++- .../views/settings/ChangePassword.js | 135 ++++++++++++++++++ src/controllers/molecules/ChangePassword.js | 76 ---------- 4 files changed, 222 insertions(+), 82 deletions(-) rename src/{controllers/molecules => components/views/settings}/ChangeAvatar.js (54%) rename src/{controllers/molecules => components/views/settings}/ChangeDisplayName.js (65%) create mode 100644 src/components/views/settings/ChangePassword.js delete mode 100644 src/controllers/molecules/ChangePassword.js diff --git a/src/controllers/molecules/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js similarity index 54% rename from src/controllers/molecules/ChangeAvatar.js rename to src/components/views/settings/ChangeAvatar.js index 7e8f959ebf..2ae50a0cae 100644 --- a/src/controllers/molecules/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -15,9 +15,11 @@ limitations under the License. */ var React = require('react'); -var MatrixClientPeg = require("../../MatrixClientPeg"); +var MatrixClientPeg = require("../../../MatrixClientPeg"); +var sdk = require('../../../index'); -module.exports = { +module.exports = React.createClass({ + displayName: 'ChangeAvatar', propTypes: { initialAvatarUrl: React.PropTypes.string, room: React.PropTypes.object, @@ -77,4 +79,53 @@ module.exports = { self.onError(error); }); }, -} + + onFileSelected: function(ev) { + this.avatarSet = true; + this.setAvatarFromFile(ev.target.files[0]); + }, + + onError: function(error) { + this.setState({ + errorText: "Failed to upload profile picture!" + }); + }, + + render: function() { + var RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); + var avatarImg; + // Having just set an avatar we just display that since it will take a little + // time to propagate through to the RoomAvatar. + if (this.props.room && !this.avatarSet) { + avatarImg = ; + } else { + var style = { + maxWidth: 320, + maxHeight: 240, + }; + avatarImg = ; + } + + switch (this.state.phase) { + case this.Phases.Display: + case this.Phases.Error: + return ( +
    +
    + {avatarImg} +
    +
    + Upload new: + + {this.state.errorText} +
    +
    + ); + case this.Phases.Uploading: + var Loader = sdk.getComponent("elements.Spinner"); + return ( + + ); + } + } +}); diff --git a/src/controllers/molecules/ChangeDisplayName.js b/src/components/views/settings/ChangeDisplayName.js similarity index 65% rename from src/controllers/molecules/ChangeDisplayName.js rename to src/components/views/settings/ChangeDisplayName.js index afef82772c..4af413cfbe 100644 --- a/src/controllers/molecules/ChangeDisplayName.js +++ b/src/components/views/settings/ChangeDisplayName.js @@ -16,9 +16,11 @@ limitations under the License. 'use strict'; var React = require('react'); -var MatrixClientPeg = require("../../MatrixClientPeg"); +var sdk = require('../../../index'); +var MatrixClientPeg = require("../../../MatrixClientPeg"); -module.exports = { +module.exports = React.createClass({ + displayName: 'ChangeDisplayName', propTypes: { onFinished: React.PropTypes.func }, @@ -72,4 +74,32 @@ module.exports = { }); }); }, -} + + edit: function() { + this.refs.displayname_edit.edit() + }, + + onValueChanged: function(new_value, shouldSubmit) { + if (shouldSubmit) { + this.changeDisplayname(new_value); + } + }, + + render: function() { + if (this.state.busy) { + var Loader = sdk.getComponent("elements.Spinner"); + return ( + + ); + } else if (this.state.errorString) { + return ( +
    {this.state.errorString}
    + ); + } else { + var EditableText = sdk.getComponent('elements.EditableText'); + return ( + + ); + } + } +}); diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js new file mode 100644 index 0000000000..a6666b7ed1 --- /dev/null +++ b/src/components/views/settings/ChangePassword.js @@ -0,0 +1,135 @@ +/* +Copyright 2015 OpenMarket 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. +*/ + +'use strict'; + +var React = require('react'); +var MatrixClientPeg = require("../../../MatrixClientPeg"); + +module.exports = React.createClass({ + displayName: 'ChangePassword', + propTypes: { + onFinished: React.PropTypes.func, + }, + + Phases: { + Edit: "edit", + Uploading: "uploading", + Error: "error", + Success: "Success" + }, + + getDefaultProps: function() { + return { + onFinished: function() {}, + }; + }, + + getInitialState: function() { + return { + phase: this.Phases.Edit, + errorString: '' + } + }, + + changePassword: function(old_password, new_password) { + var cli = MatrixClientPeg.get(); + + var authDict = { + type: 'm.login.password', + user: cli.credentials.userId, + password: old_password + }; + + this.setState({ + phase: this.Phases.Uploading, + errorString: '', + }) + + var d = cli.setPassword(authDict, new_password); + + var self = this; + d.then(function() { + self.setState({ + phase: self.Phases.Success, + errorString: '', + }) + }, function(err) { + self.setState({ + phase: self.Phases.Error, + errorString: err.toString() + }) + }); + }, + + onClickChange: function() { + var old_password = this.refs.old_input.value; + var new_password = this.refs.new_input.value; + var confirm_password = this.refs.confirm_input.value; + if (new_password != confirm_password) { + this.setState({ + state: this.Phases.Error, + errorString: "Passwords don't match" + }); + } else if (new_password == '' || old_password == '') { + this.setState({ + state: this.Phases.Error, + errorString: "Passwords can't be empty" + }); + } else { + this.changePassword(old_password, new_password); + } + }, + + render: function() { + switch (this.state.phase) { + case this.Phases.Edit: + case this.Phases.Error: + return ( +
    +
    +
    {this.state.errorString}
    +
    +
    +
    +
    +
    + + +
    +
    + ); + case this.Phases.Uploading: + var Loader = sdk.getComponent("elements.Spinner"); + return ( +
    + +
    + ); + case this.Phases.Success: + return ( +
    +
    + Success! +
    +
    + +
    +
    + ) + } + } +}); diff --git a/src/controllers/molecules/ChangePassword.js b/src/controllers/molecules/ChangePassword.js deleted file mode 100644 index 637e133a79..0000000000 --- a/src/controllers/molecules/ChangePassword.js +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright 2015 OpenMarket 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. -*/ - -'use strict'; - -var React = require('react'); -var MatrixClientPeg = require("../../MatrixClientPeg"); - -module.exports = { - propTypes: { - onFinished: React.PropTypes.func, - }, - - Phases: { - Edit: "edit", - Uploading: "uploading", - Error: "error", - Success: "Success" - }, - - getDefaultProps: function() { - return { - onFinished: function() {}, - }; - }, - - getInitialState: function() { - return { - phase: this.Phases.Edit, - errorString: '' - } - }, - - changePassword: function(old_password, new_password) { - var cli = MatrixClientPeg.get(); - - var authDict = { - type: 'm.login.password', - user: cli.credentials.userId, - password: old_password - }; - - this.setState({ - phase: this.Phases.Uploading, - errorString: '', - }) - - var d = cli.setPassword(authDict, new_password); - - var self = this; - d.then(function() { - self.setState({ - phase: self.Phases.Success, - errorString: '', - }) - }, function(err) { - self.setState({ - phase: self.Phases.Error, - errorString: err.toString() - }) - }); - }, -} From 75afc3a7dee0312a3484fbfd45327803cde4550a Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 17:21:08 +0000 Subject: [PATCH 052/152] Move and merge ProgressBar --- .../views/elements}/ProgressBar.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) rename src/{controllers/molecules => components/views/elements}/ProgressBar.js (59%) diff --git a/src/controllers/molecules/ProgressBar.js b/src/components/views/elements/ProgressBar.js similarity index 59% rename from src/controllers/molecules/ProgressBar.js rename to src/components/views/elements/ProgressBar.js index c711650a25..bab6a701dd 100644 --- a/src/controllers/molecules/ProgressBar.js +++ b/src/components/views/elements/ProgressBar.js @@ -18,9 +18,21 @@ limitations under the License. var React = require('react'); -module.exports = { +module.exports = React.createClass({ + displayName: 'ProgressBar', propTypes: { value: React.PropTypes.number, max: React.PropTypes.number }, -}; + + render: function() { + // Would use an HTML5 progress tag but if that doesn't animate if you + // use the HTML attributes rather than styles + var progressStyle = { + width: ((this.props.value / this.props.max) * 100)+"%" + }; + return ( +
    + ); + } +}); \ No newline at end of file From 206c45e703de64f4d6f550600d53a0d3c1e04138 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 26 Nov 2015 17:31:10 +0000 Subject: [PATCH 053/152] Move and merge MessageComposer --- .../views/messages}/MessageComposer.js | 81 ++++++++++++++++--- 1 file changed, 72 insertions(+), 9 deletions(-) rename src/{controllers/molecules => components/views/messages}/MessageComposer.js (85%) diff --git a/src/controllers/molecules/MessageComposer.js b/src/components/views/messages/MessageComposer.js similarity index 85% rename from src/controllers/molecules/MessageComposer.js rename to src/components/views/messages/MessageComposer.js index 237c710395..869e9f7614 100644 --- a/src/controllers/molecules/MessageComposer.js +++ b/src/components/views/messages/MessageComposer.js @@ -13,7 +13,7 @@ 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. */ - +var React = require("react"); var marked = require("marked"); marked.setOptions({ renderer: new marked.Renderer(), @@ -25,12 +25,12 @@ marked.setOptions({ smartLists: true, smartypants: false }); -var MatrixClientPeg = require("../../MatrixClientPeg"); -var SlashCommands = require("../../SlashCommands"); -var Modal = require("../../Modal"); -var sdk = require('../../index'); +var MatrixClientPeg = require("../../../MatrixClientPeg"); +var SlashCommands = require("../../../SlashCommands"); +var Modal = require("../../../Modal"); +var sdk = require('../../../index'); -var dis = require("../../dispatcher"); +var dis = require("../../../dispatcher"); var KeyCode = { ENTER: 13, BACKSPACE: 8, @@ -58,10 +58,11 @@ function mdownToHtml(mdown) { return html; } -module.exports = { - oldScrollHeight: 0, +module.exports = React.createClass({ + displayName: 'MessageComposer', componentWillMount: function() { + this.oldScrollHeight = 0; this.markdownEnabled = MARKDOWN_ENABLED; this.tabStruct = { completing: false, @@ -501,7 +502,69 @@ module.exports = { clearTimeout(this.typingTimeout); this.typingTimeout = null; } + }, + onInputClick: function(ev) { + this.refs.textarea.focus(); + }, + + onUploadClick: function(ev) { + this.refs.uploadInput.click(); + }, + + onUploadFileSelected: function(ev) { + var files = ev.target.files; + // MessageComposer shouldn't have to rely on it's parent passing in a callback to upload a file + if (files && files.length > 0) { + this.props.uploadFile(files[0]); + } + this.refs.uploadInput.value = null; + }, + + onCallClick: function(ev) { + dis.dispatch({ + action: 'place_call', + type: ev.shiftKey ? "screensharing" : "video", + room_id: this.props.room.roomId + }); + }, + + onVoiceCallClick: function(ev) { + dis.dispatch({ + action: 'place_call', + type: 'voice', + room_id: this.props.room.roomId + }); + }, + + render: function() { + var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); + var uploadInputStyle = {display: 'none'}; + var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + return ( +
    +
    +
    +
    + +
    +
    +
    + cancel_button =
    Cancel
    + save_button =
    Save Changes
    + } else { + // + name = +
    +
    { this.props.room.name }
    +
    + +
    +
    + if (topic) topic_el =
    { topic.getContent().topic }
    ; + } + + var roomAvatar = null; + if (this.props.room) { + roomAvatar = ( + + ); + } + + var zoom_button, video_button, voice_button; + if (activeCall) { + if (activeCall.type == "video") { + zoom_button = ( +
    + Fullscreen +
    + ); + } + video_button = +
    + Video call +
    ; + voice_button = +
    + VoIP call +
    ; + } + + header = +
    +
    +
    + { roomAvatar } +
    +
    + { name } + { topic_el } +
    +
    + {call_buttons} + {cancel_button} + {save_button} +
    + { video_button } + { voice_button } + { zoom_button } +
    + Search +
    +
    +
    + } + + return ( +
    + { header } +
    + ); + }, +}); diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js new file mode 100644 index 0000000000..eb9bfd90c8 --- /dev/null +++ b/src/components/views/rooms/RoomSettings.js @@ -0,0 +1,237 @@ +/* +Copyright 2015 OpenMarket 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. +*/ + +var React = require('react'); +var MatrixClientPeg = require('../../../MatrixClientPeg'); +var sdk = require('../../../index'); + +module.exports = React.createClass({ + displayName: 'RoomSettings', + + propTypes: { + room: React.PropTypes.object.isRequired, + }, + + getInitialState: function() { + return { + power_levels_changed: false + }; + }, + + getTopic: function() { + return this.refs.topic.value; + }, + + getJoinRules: function() { + return this.refs.is_private.checked ? "invite" : "public"; + }, + + getHistoryVisibility: function() { + return this.refs.share_history.checked ? "shared" : "invited"; + }, + + getPowerLevels: function() { + if (!this.state.power_levels_changed) return undefined; + + var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', ''); + power_levels = power_levels.getContent(); + + var new_power_levels = { + ban: parseInt(this.refs.ban.value), + kick: parseInt(this.refs.kick.value), + redact: parseInt(this.refs.redact.value), + invite: parseInt(this.refs.invite.value), + events_default: parseInt(this.refs.events_default.value), + state_default: parseInt(this.refs.state_default.value), + users_default: parseInt(this.refs.users_default.value), + users: power_levels.users, + events: power_levels.events, + }; + + return new_power_levels; + }, + + onPowerLevelsChanged: function() { + this.setState({ + power_levels_changed: true + }); + }, + + render: function() { + var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); + + var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); + if (topic) topic = topic.getContent().topic; + + var join_rule = this.props.room.currentState.getStateEvents('m.room.join_rules', ''); + if (join_rule) join_rule = join_rule.getContent().join_rule; + + var history_visibility = this.props.room.currentState.getStateEvents('m.room.history_visibility', ''); + if (history_visibility) history_visibility = history_visibility.getContent().history_visibility; + + var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', ''); + + var events_levels = power_levels.events || {}; + + if (power_levels) { + power_levels = power_levels.getContent(); + + var ban_level = parseInt(power_levels.ban); + var kick_level = parseInt(power_levels.kick); + var redact_level = parseInt(power_levels.redact); + var invite_level = parseInt(power_levels.invite || 0); + var send_level = parseInt(power_levels.events_default || 0); + var state_level = parseInt(power_levels.state_default || 0); + var default_user_level = parseInt(power_levels.users_default || 0); + + if (power_levels.ban == undefined) ban_level = 50; + if (power_levels.kick == undefined) kick_level = 50; + if (power_levels.redact == undefined) redact_level = 50; + + var user_levels = power_levels.users || {}; + + var user_id = MatrixClientPeg.get().credentials.userId; + + var current_user_level = user_levels[user_id]; + if (current_user_level == undefined) current_user_level = default_user_level; + + var power_level_level = events_levels["m.room.power_levels"]; + if (power_level_level == undefined) { + power_level_level = state_level; + } + + var can_change_levels = current_user_level >= power_level_level; + } else { + var ban_level = 50; + var kick_level = 50; + var redact_level = 50; + var invite_level = 0; + var send_level = 0; + var state_level = 0; + var default_user_level = 0; + + var user_levels = []; + var events_levels = []; + + var current_user_level = 0; + + var power_level_level = 0; + + var can_change_levels = false; + } + + var room_avatar_level = parseInt(power_levels.state_default || 0); + if (events_levels['m.room.avatar'] !== undefined) { + room_avatar_level = events_levels['m.room.avatar']; + } + var can_set_room_avatar = current_user_level >= room_avatar_level; + + var change_avatar; + if (can_set_room_avatar) { + change_avatar =
    +

    Room Icon

    + +
    ; + } + + var banned = this.props.room.getMembersWithMembership("ban"); + + return ( +
    +