diff --git a/.gitignore b/.gitignore index 1917768116..491fc35975 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ node_modules -build -bundle.css -bundle.js +lib diff --git a/.npmignore b/.npmignore index 1ce5400d43..598224712f 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,3 @@ example examples -build/.module-cache +.module-cache diff --git a/README.md b/README.md index 499dbee4a5..2b38265713 100644 --- a/README.md +++ b/README.md @@ -2,58 +2,29 @@ matrix-react-sdk ================ This is a react-based SDK for inserting a Matrix chat/voip client into a web page. -It provides reusable and customisable UI components backed by the matrix-js-sdk. -Getting started with the trivial example -======================================== +This package provides the logic and 'controller' parts for the UI components. This +forms one part of a complete matrix client, but it not useable in isolation. It +must be used from a 'skin'. A skin provides: + * The HTML for the UI components (in the form of React `render` methods) + * The CSS for this HTML + * The containing application + * Zero or more 'modules' containing non-UI functionality -1. Install or update `node.js` so that your `npm` is at least at version `2.0.0` -2. Clone the repo: `git clone https://github.com/matrix-org/matrix-react-sdk.git` -3. Switch to the SDK directory: `cd matrix-react-sdk` -4. Install the prerequisites: `npm install` -5. Switch to the example directory: `cd examples/trivial` -6. Install the example app prerequisites: `npm install` -7. Build the example and start a server: `npm start` +Skins are modules are exported from such a package in the `lib` directory. +`lib/skins` contains one directory per-skin, named after the skin, and the +`modules` directory contains modules as their javascript files. -Now open http://127.0.0.1:8080/ in your browser to see your newly built -Matrix client. +A basic skin is provided in the matrix-react-skin package. This also contains +a minimal application that instantiates the basic skin making a working matrix +client. -Using the example app for development -===================================== - -To work on the CSS and Javascript and have the bundle files update as you -change the source files, you'll need to do two extra things: - -1. Link the react sdk package into the example: - `cd matrix-react-sdk/examples/trivial; npm link ../../` -2. Start a watcher for the CSS files: - `cd matrix-react-sdk; npm run start:css` - -Note that you may need to restart the CSS builder if you add a new file. Note -that `npm start` builds debug versions of the javascript and CSS, which are -much larger than the production versions build by the `npm run build` commands. - -IMPORTANT: If you customise components in your application (and hence require -react from your app) you must be sure to: - -1. Make your app depend on react directly -2. If you `npm link` matrix-react-sdk, manually remove the 'react' directory - from matrix-react-sdk's `node_modules` folder, otherwise browserify will - pull in both copies of react which causes the app to break. +You can use matrix-react-sdk directly, but to do this you would have to provide +'views' for each UI component. To get started quickly, use matrix-react-skin. How to customise the SDK ======================== -The matrix-react-sdk provides well-defined reusable UI components which may be -customised/replaced by the developer to build into an app. A set of consistent -UI components (View + CSS classes) is called a 'skin' - currently the SDK -provides a very vanilla whitelabelled 'base skin'. In future the SDK could -provide alternative skins (probably by extending the base skin) that provide more -specific look and feels (e.g. "IRC-style", "Skype-style") etc. However, unlike -Wordpress themes and similar, we don't normally expect app developers to define -reusable skins. Instead you just go and incorporate your view customisations -into your actual app. - The SDK uses the 'atomic' design pattern as seen at http://patternlab.io to encourage a very modular and reusable architecture, making it easy to customise and use UI widgets independently of the rest of the SDK and your app. @@ -131,18 +102,41 @@ components to embed a Matrix client into your app: * Create a new NPM project. Be sure to directly depend on react, (otherwise you can end up with two copies of react). * Create an index.js file that sets up react. Add require statements for - React, the ComponentBroker and matrix-react-sdk and a call to Render - the root React element as in the examples. - * Create React classes for any custom components you wish to add. These - can be based off the files in `views` in the `matrix-react-sdk` package, - modifying the require() statement appropriately. - You only need to copy files you want to customise. - * Add a ComponentBroker.set() call for each of your custom components. These - must come *before* `require("matrix-react-sdk")`. - * Add a way to build your project: we suggest copying the browserify calls - from the example projects, but you could use grunt or gulp. - * Create an index.html file pulling in your compiled index.js file, the - CSS bundle from matrix-react-sdk. + React and matrix-react-sdk. Load a skin using the 'loadSkin' method on the + SDK and call Render. This can be a skin provided by a separate package or + a skin in the same package. + * Add a way to build your project: we suggest copying the scripts block + from matrix-react-skin (which uses babel and webpack). You could use + different tools but remember that at least the skins and modules of + your project should end up in plain (ie. non ES6, non JSX) javascript in + the lib directory at the end of the build process, as well as any + packaging that you might do. + * Create an index.html file pulling in your compiled javascript and the + CSS bundle from the skin you use. For now, you'll also need to manually + import CSS from any skins that your skin inherts from. -For more specific detail on any of these steps, look at the `custom` example in -matrix-react-sdk/examples. +To Create Your Own Skin +======================= +To actually change the look of a skin, you can create a base skin (which +does not use views from any other skin) or you can make a derived skin. +Note that derived skins are currently experimental: for example, the CSS +from the skins it is based on will not be automatically included. + +To make a skin, create React classes for any custom components you wish to add +in a skin within `src/skins/`. These can be based off the files in +`views` in the `matrix-react-skin` package, modifying the require() statement +appropriately. + +If you make a derived skin, you only need copy the files you wish to customise. + +Once you've made all your view files, you need to make a `skinfo.json`. This +contains all the metadata for a skin. This is a JSON file with, currently, a +single key, 'baseSkin'. Set this to the empty string if your skin is a base skin, +or for a derived skin, set it to the path of your base skin's skinfo.json file, as +you would use in a require call. + +Now you have the basis of a skin, you need to generate a skindex.json file. The +`reskindex.js` tool in matrix-react-sdk does this for you. It is suggested that +you add an npm script to run this, as in matrix-react-skin. + +For more specific detail on any of these steps, look at matrix-react-skin. diff --git a/examples/custom/CustomMTextTile.js b/examples/custom/CustomMTextTile.js deleted file mode 100644 index e58ed4c1e6..0000000000 --- a/examples/custom/CustomMTextTile.js +++ /dev/null @@ -1,40 +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 MTextTileController = require("matrix-react-sdk/src/controllers/molecules/MTextTile"); - -module.exports = React.createClass({ - displayName: 'MTextTile', - mixins: [MTextTileController], - - render: function() { - var content = this.props.mxEvent.getContent(); - return ( - - {content.body} - - ); - }, - - onClick: function(ev) { - global.alert(this.props.mxEvent.getContent().body); - } -}); - diff --git a/examples/custom/README.md b/examples/custom/README.md deleted file mode 100644 index 8125053ce0..0000000000 --- a/examples/custom/README.md +++ /dev/null @@ -1,4 +0,0 @@ -matrix-react-example -==================== - -An example of how to use the Matrix React SDK to build a more customised app diff --git a/examples/custom/index.html b/examples/custom/index.html deleted file mode 100644 index 04c1645c8a..0000000000 --- a/examples/custom/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Matrix React SDK Custom Example - - -
- - - - diff --git a/examples/custom/index.js b/examples/custom/index.js deleted file mode 100644 index 66602a0ada..0000000000 --- a/examples/custom/index.js +++ /dev/null @@ -1,40 +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'; - -// Remember to make your project depend on react directly as soon as -// you add a require('react') to any file in your project. Do not rely -// on react being pulled in via matrix-react-sdk: browserify breaks -// horribly in this situation and can end up pulling in multiple copies -// of react. -var React = require("react"); - -// We pull in the component broker first, separately, as we need to replace -// components before the SDK loads. -var ComponentBroker = require("matrix-react-sdk/src/ComponentBroker"); - -var CustomMTextTile = require('./CustomMTextTile'); - -ComponentBroker.set('molecules/MTextTile', CustomMTextTile); - -var MatrixReactSdk = require("matrix-react-sdk"); -//var MatrixReactSdk = require("../../src/index"); - -React.render( - , - document.getElementById('matrixchat') -); diff --git a/examples/custom/package.json b/examples/custom/package.json deleted file mode 100644 index 6acec803fa..0000000000 --- a/examples/custom/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "matrix-react-example", - "version": "0.0.1", - "description": "Example usage of matrix-react-sdk", - "author": "matrix.org", - "repository": { - "type": "git", - "url": "https://github.com/matrix-org/matrix-react-sdk" - }, - "license": "Apache-2.0", - "devDependencies": { - "browserify": "^10.2.3", - "envify": "^3.4.0", - "http-server": "^0.8.0", - "matrix-react-sdk": "../../", - "npm-css": "^0.2.3", - "parallelshell": "^1.2.0", - "reactify": "^1.1.1", - "uglify-js": "^2.4.23", - "watchify": "^3.2.1" - }, - "scripts": { - "build": "browserify -t [ envify --NODE_ENV production ] -g reactify index.js | uglifyjs -c -m -o bundle.js", - "start": "parallelshell 'watchify -v -d -g reactify index.js -o bundle.js' 'http-server'" - }, - "dependencies": { - "react": "^0.13.3" - } -} diff --git a/examples/trivial/README.md b/examples/trivial/README.md deleted file mode 100644 index ac26627732..0000000000 --- a/examples/trivial/README.md +++ /dev/null @@ -1,4 +0,0 @@ -matrix-react-example -==================== - -A simple example of how to use the Matrix React SDK diff --git a/examples/trivial/index.html b/examples/trivial/index.html deleted file mode 100644 index 4ec5b9093a..0000000000 --- a/examples/trivial/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Matrix React SDK Example - - -
- - - - diff --git a/examples/trivial/index.js b/examples/trivial/index.js deleted file mode 100644 index 2be9054954..0000000000 --- a/examples/trivial/index.js +++ /dev/null @@ -1,75 +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"); -// In normal usage of the module: -//var MatrixReactSdk = require("matrix-react-sdk"); -// Or to import the source directly from the file system: -// (This is useful for debugging the SDK as it seems source -// maps cannot pass through two stages). -var MatrixReactSdk = require("../../src/index"); - -// Here, we do some crude URL analysis to allow -// deep-linking. We only support registration -// deep-links in this example. -function routeUrl(location) { - if (location.hash.indexOf('#/register') == 0) { - var hashparts = location.hash.split('?'); - var params = {}; - if (hashparts.length == 2) { - var pairs = hashparts[1].split('&'); - for (var i = 0; i < pairs.length; ++i) { - var parts = pairs[i].split('='); - if (parts.length != 2) continue; - params[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]); - } - } - window.matrixChat.showScreen('register', params); - } -} - -var loaded = false; - -window.onload = function() { - routeUrl(window.location); - loaded = true; -} - -// This will be called whenever the SDK changes screens, -// so a web page can update the URL bar appropriately. -var onNewScreen = function(screen) { - if (!loaded) return; - window.location.hash = '#/'+screen; -} - -// We use this to work out what URL the SDK should -// pass through when registering to allow the user to -// click back to the client having registered. -// It's up to us to recognise if we're loaded with -// this URL and tell MatrixClient to resume registration. -var makeRegistrationUrl = function() { - return window.location.protocol + '//' + - window.location.host + - window.location.pathname + - '#/register'; -} - -window.matrixChat = React.render( - , - document.getElementById('matrixchat') -); diff --git a/examples/trivial/package.json b/examples/trivial/package.json deleted file mode 100644 index 40a7150731..0000000000 --- a/examples/trivial/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "matrix-react-example", - "version": "0.0.1", - "description": "Example usage of matrix-react-sdk", - "author": "matrix.org", - "repository": { - "type": "git", - "url": "https://github.com/matrix-org/matrix-react-sdk" - }, - "license": "Apache-2.0", - "devDependencies": { - "browserify": "^10.2.3", - "envify": "^3.4.0", - "http-server": "^0.8.0", - "matrix-react-sdk": "../../", - "parallelshell": "^1.2.0", - "reactify": "^1.1.1", - "uglify-js": "^2.4.23", - "watchify": "^3.2.1" - }, - "scripts": { - "build": "browserify --ignore olm -t [ envify --NODE_ENV production ] -t reactify index.js | uglifyjs -c -m -o bundle.js", - "start": "parallelshell 'watchify --ignore olm -v -d -t reactify index.js -o bundle.js' 'http-server'" - } -} diff --git a/package.json b/package.json index 0287121355..1ccc475feb 100644 --- a/package.json +++ b/package.json @@ -8,33 +8,34 @@ "url": "https://github.com/matrix-org/matrix-react-sdk" }, "license": "Apache-2.0", - "main": "src/index.js", - "style": "bundle.css", + "main": "lib/index.js", + "bin": { + "reskindex": "./reskindex.js" + }, "scripts": { - "build:skins": "jsx skins build/skins", - "build:logic": "jsx src build/src", - "build:js": "npm run build:skins && npm run build:logic", - "start:js": "jsx -w skins/base/views/ build --source-map-inline", - "build:css": "catw 'skins/base/css/**/*.css' -o bundle.css -c uglifycss --no-watch", - "start:css": "catw 'skins/base/css/**/*.css' -o bundle.css -v", - "build": "npm run build:js && npm run build:css", - "start": "parallelshell 'npm run start:js' 'npm run start:css'", + "build": "babel src -d lib --source-maps", + "start": "babel src -w -d lib --source-maps", + "clean": "rimraf lib", "prepublish": "npm run build" }, "dependencies": { "classnames": "^2.1.2", "filesize": "^3.1.2", "flux": "^2.0.3", - "matrix-js-sdk": "0.2.0", + "glob": "^5.0.14", + "linkifyjs": "^2.0.0-beta.4", + "matrix-js-sdk": "^0.2.1", + "optimist": "^0.6.1", "q": "^1.4.1", "react": "^0.13.3", - "react-loader": "^1.4.0", - "linkifyjs": "^2.0.0-beta.4" + "react-loader": "^1.4.0" }, + "//deps": "The loader packages are here because webpack in a project that depends on us needs them in this package's node_modules folder", + "//depsbuglink": "https://github.com/webpack/webpack/issues/1472", "devDependencies": { - "catw": "^1.0.1", - "parallelshell": "^1.1.1", - "react-tools": "^0.13.3", - "uglifycss": "0.0.15" + "babel": "^5.8.23", + "rimraf": "^2.4.3", + "json-loader": "^0.5.3", + "source-map-loader": "^0.1.5" } } diff --git a/reskindex.js b/reskindex.js new file mode 100755 index 0000000000..f8d45493d3 --- /dev/null +++ b/reskindex.js @@ -0,0 +1,83 @@ +#!/usr/bin/env node + +var fs = require('fs'); +var path = require('path'); +var glob = require('glob'); + +var args = require('optimist').argv; + +var header = args.h || args.header; + +if (args._.length == 0) { + console.log("No skin given"); + process.exit(1); +} + +var skin = args._[0]; + +try { + fs.accessSync(path.join('src', 'skins', skin), fs.F_OK); +} catch (e) { + console.log("Skin "+skin+" not found"); + process.exit(1); +} + +var skinfoFile = path.join('src', 'skins', skin, 'skinfo.json'); + +try { + fs.accessSync(skinfoFile, fs.F_OK); +} catch (e) { + console.log("Skin "+skin+" has no skinfo.json"); + process.exit(1); +} + +try { + fs.accessSync(path.join('src', 'skins', skin, 'views'), fs.F_OK); +} catch (e) { + console.log("Skin "+skin+" has no views directory"); + process.exit(1); +} + +var skindex = path.join('src', 'skins', skin, 'skindex.js'); +var viewsDir = path.join('src', 'skins', skin, 'views'); + +var strm = fs.createWriteStream(skindex); + +if (header) { + strm.write(fs.readFileSync(header)); + strm.write('\n'); +} + +strm.write("/*\n"); +strm.write(" * THIS FILE IS AUTO-GENERATED\n"); +strm.write(" * You can edit it you like, but your changes will be overwritten,\n"); +strm.write(" * so you'd just be trying to swim upstream like a salmon.\n"); +strm.write(" * You are not a salmon.\n"); +strm.write(" */\n\n"); + +var mySkinfo = JSON.parse(fs.readFileSync(skinfoFile, "utf8")); + +strm.write("var skin = {};\n"); +strm.write('\n'); + +var files = glob.sync('**/*.js', {cwd: viewsDir}); +for (var i = 0; i < files.length; ++i) { + var file = files[i].replace('.js', ''); + var module = (file.replace(/\//g, '.')); + + strm.write("skin['"+module+"'] = require('./views/"+file+"');\n"); + strm.uncork(); +} + +strm.write("\n"); + +if (mySkinfo.baseSkin) { + strm.write("module.exports = require('"+mySkinfo.baseSkin+"');"); + strm.write("var extend = require('matrix-react-sdk/lib/extend');\n"); + strm.write("extend(module.exports, skin);\n"); +} else { + strm.write("module.exports = skin;"); +} + +strm.end(); + diff --git a/skins/base/css/atoms/MessageTimestamp.css b/skins/base/css/atoms/MessageTimestamp.css deleted file mode 100644 index 62b3065661..0000000000 --- a/skins/base/css/atoms/MessageTimestamp.css +++ /dev/null @@ -1,20 +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. -*/ - -.mx_MessageTimestamp { - display: table-cell; - white-space: pre; -} diff --git a/skins/base/css/common.css b/skins/base/css/common.css deleted file mode 100644 index 5153f97065..0000000000 --- a/skins/base/css/common.css +++ /dev/null @@ -1,23 +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. -*/ - -body { - font-family: Helvetica, Arial, Sans-Serif; -} - -div.error { - color: red; -} diff --git a/skins/base/css/molecules/MessageComposer.css b/skins/base/css/molecules/MessageComposer.css deleted file mode 100644 index 829e25a938..0000000000 --- a/skins/base/css/molecules/MessageComposer.css +++ /dev/null @@ -1,20 +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. -*/ - -.mx_MessageComposer textarea { - width: 100%; - margin: auto; -} diff --git a/skins/base/css/molecules/ProgressBar.css b/skins/base/css/molecules/ProgressBar.css deleted file mode 100644 index 8b8adc09c1..0000000000 --- a/skins/base/css/molecules/ProgressBar.css +++ /dev/null @@ -1,25 +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. -*/ - -.mx_ProgressBar { - height: 5px; - border: 1px solid black; -} - -.mx_ProgressBar_fill { - height: 100%; - background-color: #000; -} diff --git a/skins/base/css/molecules/RoomHeader.css b/skins/base/css/molecules/RoomHeader.css deleted file mode 100644 index 63d6fc33fb..0000000000 --- a/skins/base/css/molecules/RoomHeader.css +++ /dev/null @@ -1,20 +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. -*/ - -.mx_RoomHeader { - height: 1em; - padding: 0px; -} diff --git a/skins/base/css/molecules/RoomTile.css b/skins/base/css/molecules/RoomTile.css deleted file mode 100644 index 719551cb57..0000000000 --- a/skins/base/css/molecules/RoomTile.css +++ /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. -*/ - -.mx_RoomTile { - padding: 5px; - cursor: pointer; -} - -.mx_RoomTile.selected { - text-decoration: underline; -} - -.mx_RoomTile_name { -} - -.mx_RoomTile div { - overflow: hidden; - text-overflow: ellipsis; -} - -.mx_RoomTile.unread { - font-weight: bold; -} - -.mx_RoomTile.highlight { - background-color: lime; -} - -.mx_RoomTile.invited { - font-weight: bold; -} - -.mx_RoomTile:hover { -} diff --git a/skins/base/css/molecules/SenderProfile.css b/skins/base/css/molecules/SenderProfile.css deleted file mode 100644 index 549b598458..0000000000 --- a/skins/base/css/molecules/SenderProfile.css +++ /dev/null @@ -1,20 +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. -*/ - -.mx_SenderProfile { - display: table-cell; - padding: 0px 1em 0em 1em; -} diff --git a/skins/base/css/organisms/RoomView.css b/skins/base/css/organisms/RoomView.css deleted file mode 100644 index 0c75f8fad4..0000000000 --- a/skins/base/css/organisms/RoomView.css +++ /dev/null @@ -1,86 +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. -*/ - -.mx_RoomView { - word-wrap: break-word; - position: relative; -} - -.mx_RoomView .mx_RoomHeader { - height: 30px; -} - -.mx_RoomView_roomWrapper { - display: -webkit-box; - display: -moz-box; - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - position: absolute; - width: 100%; - top: 32px; - bottom: 0px; -} - -.mx_RoomView_messagePanel { - -webkit-box-ordinal-group: 1; - -moz-box-ordinal-group: 1; - -ms-flex-order: 1; - -webkit-order: 1; - order: 1; - width: 100%; - height: 100%; - /* background-color: #ff0; */ -} - -.mx_RoomView_messageListWrapper { - height: 100%; - overflow-y: scroll; -} - -.mx_RoomView_MessageList { - display: table; -} - -.mx_RoomView_MessageList_ul { - list-style-type: none; -} - -.mx_RoomView_invitePrompt { -} - -.mx_RoomView .mx_MemberList { - -webkit-box-ordinal-group: 2; - -moz-box-ordinal-group: 2; - -ms-flex-order: 2; - -webkit-order: 2; - order: 2; - - /* background-color: #0f0; */ - width: 250px; - overflow-y: scroll; - height: 100%; -} - -.mx_RoomView .mx_MemberList ul { - margin: 0px; - padding: 0px; -} - -.mx_RoomView .mx_MessageComposer { - width: 100%; - bottom: 0px; -} diff --git a/skins/base/css/pages/MatrixChat.css b/skins/base/css/pages/MatrixChat.css deleted file mode 100644 index 7ce88ec7ff..0000000000 --- a/skins/base/css/pages/MatrixChat.css +++ /dev/null @@ -1,89 +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. -*/ - -.mx_MatrixChat { - position: relative; - width: 100%; - height: 100%; -} - -.mx_MatrixChat_chatWrapper { - display: -webkit-box; - display: -moz-box; - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - position: absolute; - width: 100%; - top: 0px; - bottom: 42px; -} - -.mx_MatrixChat_leftPanel { - -webkit-box-ordinal-group: 1; - -moz-box-ordinal-group: 1; - -ms-flex-order: 1; - -webkit-order: 1; - order: 1; - - display: -webkit-box; - display: -moz-box; - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - flex-direction: column; - -webkit-flex-direction: column; - - /* background-color: #f00; */ - width: 250px; - height: 100%; -} - -.mx_MatrixChat_leftPanel .mx_MatrixToolbar { - -webkit-box-ordinal-group: 1; - -moz-box-ordinal-group: 1; - -ms-flex-order: 1; - -webkit-order: 1; - order: 1; - - width: 100%; - height: 40px; -} - -.mx_MatrixChat_leftPanel .mx_RoomList { - -webkit-box-ordinal-group: 2; - -moz-box-ordinal-group: 2; - -ms-flex-order: 2; - -webkit-order: 2; - order: 2; - - /* background-color: #0ff; */ - width: 100%; - height: 100%; - overflow-y: scroll; -} - -.mx_MatrixChat .mx_RoomView { - -webkit-box-ordinal-group: 2; - -moz-box-ordinal-group: 2; - -ms-flex-order: 2; - -webkit-order: 2; - order: 2; - - /* background-color: #00f; */ - width: 100%; - height: 100%; -} diff --git a/skins/base/css/templates/Login.css b/skins/base/css/templates/Login.css deleted file mode 100644 index 7dbcde1caa..0000000000 --- a/skins/base/css/templates/Login.css +++ /dev/null @@ -1,22 +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. -*/ - -.mx_Login { - width: 600px; - height: 350px; - position: relative; -} - diff --git a/skins/base/views/atoms/EditableText.js b/skins/base/views/atoms/EditableText.js deleted file mode 100644 index a8f55814e7..0000000000 --- a/skins/base/views/atoms/EditableText.js +++ /dev/null @@ -1,68 +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 EditableTextController = require("../../../../src/controllers/atoms/EditableText"); - -module.exports = React.createClass({ - displayName: 'EditableText', - mixins: [EditableTextController], - - 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) { - this.setValue(ev.target.value); - }, - - render: function() { - var editable_el; - - if (this.state.phase == this.Phases.Display) { - editable_el =
{this.state.value}
; - } else if (this.state.phase == this.Phases.Edit) { - editable_el = ( -
- -
- ); - } - - return ( -
- {editable_el} -
- ); - } -}); diff --git a/skins/base/views/atoms/EnableNotificationsButton.js b/skins/base/views/atoms/EnableNotificationsButton.js deleted file mode 100644 index 7caebb76c5..0000000000 --- a/skins/base/views/atoms/EnableNotificationsButton.js +++ /dev/null @@ -1,38 +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 EnableNotificationsButtonController = require("../../../../src/controllers/atoms/EnableNotificationsButton"); - -module.exports = React.createClass({ - displayName: 'EnableNotificationsButton', - mixins: [EnableNotificationsButtonController], - - render: function() { - if (this.enabled()) { - return ( - - ); - } else { - return ( - - ); - } - } -}); diff --git a/skins/base/views/atoms/LogoutButton.js b/skins/base/views/atoms/LogoutButton.js deleted file mode 100644 index 8cc5b27d5e..0000000000 --- a/skins/base/views/atoms/LogoutButton.js +++ /dev/null @@ -1,32 +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 LogoutButtonController = require("../../../../src/controllers/atoms/LogoutButton"); - -module.exports = React.createClass({ - displayName: 'LogoutButton', - mixins: [LogoutButtonController], - - render: function() { - return ( - - ); - } -}); diff --git a/skins/base/views/atoms/MessageTimestamp.js b/skins/base/views/atoms/MessageTimestamp.js deleted file mode 100644 index 52eb1462eb..0000000000 --- a/skins/base/views/atoms/MessageTimestamp.js +++ /dev/null @@ -1,36 +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 MessageTimestampController = require("../../../../src/controllers/atoms/MessageTimestamp"); - -module.exports = React.createClass({ - displayName: 'MessageTimestamp', - mixins: [MessageTimestampController], - - render: function() { - var date = new Date(this.props.ts); - return ( - - {date.toLocaleTimeString()} - - ); - }, -}); - diff --git a/skins/base/views/atoms/create_room/CreateRoomButton.js b/skins/base/views/atoms/create_room/CreateRoomButton.js deleted file mode 100644 index 2f9ccae030..0000000000 --- a/skins/base/views/atoms/create_room/CreateRoomButton.js +++ /dev/null @@ -1,32 +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 CreateRoomButtonController = require("../../../../../src/controllers/atoms/create_room/CreateRoomButton"); - -module.exports = React.createClass({ - displayName: 'CreateRoomButton', - mixins: [CreateRoomButtonController], - - render: function() { - return ( - - ); - } -}); diff --git a/skins/base/views/atoms/create_room/Presets.js b/skins/base/views/atoms/create_room/Presets.js deleted file mode 100644 index 83fe61bdbb..0000000000 --- a/skins/base/views/atoms/create_room/Presets.js +++ /dev/null @@ -1,39 +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 PresetsController = require("../../../../../src/controllers/atoms/create_room/Presets"); - -module.exports = React.createClass({ - displayName: 'CreateRoomPresets', - mixins: [PresetsController], - - onValueChanged: function(ev) { - this.setState({preset: ev.target.value}) - }, - - render: function() { - return ( - - ); - } -}); diff --git a/skins/base/views/atoms/create_room/RoomNameTextbox.js b/skins/base/views/atoms/create_room/RoomNameTextbox.js deleted file mode 100644 index c358a14cb3..0000000000 --- a/skins/base/views/atoms/create_room/RoomNameTextbox.js +++ /dev/null @@ -1,36 +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 RoomNameTextboxController = require("../../../../../src/controllers/atoms/create_room/RoomNameTextbox"); - -module.exports = React.createClass({ - displayName: 'RoomNameTextbox', - mixins: [RoomNameTextboxController], - - onValueChanged: function(ev) { - this.setState({room_name: ev.target.value}) - }, - - render: function() { - return ( - - ); - } -}); diff --git a/skins/base/views/molecules/MEmoteTile.js b/skins/base/views/molecules/MEmoteTile.js deleted file mode 100644 index e1b5045db7..0000000000 --- a/skins/base/views/molecules/MEmoteTile.js +++ /dev/null @@ -1,38 +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 MEmoteTileController = require("../../../../src/controllers/molecules/MEmoteTile"); - -module.exports = React.createClass({ - displayName: 'MEmoteTile', - mixins: [MEmoteTileController], - - render: function() { - var mxEvent = this.props.mxEvent; - var content = mxEvent.getContent(); - var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); - return ( -
  • - * {name} {content.body} -
  • - ); - }, -}); - diff --git a/skins/base/views/molecules/MFileTile.js b/skins/base/views/molecules/MFileTile.js deleted file mode 100644 index 7685fc75e0..0000000000 --- a/skins/base/views/molecules/MFileTile.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'); - -var MFileTileController = require("../../../../src/controllers/molecules/MFileTile"); - -var MatrixClientPeg = require('../../../../src/MatrixClientPeg'); - -module.exports = React.createClass({ - displayName: 'MFileTile', - mixins: [MFileTileController], - - render: function() { - var content = this.props.mxEvent.getContent(); - var cli = MatrixClientPeg.get(); - - return ( -
  • - - {this.presentableTextForFile(content)} - -
  • - ); - }, -}); diff --git a/skins/base/views/molecules/MImageTile.js b/skins/base/views/molecules/MImageTile.js deleted file mode 100644 index 97cefc538e..0000000000 --- a/skins/base/views/molecules/MImageTile.js +++ /dev/null @@ -1,69 +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 MImageTileController = require("../../../../src/controllers/molecules/MImageTile"); - -var MatrixClientPeg = require('../../../../src/MatrixClientPeg'); - -module.exports = React.createClass({ - displayName: 'MImageTile', - mixins: [MImageTileController], - - thumbHeight: function(fullWidth, fullHeight, thumbWidth, thumbHeight) { - if (!fullWidth || !fullHeight) { - // Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even - // log this because it's spammy - return undefined; - } - if (fullWidth < thumbWidth && fullHeight < thumbHeight) { - // no scaling needs to be applied - return fullHeight; - } - var widthMulti = thumbWidth / fullWidth; - var heightMulti = thumbHeight / fullHeight; - if (widthMulti < heightMulti) { - // width is the dominant dimension so scaling will be fixed on that - return Math.floor(widthMulti * fullHeight); - } - else { - // height is the dominant dimension so scaling will be fixed on that - return Math.floor(heightMulti * fullHeight); - } - }, - - render: function() { - var content = this.props.mxEvent.getContent(); - var cli = MatrixClientPeg.get(); - - var thumbHeight = null; - if (content.info) thumbHeight = this.thumbHeight(content.info.w, content.info.h, 320, 240); - - var imgStyle = {}; - if (thumbHeight) imgStyle['height'] = thumbHeight; - - return ( -
  • - - {content.body} - -
  • - ); - }, -}); diff --git a/skins/base/views/molecules/MNoticeTile.js b/skins/base/views/molecules/MNoticeTile.js deleted file mode 100644 index f63a8c2cea..0000000000 --- a/skins/base/views/molecules/MNoticeTile.js +++ /dev/null @@ -1,36 +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 MNoticeTileController = require("../../../../src/controllers/molecules/MNoticeTile"); - -module.exports = React.createClass({ - displayName: 'MNoticeTile', - mixins: [MNoticeTileController], - - render: function() { - var content = this.props.mxEvent.getContent(); - return ( - - {content.body} - - ); - }, -}); - diff --git a/skins/base/views/molecules/MRoomMemberTile.js b/skins/base/views/molecules/MRoomMemberTile.js deleted file mode 100644 index f0755e2614..0000000000 --- a/skins/base/views/molecules/MRoomMemberTile.js +++ /dev/null @@ -1,57 +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 MRoomMemberTileController = require("../../../../src/controllers/molecules/MRoomMemberTile"); - -var ComponentBroker = require('../../../../src/ComponentBroker'); -var MessageTimestamp = ComponentBroker.get('atoms/MessageTimestamp'); - -module.exports = React.createClass({ - displayName: 'MRoomMemberTile', - mixins: [MRoomMemberTileController], - - getMemberEventText: function() { - var ev = this.props.mxEvent; - // XXX: SYJS-16 - var senderName = ev.sender ? ev.sender.name : "Someone"; - switch (ev.getContent().membership) { - case 'invite': - return senderName + " invited " + ev.target.name + "."; - case 'join': - return senderName + " joined the room."; - case 'leave': - return senderName + " left the room."; - } - }, - - render: function() { - // XXX: for now, just cheekily borrow the css from message tile... - return ( -
    - - - - {this.getMemberEventText()} - -
    - ); - }, -}); - diff --git a/skins/base/views/molecules/MTextTile.js b/skins/base/views/molecules/MTextTile.js deleted file mode 100644 index d08f42ed9a..0000000000 --- a/skins/base/views/molecules/MTextTile.js +++ /dev/null @@ -1,36 +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 MTextTileController = require("../../../../src/controllers/molecules/MTextTile"); - -module.exports = React.createClass({ - displayName: 'MTextTile', - mixins: [MTextTileController], - - render: function() { - var content = this.props.mxEvent.getContent(); - return ( - - {content.body} - - ); - }, -}); - diff --git a/skins/base/views/molecules/MatrixToolbar.js b/skins/base/views/molecules/MatrixToolbar.js deleted file mode 100644 index e4444ee9c8..0000000000 --- a/skins/base/views/molecules/MatrixToolbar.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'); - -var ComponentBroker = require('../../../../src/ComponentBroker'); - -var LogoutButton = ComponentBroker.get("atoms/LogoutButton"); -var EnableNotificationsButton = ComponentBroker.get("atoms/EnableNotificationsButton"); - -var MatrixToolbarController = require("../../../../src/controllers/molecules/MatrixToolbar"); - -module.exports = React.createClass({ - displayName: 'MatrixToolbar', - mixins: [MatrixToolbarController], - - render: function() { - return ( -
    - - -
    - ); - } -}); - diff --git a/skins/base/views/molecules/MemberTile.js b/skins/base/views/molecules/MemberTile.js deleted file mode 100644 index 60d1cadd84..0000000000 --- a/skins/base/views/molecules/MemberTile.js +++ /dev/null @@ -1,33 +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 MemberTileController = require("../../../../src/controllers/molecules/MemberTile"); - -module.exports = React.createClass({ - displayName: 'MemberTile', - mixins: [MemberTileController], - render: function() { - return ( -
    -
    {this.props.member.name}
    -
    - ); - } -}); diff --git a/skins/base/views/molecules/MessageTile.js b/skins/base/views/molecules/MessageTile.js deleted file mode 100644 index b28e562b20..0000000000 --- a/skins/base/views/molecules/MessageTile.js +++ /dev/null @@ -1,66 +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 classNames = require("classnames"); - -var ComponentBroker = require('../../../../src/ComponentBroker'); - -var MessageTimestamp = ComponentBroker.get('atoms/MessageTimestamp'); -var SenderProfile = ComponentBroker.get('molecules/SenderProfile'); - -var UnknownMessageTile = ComponentBroker.get('molecules/UnknownMessageTile'); - -var tileTypes = { - 'm.text': ComponentBroker.get('molecules/MTextTile'), - 'm.notice': ComponentBroker.get('molecules/MNoticeTile'), - 'm.emote': ComponentBroker.get('molecules/MEmoteTile'), - 'm.image': ComponentBroker.get('molecules/MImageTile'), - 'm.file': ComponentBroker.get('molecules/MFileTile') -}; - -var MessageTileController = require("../../../../src/controllers/molecules/MessageTile"); - -module.exports = React.createClass({ - displayName: 'MessageTile', - mixins: [MessageTileController], - - render: function() { - var content = this.props.mxEvent.getContent(); - var msgtype = content.msgtype; - var TileType = UnknownMessageTile; - if (msgtype && tileTypes[msgtype]) { - TileType = tileTypes[msgtype]; - } - var classes = classNames({ - mx_MessageTile: true, - mx_MessageTile_sending: this.props.mxEvent.status == 'sending', - mx_MessageTile_notSent: this.props.mxEvent.status == 'not_sent', - mx_MessageTile_highlight: this.shouldHighlight() - }); - return ( -
  • - - - -
  • - ); - }, -}); - diff --git a/skins/base/views/molecules/ProgressBar.js b/skins/base/views/molecules/ProgressBar.js deleted file mode 100644 index 0946ffcc26..0000000000 --- a/skins/base/views/molecules/ProgressBar.js +++ /dev/null @@ -1,37 +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 ProgressBarController = require("../../../../src/controllers/molecules/ProgressBar"); - -module.exports = React.createClass({ - displayName: 'ProgressBar', - mixins: [ProgressBarController], - - 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 ( -
    - ); - } -}); diff --git a/skins/base/views/molecules/RoomTile.js b/skins/base/views/molecules/RoomTile.js deleted file mode 100644 index 0e80fc2015..0000000000 --- a/skins/base/views/molecules/RoomTile.js +++ /dev/null @@ -1,44 +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 classNames = require('classnames'); - -var RoomTileController = require("../../../../src/controllers/molecules/RoomTile"); - -var MatrixClientPeg = require("../../../../src/MatrixClientPeg"); - -module.exports = React.createClass({ - displayName: 'RoomTile', - mixins: [RoomTileController], - render: function() { - var myUserId = MatrixClientPeg.get().credentials.userId; - var classes = classNames({ - 'mx_RoomTile': true, - 'selected': this.props.selected, - 'unread': this.props.unread, - 'highlight': this.props.highlight, - 'invited': this.props.room.currentState.members[myUserId].membership == 'invite' - }); - return ( -
    -
    {this.props.room.name}
    -
    - ); - } -}); diff --git a/skins/base/views/molecules/SenderProfile.js b/skins/base/views/molecules/SenderProfile.js deleted file mode 100644 index d71d1c2226..0000000000 --- a/skins/base/views/molecules/SenderProfile.js +++ /dev/null @@ -1,42 +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 SenderProfileController = require("../../../../src/controllers/molecules/SenderProfile"); - -module.exports = React.createClass({ - displayName: 'SenderProfile', - mixins: [SenderProfileController], - - render: function() { - var mxEvent = this.props.mxEvent; - var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); - - var msgtype = mxEvent.getContent().msgtype; - if (msgtype && msgtype == 'm.emote') { - name = ''; // emote message must include the name so don't duplicate it - } - return ( - - {name} - - ); - }, -}); - diff --git a/skins/base/views/molecules/ServerConfig.js b/skins/base/views/molecules/ServerConfig.js deleted file mode 100644 index e06536c552..0000000000 --- a/skins/base/views/molecules/ServerConfig.js +++ /dev/null @@ -1,43 +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 ServerConfigController = require("../../../../src/controllers/molecules/ServerConfig"); - -module.exports = React.createClass({ - displayName: 'ServerConfig', - mixins: [ServerConfigController], - - render: function() { - return ( -
    - - - - - - - - - -
    Home Server URL
    Identity Server URL
    -
    - ); - } -}); diff --git a/skins/base/views/molecules/UnknownMessageTile.js b/skins/base/views/molecules/UnknownMessageTile.js deleted file mode 100644 index 27e801c994..0000000000 --- a/skins/base/views/molecules/UnknownMessageTile.js +++ /dev/null @@ -1,34 +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 UnknownMessageTileController = require("../../../../src/controllers/molecules/UnknownMessageTile"); - -module.exports = React.createClass({ - displayName: 'UnknownMessageTile', - mixins: [UnknownMessageTileController], - - render: function() { - return ( - - ? - - ); - }, -}); diff --git a/skins/base/views/molecules/UserSelector.js b/skins/base/views/molecules/UserSelector.js deleted file mode 100644 index 7517e29d0f..0000000000 --- a/skins/base/views/molecules/UserSelector.js +++ /dev/null @@ -1,44 +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 UserSelectorController = require("../../../../src/controllers/molecules/UserSelector"); - -module.exports = React.createClass({ - displayName: 'UserSelector', - mixins: [UserSelectorController], - - onAddUserId: function() { - this.addUser(this.refs.user_id_input.getDOMNode().value); - }, - - render: function() { - return ( -
    -
      - {this.state.selected_users.map(function(user_id, i) { - return
    • {user_id}
    • - })} -
    - - -
    - ); - } -}); diff --git a/skins/base/views/organisms/CreateRoom.js b/skins/base/views/organisms/CreateRoom.js deleted file mode 100644 index 36f6e466e5..0000000000 --- a/skins/base/views/organisms/CreateRoom.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. -*/ - -'use strict'; - -var React = require('react'); - -var CreateRoomController = require("../../../../src/controllers/organisms/CreateRoom"); - -var ComponentBroker = require('../../../../src/ComponentBroker'); - -var CreateRoomButton = ComponentBroker.get("atoms/create_room/CreateRoomButton"); -var RoomNameTextbox = ComponentBroker.get("atoms/create_room/RoomNameTextbox"); -var Presets = ComponentBroker.get("atoms/create_room/Presets"); -var UserSelector = ComponentBroker.get("molecules/UserSelector"); - - -module.exports = React.createClass({ - displayName: 'CreateRoom', - mixins: [CreateRoomController], - - getPreset: function() { - return this.refs.presets.getPreset(); - }, - - getName: function() { - return this.refs.name_textbox.getName(); - }, - - getInvitedUsers: function() { - return this.refs.user_selector.getUserIds(); - }, - - render: function() { - var curr_phase = this.state.phase; - if (curr_phase == this.phases.CREATING) { - return ( -
    Creating...
    - ); - } else { - var error_box = ""; - if (curr_phase == this.phases.ERROR) { - error_box = ( -
    - An error occured: {this.state.error_string} -
    - ); - } - return ( -
    - - - - - {error_box} -
    - ); - } - } -}); diff --git a/skins/base/views/organisms/MemberList.js b/skins/base/views/organisms/MemberList.js deleted file mode 100644 index 5d1b2fd0f9..0000000000 --- a/skins/base/views/organisms/MemberList.js +++ /dev/null @@ -1,56 +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 MemberListController = require("../../../../src/controllers/organisms/MemberList"); - -var ComponentBroker = require('../../../../src/ComponentBroker'); - -var MemberTile = ComponentBroker.get("molecules/MemberTile"); - - -module.exports = React.createClass({ - displayName: 'MemberList', - mixins: [MemberListController], - - makeMemberTiles: function() { - var that = this; - return Object.keys(that.state.memberDict).map(function(userId) { - var m = that.state.memberDict[userId]; - return ( -
  • - -
  • - ); - }); - }, - - render: function() { - return ( -
    -
      - {this.makeMemberTiles()} -
    -
    - ); - } -}); - diff --git a/skins/base/views/organisms/Notifier.js b/skins/base/views/organisms/Notifier.js deleted file mode 100644 index 09f1921ac3..0000000000 --- a/skins/base/views/organisms/Notifier.js +++ /dev/null @@ -1,102 +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 NotifierController = require("../../../../src/controllers/organisms/Notifier"); - -var MatrixClientPeg = require("../../../../src/MatrixClientPeg"); -var extend = require("../../../../src/extend"); -var dis = require("../../../../src/dispatcher"); - - -var NotifierView = { - notificationMessageForEvent: function(ev) { - var senderDisplayName = ev.sender ? ev.sender.name : ''; - var message = null; - - if (ev.event.type === "m.room.message") { - message = ev.getContent().body; - if (ev.getContent().msgtype === "m.emote") { - message = "* " + senderDisplayName + " " + message; - } else if (ev.getContent().msgtype === "m.image") { - message = senderDisplayName + " sent an image."; - } - } else if (ev.event.type == "m.room.member") { - if (ev.event.state_key !== MatrixClientPeg.get().credentials.userId && "join" === ev.getContent().membership) { - // Notify when another user joins - message = senderDisplayName + " joined"; - } else if (ev.event.state_key === MatrixClientPeg.get().credentials.userId && "invite" === ev.getContent().membership) { - // notify when you are invited - message = senderDisplayName + " invited you to a room"; - } - } - return message; - }, - - displayNotification: function(ev, room) { - if (!global.Notification || global.Notification.permission != 'granted') { - return; - } - if (global.document.hasFocus()) { - return; - } - - var msg = this.notificationMessageForEvent(ev); - if (!msg) return; - - var title; - if (!ev.sender || room.name == ev.sender.name) { - title = room.name; - } else if (ev.sender) { - title = ev.sender.name + " (" + room.name + ")"; - } - - var notification = new global.Notification( - title, - { - "body": msg, - "icon": MatrixClientPeg.get().getAvatarUrlForMember(ev.sender) - } - ); - - notification.onclick = function() { - dis.dispatch({ - action: 'view_room', - room_id: room.roomId - }); - global.focus(); - }; - - /*var audioClip; - - if (audioNotification) { - audioClip = playAudio(audioNotification); - }*/ - - global.setTimeout(function() { - notification.close(); - }, 5 * 1000); - - } -}; - -var NotifierClass = function() {}; -extend(NotifierClass.prototype, NotifierController); -extend(NotifierClass.prototype, NotifierView); - -module.exports = new NotifierClass(); - diff --git a/skins/base/views/organisms/RoomList.js b/skins/base/views/organisms/RoomList.js deleted file mode 100644 index f8be66f76e..0000000000 --- a/skins/base/views/organisms/RoomList.js +++ /dev/null @@ -1,36 +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 RoomListController = require("../../../../src/controllers/organisms/RoomList"); - - -module.exports = React.createClass({ - displayName: 'RoomList', - mixins: [RoomListController], - - render: function() { - return ( -
    - {this.makeRoomTiles()} -
    - ); - } -}); - diff --git a/skins/base/views/organisms/RoomView.js b/skins/base/views/organisms/RoomView.js deleted file mode 100644 index 20d073b990..0000000000 --- a/skins/base/views/organisms/RoomView.js +++ /dev/null @@ -1,99 +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("../../../../src/MatrixClientPeg"); - -var ComponentBroker = require('../../../../src/ComponentBroker'); -var classNames = require("classnames"); - -var MessageTile = ComponentBroker.get('molecules/MessageTile'); -var RoomHeader = ComponentBroker.get('molecules/RoomHeader'); -var MemberList = ComponentBroker.get('organisms/MemberList'); -var MessageComposer = ComponentBroker.get('molecules/MessageComposer'); - -var RoomViewController = require("../../../../src/controllers/organisms/RoomView"); - -var Loader = require("react-loader"); - - -module.exports = React.createClass({ - displayName: 'RoomView', - mixins: [RoomViewController], - - render: function() { - if (!this.state.room) { - return ( -
    - ); - } - - var myUserId = MatrixClientPeg.get().credentials.userId; - if (this.state.room.currentState.members[myUserId].membership == 'invite') { - if (this.state.joining) { - return ( -
    - -
    - ); - } else { - var inviteEvent = this.state.room.currentState.members[myUserId].events.member.event; - // XXX: Leaving this intentionally basic for now because invites are about to change totally - var joinErrorText = this.state.joinError ? "Failed to join room!" : ""; - return ( -
    -
    -
    {inviteEvent.user_id} has invited you to a room
    - -
    {joinErrorText}
    -
    -
    - ); - } - } else { - var scrollheader_classes = classNames({ - mx_RoomView_scrollheader: true, - loading: this.state.paginating - }); - return ( -
    - -
    -
    -
    -
    -
    -
    -
      - {this.getEventTiles()} -
    -
    -
    - -
    - -
    -
    - ); - } - }, -}); - diff --git a/skins/base/views/pages/MatrixChat.js b/skins/base/views/pages/MatrixChat.js deleted file mode 100644 index be4cd43417..0000000000 --- a/skins/base/views/pages/MatrixChat.js +++ /dev/null @@ -1,70 +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 ComponentBroker = require('../../../../src/ComponentBroker'); - -var RoomList = ComponentBroker.get('organisms/RoomList'); -var RoomView = ComponentBroker.get('organisms/RoomView'); -var MatrixToolbar = ComponentBroker.get('molecules/MatrixToolbar'); -var Login = ComponentBroker.get('templates/Login'); -var Register = ComponentBroker.get('templates/Register'); - -var MatrixChatController = require("../../../../src/controllers/pages/MatrixChat"); - -// should be atomised -var Loader = require("react-loader"); - - -module.exports = React.createClass({ - displayName: 'MatrixChat', - mixins: [MatrixChatController], - - render: function() { - if (this.state.logged_in && this.state.ready) { - return ( -
    -
    - - -
    -
    - ); - } else if (this.state.logged_in) { - return ( - - ); - } else if (this.state.screen == 'register') { - return ( - - ); - } else { - return ( - - ); - } - } -}); - diff --git a/skins/base/views/templates/Login.js b/skins/base/views/templates/Login.js deleted file mode 100644 index f71e307068..0000000000 --- a/skins/base/views/templates/Login.js +++ /dev/null @@ -1,102 +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 ComponentBroker = require("../../../../src/ComponentBroker"); - -var ProgressBar = ComponentBroker.get("molecules/ProgressBar"); -var Loader = require("react-loader"); - -var LoginController = require("../../../../src/controllers/templates/Login"); - -var ServerConfig = ComponentBroker.get("molecules/ServerConfig"); - -module.exports = React.createClass({ - displayName: 'Login', - mixins: [LoginController], - - getHsUrl: function() { - return this.refs.serverConfig.getHsUrl(); - }, - - getIsUrl: function() { - return this.refs.serverConfig.getIsUrl(); - }, - - /** - * Gets the form field values for the current login stage - */ - getFormVals: function() { - return { - 'username': this.refs.user.getDOMNode().value, - 'password': this.refs.pass.getDOMNode().value - }; - }, - - componentForStep: function(step) { - switch (step) { - case 'choose_hs': - return ( -
    -
    - - - -
    - ); - // XXX: clearly these should be separate organisms - case 'stage_m.login.password': - return ( -
    -
    -
    -
    - -
    -
    - ); - } - }, - - loginContent: function() { - if (this.state.busy) { - return ( - - ); - } else { - return ( -
    -

    Please log in:

    - {this.componentForStep(this.state.step)} -
    {this.state.errorText}
    - Create a new account -
    - ); - } - }, - - render: function() { - return ( -
    - - {this.loginContent()} -
    - ); - } -}); diff --git a/skins/base/views/templates/Register.js b/skins/base/views/templates/Register.js deleted file mode 100644 index 94f3b96971..0000000000 --- a/skins/base/views/templates/Register.js +++ /dev/null @@ -1,131 +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 ComponentBroker = require("../../../../src/ComponentBroker"); - -var Loader = require("react-loader"); - -var RegisterController = require("../../../../src/controllers/templates/Register"); - -var ServerConfig = ComponentBroker.get("molecules/ServerConfig"); - -module.exports = React.createClass({ - displayName: 'Register', - mixins: [RegisterController], - - getRegFormVals: function() { - return { - email: this.refs.email.getDOMNode().value, - username: this.refs.username.getDOMNode().value, - password: this.refs.password.getDOMNode().value, - confirmPassword: this.refs.confirmPassword.getDOMNode().value - }; - }, - - getHsUrl: function() { - return this.refs.serverConfig.getHsUrl(); - }, - - getIsUrl: function() { - return this.refs.serverConfig.getIsUrl(); - }, - - componentForStep: function(step) { - switch (step) { - case 'initial': - return ( -
    -
    - Email:
    - Username:
    - Password:
    - Confirm Password:
    - - - -
    - ); - // XXX: clearly these should be separate organisms - case 'stage_m.login.email.identity': - return ( -
    - Please check your email to continue registration. -
    - ); - case 'stage_m.login.recaptcha': - return ( -
    - This Home Server would like to make sure you're not a robot -
    -
    - ); - } - }, - - registerContent: function() { - if (this.state.busy) { - return ( - - ); - } else { - return ( -
    -

    Create a new account:

    - {this.componentForStep(this.state.step)} -
    {this.state.errorText}
    - Sign in with existing account -
    - ); - } - }, - - onBadFields: function(bad) { - var keys = Object.keys(bad); - var strings = []; - for (var i = 0; i < keys.length; ++i) { - switch (bad[keys[i]]) { - case this.FieldErrors.PasswordMismatch: - strings.push("Passwords don't match"); - break; - case this.FieldErrors.Missing: - strings.push("Missing "+keys[i]); - break; - case this.FieldErrors.TooShort: - strings.push(keys[i]+" is too short"); - break; - case this.FieldErrors.InUse: - strings.push(keys[i]+" is already taken"); - break; - } - } - var errtxt = strings.join(', '); - this.setState({ - errorText: errtxt - }); - }, - - render: function() { - return ( -
    - {this.registerContent()} -
    - ); - } -}); diff --git a/src/CallHandler.js b/src/CallHandler.js new file mode 100644 index 0000000000..d0cf16f801 --- /dev/null +++ b/src/CallHandler.js @@ -0,0 +1,302 @@ +/* +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. +*/ + +/* + * Manages a list of all the currently active calls. + * + * This handler dispatches when voip calls are added/updated/removed from this list: + * { + * action: 'call_state' + * room_id: + * } + * + * To know the state of the call, this handler exposes a getter to + * obtain the call for a room: + * var call = CallHandler.getCall(roomId) + * var state = call.call_state; // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing + * + * This handler listens for and handles the following actions: + * { + * action: 'place_call', + * type: 'voice|video', + * room_id: + * } + * + * { + * action: 'incoming_call' + * call: MatrixCall + * } + * + * { + * action: 'hangup' + * room_id: + * } + * + * { + * action: 'answer' + * room_id: + * } + */ + +var MatrixClientPeg = require('./MatrixClientPeg'); +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; + +function play(audioId) { + // TODO: Attach an invisible element for this instead + // which listens? + var audio = document.getElementById(audioId); + if (audio) { + audio.load(); + audio.play(); + } +} + +function pause(audioId) { + // TODO: Attach an invisible element for this instead + // which listens? + var audio = document.getElementById(audioId); + if (audio) { + audio.pause(); + } +} + +function _setCallListeners(call) { + call.on("error", function(err) { + console.error("Call error: %s", err); + console.error(err.stack); + call.hangup(); + _setCallState(undefined, call.roomId, "ended"); + }); + call.on("hangup", function() { + _setCallState(undefined, call.roomId, "ended"); + }); + // map web rtc states to dummy UI state + // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing + call.on("state", function(newState, oldState) { + if (newState === "ringing") { + _setCallState(call, call.roomId, "ringing"); + pause("ringbackAudio"); + } + else if (newState === "invite_sent") { + _setCallState(call, call.roomId, "ringback"); + play("ringbackAudio"); + } + else if (newState === "ended" && oldState === "connected") { + _setCallState(undefined, call.roomId, "ended"); + pause("ringbackAudio"); + play("callendAudio"); + } + else if (newState === "ended" && oldState === "invite_sent" && + (call.hangupParty === "remote" || + (call.hangupParty === "local" && call.hangupReason === "invite_timeout") + )) { + _setCallState(call, call.roomId, "busy"); + pause("ringbackAudio"); + play("busyAudio"); + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Call Timeout", + description: "The remote side failed to pick up." + }); + } + else if (oldState === "invite_sent") { + _setCallState(call, call.roomId, "stop_ringback"); + pause("ringbackAudio"); + } + else if (oldState === "ringing") { + _setCallState(call, call.roomId, "stop_ringing"); + pause("ringbackAudio"); + } + else if (newState === "connected") { + _setCallState(call, call.roomId, "connected"); + pause("ringbackAudio"); + } + }); +} + +function _setCallState(call, roomId, status) { + console.log( + "Call state in %s changed to %s (%s)", roomId, status, (call ? call.state : "-") + ); + calls[roomId] = call; + if (call) { + call.call_state = status; + } + dis.dispatch({ + action: 'call_state', + room_id: roomId + }); +} + +function _onAction(payload) { + function placeCall(newCall) { + _setCallListeners(newCall); + _setCallState(newCall, newCall.roomId, "ringback"); + if (payload.type === 'voice') { + newCall.placeVoiceCall(); + } + else if (payload.type === 'video') { + newCall.placeVideoCall( + payload.remote_element, + payload.local_element + ); + } + else { + console.error("Unknown conf call type: %s", payload.type); + } + } + + switch (payload.action) { + case 'place_call': + if (module.exports.getAnyActiveCall()) { + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Existing Call", + description: "You are already in a call." + }); + return; // don't allow >1 call to be placed. + } + var room = MatrixClientPeg.get().getRoom(payload.room_id); + if (!room) { + console.error("Room %s does not exist.", payload.room_id); + return; + } + + var members = room.getJoinedMembers(); + if (members.length <= 1) { + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + description: "You cannot place a call with yourself." + }); + return; + } + else if (members.length === 2) { + console.log("Place %s call in %s", payload.type, payload.room_id); + var call = Matrix.createNewMatrixCall( + MatrixClientPeg.get(), payload.room_id + ); + placeCall(call); + } + else { // > 2 + dis.dispatch({ + action: "place_conference_call", + room_id: payload.room_id, + type: payload.type, + remote_element: payload.remote_element, + local_element: payload.local_element + }); + } + break; + case 'place_conference_call': + console.log("Place conference call in %s", payload.room_id); + if (!Modulator.hasConferenceHandler()) { + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + description: "Conference calls are not supported in this client" + }); + } else { + var ConferenceHandler = Modulator.getConferenceHandler(); + ConferenceHandler.createNewMatrixCall( + MatrixClientPeg.get(), payload.room_id + ).done(function(call) { + placeCall(call); + }, function(err) { + console.error("Failed to setup conference call: %s", err); + }); + } + break; + case 'incoming_call': + if (module.exports.getAnyActiveCall()) { + payload.call.hangup("busy"); + return; // don't allow >1 call to be received, hangup newer one. + } + var call = payload.call; + _setCallListeners(call); + _setCallState(call, call.roomId, "ringing"); + break; + case 'hangup': + if (!calls[payload.room_id]) { + return; // no call to hangup + } + calls[payload.room_id].hangup(); + _setCallState(null, payload.room_id, "ended"); + break; + case 'answer': + if (!calls[payload.room_id]) { + return; // no call to answer + } + calls[payload.room_id].answer(); + _setCallState(calls[payload.room_id], payload.room_id, "connected"); + dis.dispatch({ + action: "view_room", + room_id: payload.room_id + }); + break; + } +} +// FIXME: Nasty way of making sure we only register +// with the dispatcher once +if (!global.mxCallHandler) { + dis.register(_onAction); +} + +var callHandler = { + getCallForRoom: function(roomId) { + var call = module.exports.getCall(roomId); + if (call) return call; + + if (Modulator.hasConferenceHandler()) { + var ConferenceHandler = Modulator.getConferenceHandler(); + call = ConferenceHandler.getConferenceCallForRoom(roomId); + } + if (call) return call; + + return null; + }, + + getCall: function(roomId) { + return calls[roomId] || null; + }, + + getAnyActiveCall: function() { + var roomsWithCalls = Object.keys(calls); + for (var i = 0; i < roomsWithCalls.length; i++) { + if (calls[roomsWithCalls[i]] && + calls[roomsWithCalls[i]].call_state !== "ended") { + return calls[roomsWithCalls[i]]; + } + } + return null; + } +}; +// Only things in here which actually need to be global are the +// calls list (done separately) and making sure we only register +// with the dispatcher once (which uses this mechanism but checks +// separately). This could be tidied up. +if (global.mxCallHandler === undefined) { + global.mxCallHandler = callHandler; +} + +module.exports = global.mxCallHandler; diff --git a/src/ComponentBroker.js b/src/ComponentBroker.js deleted file mode 100644 index 6445e9472f..0000000000 --- a/src/ComponentBroker.js +++ /dev/null @@ -1,92 +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'; - -function load(name) { - var module = require("../skins/base/views/"+name); - return module; -}; - -var ComponentBroker = function() { - this.components = {}; -}; - -ComponentBroker.prototype = { - get: function(name) { - if (this.components[name]) { - return this.components[name]; - } - - this.components[name] = load(name); - return this.components[name]; - }, - - set: function(name, module) { - this.components[name] = module; - } -}; - -// We define one Component Broker globally, because the intention is -// very much that it is a singleton. Relying on there only being one -// copy of the module can be dicey and not work as browserify's -// behaviour with multiple copies of files etc. is erratic at best. -// XXX: We can still end up with the same file twice in the resulting -// JS bundle which is nonideal. -if (global.componentBroker === undefined) { - global.componentBroker = new ComponentBroker(); -} -module.exports = global.componentBroker; - -// We need to tell browserify to include all the components -// by direct require syntax in here, but we don't want them -// to be evaluated in this file because then we wouldn't be -// able to override them. if (0) does this. -// Must be in this file (because the require is file-specific) and -// must be at the end because the components include this file. -if (0) { -require('../skins/base/views/atoms/LogoutButton'); -require('../skins/base/views/atoms/EnableNotificationsButton'); -require('../skins/base/views/atoms/MessageTimestamp'); -require('../skins/base/views/atoms/create_room/CreateRoomButton'); -require('../skins/base/views/atoms/create_room/RoomNameTextbox'); -require('../skins/base/views/atoms/create_room/Presets'); -require('../skins/base/views/atoms/EditableText'); -require('../skins/base/views/molecules/MatrixToolbar'); -require('../skins/base/views/molecules/RoomTile'); -require('../skins/base/views/molecules/MessageTile'); -require('../skins/base/views/molecules/SenderProfile'); -require('../skins/base/views/molecules/UnknownMessageTile'); -require('../skins/base/views/molecules/MTextTile'); -require('../skins/base/views/molecules/MNoticeTile'); -require('../skins/base/views/molecules/MEmoteTile'); -require('../skins/base/views/molecules/MImageTile'); -require('../skins/base/views/molecules/MFileTile'); -require('../skins/base/views/molecules/MRoomMemberTile'); -require('../skins/base/views/molecules/RoomHeader'); -require('../skins/base/views/molecules/MessageComposer'); -require('../skins/base/views/molecules/ProgressBar'); -require('../skins/base/views/molecules/ServerConfig'); -require('../skins/base/views/organisms/MemberList'); -require('../skins/base/views/molecules/MemberTile'); -require('../skins/base/views/organisms/RoomList'); -require('../skins/base/views/organisms/RoomView'); -require('../skins/base/views/templates/Login'); -require('../skins/base/views/templates/Register'); -require('../skins/base/views/organisms/Notifier'); -require('../skins/base/views/organisms/CreateRoom'); -require('../skins/base/views/molecules/UserSelector'); -} diff --git a/src/ContentMessages.js b/src/ContentMessages.js index fdd29fd58a..eba3011917 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -53,10 +53,14 @@ function sendContentToRoom(file, roomId, matrixClient) { body: file.name, info: { size: file.size, - mimetype: file.type } }; + // if we have a mime type for the file, add it to the message metadata + if (file.type) { + content.info.mimetype = file.type; + } + var def = q.defer(); if (file.type.indexOf('image/') == 0) { content.msgtype = 'm.image'; diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 6b36e67e6b..62c49a5f2d 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -23,6 +23,16 @@ var matrixClient = null; var localStorage = window.localStorage; +function deviceId() { + var id = Math.floor(Math.random()*16777215).toString(16); + id = "W" + "000000".substring(id.length) + id; + if (localStorage) { + id = localStorage.getItem("mx_device_id") || id; + localStorage.setItem("mx_device_id", id); + } + return id; +} + function createClient(hs_url, is_url, user_id, access_token) { var opts = { baseUrl: hs_url, @@ -31,6 +41,11 @@ function createClient(hs_url, is_url, user_id, access_token) { userId: user_id }; + if (localStorage) { + opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage); + opts.deviceId = deviceId(); + } + matrixClient = Matrix.createClient(opts); } @@ -44,23 +59,33 @@ if (localStorage) { } } -module.exports = { - get: function() { +class MatrixClient { + get() { return matrixClient; - }, + } - replaceUsingUrls: function(hs_url, is_url) { + unset() { + matrixClient = null; + } + + replaceUsingUrls(hs_url, is_url) { matrixClient = Matrix.createClient({ baseUrl: hs_url, idBaseUrl: is_url }); - }, + } - replaceUsingAccessToken: function(hs_url, is_url, user_id, access_token) { - createClient(hs_url, is_url, user_id, access_token); + replaceUsingAccessToken(hs_url, is_url, user_id, access_token) { if (localStorage) { try { localStorage.clear(); + } catch (e) { + console.warn("Error using local storage"); + } + } + createClient(hs_url, is_url, user_id, access_token); + if (localStorage) { + try { localStorage.setItem("mx_hs_url", hs_url); localStorage.setItem("mx_is_url", is_url); localStorage.setItem("mx_user_id", user_id); @@ -72,5 +97,9 @@ module.exports = { console.warn("No local storage available: can't persist session!"); } } -}; +} +if (!global.mxMatrixClient) { + global.mxMatrixClient = new MatrixClient(); +} +module.exports = global.mxMatrixClient; diff --git a/src/MatrixTools.js b/src/MatrixTools.js new file mode 100644 index 0000000000..5c6dca8b33 --- /dev/null +++ b/src/MatrixTools.js @@ -0,0 +1,36 @@ +/* +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. +*/ + +module.exports = { + /** + * Given a room object, return the canonical alias for it + * if there is one. Otherwise return null; + */ + getCanonicalAliasForRoom: function(room) { + var aliasEvents = room.currentState.getStateEvents( + "m.room.aliases" + ); + // Canonical aliases aren't implemented yet, so just return the first + for (var j = 0; j < aliasEvents.length; j++) { + var aliases = aliasEvents[j].getContent().aliases; + if (aliases && aliases.length) { + return aliases[0]; + } + } + return null; + } +} + diff --git a/src/Modal.js b/src/Modal.js new file mode 100644 index 0000000000..f34ef65d59 --- /dev/null +++ b/src/Modal.js @@ -0,0 +1,61 @@ +/* +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 = { + DialogContainerId: "mx_Dialog_Container", + + getOrCreateContainer: function() { + var container = document.getElementById(this.DialogContainerId); + + if (!container) { + container = document.createElement("div"); + container.id = this.DialogContainerId; + document.body.appendChild(container); + } + + return container; + }, + + createDialog: function (Element, props) { + var self = this; + + var closeDialog = function() { + React.unmountComponentAtNode(self.getOrCreateContainer()); + + if (props && props.onFinished) props.onFinished.apply(null, arguments); + }; + + // FIXME: If a dialog uses getDefaultProps it clobbers the onFinished + // property set here so you can't close the dialog from a button click! + var dialog = ( +
    +
    + +
    +
    +
    + ); + + React.render(dialog, this.getOrCreateContainer()); + + return {close: closeDialog}; + }, +}; diff --git a/src/Modulator.js b/src/Modulator.js new file mode 100644 index 0000000000..72fcc14d89 --- /dev/null +++ b/src/Modulator.js @@ -0,0 +1,111 @@ +/* +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/Presence.js b/src/Presence.js new file mode 100644 index 0000000000..d77058abd8 --- /dev/null +++ b/src/Presence.js @@ -0,0 +1,107 @@ +/* +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"); + + // 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 = { + + /** + * 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; + 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(); }; + this._resetTimer(); + } + }, + + /** + * Stop tracking user activity + */ + stop: function() { + this.running = false; + if (timer) { + clearTimeout(timer); + timer = undefined; + } + state = undefined; + }, + + /** + * Get the current presence state. + * @returns {string} the presence state (see PRESENCE enum) + */ + getState: function() { + return 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) { + return; + } + if (PRESENCE_STATES.indexOf(newState) === -1) { + throw new Error("Bad presence state: " + newState); + } + if (!this.running) { + return; + } + state = newState; + MatrixClientPeg.get().setPresence(state).done(function() { + console.log("Presence: %s", newState); + }, function(err) { + console.error("Failed to set presence: %s", err); + }); + }, + + /** + * Callback called when the user made no action on the page for UNAVAILABLE_TIME ms. + * @private + */ + _onUnavailableTimerFire: function() { + this.setState("unavailable"); + }, + + /** + * Callback called when the user made an action on the page + * @private + */ + _resetTimer: function() { + var self = this; + this.setState("online"); + // Re-arm the timer + clearTimeout(timer); + timer = setTimeout(function() { + self._onUnavailableTimerFire(); + }, UNAVAILABLE_TIME_MS); + } +}; diff --git a/src/RoomListSorter.js b/src/RoomListSorter.js index bc7a001670..730a0de18b 100644 --- a/src/RoomListSorter.js +++ b/src/RoomListSorter.js @@ -17,7 +17,12 @@ limitations under the License. 'use strict'; function tsOfNewestEvent(room) { - return room.timeline[room.timeline.length - 1].getTs(); + if (room.timeline.length) { + return room.timeline[room.timeline.length - 1].getTs(); + } + else { + return Number.MAX_SAFE_INTEGER; + } } function mostRecentActivityFirst(roomList) { diff --git a/src/Skinner.js b/src/Skinner.js new file mode 100644 index 0000000000..ae48d85633 --- /dev/null +++ b/src/Skinner.js @@ -0,0 +1,63 @@ +/* +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. +*/ + +class Skinner { + constructor() { + this.components = null; + } + + getComponent(name) { + if (this.components === null) { + throw new Error( + "Attempted to get a component before a skin has been loaded."+ + "This is probably because either:"+ + " a) Your app has not called sdk.loadSkin(), or"+ + " b) A component has called getComponent at the root level" + ); + } + var comp = this.components[name]; + if (comp) { + return comp; + } + throw new Error("No such component: "+name); + } + + load(skinObject) { + if (this.components !== null) { + throw new Error( + "Attempted to load a skin while a skin is already loaded"+ + "If you want to change the active skin, call resetSkin first" + ); + } + this.components = skinObject; + } + + reset() { + this.components = null; + } +} + +// We define one Skinner globally, because the intention is +// very much that it is a singleton. Relying on there only being one +// copy of the module can be dicey and not work as browserify's +// behaviour with multiple copies of files etc. is erratic at best. +// XXX: We can still end up with the same file twice in the resulting +// JS bundle which is nonideal. +if (global.mxSkinner === undefined) { + global.mxSkinner = new Skinner(); +} +module.exports = global.mxSkinner; + diff --git a/src/SlashCommands.js b/src/SlashCommands.js new file mode 100644 index 0000000000..08d68331f8 --- /dev/null +++ b/src/SlashCommands.js @@ -0,0 +1,312 @@ +/* +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"); +var encryption = require("./encryption"); + +var reject = function(msg) { + return { + error: msg + }; +}; + +var success = function(promise) { + return { + promise: promise + }; +}; + +var commands = { + // Change your nickname + nick: function(room_id, args) { + if (args) { + return success( + MatrixClientPeg.get().setDisplayName(args) + ); + } + return reject("Usage: /nick "); + }, + + encrypt: function(room_id, args) { + if (args == "on") { + var client = MatrixClientPeg.get(); + var members = client.getRoom(room_id).currentState.members; + var user_ids = Object.keys(members); + return success( + encryption.enableEncryption(client, room_id, user_ids) + ); + } + if (args == "off") { + var client = MatrixClientPeg.get(); + return success( + encryption.disableEncryption(client, room_id) + ); + + } + return reject("Usage: encrypt "); + }, + + // Change the room topic + topic: function(room_id, args) { + if (args) { + return success( + MatrixClientPeg.get().setRoomTopic(room_id, args) + ); + } + return reject("Usage: /topic "); + }, + + // Invite a user + invite: function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+)$/); + if (matches) { + return success( + MatrixClientPeg.get().invite(room_id, matches[1]) + ); + } + } + return reject("Usage: /invite "); + }, + + // Join a room + join: function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+)$/); + if (matches) { + var room_alias = matches[1]; + if (room_alias[0] !== '#') { + return reject("Usage: /join #alias:domain"); + } + if (!room_alias.match(/:/)) { + var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, ''); + room_alias += ':' + domain; + } + + // Try to find a room with this alias + var rooms = MatrixClientPeg.get().getRooms(); + var roomId; + for (var i = 0; i < rooms.length; i++) { + var aliasEvents = rooms[i].currentState.getStateEvents( + "m.room.aliases" + ); + for (var j = 0; j < aliasEvents.length; j++) { + var aliases = aliasEvents[j].getContent().aliases || []; + for (var k = 0; k < aliases.length; k++) { + if (aliases[k] === room_alias) { + roomId = rooms[i].roomId; + break; + } + } + if (roomId) { break; } + } + if (roomId) { break; } + } + if (roomId) { // we've already joined this room, view it. + dis.dispatch({ + action: 'view_room', + room_id: roomId + }); + return success(); + } + else { + // attempt to join this alias. + return success( + MatrixClientPeg.get().joinRoom(room_alias).then( + function(room) { + dis.dispatch({ + action: 'view_room', + room_id: room.roomId + }); + }) + ); + } + } + } + return reject("Usage: /join "); + }, + + part: function(room_id, args) { + var targetRoomId; + if (args) { + var matches = args.match(/^(\S+)$/); + if (matches) { + var room_alias = matches[1]; + if (room_alias[0] !== '#') { + return reject("Usage: /part [#alias:domain]"); + } + if (!room_alias.match(/:/)) { + var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, ''); + room_alias += ':' + domain; + } + + // Try to find a room with this alias + var rooms = MatrixClientPeg.get().getRooms(); + for (var i = 0; i < rooms.length; i++) { + var aliasEvents = rooms[i].currentState.getStateEvents( + "m.room.aliases" + ); + for (var j = 0; j < aliasEvents.length; j++) { + var aliases = aliasEvents[j].getContent().aliases || []; + for (var k = 0; k < aliases.length; k++) { + if (aliases[k] === room_alias) { + targetRoomId = rooms[i].roomId; + break; + } + } + if (targetRoomId) { break; } + } + if (targetRoomId) { break; } + } + } + if (!targetRoomId) { + return reject("Unrecognised room alias: " + room_alias); + } + } + if (!targetRoomId) targetRoomId = room_id; + return success( + MatrixClientPeg.get().leave(targetRoomId).then( + function() { + dis.dispatch({action: 'view_next_room'}); + }) + ); + }, + + // Kick a user from the room with an optional reason + kick: function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+?)( +(.*))?$/); + if (matches) { + return success( + MatrixClientPeg.get().kick(room_id, matches[1], matches[3]) + ); + } + } + return reject("Usage: /kick []"); + }, + + // Ban a user from the room with an optional reason + ban: function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+?)( +(.*))?$/); + if (matches) { + return success( + MatrixClientPeg.get().ban(room_id, matches[1], matches[3]) + ); + } + } + return reject("Usage: /ban []"); + }, + + // Unban a user from the room + unban: function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+)$/); + if (matches) { + // Reset the user membership to "leave" to unban him + return success( + MatrixClientPeg.get().unban(room_id, matches[1]) + ); + } + } + return reject("Usage: /unban "); + }, + + // Define the power level of a user + op: function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+?)( +(\d+))?$/); + var powerLevel = 50; // default power level for op + if (matches) { + var user_id = matches[1]; + if (matches.length === 4 && undefined !== matches[3]) { + powerLevel = parseInt(matches[3]); + } + if (powerLevel !== NaN) { + var room = MatrixClientPeg.get().getRoom(room_id); + if (!room) { + return reject("Bad room ID: " + room_id); + } + var powerLevelEvent = room.currentState.getStateEvents( + "m.room.power_levels", "" + ); + return success( + MatrixClientPeg.get().setPowerLevel( + room_id, user_id, powerLevel, powerLevelEvent + ) + ); + } + } + } + return reject("Usage: /op []"); + }, + + // Reset the power level of a user + deop: function(room_id, args) { + if (args) { + var matches = args.match(/^(\S+)$/); + if (matches) { + var room = MatrixClientPeg.get().getRoom(room_id); + if (!room) { + return reject("Bad room ID: " + room_id); + } + + var powerLevelEvent = room.currentState.getStateEvents( + "m.room.power_levels", "" + ); + return success( + MatrixClientPeg.get().setPowerLevel( + room_id, args, undefined, powerLevelEvent + ) + ); + } + } + return reject("Usage: /deop "); + } +}; + +// helpful aliases +commands.j = commands.join; + +module.exports = { + /** + * Process the given text for /commands and perform them. + * @param {string} roomId The room in which the command was performed. + * @param {string} input The raw text input by the user. + * @return {Object|null} An object with the property 'error' if there was an error + * processing the command, or 'promise' if a request was sent out. + * Returns null if the input didn't match a command. + */ + processInput: function(roomId, input) { + // trim any trailing whitespace, as it can confuse the parser for + // IRC-style commands + input = input.replace(/\s+$/, ""); + if (input[0] === "/" && input[1] !== "/") { + var bits = input.match(/^(\S+?)( +(.*))?$/); + var cmd = bits[1].substring(1).toLowerCase(); + var args = bits[3]; + if (cmd === "me") return null; + if (commands[cmd]) { + return commands[cmd](roomId, args); + } + else { + return reject("Unrecognised command: " + input); + } + } + return null; // not a command + } +}; diff --git a/src/TextForEvent.js b/src/TextForEvent.js new file mode 100644 index 0000000000..3d6ba2cf64 --- /dev/null +++ b/src/TextForEvent.js @@ -0,0 +1,106 @@ + +function textForMemberEvent(ev) { + // XXX: SYJS-16 + var senderName = ev.sender ? ev.sender.name : ev.getSender(); + var targetName = ev.target ? ev.target.name : ev.getStateKey(); + var reason = ev.getContent().reason ? ( + " Reason: " + ev.getContent().reason + ) : ""; + switch (ev.getContent().membership) { + case 'invite': + return senderName + " invited " + targetName + "."; + case 'ban': + return senderName + " banned " + targetName + "." + reason; + case 'join': + if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') { + if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) { + return ev.getSender() + " changed their display name from " + + ev.getPrevContent().displayname + " to " + + ev.getContent().displayname; + } else if (!ev.getPrevContent().displayname && ev.getContent().displayname) { + return ev.getSender() + " set their display name to " + ev.getContent().displayname; + } else if (ev.getPrevContent().displayname && !ev.getContent().displayname) { + return ev.getSender() + " removed their display name"; + } else if (ev.getPrevContent().avatar_url && !ev.getContent().avatar_url) { + return ev.getSender() + " removed their profile picture"; + } else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) { + return ev.getSender() + " changed their profile picture"; + } else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) { + return ev.getSender() + " set a profile picture"; + } + } else { + if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key); + return targetName + " joined the room."; + } + return ''; + case 'leave': + if (ev.getSender() === ev.getStateKey()) { + return targetName + " left the room."; + } + else if (ev.getPrevContent().membership === "ban") { + return senderName + " unbanned " + targetName + "."; + } + else if (ev.getPrevContent().membership === "join") { + return senderName + " kicked " + targetName + "." + reason; + } + else { + return targetName + " left the room."; + } + } +}; + +function textForTopicEvent(ev) { + var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); + + return senderDisplayName + ' changed the topic to, "' + ev.getContent().topic + '"'; +}; + +function textForMessageEvent(ev) { + var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); + + var message = senderDisplayName + ': ' + ev.getContent().body; + if (ev.getContent().msgtype === "m.emote") { + message = "* " + senderDisplayName + " " + message; + } else if (ev.getContent().msgtype === "m.image") { + message = senderDisplayName + " sent an image."; + } + return message; +}; + +function textForCallAnswerEvent(event) { + var senderName = event.sender ? event.sender.name : "Someone"; + return senderName + " answered the call."; +}; + +function textForCallHangupEvent(event) { + var senderName = event.sender ? event.sender.name : "Someone"; + return senderName + " ended the call."; +}; + +function textForCallInviteEvent(event) { + var senderName = event.sender ? event.sender.name : "Someone"; + // FIXME: Find a better way to determine this from the event? + var type = "voice"; + if (event.getContent().offer && event.getContent().offer.sdp && + event.getContent().offer.sdp.indexOf('m=video') !== -1) { + type = "video"; + } + return senderName + " placed a " + type + " call."; +}; + +var handlers = { + 'm.room.message': textForMessageEvent, + 'm.room.topic': textForTopicEvent, + 'm.room.member': textForMemberEvent, + 'm.call.invite': textForCallInviteEvent, + 'm.call.answer': textForCallAnswerEvent, + 'm.call.hangup': textForCallHangupEvent, +}; + +module.exports = { + textForEvent: function(ev) { + var hdlr = handlers[ev.getType()]; + if (!hdlr) return ""; + return hdlr(ev); + } +} diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js new file mode 100644 index 0000000000..4fb5399027 --- /dev/null +++ b/src/WhoIsTyping.js @@ -0,0 +1,49 @@ +var MatrixClientPeg = require("./MatrixClientPeg"); + +module.exports = { + usersTypingApartFromMe: function(room) { + return this.usersTyping( + room, [MatrixClientPeg.get().credentials.userId] + ); + }, + + /** + * Given a Room object and, optionally, a list of userID strings + * to exclude, return a list of user objects who are typing. + */ + usersTyping: function(room, exclude) { + var whoIsTyping = []; + + if (exclude === undefined) { + exclude = []; + } + + var memberKeys = Object.keys(room.currentState.members); + for (var i = 0; i < memberKeys.length; ++i) { + var userId = memberKeys[i]; + + if (room.currentState.members[userId].typing) { + if (exclude.indexOf(userId) == -1) { + whoIsTyping.push(room.currentState.members[userId]); + } + } + } + + return whoIsTyping; + }, + + whoIsTypingString: function(room) { + var whoIsTyping = this.usersTypingApartFromMe(room); + if (whoIsTyping.length == 0) { + return null; + } else if (whoIsTyping.length == 1) { + return whoIsTyping[0].name + ' is typing'; + } else { + var names = whoIsTyping.map(function(m) { + return m.name; + }); + var lastPerson = names.shift(); + return names.join(', ') + ' and ' + lastPerson + ' are typing'; + } + } +} diff --git a/src/controllers/atoms/EditableText.js b/src/controllers/atoms/EditableText.js index ac46973613..5ea4ce8c4a 100644 --- a/src/controllers/atoms/EditableText.js +++ b/src/controllers/atoms/EditableText.js @@ -21,7 +21,9 @@ var React = require('react'); module.exports = { propTypes: { onValueChanged: React.PropTypes.func, - initalValue: React.PropTypes.string, + initialValue: React.PropTypes.string, + label: React.PropTypes.string, + placeHolder: React.PropTypes.string, }, Phases: { @@ -32,37 +34,55 @@ module.exports = { getDefaultProps: function() { return { onValueChanged: function() {}, - initalValue: '', + initialValue: '', + label: 'Click to set', + placeholder: '', }; }, getInitialState: function() { return { - value: this.props.initalValue, + value: this.props.initialValue, phase: this.Phases.Display, } }, + componentWillReceiveProps: function(nextProps) { + this.setState({ + value: nextProps.initialValue + }); + }, + getValue: function() { return this.state.value; }, - setValue: function(val) { + setValue: function(val, shouldSubmit, suppressListener) { + var self = this; this.setState({ value: val, phase: this.Phases.Display, + }, function() { + if (!suppressListener) { + self.onValueChanged(shouldSubmit); + } }); + }, - this.onValueChanged(); + edit: function() { + this.setState({ + phase: this.Phases.Edit, + }); }, cancelEdit: function() { this.setState({ phase: this.Phases.Display, }); + this.onValueChanged(false); }, - onValueChanged: function() { - this.props.onValueChanged(this.state.value); + onValueChanged: function(shouldSubmit) { + this.props.onValueChanged(this.state.value, shouldSubmit); }, }; diff --git a/src/controllers/atoms/EnableNotificationsButton.js b/src/controllers/atoms/EnableNotificationsButton.js index c600f33013..3c399484e8 100644 --- a/src/controllers/atoms/EnableNotificationsButton.js +++ b/src/controllers/atoms/EnableNotificationsButton.js @@ -15,53 +15,44 @@ limitations under the License. */ 'use strict'; +var sdk = require('../../index'); +var dis = require("../../dispatcher"); module.exports = { - notificationsAvailable: function() { - return !!global.Notification; + + componentDidMount: function() { + this.dispatcherRef = dis.register(this.onAction); }, - havePermission: function() { - return global.Notification.permission == 'granted'; + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); + }, + + onAction: function(payload) { + if (payload.action !== "notifier_enabled") { + return; + } + this.forceUpdate(); }, enabled: function() { - if (!this.havePermission()) return false; - - if (!global.localStorage) return true; - - var enabled = global.localStorage.getItem('notifications_enabled'); - if (enabled === null) return true; - return enabled === 'true'; - }, - - disable: function() { - if (!global.localStorage) return; - global.localStorage.setItem('notifications_enabled', 'false'); - this.forceUpdate(); - }, - - enable: function() { - if (!this.havePermission()) { - var that = this; - global.Notification.requestPermission(function() { - that.forceUpdate(); - }); - } - - if (!global.localStorage) return; - global.localStorage.setItem('notifications_enabled', 'true'); - this.forceUpdate(); + var Notifier = sdk.getComponent('organisms.Notifier'); + return Notifier.isEnabled(); }, onClick: function() { - if (!this.notificationsAvailable()) { + var Notifier = sdk.getComponent('organisms.Notifier'); + var self = this; + if (!Notifier.supportsDesktopNotifications()) { return; } - if (!this.enabled()) { - this.enable(); + if (!Notifier.isEnabled()) { + Notifier.setEnabled(true, function() { + self.forceUpdate(); + }); } else { - this.disable(); + Notifier.setEnabled(false); } + this.forceUpdate(); }, }; diff --git a/src/controllers/atoms/MemberAvatar.js b/src/controllers/atoms/MemberAvatar.js new file mode 100644 index 0000000000..5d93f99947 --- /dev/null +++ b/src/controllers/atoms/MemberAvatar.js @@ -0,0 +1,75 @@ +/* +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: { + member: React.PropTypes.object.isRequired, + width: React.PropTypes.number, + height: React.PropTypes.number, + resizeMethod: React.PropTypes.string, + }, + + getDefaultProps: function() { + return { + width: 40, + height: 40, + resizeMethod: 'crop' + } + }, + + defaultAvatarUrl: function(member, width, height, resizeMethod) { + if (this.skinnedDefaultAvatarUrl) { + return this.skinnedDefaultAvatarUrl(member, width, height, resizeMethod); + } + return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9QjNbxSKP4eagAFnTseHFErFYLBaLxWKxWCwWi8Vi8cX4CzAABSwCRWJw31gAAAAASUVORK5CYII="; + }, + + onError: function(ev) { + // don't tightloop if the browser can't load a data url + if (ev.target.src == this.defaultAvatarUrl(this.props.member)) { + return; + } + this.setState({ + imageUrl: this.defaultAvatarUrl(this.props.member) + }); + }, + + getInitialState: function() { + var url = MatrixClientPeg.get().getAvatarUrlForMember( + this.props.member, + this.props.width, + this.props.height, + this.props.resizeMethod, + false + ); + if (!url) { + url = this.defaultAvatarUrl( + this.props.member, + this.props.width, + this.props.height, + this.props.resizeMethod + ); + } + return { + imageUrl: url + }; + } +}; diff --git a/src/controllers/atoms/RoomAvatar.js b/src/controllers/atoms/RoomAvatar.js new file mode 100644 index 0000000000..481483949a --- /dev/null +++ b/src/controllers/atoms/RoomAvatar.js @@ -0,0 +1,61 @@ +/* +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'); + +module.exports = { + getDefaultProps: function() { + return { + width: 40, + height: 40, + resizeMethod: 'crop' + } + }, + + avatarUrlForRoom: function(room) { + var url = MatrixClientPeg.get().getAvatarUrlForRoom( + room, + this.props.width, this.props.height, this.props.resizeMethod, + false + ); + if (url === null) { + url = this.defaultAvatarUrl(room); + } + return url; + }, + + defaultAvatarUrl: function(member) { + return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9QjNbxSKP4eagAFnTseHFErFYLBaLxWKxWCwWi8Vi8cX4CzAABSwCRWJw31gAAAAASUVORK5CYII="; + }, + + onError: function(ev) { + // don't tightloop if the browser can't load a data url + if (ev.target.src == this.defaultAvatarUrl(this.props.room)) { + return; + } + this.setState({ + imageUrl: this.defaultAvatarUrl(this.props.room) + }); + }, + + getInitialState: function() { + return { + imageUrl: this.avatarUrlForRoom(this.props.room) + }; + } +}; diff --git a/src/controllers/atoms/create_room/Presets.js b/src/controllers/atoms/create_room/Presets.js index 5ff7327e5a..bcc2f51481 100644 --- a/src/controllers/atoms/create_room/Presets.js +++ b/src/controllers/atoms/create_room/Presets.js @@ -18,24 +18,23 @@ limitations under the License. var React = require('react'); +var Presets = { + PrivateChat: "private_chat", + PublicChat: "public_chat", + Custom: "custom", +}; + module.exports = { propTypes: { - default_preset: React.PropTypes.string + onChange: React.PropTypes.func, + preset: React.PropTypes.string }, + Presets: Presets, + getDefaultProps: function() { return { - default_preset: 'private_chat', + onChange: function() {}, }; }, - - getInitialState: function() { - return { - preset: this.props.default_preset, - } - }, - - getPreset: function() { - return this.state.preset; - }, }; diff --git a/src/controllers/atoms/create_room/RoomAlias.js b/src/controllers/atoms/create_room/RoomAlias.js new file mode 100644 index 0000000000..b1176a2ab5 --- /dev/null +++ b/src/controllers/atoms/create_room/RoomAlias.js @@ -0,0 +1,47 @@ +/* +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/skins/base/css/molecules/MImageTile.css b/src/controllers/atoms/voip/VideoFeed.js similarity index 96% rename from skins/base/css/molecules/MImageTile.css rename to src/controllers/atoms/voip/VideoFeed.js index 775ebca925..3d34134daa 100644 --- a/skins/base/css/molecules/MImageTile.css +++ b/src/controllers/atoms/voip/VideoFeed.js @@ -14,6 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_MImageTile { -} +module.exports = { +}; diff --git a/src/controllers/molecules/ChangeAvatar.js b/src/controllers/molecules/ChangeAvatar.js new file mode 100644 index 0000000000..0df93b02fd --- /dev/null +++ b/src/controllers/molecules/ChangeAvatar.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. +*/ + +var React = require('react'); +var MatrixClientPeg = require("../../MatrixClientPeg"); + +module.exports = { + propTypes: { + onFinished: React.PropTypes.func, + initialAvatarUrl: React.PropTypes.string.isRequired, + }, + + Phases: { + Display: "display", + Uploading: "uploading", + Error: "error", + }, + + getDefaultProps: function() { + return { + onFinished: function() {}, + }; + }, + + getInitialState: function() { + return { + avatarUrl: this.props.initialAvatarUrl, + phase: this.Phases.Display, + } + }, + + setAvatarFromFile: function(file) { + var newUrl = null; + + this.setState({ + phase: this.Phases.Uploading + }); + var self = this; + MatrixClientPeg.get().uploadContent(file).then(function(url) { + newUrl = url; + return MatrixClientPeg.get().setAvatarUrl(url); + }).done(function() { + self.setState({ + phase: self.Phases.Display, + avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl) + }); + }, function(error) { + self.setState({ + phase: this.Phases.Error + }); + self.onError(error); + }); + }, +} diff --git a/src/controllers/molecules/ChangePassword.js b/src/controllers/molecules/ChangePassword.js new file mode 100644 index 0000000000..637e133a79 --- /dev/null +++ b/src/controllers/molecules/ChangePassword.js @@ -0,0 +1,76 @@ +/* +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() + }) + }); + }, +} diff --git a/skins/base/css/organisms/RoomList.css b/src/controllers/molecules/EventAsTextTile.js similarity index 96% rename from skins/base/css/organisms/RoomList.css rename to src/controllers/molecules/EventAsTextTile.js index e2dec3c9fd..3d34134daa 100644 --- a/skins/base/css/organisms/RoomList.css +++ b/src/controllers/molecules/EventAsTextTile.js @@ -14,5 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_RoomList { -} +module.exports = { +}; + diff --git a/src/controllers/molecules/MEmoteTile.js b/src/controllers/molecules/MEmoteTile.js index 8aa688b21e..1fb117ceef 100644 --- a/src/controllers/molecules/MEmoteTile.js +++ b/src/controllers/molecules/MEmoteTile.js @@ -16,6 +16,15 @@ limitations under the License. 'use strict'; +var linkify = require('linkifyjs'); +var linkifyElement = require('linkifyjs/element'); +var linkifyMatrix = require('../../linkify-matrix'); + +linkifyMatrix(linkify); + module.exports = { + componentDidMount: function() { + linkifyElement(this.refs.content.getDOMNode(), linkifyMatrix.options); + } }; diff --git a/src/controllers/molecules/MemberInfo.js b/src/controllers/molecules/MemberInfo.js new file mode 100644 index 0000000000..24e4afe5fd --- /dev/null +++ b/src/controllers/molecules/MemberInfo.js @@ -0,0 +1,317 @@ +/* +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. +*/ + +/* + * State vars: + * 'can': { + * kick: boolean, + * ban: boolean, + * mute: boolean, + * modifyLevel: boolean + * }, + * 'muted': boolean, + * 'isTargetMod': boolean + */ + +var MatrixClientPeg = require("../../MatrixClientPeg"); +var dis = require("../../dispatcher"); +var Modal = require("../../Modal"); +var sdk = require('../../index'); +var Loader = require("react-loader"); + +module.exports = { + componentDidMount: function() { + // work out the current state + if (this.props.member) { + var memberState = this._calculateOpsPermissions(); + this.setState(memberState); + } + }, + + onKick: function() { + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + var roomId = this.props.member.roomId; + var target = this.props.member.userId; + MatrixClientPeg.get().kick(roomId, target).done(function() { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Kick success"); + }, function(err) { + Modal.createDialog(ErrorDialog, { + title: "Kick error", + description: err.message + }); + }); + this.props.onFinished(); + }, + + onBan: function() { + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + var roomId = this.props.member.roomId; + var target = this.props.member.userId; + MatrixClientPeg.get().ban(roomId, target).done(function() { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Ban success"); + }, function(err) { + Modal.createDialog(ErrorDialog, { + title: "Ban error", + description: err.message + }); + }); + this.props.onFinished(); + }, + + onMuteToggle: function() { + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + var roomId = this.props.member.roomId; + var target = this.props.member.userId; + var room = MatrixClientPeg.get().getRoom(roomId); + if (!room) { + this.props.onFinished(); + return; + } + var powerLevelEvent = room.currentState.getStateEvents( + "m.room.power_levels", "" + ); + if (!powerLevelEvent) { + this.props.onFinished(); + return; + } + var isMuted = this.state.muted; + var powerLevels = powerLevelEvent.getContent(); + var levelToSend = ( + (powerLevels.events ? powerLevels.events["m.room.message"] : null) || + powerLevels.events_default + ); + var level; + if (isMuted) { // unmute + level = levelToSend; + } + else { // mute + level = levelToSend - 1; + } + + MatrixClientPeg.get().setPowerLevel(roomId, target, level, powerLevelEvent).done( + function() { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Mute toggle success"); + }, function(err) { + Modal.createDialog(ErrorDialog, { + title: "Mute error", + description: err.message + }); + }); + this.props.onFinished(); + }, + + onModToggle: function() { + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + var roomId = this.props.member.roomId; + var target = this.props.member.userId; + var room = MatrixClientPeg.get().getRoom(roomId); + if (!room) { + this.props.onFinished(); + return; + } + var powerLevelEvent = room.currentState.getStateEvents( + "m.room.power_levels", "" + ); + if (!powerLevelEvent) { + this.props.onFinished(); + return; + } + var me = room.getMember(MatrixClientPeg.get().credentials.userId); + if (!me) { + this.props.onFinished(); + return; + } + var defaultLevel = powerLevelEvent.getContent().users_default; + var modLevel = me.powerLevel - 1; + // toggle the level + var newLevel = this.state.isTargetMod ? defaultLevel : modLevel; + MatrixClientPeg.get().setPowerLevel(roomId, target, newLevel, powerLevelEvent).done( + function() { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Mod toggle success"); + }, function(err) { + Modal.createDialog(ErrorDialog, { + title: "Mod error", + description: err.message + }); + }); + this.props.onFinished(); + }, + + onChatClick: function() { + // check if there are any existing rooms with just us and them (1:1) + // If so, just view that room. If not, create a private room with them. + var rooms = MatrixClientPeg.get().getRooms(); + var userIds = [ + this.props.member.userId, + MatrixClientPeg.get().credentials.userId + ]; + var existingRoomId = null; + for (var i = 0; i < rooms.length; i++) { + var members = rooms[i].getJoinedMembers(); + if (members.length === 2) { + var hasTargetUsers = true; + for (var j = 0; j < members.length; j++) { + if (userIds.indexOf(members[j].userId) === -1) { + hasTargetUsers = false; + break; + } + } + if (hasTargetUsers) { + existingRoomId = rooms[i].roomId; + break; + } + } + } + + if (existingRoomId) { + dis.dispatch({ + action: 'view_room', + room_id: existingRoomId + }); + } + else { + MatrixClientPeg.get().createRoom({ + invite: [this.props.member.userId], + preset: "private_chat" + }).done(function(res) { + dis.dispatch({ + action: 'view_room', + room_id: res.room_id + }); + }, function(err) { + console.error( + "Failed to create room: %s", JSON.stringify(err) + ); + }); + } + this.props.onFinished(); + }, + + // FIXME: this is horribly duplicated with MemberTile's onLeaveClick. + // Not sure what the right solution to this is. + onLeaveClick: function() { + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + var QuestionDialog = sdk.getComponent("organisms.QuestionDialog"); + + var roomId = this.props.member.roomId; + Modal.createDialog(QuestionDialog, { + title: "Leave room", + description: "Are you sure you want to leave the room?", + onFinished: function(should_leave) { + if (should_leave) { + var d = MatrixClientPeg.get().leave(roomId); + + var modal = Modal.createDialog(Loader); + + d.then(function() { + modal.close(); + dis.dispatch({action: 'view_next_room'}); + }, function(err) { + modal.close(); + Modal.createDialog(ErrorDialog, { + title: "Failed to leave room", + description: err.toString() + }); + }); + } + } + }); + this.props.onFinished(); + }, + + getInitialState: function() { + return { + can: { + kick: false, + ban: false, + mute: false, + modifyLevel: false + }, + muted: false, + isTargetMod: false + } + }, + + _calculateOpsPermissions: function() { + var defaultPerms = { + can: {}, + muted: false, + modifyLevel: false + }; + var room = MatrixClientPeg.get().getRoom(this.props.member.roomId); + if (!room) { + return defaultPerms; + } + var powerLevels = room.currentState.getStateEvents( + "m.room.power_levels", "" + ); + if (!powerLevels) { + return defaultPerms; + } + var me = room.getMember(MatrixClientPeg.get().credentials.userId); + var them = this.props.member; + return { + can: this._calculateCanPermissions( + me, them, powerLevels.getContent() + ), + muted: this._isMuted(them, powerLevels.getContent()), + isTargetMod: them.powerLevel > powerLevels.getContent().users_default + }; + }, + + _calculateCanPermissions: function(me, them, powerLevels) { + var can = { + kick: false, + ban: false, + mute: false, + modifyLevel: false + }; + var canAffectUser = them.powerLevel < me.powerLevel; + if (!canAffectUser) { + //console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel); + return can; + } + var editPowerLevel = ( + (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || + powerLevels.state_default + ); + can.kick = me.powerLevel >= powerLevels.kick; + can.ban = me.powerLevel >= powerLevels.ban; + can.mute = me.powerLevel >= editPowerLevel; + can.modifyLevel = me.powerLevel > them.powerLevel; + return can; + }, + + _isMuted: function(member, powerLevelContent) { + if (!powerLevelContent || !member) { + return false; + } + var levelToSend = ( + (powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) || + powerLevelContent.events_default + ); + return member.powerLevel < levelToSend; + } +}; + diff --git a/src/controllers/molecules/MemberTile.js b/src/controllers/molecules/MemberTile.js index 811d2a78b1..53cae43ccd 100644 --- a/src/controllers/molecules/MemberTile.js +++ b/src/controllers/molecules/MemberTile.js @@ -17,14 +17,42 @@ limitations under the License. 'use strict'; var dis = require("../../dispatcher"); +var Modal = require("../../Modal"); +var sdk = require('../../index.js'); +var Loader = require("react-loader"); var MatrixClientPeg = require("../../MatrixClientPeg"); module.exports = { - onClick: function() { - dis.dispatch({ - action: 'view_user', - user_id: this.props.member.userId - }); + getInitialState: function() { + return {}; }, + + onLeaveClick: function() { + var QuestionDialog = sdk.getComponent("organisms.QuestionDialog"); + + var roomId = this.props.member.roomId; + Modal.createDialog(QuestionDialog, { + title: "Leave room", + description: "Are you sure you want to leave the room?", + onFinished: function(should_leave) { + if (should_leave) { + var d = MatrixClientPeg.get().leave(roomId); + + var modal = Modal.createDialog(Loader); + + d.then(function() { + modal.close(); + dis.dispatch({action: 'view_next_room'}); + }, function(err) { + modal.close(); + Modal.createDialog(ErrorDialog, { + title: "Failed to leave room", + description: err.toString() + }); + }); + } + } + }); + } }; diff --git a/src/controllers/molecules/MessageComposer.js b/src/controllers/molecules/MessageComposer.js index f55546ae87..c2b67c7898 100644 --- a/src/controllers/molecules/MessageComposer.js +++ b/src/controllers/molecules/MessageComposer.js @@ -14,19 +14,130 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - var MatrixClientPeg = require("../../MatrixClientPeg"); +var SlashCommands = require("../../SlashCommands"); +var Modal = require("../../Modal"); +var sdk = require('../../index'); var dis = require("../../dispatcher"); +var KeyCode = { + ENTER: 13, + TAB: 9, + SHIFT: 16, + UP: 38, + DOWN: 40 +}; + +var TYPING_USER_TIMEOUT = 10000; +var TYPING_SERVER_TIMEOUT = 30000; module.exports = { + componentWillMount: function() { + this.tabStruct = { + completing: false, + original: null, + index: 0 + }; + this.sentHistory = { + // The list of typed messages. Index 0 is more recent + data: [], + // The position in data currently displayed + position: -1, + // The room the history is for. + roomId: null, + // The original text before they hit UP + originalText: null, + // The textarea element to set text to. + element: null, + + init: function(element, roomId) { + this.roomId = roomId; + this.element = element; + this.position = -1; + var storedData = window.sessionStorage.getItem( + "history_" + roomId + ); + if (storedData) { + this.data = JSON.parse(storedData); + } + if (this.roomId) { + this.setLastTextEntry(); + } + }, + + push: function(text) { + // store a message in the sent history + this.data.unshift(text); + window.sessionStorage.setItem( + "history_" + this.roomId, + JSON.stringify(this.data) + ); + // reset history position + this.position = -1; + this.originalText = null; + }, + + // move in the history. Returns true if we managed to move. + next: function(offset) { + if (this.position === -1) { + // user is going into the history, save the current line. + this.originalText = this.element.value; + } + else { + // user may have modified this line in the history; remember it. + this.data[this.position] = this.element.value; + } + + if (offset > 0 && this.position === (this.data.length - 1)) { + // we've run out of history + return false; + } + + // retrieve the next item (bounded). + var newPosition = this.position + offset; + newPosition = Math.max(-1, newPosition); + newPosition = Math.min(newPosition, this.data.length - 1); + this.position = newPosition; + + if (this.position !== -1) { + // show the message + this.element.value = this.data[this.position]; + } + else if (this.originalText !== undefined) { + // restore the original text the user was typing. + this.element.value = this.originalText; + } + return true; + }, + + saveLastTextEntry: function() { + // save the currently entered text in order to restore it later. + // NB: This isn't 'originalText' because we want to restore + // sent history items too! + var text = this.element.value; + window.sessionStorage.setItem("input_" + this.roomId, text); + }, + + setLastTextEntry: function() { + var text = window.sessionStorage.getItem("input_" + this.roomId); + if (text) { + this.element.value = text; + } + } + }; + }, + componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); + this.sentHistory.init( + this.refs.textarea.getDOMNode(), + this.props.room.roomId + ); }, componentWillUnmount: function() { dis.unregister(this.dispatcherRef); + this.sentHistory.saveLastTextEntry(); }, onAction: function(payload) { @@ -38,30 +149,265 @@ module.exports = { }, onKeyDown: function (ev) { - if (ev.keyCode == 13) { - var contentText = this.refs.textarea.getDOMNode().value; - - var content = null; - if (/^\/me /i.test(contentText)) { - content = { - msgtype: 'm.emote', - body: contentText.substring(4) - }; - } else { - content = { - msgtype: 'm.text', - body: contentText - }; + if (ev.keyCode === KeyCode.ENTER) { + var input = this.refs.textarea.getDOMNode().value; + if (input.length === 0) { + ev.preventDefault(); + return; } - - MatrixClientPeg.get().sendMessage(this.props.roomId, content).then(function() { - dis.dispatch({ - action: 'message_sent' - }); - }); - this.refs.textarea.getDOMNode().value = ''; + this.sentHistory.push(input); + this.onEnter(ev); + } + else if (ev.keyCode === KeyCode.TAB) { + var members = []; + if (this.props.room) { + members = this.props.room.getJoinedMembers(); + } + this.onTab(ev, members); + } + else if (ev.keyCode === KeyCode.UP || ev.keyCode === KeyCode.DOWN) { + this.sentHistory.next( + ev.keyCode === KeyCode.UP ? 1 : -1 + ); ev.preventDefault(); } + else if (ev.keyCode !== KeyCode.SHIFT && this.tabStruct.completing) { + // they're resuming typing; reset tab complete state vars. + this.tabStruct.completing = false; + this.tabStruct.index = 0; + } + + var self = this; + setTimeout(function() { + if (self.refs.textarea && self.refs.textarea.getDOMNode().value != '') { + self.onTypingActivity(); + } else { + self.onFinishedTyping(); + } + }, 10); // XXX: what is this 10ms setTimeout doing? Looks hacky :( }, + + onEnter: function(ev) { + var contentText = this.refs.textarea.getDOMNode().value; + + var cmd = SlashCommands.processInput(this.props.room.roomId, contentText); + if (cmd) { + ev.preventDefault(); + if (!cmd.error) { + this.refs.textarea.getDOMNode().value = ''; + } + if (cmd.promise) { + cmd.promise.done(function() { + console.log("Command success."); + }, function(err) { + console.error("Command failure: %s", err); + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Server error", + description: err.message + }); + }); + } + else if (cmd.error) { + console.error(cmd.error); + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Command error", + description: cmd.error + }); + } + return; + } + + var content = null; + if (/^\/me /i.test(contentText)) { + content = { + msgtype: 'm.emote', + body: contentText.substring(4) + }; + } else { + content = { + msgtype: 'm.text', + body: contentText + }; + } + + MatrixClientPeg.get().sendMessage(this.props.room.roomId, content).then(function() { + dis.dispatch({ + action: 'message_sent' + }); + }, function() { + dis.dispatch({ + action: 'message_send_failed' + }); + }); + this.refs.textarea.getDOMNode().value = ''; + ev.preventDefault(); + }, + + onTab: function(ev, sortedMembers) { + var textArea = this.refs.textarea.getDOMNode(); + if (!this.tabStruct.completing) { + this.tabStruct.completing = true; + this.tabStruct.index = 0; + // cache starting text + this.tabStruct.original = textArea.value; + } + + // loop in the right direction + if (ev.shiftKey) { + this.tabStruct.index --; + if (this.tabStruct.index < 0) { + // wrap to the last search match, and fix up to a real index + // value after we've matched. + this.tabStruct.index = Number.MAX_VALUE; + } + } + else { + this.tabStruct.index++; + } + + var searchIndex = 0; + var targetIndex = this.tabStruct.index; + var text = this.tabStruct.original; + + var search = /@?([a-zA-Z0-9_\-:\.]+)$/.exec(text); + // console.log("Searched in '%s' - got %s", text, search); + if (targetIndex === 0) { // 0 is always the original text + textArea.value = text; + } + else if (search && search[1]) { + // console.log("search found: " + search+" from "+text); + var expansion; + + // FIXME: could do better than linear search here + for (var i=0; i= targetIndex) { + break; + } + var userId = sortedMembers[i].userId; + // === 1 because mxids are @username + if (userId.toLowerCase().indexOf(search[1].toLowerCase()) === 1) { + expansion = userId; + searchIndex++; + } + } + } + + if (searchIndex === targetIndex || + targetIndex === Number.MAX_VALUE) { + // xchat-style tab complete, add a colon if tab + // completing at the start of the text + if (search[0].length === text.length) { + expansion += ": "; + } + else { + expansion += " "; + } + textArea.value = text.replace( + /@?([a-zA-Z0-9_\-:\.]+)$/, expansion + ); + // cancel blink + textArea.style["background-color"] = ""; + if (targetIndex === Number.MAX_VALUE) { + // wrap the index around to the last index found + this.tabStruct.index = searchIndex; + targetIndex = searchIndex; + } + } + else { + // console.log("wrapped!"); + textArea.style["background-color"] = "#faa"; + setTimeout(function() { + textArea.style["background-color"] = ""; + }, 150); + textArea.value = text; + this.tabStruct.index = 0; + } + } + else { + this.tabStruct.index = 0; + } + // prevent the default TAB operation (typically focus shifting) + ev.preventDefault(); + }, + + onTypingActivity: function() { + this.isTyping = true; + if (!this.userTypingTimer) { + this.sendTyping(true); + } + this.startUserTypingTimer(); + this.startServerTypingTimer(); + }, + + onFinishedTyping: function() { + this.isTyping = false; + this.sendTyping(false); + this.stopUserTypingTimer(); + this.stopServerTypingTimer(); + }, + + startUserTypingTimer: function() { + this.stopUserTypingTimer(); + var self = this; + this.userTypingTimer = setTimeout(function() { + self.isTyping = false; + self.sendTyping(self.isTyping); + self.userTypingTimer = null; + }, TYPING_USER_TIMEOUT); + }, + + stopUserTypingTimer: function() { + if (this.userTypingTimer) { + clearTimeout(this.userTypingTimer); + this.userTypingTimer = null; + } + }, + + startServerTypingTimer: function() { + if (!this.serverTypingTimer) { + var self = this; + this.serverTypingTimer = setTimeout(function() { + if (self.isTyping) { + self.sendTyping(self.isTyping); + self.startServerTypingTimer(); + } + }, TYPING_SERVER_TIMEOUT / 2); + } + }, + + stopServerTypingTimer: function() { + if (this.serverTypingTimer) { + clearTimeout(this.servrTypingTimer); + this.serverTypingTimer = null; + } + }, + + sendTyping: function(isTyping) { + MatrixClientPeg.get().sendTyping( + this.props.room.roomId, + this.isTyping, TYPING_SERVER_TIMEOUT + ).done(); + }, + + refreshTyping: function() { + if (this.typingTimeout) { + clearTimeout(this.typingTimeout); + this.typingTimeout = null; + } + + } }; diff --git a/src/controllers/molecules/MessageTile.js b/src/controllers/molecules/MessageTile.js index 953e33b516..47b616e724 100644 --- a/src/controllers/molecules/MessageTile.js +++ b/src/controllers/molecules/MessageTile.js @@ -23,6 +23,28 @@ module.exports = { var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent); if (!actions || !actions.tweaks) { return false; } return actions.tweaks.highlight; + }, + + getInitialState: function() { + return { + resending: false + }; + }, + + onResend: function() { + var self = this; + self.setState({ + resending: true + }); + MatrixClientPeg.get().resendEvent( + this.props.mxEvent, MatrixClientPeg.get().getRoom( + this.props.mxEvent.getRoomId() + ) + ).finally(function() { + self.setState({ + resending: false + }); + }) } }; diff --git a/src/controllers/molecules/RoomHeader.js b/src/controllers/molecules/RoomHeader.js index 8aa688b21e..d3afce1e49 100644 --- a/src/controllers/molecules/RoomHeader.js +++ b/src/controllers/molecules/RoomHeader.js @@ -16,6 +16,81 @@ limitations under the License. 'use strict'; -module.exports = { -}; +/* + * State vars: + * this.state.call_state = the UI state of the call (see CallHandler) + */ +var React = require('react'); +var dis = require("../../dispatcher"); +var CallHandler = require("../../CallHandler"); + +module.exports = { + propTypes: { + room: React.PropTypes.object.isRequired, + editing: React.PropTypes.bool, + onSettingsClick: React.PropTypes.func, + onSaveClick: React.PropTypes.func, + }, + + getDefaultProps: function() { + return { + editing: false, + onSettingsClick: function() {}, + onSaveClick: function() {}, + }; + }, + + componentDidMount: function() { + this.dispatcherRef = dis.register(this.onAction); + if (this.props.room) { + var call = CallHandler.getCallForRoom(this.props.room.roomId); + var callState = call ? call.call_state : "ended"; + this.setState({ + call_state: callState + }); + } + }, + + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); + }, + + onAction: function(payload) { + // 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; + } + var call = CallHandler.getCallForRoom(payload.room_id); + var callState = call ? call.call_state : "ended"; + this.setState({ + call_state: callState + }); + }, + + onVideoClick: function() { + dis.dispatch({ + action: 'place_call', + type: "video", + room_id: this.props.room.roomId + }); + }, + onVoiceClick: function() { + dis.dispatch({ + action: 'place_call', + type: "voice", + room_id: this.props.room.roomId + }); + }, + onHangupClick: function() { + var call = CallHandler.getCallForRoom(this.props.room.roomId); + if (!call) { return; } + dis.dispatch({ + action: 'hangup', + // hangup the call for this room, which may not be the room in props + // (e.g. conferences which will hangup the 1:1 room instead) + room_id: call.roomId + }); + } +}; diff --git a/skins/base/css/molecules/MessageTile.css b/src/controllers/molecules/RoomSettings.js similarity index 70% rename from skins/base/css/molecules/MessageTile.css rename to src/controllers/molecules/RoomSettings.js index dae12e1a2b..3c0682d09a 100644 --- a/skins/base/css/molecules/MessageTile.css +++ b/src/controllers/molecules/RoomSettings.js @@ -14,22 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_MessageTile { - display: table-row; -} +var React = require('react'); -.mx_MessageTile_content { - display: table-cell; -} +module.exports = { + propTypes: { + room: React.PropTypes.object.isRequired, + }, -.mx_MessageTile_sending { - color: #ddd; -} - -.mx_MessageTile_notSent { - color: #f11; -} - -.mx_MessageTile_highlight { - color: #00f; -} + getInitialState: function() { + return { + power_levels_changed: false + }; + } +}; diff --git a/src/controllers/molecules/ServerConfig.js b/src/controllers/molecules/ServerConfig.js index 3cd5156ba8..3f5dd99bb5 100644 --- a/src/controllers/molecules/ServerConfig.js +++ b/src/controllers/molecules/ServerConfig.js @@ -30,26 +30,28 @@ module.exports = { return { onHsUrlChanged: function() {}, onIsUrlChanged: function() {}, - default_hs_url: 'https://matrix.org/', - default_is_url: 'https://matrix.org/' + defaultHsUrl: 'https://matrix.org/', + defaultIsUrl: 'https://matrix.org/' }; }, getInitialState: function() { return { - hs_url: this.props.default_hs_url, - is_url: this.props.default_is_url, + hs_url: this.props.defaultHsUrl, + is_url: this.props.defaultIsUrl, } }, hsChanged: function(ev) { - this.setState({hs_url: ev.target.value}); - this.props.onHsUrlChanged(this.state.hs_url); + this.setState({hs_url: ev.target.value}, function() { + this.props.onHsUrlChanged(this.state.hs_url); + }); }, isChanged: function(ev) { - this.setState({is_url: ev.target.value}); - this.props.onIsUrlChanged(this.state.is_url); + this.setState({is_url: ev.target.value}, function() { + this.props.onIsUrlChanged(this.state.is_url); + }); }, getHsUrl: function() { diff --git a/src/controllers/molecules/UserSelector.js b/src/controllers/molecules/UserSelector.js index e7e0509690..67a56163fa 100644 --- a/src/controllers/molecules/UserSelector.js +++ b/src/controllers/molecules/UserSelector.js @@ -20,38 +20,26 @@ var React = require('react'); module.exports = { propTypes: { - initially_selected: React.PropTypes.arrayOf(React.PropTypes.string), + onChange: React.PropTypes.func, + selected_users: React.PropTypes.arrayOf(React.PropTypes.string), }, getDefaultProps: function() { return { - initially_selected: [], + onChange: function() {}, + selected: [], }; }, - getInitialState: function() { - return { - selected_users: this.props.initially_selected, - } - }, - addUser: function(user_id) { - if (this.state.selected_users.indexOf(user_id == -1)) { - this.setState({ - selected_users: this.state.selected_users.concat([user_id]), - }); + if (this.props.selected_users.indexOf(user_id == -1)) { + this.props.onChange(this.props.selected_users.concat([user_id])); } }, removeUser: function(user_id) { - this.setState({ - selected_users: this.state.selected_users.filter(function(e) { - return e != user_id; - }), - }); + this.props.onChange(this.props.selected_users.filter(function(e) { + return e != user_id; + })); }, - - getUserIds: function() { - return this.state.selected_users; - } }; diff --git a/src/controllers/molecules/voip/CallView.js b/src/controllers/molecules/voip/CallView.js new file mode 100644 index 0000000000..c8fab1fba4 --- /dev/null +++ b/src/controllers/molecules/voip/CallView.js @@ -0,0 +1,70 @@ +/* +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"); + +/* + * State vars: + * this.state.call = MatrixCall|null + * + * Props: + * this.props.room = Room (JS SDK) + */ + +module.exports = { + + componentDidMount: function() { + this.dispatcherRef = dis.register(this.onAction); + if (this.props.room) { + this.showCall(this.props.room.roomId); + } + }, + + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); + }, + + 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') { + return; + } + this.showCall(payload.room_id); + }, + + showCall: function(roomId) { + var call = CallHandler.getCall(roomId); + if (call) { + call.setLocalVideoElement(this.getVideoView().getLocalVideoElement()); + // N.B. the remote video element is used for playback for audio for voice calls + call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement()); + } + if (call && call.type === "video" && call.state !== 'ended') { + this.getVideoView().getLocalVideoElement().style.display = "initial"; + this.getVideoView().getRemoteVideoElement().style.display = "initial"; + } + else { + this.getVideoView().getLocalVideoElement().style.display = "none"; + this.getVideoView().getRemoteVideoElement().style.display = "none"; + } + } +}; + diff --git a/src/controllers/molecules/voip/IncomingCallBox.js b/src/controllers/molecules/voip/IncomingCallBox.js new file mode 100644 index 0000000000..9ecced56c5 --- /dev/null +++ b/src/controllers/molecules/voip/IncomingCallBox.js @@ -0,0 +1,73 @@ +/* +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 + }); + } +}; + diff --git a/skins/base/css/molecules/MNoticeTile.css b/src/controllers/molecules/voip/VideoView.js similarity index 96% rename from skins/base/css/molecules/MNoticeTile.css rename to src/controllers/molecules/voip/VideoView.js index cac13e9b89..3d34134daa 100644 --- a/skins/base/css/molecules/MNoticeTile.css +++ b/src/controllers/molecules/voip/VideoView.js @@ -14,5 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_MNoticeTile { -} +module.exports = { +}; + diff --git a/src/controllers/organisms/CreateRoom.js b/src/controllers/organisms/CreateRoom.js index c2112ce58f..f6404eb231 100644 --- a/src/controllers/organisms/CreateRoom.js +++ b/src/controllers/organisms/CreateRoom.js @@ -18,6 +18,9 @@ limitations under the License. var React = require("react"); var MatrixClientPeg = require("../../MatrixClientPeg"); +var PresetValues = require('../atoms/create_room/Presets').Presets; +var q = require('q'); +var encryption = require("../../encryption"); module.exports = { propTypes: { @@ -41,25 +44,52 @@ module.exports = { return { phase: this.phases.CONFIG, error_string: "", + is_private: true, + share_history: false, + default_preset: PresetValues.PrivateChat, + topic: '', + room_name: '', + invited_users: [], }; }, onCreateRoom: function() { var options = {}; - var room_name = this.getName(); - if (room_name) { - options.name = room_name; + if (this.state.room_name) { + options.name = this.state.room_name; } - var preset = this.getPreset(); - if (preset) { - options.preset = preset; + if (this.state.topic) { + options.topic = this.state.topic; } - var invited_users = this.getInvitedUsers(); - if (invited_users) { - options.invite = invited_users; + if (this.state.preset) { + if (this.state.preset != PresetValues.Custom) { + options.preset = this.state.preset; + } else { + options.initial_state = [ + { + type: "m.room.join_rules", + content: { + "join_rules": this.state.is_private ? "invite" : "public" + } + }, + { + type: "m.room.history_visibility", + content: { + "history_visibility": this.state.share_history ? "shared" : "invited" + } + }, + ]; + } + } + + options.invite = this.state.invited_users; + + var alias = this.getAliasLocalpart(); + if (alias) { + options.room_alias_name = alias; } var cli = MatrixClientPeg.get(); @@ -69,7 +99,20 @@ module.exports = { return; } - var deferred = MatrixClientPeg.get().createRoom(options); + var deferred = cli.createRoom(options); + + var response; + + if (this.state.encrypt) { + deferred = deferred.then(function(res) { + response = res; + return encryption.enableEncryption( + cli, response.roomId, options.invite + ); + }).then(function() { + return q(response) } + ); + } this.setState({ phase: this.phases.CREATING, @@ -77,11 +120,11 @@ module.exports = { var self = this; - deferred.then(function () { + deferred.then(function (resp) { self.setState({ phase: self.phases.CREATED, }); - self.props.onRoomCreated(); + self.props.onRoomCreated(resp.room_id); }, function(err) { self.setState({ phase: self.phases.ERROR, diff --git a/skins/base/views/molecules/RoomHeader.js b/src/controllers/organisms/ErrorDialog.js similarity index 54% rename from skins/base/views/molecules/RoomHeader.js rename to src/controllers/organisms/ErrorDialog.js index b5296f4e82..6b7c35b3f2 100644 --- a/skins/base/views/molecules/RoomHeader.js +++ b/src/controllers/organisms/ErrorDialog.js @@ -14,22 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; +var React = require("react"); -var React = require('react'); - -var RoomHeaderController = require("../../../../src/controllers/molecules/RoomHeader"); - -module.exports = React.createClass({ - displayName: 'RoomHeader', - mixins: [RoomHeaderController], - - render: function() { - return ( -
    - {this.props.room.name} -
    - ); +module.exports = { + propTypes: { + title: React.PropTypes.string, + description: React.PropTypes.string, + button: React.PropTypes.string, + focus: React.PropTypes.bool, + onFinished: React.PropTypes.func.isRequired, }, -}); + getDefaultProps: function() { + return { + title: "Error", + description: "An error has occurred.", + button: "OK", + focus: true, + }; + }, +}; diff --git a/skins/base/css/molecules/MTextTile.css b/src/controllers/organisms/LogoutPrompt.js similarity index 62% rename from skins/base/css/molecules/MTextTile.css rename to src/controllers/organisms/LogoutPrompt.js index 5b117e41b8..5e5011ea97 100644 --- a/skins/base/css/molecules/MTextTile.css +++ b/src/controllers/organisms/LogoutPrompt.js @@ -14,7 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_MTextTile { - white-space: pre-wrap; -} +var dis = require("../../dispatcher"); + +module.exports = { + logOut: function() { + dis.dispatch({action: 'logout'}); + if (this.props.onFinished) { + this.props.onFinished(); + } + }, + + cancelPrompt: function() { + if (this.props.onFinished) { + this.props.onFinished(); + } + } +}; diff --git a/src/controllers/organisms/MemberList.js b/src/controllers/organisms/MemberList.js index a511816d53..bf61a80048 100644 --- a/src/controllers/organisms/MemberList.js +++ b/src/controllers/organisms/MemberList.js @@ -14,10 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -var React = require("react"); var MatrixClientPeg = require("../../MatrixClientPeg"); +var Modal = require("../../Modal"); +var sdk = require('../../index'); var INITIAL_LOAD_NUM_MEMBERS = 50; @@ -32,39 +31,137 @@ module.exports = { componentWillMount: function() { var cli = MatrixClientPeg.get(); cli.on("RoomState.members", this.onRoomStateMember); + cli.on("Room", this.onRoom); // invites }, componentWillUnmount: function() { if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("Room", this.onRoom); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); + MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn); } }, componentDidMount: function() { - var that = this; + var self = this; + + // Lazy-load in more than the first N members setTimeout(function() { - if (!that.isMounted()) return; - that.setState({ - memberDict: that.roomMembers() + if (!self.isMounted()) return; + self.setState({ + memberDict: self.roomMembers() }); }, 50); - }, + // Attach a SINGLE listener for global presence changes then locate the + // member tile and re-render it. This is more efficient than every tile + // evar attaching their own listener. + function updateUserState(event, user) { + // XXX: evil hack to track the age of this presence info. + // this should be removed once syjs-28 is resolved in the JS SDK itself. + user.lastPresenceTs = Date.now(); + + var tile = self.refs[user.userId]; + + console.log("presence event " + JSON.stringify(event) + " user = " + user + " tile = " + tile); + + if (tile) { + self._updateList(); // reorder the membership list + self.forceUpdate(); // FIXME: is the a more efficient way of reordering with react? + // XXX: do we even need to do this, or is it done by the main list? + tile.forceUpdate(); + } + } + // FIXME: we should probably also reset 'lastActiveAgo' to zero whenever + // we see a typing notif from a user, as we don't get presence updates for those. + MatrixClientPeg.get().on("User.presence", updateUserState); + this.userPresenceFn = updateUserState; + }, // Remember to set 'key' on a MemberList to the ID of the room it's for /*componentWillReceiveProps: function(newProps) { },*/ + onRoom: function(room) { + if (room.roomId !== this.props.roomId) { + return; + } + // We listen for room events because when we accept an invite + // we need to wait till the room is fully populated with state + // before refreshing the member list else we get a stale list. + this._updateList(); + }, + onRoomStateMember: function(ev, state, member) { + this._updateList(); + }, + + _updateList: function() { var members = this.roomMembers(); this.setState({ memberDict: members }); }, + onInvite: function(inputText) { + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + var self = this; + // sanity check the input + inputText = inputText.trim(); // react requires es5-shim so we know trim() exists + if (inputText[0] !== '@' || inputText.indexOf(":") === -1) { + console.error("Bad user ID to invite: %s", inputText); + Modal.createDialog(ErrorDialog, { + title: "Invite Error", + description: "Malformed user ID. Should look like '@localpart:domain'" + }); + return; + } + self.setState({ + inviting: true + }); + console.log("Invite %s to %s", inputText, this.props.roomId); + MatrixClientPeg.get().invite(this.props.roomId, inputText).done( + function(res) { + console.log("Invited"); + self.setState({ + inviting: false + }); + }, function(err) { + console.error("Failed to invite: %s", JSON.stringify(err)); + Modal.createDialog(ErrorDialog, { + title: "Server error whilst inviting", + description: err.message + }); + self.setState({ + inviting: false + }); + }); + }, + roomMembers: function(limit) { + if (!this.props.roomId) return {}; var cli = MatrixClientPeg.get(); - var all_members = cli.getRoom(this.props.roomId).currentState.members; + var room = cli.getRoom(this.props.roomId); + if (!room) return {}; + var all_members = room.currentState.members; var all_user_ids = Object.keys(all_members); + + // XXX: evil hack until SYJS-28 is fixed + all_user_ids.map(function(userId) { + if (all_members[userId].user && !all_members[userId].user.lastPresenceTs) { + all_members[userId].user.lastPresenceTs = Date.now(); + } + }); + + all_user_ids.sort(function(userIdA, userIdB) { + var userA = all_members[userIdA].user; + var userB = all_members[userIdB].user; + + var latA = userA ? (userA.lastPresenceTs - (userA.lastActiveAgo || userA.lastPresenceTs)) : 0; + var latB = userB ? (userB.lastPresenceTs - (userB.lastActiveAgo || userB.lastPresenceTs)) : 0; + + return latB - latA; + }); + var to_display = {}; var count = 0; for (var i = 0; i < all_user_ids.length && (limit === undefined || count < limit); ++i) { @@ -72,6 +169,8 @@ module.exports = { var m = all_members[user_id]; if (m.membership == 'join' || m.membership == 'invite') { + // XXX: this is evil, and relies on the fact that Object.keys() iterates + // over the keys of a dict in insertion order (if those keys are strings) to_display[user_id] = m; ++count; } diff --git a/src/controllers/organisms/Notifier.js b/src/controllers/organisms/Notifier.js index 63e937780d..8fb62abe40 100644 --- a/src/controllers/organisms/Notifier.js +++ b/src/controllers/organisms/Notifier.js @@ -17,11 +17,21 @@ limitations under the License. 'use strict'; var MatrixClientPeg = require("../../MatrixClientPeg"); +var dis = require("../../dispatcher"); + +/* + * Dispatches: + * { + * action: "notifier_enabled", + * value: boolean + * } + */ module.exports = { start: function() { this.boundOnRoomTimeline = this.onRoomTimeline.bind(this); MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline); + this.state = { 'toolbarHidden' : false }; }, stop: function() { @@ -30,12 +40,80 @@ module.exports = { } }, + supportsDesktopNotifications: function() { + return !!global.Notification; + }, + + havePermission: function() { + if (!this.supportsDesktopNotifications()) return false; + return global.Notification.permission == 'granted'; + }, + + setEnabled: function(enable, callback) { + if(enable) { + if (!this.havePermission()) { + global.Notification.requestPermission(function() { + if (callback) { + callback(); + dis.dispatch({ + action: "notifier_enabled", + value: true + }); + } + }); + } + + if (!global.localStorage) return; + global.localStorage.setItem('notifications_enabled', 'true'); + + if (this.havePermission) { + dis.dispatch({ + action: "notifier_enabled", + value: true + }); + } + } + else { + if (!global.localStorage) return; + global.localStorage.setItem('notifications_enabled', 'false'); + dis.dispatch({ + action: "notifier_enabled", + value: false + }); + } + + this.setToolbarHidden(false); + }, + + isEnabled: function() { + if (!this.havePermission()) return false; + + if (!global.localStorage) return true; + + var enabled = global.localStorage.getItem('notifications_enabled'); + if (enabled === null) return true; + return enabled === 'true'; + }, + + setToolbarHidden: function(hidden) { + this.state.toolbarHidden = hidden; + dis.dispatch({ + action: "notifier_enabled", + value: this.isEnabled() + }); + }, + + isToolbarHidden: function() { + return this.state.toolbarHidden; + }, + onRoomTimeline: function(ev, room, toStartOfTimeline) { if (toStartOfTimeline) return; if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return; - var enabled = global.localStorage.getItem('notifications_enabled'); - if (enabled === 'false') return; + if (!this.isEnabled()) { + return; + } var actions = MatrixClientPeg.get().getPushActionsForEvent(ev); if (actions && actions.notify) { diff --git a/skins/base/views/molecules/MessageComposer.js b/src/controllers/organisms/QuestionDialog.js similarity index 55% rename from skins/base/views/molecules/MessageComposer.js rename to src/controllers/organisms/QuestionDialog.js index 89c426cb2b..30891b839d 100644 --- a/skins/base/views/molecules/MessageComposer.js +++ b/src/controllers/organisms/QuestionDialog.js @@ -14,22 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; +var React = require("react"); -var React = require('react'); - -var MessageComposerController = require("../../../../src/controllers/molecules/MessageComposer"); - -module.exports = React.createClass({ - displayName: 'MessageComposer', - mixins: [MessageComposerController], - - render: function() { - return ( -
    -