diff --git a/src/CallHandler.js b/src/CallHandler.js index d5bb3a5b4d..187449924f 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -56,12 +56,12 @@ var Modal = require('./Modal'); var sdk = require('./index'); var Matrix = require("matrix-js-sdk"); var dis = require("./dispatcher"); -var Modulator = require("./Modulator"); global.mxCalls = { //room_id: MatrixCall }; var calls = global.mxCalls; +var ConferenceHandler = null; function play(audioId) { // TODO: Attach an invisible element for this instead @@ -227,7 +227,7 @@ function _onAction(payload) { break; case 'place_conference_call': console.log("Place conference call in %s", payload.room_id); - if (!Modulator.hasConferenceHandler()) { + if (!ConferenceHandler) { Modal.createDialog(ErrorDialog, { description: "Conference calls are not supported in this client" }); @@ -239,7 +239,6 @@ function _onAction(payload) { }); } else { - var ConferenceHandler = Modulator.getConferenceHandler(); ConferenceHandler.createNewMatrixCall( MatrixClientPeg.get(), payload.room_id ).done(function(call) { @@ -295,8 +294,7 @@ var callHandler = { var call = module.exports.getCall(roomId); if (call) return call; - if (Modulator.hasConferenceHandler()) { - var ConferenceHandler = Modulator.getConferenceHandler(); + if (ConferenceHandler) { call = ConferenceHandler.getConferenceCallForRoom(roomId); } if (call) return call; @@ -317,6 +315,10 @@ var callHandler = { } } return null; + }, + + setConferenceHandler: function(confHandler) { + ConferenceHandler = confHandler; } }; // Only things in here which actually need to be global are the diff --git a/src/DateUtils.js b/src/DateUtils.js new file mode 100644 index 0000000000..fe363586ab --- /dev/null +++ b/src/DateUtils.js @@ -0,0 +1,45 @@ +/* +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 days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; +var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + +module.exports = { + formatDate: function(date) { + // date.toLocaleTimeString is completely system dependent. + // just go 24h for now + function pad(n) { + return (n < 10 ? '0' : '') + n; + } + + var now = new Date(); + if (date.toDateString() === now.toDateString()) { + return pad(date.getHours()) + ':' + pad(date.getMinutes()); + } + else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { + return days[date.getDay()] + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); + } + else if (now.getFullYear() === date.getFullYear()) { + return days[date.getDay()] + ", " + months[date.getMonth()] + " " + (date.getDay()+1) + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); + } + else { + return days[date.getDay()] + ", " + months[date.getMonth()] + " " + (date.getDay()+1) + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); + } + } +} + diff --git a/src/Modulator.js b/src/Modulator.js deleted file mode 100644 index 72fcc14d89..0000000000 --- a/src/Modulator.js +++ /dev/null @@ -1,111 +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. -*/ - -/** - * The modulator stores 'modules': classes that provide - * functionality and are not React UI components. - * Modules go into named slots, eg. a conference calling - * module goes into the 'conference' slot. If two modules - * that use the same slot are loaded, this is considered - * to be an error. - * - * There are some module slots that the react SDK knows - * about natively: these have explicit getters. - * - * A module must define: - * - 'slot' (string): The name of the slot it goes into - * and may define: - * - 'start' (function): Called on module load - * - 'stop' (function): Called on module unload - */ -class Modulator { - constructor() { - this.modules = {}; - } - - getModule(name) { - var m = this.getModuleOrNull(name); - if (m === null) { - throw new Error("No such module: "+name); - } - return m; - } - - getModuleOrNull(name) { - if (this.modules == {}) { - throw new Error( - "Attempted to get a module before a skin has been loaded."+ - "This is probably because a component has called "+ - "getModule at the root level." - ); - } - var module = this.modules[name]; - if (module) { - return module; - } - return null; - } - - hasModule(name) { - var m = this.getModuleOrNull(name); - return m !== null; - } - - loadModule(moduleObject) { - if (!moduleObject.slot) { - throw new Error( - "Attempted to load something that is not a module "+ - "(does not have a slot name)" - ); - } - if (this.modules[moduleObject.slot] !== undefined) { - throw new Error( - "Cannot load module: slot '"+moduleObject.slot+"' is occupied!" - ); - } - this.modules[moduleObject.slot] = moduleObject; - } - - reset() { - var keys = Object.keys(this.modules); - for (var i = 0; i < keys.length; ++i) { - var k = keys[i]; - var m = this.modules[k]; - - if (m.stop) m.stop(); - } - this.modules = {}; - } - - // *********** - // known slots - // *********** - - getConferenceHandler() { - return this.getModule('conference'); - } - - hasConferenceHandler() { - return this.hasModule('conference'); - } -} - -// Define one Modulator globally (see Skinner.js) -if (global.mxModulator === undefined) { - global.mxModulator = new Modulator(); -} -module.exports = global.mxModulator; - diff --git a/src/Notifier.js b/src/Notifier.js index 66d0fdc85a..66e96fb15c 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -72,7 +72,7 @@ module.exports = { { "body": msg, "icon": avatarUrl, - "tag": "vector" + "tag": "matrixreactsdk" } ); diff --git a/src/controllers/pages/MatrixChat.js b/src/components/structures/MatrixChat.js similarity index 69% rename from src/controllers/pages/MatrixChat.js rename to src/components/structures/MatrixChat.js index 8d48955a0c..5d25450200 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -13,21 +13,36 @@ 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 Matrix = require("matrix-js-sdk"); +var url = require('url'); var MatrixClientPeg = require("../../MatrixClientPeg"); var Notifier = require("../../Notifier"); +var ContextualMenu = require("../../ContextualMenu"); var RoomListSorter = require("../../RoomListSorter"); var UserActivity = require("../../UserActivity"); var Presence = require("../../Presence"); var dis = require("../../dispatcher"); +var Login = require("./login/Login"); +var Registration = require("./login/Registration"); +var PostRegistration = require("./login/PostRegistration"); + var sdk = require('../../index'); var MatrixTools = require('../../MatrixTools'); var linkifyMatrix = require("../../linkify-matrix"); -var url = require('url'); +module.exports = React.createClass({ + displayName: 'MatrixChat', + + propTypes: { + config: React.PropTypes.object.isRequired, + ConferenceHandler: React.PropTypes.any, + onNewScreen: React.PropTypes.func, + registrationUrl: React.PropTypes.string + }, -module.exports = { PageTypes: { RoomView: "room_view", UserSettings: "user_settings", @@ -45,6 +60,7 @@ module.exports = { collapse_lhs: false, collapse_rhs: false, ready: false, + width: 10000 }; if (s.logged_in) { if (MatrixClientPeg.get().getRooms().length) { @@ -80,12 +96,16 @@ module.exports = { if (this.onUserClick) { linkifyMatrix.onUserClick = this.onUserClick; } + + window.addEventListener('resize', this.handleResize); + this.handleResize(); }, componentWillUnmount: function() { dis.unregister(this.dispatcherRef); document.removeEventListener("keydown", this.onKeyDown); window.removeEventListener("focus", this.onFocus); + window.removeEventListener('resize', this.handleResize); }, componentDidUpdate: function() { @@ -467,5 +487,181 @@ module.exports = { if (this.props.onNewScreen) { this.props.onNewScreen(screen); } + }, + + onAliasClick: function(event, alias) { + event.preventDefault(); + dis.dispatch({action: 'view_room_alias', room_alias: alias}); + }, + + onUserClick: function(event, userId) { + event.preventDefault(); + var MemberInfo = sdk.getComponent('rooms.MemberInfo'); + var member = new Matrix.RoomMember(null, userId); + ContextualMenu.createMenu(MemberInfo, { + member: member, + right: window.innerWidth - event.pageX, + top: event.pageY + }); + }, + + onLogoutClick: function(event) { + dis.dispatch({ + action: 'logout' + }); + event.stopPropagation(); + event.preventDefault(); + }, + + handleResize: function(e) { + var hideLhsThreshold = 1000; + var showLhsThreshold = 1000; + var hideRhsThreshold = 820; + var showRhsThreshold = 820; + + if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) { + dis.dispatch({ action: 'hide_left_panel' }); + } + if (this.state.width <= showLhsThreshold && window.innerWidth > showLhsThreshold) { + dis.dispatch({ action: 'show_left_panel' }); + } + if (this.state.width > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) { + dis.dispatch({ action: 'hide_right_panel' }); + } + if (this.state.width <= showRhsThreshold && window.innerWidth > showRhsThreshold) { + dis.dispatch({ action: 'show_right_panel' }); + } + + this.setState({width: window.innerWidth}); + }, + + onRoomCreated: function(room_id) { + dis.dispatch({ + action: "view_room", + room_id: room_id, + }); + }, + + onRegisterClick: function() { + this.showScreen("register"); + }, + + onLoginClick: function() { + this.showScreen("login"); + }, + + onRegistered: function(credentials) { + this.onLoggedIn(credentials); + // do post-registration stuff + this.showScreen("post_registration"); + }, + + onFinishPostRegistration: function() { + // Don't confuse this with "PageType" which is the middle window to show + this.setState({ + screen: undefined + }); + this.showScreen("settings"); + }, + + render: function() { + var LeftPanel = sdk.getComponent('organisms.LeftPanel'); + var RoomView = sdk.getComponent('structures.RoomView'); + var RightPanel = sdk.getComponent('organisms.RightPanel'); + var UserSettings = sdk.getComponent('structures.UserSettings'); + var CreateRoom = sdk.getComponent('structures.CreateRoom'); + var RoomDirectory = sdk.getComponent('organisms.RoomDirectory'); + var MatrixToolbar = sdk.getComponent('molecules.MatrixToolbar'); + + // needs to be before normal PageTypes as you are logged in technically + if (this.state.screen == 'post_registration') { + return ( + + ); + } + else if (this.state.logged_in && this.state.ready) { + var page_element; + var right_panel = ""; + + switch (this.state.page_type) { + case this.PageTypes.RoomView: + page_element = ( + + ); + right_panel = + break; + case this.PageTypes.UserSettings: + page_element = + right_panel = + break; + case this.PageTypes.CreateRoom: + page_element = + right_panel = + break; + case this.PageTypes.RoomDirectory: + page_element = + right_panel = + break; + } + + // TODO: Fix duplication here and do conditionals like we do above + if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) { + return ( +
+ +
+ +
+ {page_element} +
+ {right_panel} +
+
+ ); + } + else { + return ( +
+ +
+ {page_element} +
+ {right_panel} +
+ ); + } + } else if (this.state.logged_in) { + var Spinner = sdk.getComponent('elements.Spinner'); + return ( +
+ + Logout +
+ ); + } else if (this.state.screen == 'register') { + return ( + + ); + } else { + return ( + + ); + } } -}; +}); diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js new file mode 100644 index 0000000000..4fffcaf7cf --- /dev/null +++ b/src/components/structures/login/Login.js @@ -0,0 +1,199 @@ +/* +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 Signup = require("../../../Signup"); +var PasswordLogin = require("../../views/login/PasswordLogin"); +var CasLogin = require("../../views/login/CasLogin"); +var ServerConfig = require("../../views/login/ServerConfig"); + +/** + * A wire component which glues together login UI components and Signup logic + */ +module.exports = React.createClass({displayName: 'Login', + propTypes: { + onLoggedIn: React.PropTypes.func.isRequired, + homeserverUrl: React.PropTypes.string, + identityServerUrl: React.PropTypes.string, + // login shouldn't know or care how registration is done. + onRegisterClick: React.PropTypes.func.isRequired + }, + + getDefaultProps: function() { + return { + homeserverUrl: 'https://matrix.org/', + identityServerUrl: 'https://vector.im' + }; + }, + + getInitialState: function() { + return { + busy: false, + errorText: null, + enteredHomeserverUrl: this.props.homeserverUrl, + enteredIdentityServerUrl: this.props.identityServerUrl + }; + }, + + componentWillMount: function() { + this._initLoginLogic(); + }, + + onPasswordLogin: function(username, password) { + var self = this; + self.setState({ + busy: true + }); + + this._loginLogic.loginViaPassword(username, password).then(function(data) { + self.props.onLoggedIn(data); + }, function(error) { + self._setErrorTextFromError(error); + }).finally(function() { + self.setState({ + busy: false + }); + }); + }, + + onHsUrlChanged: function(newHsUrl) { + this._initLoginLogic(newHsUrl); + }, + + onIsUrlChanged: function(newIsUrl) { + this._initLoginLogic(null, newIsUrl); + }, + + _initLoginLogic: function(hsUrl, isUrl) { + var self = this; + hsUrl = hsUrl || this.state.enteredHomeserverUrl; + isUrl = isUrl || this.state.enteredIdentityServerUrl; + + var loginLogic = new Signup.Login(hsUrl, isUrl); + this._loginLogic = loginLogic; + + loginLogic.getFlows().then(function(flows) { + // old behaviour was to always use the first flow without presenting + // options. This works in most cases (we don't have a UI for multiple + // logins so let's skip that for now). + loginLogic.chooseFlow(0); + }, function(err) { + self._setErrorTextFromError(err); + }).finally(function() { + self.setState({ + busy: false + }); + }); + + this.setState({ + enteredHomeserverUrl: hsUrl, + enteredIdentityServerUrl: isUrl, + busy: true, + errorText: null // reset err messages + }); + }, + + _getCurrentFlowStep: function() { + return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null + }, + + _setErrorTextFromError: function(err) { + if (err.friendlyText) { + this.setState({ + errorText: err.friendlyText + }); + return; + } + + var errCode = err.errcode; + if (!errCode && err.httpStatus) { + errCode = "HTTP " + err.httpStatus; + } + this.setState({ + errorText: ( + "Error: Problem communicating with the given homeserver " + + (errCode ? "(" + errCode + ")" : "") + ) + }); + }, + + componentForStep: function(step) { + switch (step) { + case 'm.login.password': + return ( + + ); + case 'm.login.cas': + return ( + + ); + default: + if (!step) { + return; + } + return ( +
+ Sorry, this homeserver is using a login which is not + recognised by Vector ({step}) +
+ ); + } + }, + + render: function() { + var Loader = sdk.getComponent("elements.Spinner"); + var loader = this.state.busy ?
: null; + + return ( +
+
+
+ vector +
+
+

Sign in

+ { this.componentForStep(this._getCurrentFlowStep()) } + +
+ { loader } + { this.state.errorText } +
+ + Create a new account + +
+
+ blog  ·   + twitter  ·   + github  ·   + powered by Matrix +
+
+
+
+ ); + } +}); diff --git a/src/components/structures/login/PostRegistration.js b/src/components/structures/login/PostRegistration.js new file mode 100644 index 0000000000..5af8d37dd8 --- /dev/null +++ b/src/components/structures/login/PostRegistration.js @@ -0,0 +1,80 @@ +/* +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 sdk = require('../../../index'); +var MatrixClientPeg = require('../../../MatrixClientPeg'); + +module.exports = React.createClass({ + displayName: 'PostRegistration', + + propTypes: { + onComplete: React.PropTypes.func.isRequired + }, + + getInitialState: function() { + return { + avatarUrl: null, + errorString: null, + busy: false + }; + }, + + componentWillMount: function() { + // There is some assymetry between ChangeDisplayName and ChangeAvatar, + // as ChangeDisplayName will auto-get the name but ChangeAvatar expects + // the URL to be passed to you (because it's also used for room avatars). + var cli = MatrixClientPeg.get(); + this.setState({busy: true}); + var self = this; + cli.getProfileInfo(cli.credentials.userId).done(function(result) { + self.setState({ + avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url), + busy: false + }); + }, function(error) { + self.setState({ + errorString: "Failed to fetch avatar URL", + busy: false + }); + }); + }, + + render: function() { + var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName'); + var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); + return ( +
+
+
+ vector +
+
+ Set a display name: + + Upload an avatar: + + + {this.state.errorString} +
+
+
+ ); + } +}); diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js new file mode 100644 index 0000000000..0cb1a87752 --- /dev/null +++ b/src/components/structures/login/Registration.js @@ -0,0 +1,248 @@ +/* +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 sdk = require('../../../index'); +var MatrixClientPeg = require('../../../MatrixClientPeg'); +var dis = require('../../../dispatcher'); +var Signup = require("../../../Signup"); +var ServerConfig = require("../../views/login/ServerConfig"); +var RegistrationForm = require("../../views/login/RegistrationForm"); +var CaptchaForm = require("../../views/login/CaptchaForm"); + +var MIN_PASSWORD_LENGTH = 6; + +module.exports = React.createClass({ + displayName: 'Registration', + + propTypes: { + onLoggedIn: React.PropTypes.func.isRequired, + clientSecret: React.PropTypes.string, + sessionId: React.PropTypes.string, + registrationUrl: React.PropTypes.string, + idSid: React.PropTypes.string, + hsUrl: React.PropTypes.string, + isUrl: React.PropTypes.string, + // registration shouldn't know or care how login is done. + onLoginClick: React.PropTypes.func.isRequired + }, + + getInitialState: function() { + return { + busy: false, + errorText: null, + enteredHomeserverUrl: this.props.hsUrl, + enteredIdentityServerUrl: this.props.isUrl + }; + }, + + componentWillMount: function() { + this.dispatcherRef = dis.register(this.onAction); + // attach this to the instance rather than this.state since it isn't UI + this.registerLogic = new Signup.Register( + this.props.hsUrl, this.props.isUrl + ); + this.registerLogic.setClientSecret(this.props.clientSecret); + this.registerLogic.setSessionId(this.props.sessionId); + this.registerLogic.setRegistrationUrl(this.props.registrationUrl); + this.registerLogic.setIdSid(this.props.idSid); + this.registerLogic.recheckState(); + }, + + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); + }, + + componentDidMount: function() { + // may have already done an HTTP hit (e.g. redirect from an email) so + // check for any pending response + var promise = this.registerLogic.getPromise(); + if (promise) { + this.onProcessingRegistration(promise); + } + }, + + onHsUrlChanged: function(newHsUrl) { + this.registerLogic.setHomeserverUrl(newHsUrl); + }, + + onIsUrlChanged: function(newIsUrl) { + this.registerLogic.setIdentityServerUrl(newIsUrl); + }, + + onAction: function(payload) { + if (payload.action !== "registration_step_update") { + return; + } + this.forceUpdate(); // registration state has changed. + }, + + onFormSubmit: function(formVals) { + var self = this; + this.setState({ + errorText: "", + busy: true + }); + this.onProcessingRegistration(this.registerLogic.register(formVals)); + }, + + // Promise is resolved when the registration process is FULLY COMPLETE + onProcessingRegistration: function(promise) { + var self = this; + promise.done(function(response) { + if (!response || !response.access_token) { + console.warn( + "FIXME: Register fulfilled without a final response, " + + "did you break the promise chain?" + ); + // no matter, we'll grab it direct + response = self.registerLogic.getCredentials(); + } + if (!response || !response.user_id || !response.access_token) { + console.error("Final response is missing keys."); + self.setState({ + errorText: "There was a problem processing the response." + }); + return; + } + self.props.onLoggedIn({ + userId: response.user_id, + homeserverUrl: self.registerLogic.getHomeserverUrl(), + identityServerUrl: self.registerLogic.getIdentityServerUrl(), + accessToken: response.access_token + }); + self.setState({ + busy: false + }); + }, function(err) { + if (err.message) { + self.setState({ + errorText: err.message + }); + } + self.setState({ + busy: false + }); + console.log(err); + }); + }, + + onFormValidationFailed: function(errCode) { + var errMsg; + switch (errCode) { + case "RegistrationForm.ERR_PASSWORD_MISSING": + errMsg = "Missing password."; + break; + case "RegistrationForm.ERR_PASSWORD_MISMATCH": + errMsg = "Passwords don't match."; + break; + case "RegistrationForm.ERR_PASSWORD_LENGTH": + errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`; + break; + default: + console.error("Unknown error code: %s", errCode); + errMsg = "An unknown error occurred."; + break; + } + this.setState({ + errorText: errMsg + }); + }, + + onCaptchaLoaded: function(divIdName) { + this.registerLogic.tellStage("m.login.recaptcha", { + divId: divIdName + }); + this.setState({ + busy: false // requires user input + }); + }, + + _getRegisterContentJsx: function() { + var currStep = this.registerLogic.getStep(); + var registerStep; + switch (currStep) { + case "Register.COMPLETE": + break; // NOP + case "Register.START": + case "Register.STEP_m.login.dummy": + registerStep = ( + + ); + break; + case "Register.STEP_m.login.email.identity": + registerStep = ( +
+ Please check your email to continue registration. +
+ ); + break; + case "Register.STEP_m.login.recaptcha": + registerStep = ( + + ); + break; + default: + console.error("Unknown register state: %s", currStep); + break; + } + var busySpinner; + if (this.state.busy) { + var Spinner = sdk.getComponent("elements.Spinner"); + busySpinner = ( + + ); + } + return ( +
+

Create an account

+ {registerStep} +
{this.state.errorText}
+ {busySpinner} + + + I already have an account + +
+ ); + }, + + render: function() { + return ( +
+
+
+ vector +
+ {this._getRegisterContentJsx()} +
+
+ ); + } +}); diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js new file mode 100644 index 0000000000..5c4887955b --- /dev/null +++ b/src/components/views/login/RegistrationForm.js @@ -0,0 +1,126 @@ +/* +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 sdk = require('../../../index'); + +/** + * A pure UI component which displays a registration form. + */ +module.exports = React.createClass({ + displayName: 'RegistrationForm', + + propTypes: { + defaultEmail: React.PropTypes.string, + defaultUsername: React.PropTypes.string, + showEmail: React.PropTypes.bool, + minPasswordLength: React.PropTypes.number, + onError: React.PropTypes.func, + onRegisterClick: React.PropTypes.func // onRegisterClick(Object) => ?Promise + }, + + getDefaultProps: function() { + return { + showEmail: false, + minPasswordLength: 6, + onError: function(e) { + console.error(e); + } + }; + }, + + getInitialState: function() { + return { + email: this.props.defaultEmail, + username: this.props.defaultUsername, + password: null, + passwordConfirm: null + }; + }, + + onSubmit: function(ev) { + ev.preventDefault(); + + var pwd1 = this.refs.password.value.trim(); + var pwd2 = this.refs.passwordConfirm.value.trim() + + var errCode; + if (!pwd1 || !pwd2) { + errCode = "RegistrationForm.ERR_PASSWORD_MISSING"; + } + else if (pwd1 !== pwd2) { + errCode = "RegistrationForm.ERR_PASSWORD_MISMATCH"; + } + else if (pwd1.length < this.props.minPasswordLength) { + errCode = "RegistrationForm.ERR_PASSWORD_LENGTH"; + } + if (errCode) { + this.props.onError(errCode); + return; + } + + var promise = this.props.onRegisterClick({ + username: this.refs.username.value.trim(), + password: pwd1, + email: this.refs.email.value.trim() + }); + + if (promise) { + ev.target.disabled = true; + promise.finally(function() { + ev.target.disabled = false; + }); + } + }, + + render: function() { + var emailSection, registerButton; + if (this.props.showEmail) { + emailSection = ( + + ); + } + if (this.props.onRegisterClick) { + registerButton = ( + + ); + } + + return ( +
+
+ {emailSection} +
+ +
+ +
+ +
+ {registerButton} +
+
+ ); + } +}); diff --git a/src/components/views/login/ServerConfig.js b/src/components/views/login/ServerConfig.js new file mode 100644 index 0000000000..39f9dc4594 --- /dev/null +++ b/src/components/views/login/ServerConfig.js @@ -0,0 +1,161 @@ +/* +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 Modal = require('../../../Modal'); +var sdk = require('../../../index'); + +/** + * A pure UI component which displays the HS and IS to use. + */ +module.exports = React.createClass({ + displayName: 'ServerConfig', + + propTypes: { + onHsUrlChanged: React.PropTypes.func, + onIsUrlChanged: React.PropTypes.func, + defaultHsUrl: React.PropTypes.string, + defaultIsUrl: React.PropTypes.string, + withToggleButton: React.PropTypes.bool, + delayTimeMs: React.PropTypes.number // time to wait before invoking onChanged + }, + + getDefaultProps: function() { + return { + onHsUrlChanged: function() {}, + onIsUrlChanged: function() {}, + withToggleButton: false, + delayTimeMs: 0 + }; + }, + + 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, + // no toggle button = show, toggle button = hide + configVisible: !this.props.withToggleButton + } + }, + + onHomeserverChanged: function(ev) { + this.setState({hs_url: ev.target.value}, function() { + this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() { + this.props.onHsUrlChanged(this.state.hs_url); + }); + }); + }, + + onIdentityServerChanged: function(ev) { + this.setState({is_url: ev.target.value}, function() { + this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() { + this.props.onIsUrlChanged(this.state.is_url); + }); + }); + }, + + _waitThenInvoke: function(existingTimeoutId, fn) { + if (existingTimeoutId) { + clearTimeout(existingTimeoutId); + } + return setTimeout(fn.bind(this), this.props.delayTimeMs); + }, + + getHsUrl: function() { + return this.state.hs_url; + }, + + getIsUrl: function() { + return this.state.is_url; + }, + + onServerConfigVisibleChange: function(ev) { + this.setState({ + configVisible: ev.target.checked + }); + }, + + showHelpPopup: function() { + var ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createDialog(ErrorDialog, { + title: 'Custom Server Options', + description: + You can use the custom server options to log into other Matrix + servers by specifying a different Home server URL. +
+ This allows you to use Vector with an existing Matrix account on + a different Home server. +
+
+ You can also set a custom Identity server but this will affect + people's ability to find you if you use a server in a group other + than the main Matrix.org group. +
, + button: "Dismiss", + focus: true + }); + }, + + render: function() { + var serverConfigStyle = {}; + serverConfigStyle.display = this.state.configVisible ? 'block' : 'none'; + + var toggleButton; + if (this.props.withToggleButton) { + toggleButton = ( +
+ + +
+ ); + } + + return ( +
+ {toggleButton} +
+
+ + + + + + What does this mean? + +
+
+
+ ); + } +}); diff --git a/src/index.js b/src/index.js index 5412c051d6..a631538622 100644 --- a/src/index.js +++ b/src/index.js @@ -15,16 +15,11 @@ limitations under the License. */ var Skinner = require('./Skinner'); -var Modulator = require('./Modulator'); module.exports.loadSkin = function(skinObject) { Skinner.load(skinObject); }; -module.exports.loadModule = function(moduleObject) { - Modulator.loadModule(moduleObject); -}; - module.exports.resetSkin = function() { Skinner.reset(); };