Compare commits

...

109 Commits

Author SHA1 Message Date
David Baker
966f44baa1 bump js-sdk -> 0.3.0 on the right branch 2015-10-28 18:06:10 +00:00
Kegsay
184af9df76 Merge pull request #25 from matrix-org/246-creating-room-state
Add creatingRoom state to know when to show a spinner.
2015-10-28 11:38:47 +00:00
Kegan Dougal
2a1b9cd716 Add creatingRoom state to know when to show a spinner. 2015-10-27 17:01:03 +00:00
Kegsay
15af44f5fc Merge pull request #24 from matrix-org/linkify
Add callback support for linkified users/aliases
2015-10-27 12:25:20 +00:00
Kegsay
e6f9c6e777 Merge pull request #23 from matrix-org/kegan/reg-errors-176
Add missing enum value to registration password complexity check
2015-10-27 11:11:39 +00:00
Kegan Dougal
16ddb47466 Defer entirely to the end app for handling links 2015-10-27 10:44:41 +00:00
Kegsay
66b577dc89 Merge pull request #22 from matrix-org/kegan/delete-empty-files
Remove empty controllers
2015-10-27 10:19:14 +00:00
Kegan Dougal
77d1b9af04 Hook up aliases via listeners too. 2015-10-27 09:58:55 +00:00
Kegan Dougal
24ac801417 Invoke onUserClick to allow impls to do whatever on user clicks. 2015-10-26 17:59:49 +00:00
Kegan Dougal
11ef1ac336 Prevent the url from being butchered when clicking user IDs 2015-10-26 17:36:03 +00:00
Kegan Dougal
a1444d3214 Linkify room aliases. Add listener for user ID clicks. 2015-10-26 17:32:31 +00:00
Kegsay
a2b77ad5b5 Merge pull request #21 from matrix-org/220-login-error-msgs
Login error messages
2015-10-26 16:58:46 +00:00
David Baker
5a760b71d0 Make ChangeAvatar support room avatars and tweak RoomAvatar respond to componentWillReceiveProps 2015-10-23 17:34:53 +01:00
Matthew Hodgson
03dfd57a79 really kill mime types as they are ugly (3rd time lucky) 2015-10-22 16:32:27 +01:00
David Baker
7e93b75aa0 API Change: better fallback for room avatars 2015-10-22 13:08:35 +01:00
David Baker
549d992293 API change: Make EventTiles which hold stuff common to all events that appear in a room timeline. 2015-10-21 17:50:40 +01:00
Kegan Dougal
ac5111c162 Add missing enum value 2015-10-21 17:44:05 +01:00
Kegan Dougal
0488f03b5a Remove empty controllers 2015-10-21 15:36:59 +01:00
Kegan Dougal
d4a5ab11d4 Fix NPE if you cold boot vector on a URL with a room which you were invited to but not yet joined. 2015-10-21 14:45:39 +01:00
Kegan Dougal
d1af5a2232 More tweaks on error messages 2015-10-21 14:30:59 +01:00
Matthew Hodgson
feaf2a319b Merge pull request #20 from matrix-org/screen-sharing
Screen sharing
2015-10-21 01:35:16 +01:00
Matthew Hodgson
3b988b0eac set up remoteAudioElement 2015-10-21 01:21:39 +01:00
Matthew Hodgson
98ea35253a shift-click the video button to screenshare rather than overriding the button entirely. 2015-10-21 01:21:21 +01:00
Kegan Dougal
8ff7d87b38 Bodge to make video = screen sharing 2015-10-20 16:45:26 +01:00
Kegan Dougal
48f162b9df Better error messages 2015-10-20 14:03:37 +01:00
David Baker
3d8d9bac8e Allow the dispatcher to dispatch sync if required. 2015-10-20 11:02:54 +01:00
David Baker
1041ee654e Update for breaking js-sdk RoomAvatar / MemberAvatar changes 2015-10-20 10:31:29 +01:00
Kegan Dougal
78f2f7cfd0 Add in voip mute video/audio code. Needs dev js-sdk 2015-10-20 09:55:00 +01:00
David Baker
6baf405a05 Remove the 'resending' state which was duplicating a property of the event itself for no obvious reason. Remove onResend whose purpose was mostly to manage that state (and really should have been 'onResendClicked'). Listen for action to see when a message is resent. 2015-10-15 14:09:19 +01:00
David Baker
02a2e06d52 unused code 2015-10-13 16:03:24 +01:00
David Baker
9e596ebb75 Merge pull request #19 from stevenhammerton/sh-cas-auth
Add support for CAS login
2015-10-13 14:25:39 +01:00
David Baker
f7d3d4f9a9 Remove console.log 2015-10-13 11:44:45 +01:00
David Baker
d12ca92ea7 Avoid double updating: setting the state will cause a re-render so forcing an update is redundant. Also bump js sdk dep to newest to match vector. 2015-10-13 11:12:06 +01:00
Steven Hammerton
fc333067c2 Rename required var to match convention 2015-10-12 17:38:04 +01:00
David Baker
030124a59a Make state.members always defined 2015-10-12 16:25:49 +01:00
Steven Hammerton
4e0d930014 Pull down some CAS stuff from vector into controller and logic class 2015-10-12 10:20:03 +01:00
Matthew Hodgson
f6d577d0c6 track RHS collapse state 2015-10-11 16:07:01 +01:00
Matthew Hodgson
8228a7d485 track whether the LHS is collapsed. (shouldn't this be vector specific too?) 2015-10-11 13:49:44 +01:00
Matthew Hodgson
c5e3891a5a shrink default roomavatar size to 36x36. surely this is vector specific... 2015-10-11 02:08:39 +01:00
Steven Hammerton
3f67d8541f Add support for CAS login 2015-10-10 18:54:19 +01:00
David Baker
05d19121d8 Slightly change memberlist api to shift the sorting into the right place (in the skin). 2015-10-09 17:24:48 +01:00
David Baker
e158eec94d Unset matrix client first otherwise login sets it, the it gets unset and you can't log in again. 2015-10-09 13:48:17 +01:00
David Baker
53a7f4b3a8 Set state to ready only if the SDK is synced. 2015-10-09 12:05:40 +01:00
David Baker
0791cac572 Add method to Modal to create dialog with instantiated React elements as well as Classes. 2015-10-09 11:54:46 +01:00
David Baker
05f7a3b4d1 Remove now redundant displayname stuff from UserSettings now it's in ChangeDisplayName 2015-10-07 18:44:32 +01:00
David Baker
79e468217a Add button for user settings and a change display name widget 2015-10-07 18:19:29 +01:00
David Baker
27ca7b48f7 Just do all dispatches async: setting the flag obviously does not work for more than 2 nested dispatches. 2015-10-05 18:43:22 +01:00
David Baker
b8dd2452db Display correct message for when an invited but not joined user is kicked. 2015-10-05 16:44:50 +01:00
David Baker
a1892ee963 Improve url / screen handling
Including taking you to the room you asked for originally after login
2015-10-05 15:31:08 +01:00
David Baker
ad313d7711 Merge branch 'vector-merge' 2015-10-02 18:55:53 +01:00
David Baker
55f656f150 0.0.2 2015-10-02 18:55:09 +01:00
David Baker
711272a7c9 Merge pull request #18 from matrix-org/vector-merge
Vector merge
2015-10-02 18:50:52 +01:00
David Baker
2d3b87d56d Don't set empty mime types on metadata. 2015-10-02 18:37:15 +01:00
David Baker
2bce4e4d62 Merge c62d97ca04 from vector-im
The hack is *evil*. Not dirty.
2015-10-02 14:37:34 +01:00
David Baker
54048ee37c Update Readme 2015-10-01 16:02:21 +01:00
David Baker
7de136a930 Port over new logic for filtering actions: makes the end call button appear & disappear approriately 2015-10-01 10:19:18 +01:00
David Baker
5004a3a5b3 Make end call button work for conf calls 2015-10-01 09:42:58 +01:00
David Baker
cb89d3760a Hacks to make sure we don't end up with multiple split-brain CallHandlers when npm linked. 2015-09-30 18:21:25 +01:00
David Baker
b68665ead5 Add support for the basic notion of conference calls and an experimental concept of modules to provide the actual functionality. Rejig Skinner to be simpler. 2015-09-30 16:50:46 +01:00
David Baker
9fb5702c2f make MatrixClientPeg an actual global too otherwise things go very wierd 2015-09-28 17:46:49 +01:00
David Baker
8af6c2275b Make it easier to override default avatar urls 2015-09-28 17:06:13 +01:00
David Baker
3792d5494a Dispatcher should be a global too 2015-09-28 14:48:50 +01:00
David Baker
3be50e327d Manually merge memberlist fix from vector master 2015-09-28 11:32:00 +01:00
David Baker
4472f63b5f We need js sdk 0.2.1 for getRoomIdForAlias 2015-09-25 17:49:35 +01:00
David Baker
6348c2cf99 Change how viewing a room alias works to make way for jumping into a room once you've logged in. 2015-09-25 17:22:42 +01:00
David Baker
bc2eca16f9 Unused guff 2015-09-25 16:25:20 +01:00
David Baker
fe369858b7 Unused variables & redundant stuff 2015-09-25 15:17:46 +01:00
David Baker
56530d80d7 Add link to guthub issue about webpack's loader path silliness 2015-09-25 13:53:58 +01:00
David Baker
d172aaf41f Add -loader packages as deps
Even though we don't use webpack directly, webpack needs the loaders
to be in the dependency package's node_modules directory, so this
lets packages that depend on us use webpack.
2015-09-25 13:39:21 +01:00
David Baker
5af43dc6a9 Remove unused action dispatch 2015-09-23 09:39:49 +01:00
David Baker
96627d4477 Port membertile fix 2015-09-22 16:51:16 +01:00
David Baker
3838569625 Port memberlist branch fixes 2015-09-22 16:37:39 +01:00
David Baker
b32658cfd0 Load fewer events when switching to a room: they take surprisingly long to render. 2015-09-22 15:18:16 +01:00
David Baker
980ce7fdae Remainder of the controllers from vector 2015-09-21 17:23:51 +01:00
David Baker
49c5f7cb95 Use same protocol as client was loaded over for recaptcha to prevent JS origin errors. 2015-09-21 16:53:50 +01:00
David Baker
1b82d92fa1 Port registration fixes 2015-09-21 16:38:12 +01:00
David Baker
65498600de port login fixes 2015-09-21 16:36:17 +01:00
David Baker
28c4a648be Port roomlist fixes 2015-09-21 16:31:31 +01:00
David Baker
e2c9afb278 port create room 2015-09-21 16:28:39 +01:00
David Baker
29d2ed7191 Merge userselector changes 2015-09-21 16:17:29 +01:00
David Baker
82aa603596 Merge in resend support 2015-09-21 16:14:19 +01:00
David Baker
a8eb93bd6f Member list 2015-09-18 18:39:16 +01:00
David Baker
31ee667102 Room header 2015-09-18 14:34:36 +01:00
David Baker
b9538a077c Missed files from last commit 2015-09-18 13:54:20 +01:00
David Baker
343de6245f Port ServerConfig changes 2015-09-18 13:33:51 +01:00
David Baker
08b5888d03 More porting: make sending messages work again! 2015-09-18 10:44:57 +01:00
David Baker
abeed92501 Partial porting over of vector controller logic to react sdk. 2015-09-17 18:23:38 +01:00
David Baker
6eb18f0268 Fix reskindex 2015-09-17 18:21:35 +01:00
David Baker
d938ba70d3 Port over room leaving 2015-09-17 12:10:01 +01:00
David Baker
88aaf82c88 Backport: linkify emotes 2015-09-16 16:23:35 +01:00
David Baker
f3b30477ce Backport: add other presets into room presets 2015-09-16 16:10:39 +01:00
David Baker
a4cbbf0d92 Backport Notifier improvements from Vector, including TextForEvent 2015-09-16 14:48:49 +01:00
David Baker
25ab56106a Backport labels & placeholders for Editable text from Vector 2015-09-16 14:18:25 +01:00
David Baker
6cca5f4c05 backport fixes from vector 2015-09-16 13:48:24 +01:00
David Baker
aba4c1e9af We don't use catw here anymore 2015-09-15 14:46:41 +01:00
David Baker
2d0c8ac9ff Working skin stuff, minus css 2015-09-15 13:34:36 +01:00
David Baker
f3b9f8c799 WIP reworking of skinning and app integration process 2015-09-11 15:42:11 +01:00
David Baker
9b73d6ed6d react-tools is dead. Long live Babel. 2015-09-10 15:28:30 +01:00
David Baker
a06e1f23ea Spurious react-loader require 2015-09-10 15:08:26 +01:00
David Baker
635041470f There is no MatrixChat in the sdk anymore (well, only a controller). Advertise the Component Broker instead. 2015-09-10 15:07:34 +01:00
David Baker
a124e53a9a Component broker loading files never really worked very well anyway, and now it can't work because they're all defined elsewhere. 2015-09-10 15:06:42 +01:00
David Baker
6cc88e4ef3 Remove stuff that's going into the base skin 2015-09-09 16:57:55 +01:00
David Baker
e1a6ede17b Ignore all module caches and make the built js the default include root 2015-09-08 18:45:00 +01:00
David Baker
8fbce5fce8 de-tab 2015-09-08 16:17:06 +01:00
David Baker
2d9419c380 Merge pull request #17 from ndarilek/master
Make HTML more semantic to improve accessibility
2015-08-10 13:45:36 +01:00
Nolan Darilek
bd0db01515 Add landmarks around major view regions for easier navigation. 2015-08-08 18:09:37 -05:00
Nolan Darilek
517bb01f33 Event tiles are now items in a list, improves accessibility. 2015-08-08 17:36:19 -05:00
David Baker
ec8a815688 Ignore olm. There is no olm. 2015-08-07 11:51:13 +01:00
David Baker
5a87b9759f update to new js-sdk 2015-08-07 11:27:13 +01:00
117 changed files with 4073 additions and 2948 deletions

4
.gitignore vendored
View File

@@ -1,4 +1,2 @@
node_modules
build
bundle.css
bundle.js
lib

View File

@@ -1,3 +1,3 @@
example
examples
build/.module-cache
.module-cache

110
README.md
View File

@@ -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/<skin name>`. 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.

View File

@@ -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 (
<span ref="content" className="mx_MTextTile mx_MessageTile_content" onClick={this.onClick}>
{content.body}
</span>
);
},
onClick: function(ev) {
global.alert(this.props.mxEvent.getContent().body);
}
});

View File

@@ -1,4 +0,0 @@
matrix-react-example
====================
An example of how to use the Matrix React SDK to build a more customised app

View File

@@ -1,12 +0,0 @@
<!doctype html>
<html lang="en" style="height: 100%; overflow: hidden">
<head>
<meta charset="utf-8">
<title>Matrix React SDK Custom Example</title>
</head>
<body style="height: 100%; ">
<section id="matrixchat" style="height: 100%; "></section>
<script src="bundle.js"></script>
<link rel="stylesheet" href="node_modules/matrix-react-sdk/bundle.css">
</body>
</html>

View File

@@ -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(
<MatrixReactSdk.MatrixChat />,
document.getElementById('matrixchat')
);

View File

@@ -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"
}
}

View File

@@ -1,4 +0,0 @@
matrix-react-example
====================
A simple example of how to use the Matrix React SDK

View File

@@ -1,12 +0,0 @@
<!doctype html>
<html lang="en" style="height: 100%; overflow: hidden">
<head>
<meta charset="utf-8">
<title>Matrix React SDK Example</title>
</head>
<body style="height: 100%;">
<section id="matrixchat" style="height: 100%;"></section>
<script src="bundle.js"></script>
<link rel="stylesheet" href="node_modules/matrix-react-sdk/bundle.css">
</body>
</html>

View File

@@ -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(
<MatrixReactSdk.MatrixChat onNewScreen={onNewScreen} registrationUrl={makeRegistrationUrl()} />,
document.getElementById('matrixchat')
);

View File

@@ -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 -t [ envify --NODE_ENV production ] -t reactify index.js | uglifyjs -c -m -o bundle.js",
"start": "parallelshell 'watchify -v -d -t reactify index.js -o bundle.js' 'http-server'"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "0.0.1",
"version": "0.0.2",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@@ -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.1.1",
"glob": "^5.0.14",
"linkifyjs": "^2.0.0-beta.4",
"matrix-js-sdk": "^0.3.0",
"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"
}
}

83
reskindex.js Executable file
View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -1,19 +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_MImageTile {
}

View File

@@ -1,18 +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_MNoticeTile {
}

View File

@@ -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_MTextTile {
white-space: pre-wrap;
}

View File

@@ -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;
}

View File

@@ -1,35 +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_MessageTile {
display: table-row;
}
.mx_MessageTile_content {
display: table-cell;
}
.mx_MessageTile_sending {
color: #ddd;
}
.mx_MessageTile_notSent {
color: #f11;
}
.mx_MessageTile_highlight {
color: #00f;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 {
}

View File

@@ -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;
}

View File

@@ -1,18 +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_RoomList {
}

View File

@@ -1,82 +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_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;
}

View File

@@ -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%;
}

View File

@@ -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;
}

View File

@@ -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 = <div ref="display_div" onClick={this.onClickDiv}>{this.state.value}</div>;
} else if (this.state.phase == this.Phases.Edit) {
editable_el = (
<div>
<input type="text" defaultValue={this.state.value} onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onFinish} autoFocus/>
</div>
);
}
return (
<div className="mx_EditableText">
{editable_el}
</div>
);
}
});

View File

@@ -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 (
<button className="mx_EnableNotificationsButton" onClick={this.onClick}>Disable Notifications</button>
);
} else {
return (
<button className="mx_EnableNotificationsButton" onClick={this.onClick}>Enable Notifications</button>
);
}
}
});

View File

@@ -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 (
<button className="mx_LogoutButton" onClick={this.onClick}>Sign out</button>
);
}
});

View File

@@ -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 (
<span className="mx_MessageTimestamp">
{date.toLocaleTimeString()}
</span>
);
},
});

View File

@@ -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 (
<button className="mx_CreateRoomButton" onClick={this.onClick}>Create Room</button>
);
}
});

View File

@@ -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 (
<select className="mx_Presets" onChange={this.onValueChanged} defaultValue={this.state.preset}>
<option value="private_chat">Private Chat</option>
<option value="public_chat">Public Chat</option>
</select>
);
}
});

View File

@@ -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 (
<input type="text" className="mx_RoomNameTextbox" placeholder="ex. MyNewRoom" onChange={this.onValueChanged}/>
);
}
});

View File

@@ -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 (
<span className="mx_MEmoteTile mx_MessageTile_content">
* {name} {content.body}
</span>
);
},
});

View File

@@ -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 (
<span className="mx_MFileTile">
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
{this.presentableTextForFile(content)}
</a>
</span>
);
},
});

View File

@@ -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 (
<span className="mx_MImageTile">
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
<img src={cli.mxcUrlToHttp(content.url, 320, 240)} alt={content.body} style={imgStyle} />
</a>
</span>
);
},
});

View File

@@ -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 (
<span ref="content" className="mx_MNoticeTile mx_MessageTile_content">
{content.body}
</span>
);
},
});

View File

@@ -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 (
<div className="mx_MessageTile">
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
<span className="mx_SenderProfile"></span>
<span className="mx_MessageTile_content">
{this.getMemberEventText()}
</span>
</div>
);
},
});

View File

@@ -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 (
<span ref="content" className="mx_MTextTile mx_MessageTile_content">
{content.body}
</span>
);
},
});

View File

@@ -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 (
<div className="mx_MatrixToolbar">
<LogoutButton />
<EnableNotificationsButton />
</div>
);
}
});

View File

@@ -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 (
<div className="mx_MemberTile">
<div className="mx_MemberTile_name">{this.props.member.name}</div>
</div>
);
}
});

View File

@@ -1,35 +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 MessageComposerController = require("../../../../src/controllers/molecules/MessageComposer");
module.exports = React.createClass({
displayName: 'MessageComposer',
mixins: [MessageComposerController],
render: function() {
return (
<div className="mx_MessageComposer">
<textarea ref="textarea" onKeyDown={this.onKeyDown} />
</div>
);
},
});

View File

@@ -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 (
<div className={classes}>
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
<SenderProfile mxEvent={this.props.mxEvent} />
<TileType mxEvent={this.props.mxEvent} />
</div>
);
},
});

View File

@@ -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 (
<div className="mx_ProgressBar"><div className="mx_ProgressBar_fill" style={progressStyle}></div></div>
);
}
});

View File

@@ -1,35 +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 RoomHeaderController = require("../../../../src/controllers/molecules/RoomHeader");
module.exports = React.createClass({
displayName: 'RoomHeader',
mixins: [RoomHeaderController],
render: function() {
return (
<div className="mx_RoomHeader">
{this.props.room.name}
</div>
);
},
});

View File

@@ -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 (
<div className={classes} onClick={this.onClick}>
<div className="mx_RoomTile_name">{this.props.room.name}</div>
</div>
);
}
});

View File

@@ -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 (
<span className="mx_SenderProfile">
{name}
</span>
);
},
});

View File

@@ -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 (
<div className="HomeServerTextBox">
<table className="serverConfig">
<tr>
<td>Home Server URL</td>
<td><input type="text" value={this.state.hs_url} onChange={this.hsChanged} /></td>
</tr>
<tr>
<td>Identity Server URL</td>
<td><input type="text" value={this.state.is_url} onChange={this.isChanged} /></td>
</tr>
</table>
</div>
);
}
});

View File

@@ -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 (
<span className="mx_UnknownMessageTile">
?
</span>
);
},
});

View File

@@ -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 (
<div>
<ul className="mx_UserSelector_UserIdList" ref="list">
{this.state.selected_users.map(function(user_id, i) {
return <li key={user_id}>{user_id}</li>
})}
</ul>
<input type="text" ref="user_id_input" className="mx_UserSelector_userIdInput" placeholder="ex. @bob:example.com"/>
<button onClick={this.onAddUserId} className="mx_UserSelector_AddUserId">Add User</button>
</div>
);
}
});

View File

@@ -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 (
<div>Creating...</div>
);
} else {
var error_box = "";
if (curr_phase == this.phases.ERROR) {
error_box = (
<div className="mx_Error">
An error occured: {this.state.error_string}
</div>
);
}
return (
<div className="mx_CreateRoom">
<label>Room Name <RoomNameTextbox ref="name_textbox" /></label>
<Presets ref="presets"/>
<UserSelector ref="user_selector"/>
<CreateRoomButton onCreateRoom={this.onCreateRoom} />
{error_box}
</div>
);
}
}
});

View File

@@ -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 (
<li key={userId}>
<MemberTile
member={m}
/>
</li>
);
});
},
render: function() {
return (
<div className="mx_MemberList">
<ul>
{this.makeMemberTiles()}
</ul>
</div>
);
}
});

View File

@@ -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();

View File

@@ -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 (
<div className="mx_RoomList">
{this.makeRoomTiles()}
</div>
);
}
});

View File

@@ -1,95 +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 (
<div />
);
}
var myUserId = MatrixClientPeg.get().credentials.userId;
if (this.state.room.currentState.members[myUserId].membership == 'invite') {
if (this.state.joining) {
return (
<div className="mx_RoomView">
<Loader />
</div>
);
} 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 (
<div className="mx_RoomView">
<div className="mx_RoomView_invitePrompt">
<div>{inviteEvent.user_id} has invited you to a room</div>
<button ref="joinButton" onClick={this.onJoinButtonClicked}>Join</button>
<div className="error">{joinErrorText}</div>
</div>
</div>
);
}
} else {
var scrollheader_classes = classNames({
mx_RoomView_scrollheader: true,
loading: this.state.paginating
});
return (
<div className="mx_RoomView">
<RoomHeader room={this.state.room} />
<div className="mx_RoomView_roomWrapper">
<div className="mx_RoomView_messagePanel">
<div ref="messageWrapper" className="mx_RoomView_messageListWrapper" onScroll={this.onMessageListScroll}>
<div className="mx_RoomView_MessageList" aria-live="polite">
<div className={scrollheader_classes}>
</div>
{this.getEventTiles()}
</div>
</div>
<MessageComposer roomId={this.props.roomId} />
</div>
<MemberList roomId={this.props.roomId} key={this.props.roomId} />
</div>
</div>
);
}
},
});

View File

@@ -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 (
<div className="mx_MatrixChat">
<div className="mx_MatrixChat_chatWrapper">
<div className="mx_MatrixChat_leftPanel">
<RoomList selectedRoom={this.state.currentRoom} />
<MatrixToolbar />
</div>
<RoomView roomId={this.state.currentRoom} key={this.state.currentRoom} />
</div>
</div>
);
} else if (this.state.logged_in) {
return (
<Loader />
);
} else if (this.state.screen == 'register') {
return (
<Register onLoggedIn={this.onLoggedIn} clientSecret={this.state.register_client_secret}
sessionId={this.state.register_session_id} idSid={this.state.register_id_sid}
hsUrl={this.state.register_hs_url} isUrl={this.state.register_is_url}
registrationUrl={this.props.registrationUrl}
/>
);
} else {
return (
<Login onLoggedIn={this.onLoggedIn} />
);
}
}
});

View File

@@ -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 (
<div>
<form onSubmit={this.onHSChosen}>
<ServerConfig ref="serverConfig" />
<input type="submit" value="Continue" />
</form>
</div>
);
// XXX: clearly these should be separate organisms
case 'stage_m.login.password':
return (
<div>
<form onSubmit={this.onUserPassEntered}>
<input ref="user" type="text" placeholder="username" /><br />
<input ref="pass" type="password" placeholder="password" /><br />
<input type="submit" value="Log in" />
</form>
</div>
);
}
},
loginContent: function() {
if (this.state.busy) {
return (
<Loader />
);
} else {
return (
<div>
<h1>Please log in:</h1>
{this.componentForStep(this.state.step)}
<div className="error">{this.state.errorText}</div>
<a onClick={this.showRegister} href="#">Create a new account</a>
</div>
);
}
},
render: function() {
return (
<div className="mx_Login">
<ProgressBar value={this.state.currentStep} max={this.state.totalSteps} />
{this.loginContent()}
</div>
);
}
});

View File

@@ -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 (
<div>
<form onSubmit={this.onInitialStageSubmit}>
Email: <input type="text" ref="email" defaultValue={this.savedParams.email} /><br />
Username: <input type="text" ref="username" defaultValue={this.savedParams.username} /><br />
Password: <input type="password" ref="password" defaultValue={this.savedParams.password} /><br />
Confirm Password: <input type="password" ref="confirmPassword" defaultValue={this.savedParams.confirmPassword} /><br />
<ServerConfig ref="serverConfig" />
<input type="submit" value="Continue" />
</form>
</div>
);
// XXX: clearly these should be separate organisms
case 'stage_m.login.email.identity':
return (
<div>
Please check your email to continue registration.
</div>
);
case 'stage_m.login.recaptcha':
return (
<div ref="recaptchaContainer">
This Home Server would like to make sure you're not a robot
<div id="mx_recaptcha"></div>
</div>
);
}
},
registerContent: function() {
if (this.state.busy) {
return (
<Loader />
);
} else {
return (
<div>
<h1>Create a new account:</h1>
{this.componentForStep(this.state.step)}
<div className="error">{this.state.errorText}</div>
<a onClick={this.showLogin} href="#">Sign in with existing account</a>
</div>
);
}
},
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 (
<div className="mx_Register">
{this.registerContent()}
</div>
);
}
});

308
src/CallHandler.js Normal file
View File

@@ -0,0 +1,308 @@
/*
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: <room ID of the call>
* }
*
* 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: <room that the place call button was pressed in>
* }
*
* {
* action: 'incoming_call'
* call: MatrixCall
* }
*
* {
* action: 'hangup'
* room_id: <room that the hangup button was pressed in>
* }
*
* {
* action: 'answer'
* room_id: <room that the answer button was pressed in>
* }
*/
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 if (payload.type === 'screensharing') {
newCall.placeScreenSharingCall(
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;

View File

@@ -16,6 +16,13 @@ limitations under the License.
'use strict';
module.exports = {
};
var url = require ('url');
function getServiceUrl() {
var parsedUrl = url.parse(window.location.href);
return parsedUrl.protocol + "//" + parsedUrl.host + parsedUrl.pathname;
}
module.exports = {
getServiceUrl: getServiceUrl
};

View File

@@ -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');
}

View File

@@ -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';

View File

@@ -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;

61
src/MatrixTools.js Normal file
View File

@@ -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.
*/
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;
},
/**
* Given a list of room objects, return the room which has the given alias,
* else null.
*/
getRoomForAlias: function(rooms, room_alias) {
var room;
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) {
room = rooms[i];
break;
}
}
if (room) { break; }
}
if (room) { break; }
}
return room || null;
}
}

84
src/Modal.js Normal file
View File

@@ -0,0 +1,84 @@
/*
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;
},
createDialogWithElement: function(element, props) {
var self = this;
var closeDialog = function() {
React.unmountComponentAtNode(self.getOrCreateContainer());
if (props && props.onFinished) props.onFinished.apply(null, arguments);
};
var dialog = (
<div className="mx_Dialog_wrapper">
<div className="mx_Dialog">
{element}
</div>
<div className="mx_Dialog_background" onClick={closeDialog}></div>
</div>
);
React.render(dialog, this.getOrCreateContainer());
return {close: closeDialog};
},
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 = (
<div className="mx_Dialog_wrapper">
<div className="mx_Dialog">
<Element {...props} onFinished={closeDialog}/>
</div>
<div className="mx_Dialog_background" onClick={closeDialog}></div>
</div>
);
React.render(dialog, this.getOrCreateContainer());
return {close: closeDialog};
},
};

111
src/Modulator.js Normal file
View File

@@ -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;

107
src/Presence.js Normal file
View File

@@ -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);
}
};

View File

@@ -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) {

63
src/Skinner.js Normal file
View File

@@ -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;

299
src/SlashCommands.js Normal file
View File

@@ -0,0 +1,299 @@
/*
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 MatrixTools = require("./MatrixTools");
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 <display_name>");
},
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 <on/off>");
},
// Change the room topic
topic: function(room_id, args) {
if (args) {
return success(
MatrixClientPeg.get().setRoomTopic(room_id, args)
);
}
return reject("Usage: /topic <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 <userId>");
},
// 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 foundRoom = MatrixTools.getRoomForAlias(
MatrixClientPeg.get().getRooms(),
room_alias
);
if (foundRoom) { // we've already joined this room, view it.
dis.dispatch({
action: 'view_room',
room_id: foundRoom.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 <room_alias>");
},
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 <userId> [<reason>]");
},
// 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 <userId> [<reason>]");
},
// 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 <userId>");
},
// 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 <userId> [<power level>]");
},
// 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 <userId>");
}
};
// 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
}
};

109
src/TextForEvent.js Normal file
View File

@@ -0,0 +1,109 @@
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 if (ev.getPrevContent().membership === "invite") {
return senderName + " withdrew " + targetName + "'s invitation." + 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);
}
}

49
src/WhoIsTyping.js Normal file
View File

@@ -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';
}
}
}

View File

@@ -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);
},
};

View File

@@ -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();
},
};

View File

@@ -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 = this.props.member.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
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
};
}
};

View File

@@ -0,0 +1,112 @@
/*
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');
/*
* View class should provide:
* - getUrlList() returning an array of URLs to try for the room avatar
in order of preference from the most preferred at index 0. null entries
in the array will be skipped over.
*/
module.exports = {
getDefaultProps: function() {
return {
width: 36,
height: 36,
resizeMethod: 'crop'
}
},
getInitialState: function() {
this._update();
return {
imageUrl: this._nextUrl()
};
},
componentWillReceiveProps: function(nextProps) {
this._update();
this.setState({
imageUrl: this._nextUrl()
});
},
_update: function() {
this.urlList = this.getUrlList();
this.urlListIndex = -1;
},
_nextUrl: function() {
do {
++this.urlListIndex;
} while (
this.urlList[this.urlListIndex] === null &&
this.urlListIndex < this.urlList.length
);
if (this.urlListIndex < this.urlList.length) {
return this.urlList[this.urlListIndex];
} else {
return null;
}
},
// provided to the view class for convenience
roomAvatarUrl: function() {
var url = this.props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
this.props.width, this.props.height, this.props.resizeMethod,
false
);
return url;
},
// provided to the view class for convenience
getOneToOneAvatar: function() {
var userIds = Object.keys(this.props.room.currentState.members);
if (userIds.length == 2) {
var theOtherGuy = null;
if (this.props.room.currentState.members[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
theOtherGuy = this.props.room.currentState.members[userIds[1]];
} else {
theOtherGuy = this.props.room.currentState.members[userIds[0]];
}
return theOtherGuy.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
this.props.width, this.props.height, this.props.resizeMethod,
false
);
} else if (userIds.length == 1) {
return this.props.room.currentState.members[userIds[0]].getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
this.props.width, this.props.height, this.props.resizeMethod,
false
);
} else {
return null;
}
},
onError: function(ev) {
this.setState({
imageUrl: this._nextUrl()
});
}
};

View File

@@ -16,6 +16,12 @@ limitations under the License.
'use strict';
module.exports = {
};
var dis = require("../../dispatcher");
module.exports = {
onClick: function() {
dis.dispatch({
action: 'view_user_settings'
});
},
};

View File

@@ -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;
},
};

View File

@@ -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;
},
};

View File

@@ -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 React = require('react');
var MatrixClientPeg = require("../../MatrixClientPeg");
module.exports = {
propTypes: {
initialAvatarUrl: React.PropTypes.string,
room: React.PropTypes.object,
},
Phases: {
Display: "display",
Uploading: "uploading",
Error: "error",
},
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;
if (self.props.room) {
return MatrixClientPeg.get().sendStateEvent(
self.props.room.roomId,
'm.room.avatar',
{url: url},
''
);
} else {
return MatrixClientPeg.get().setAvatarUrl(url);
}
}).done(function() {
self.setState({
phase: self.Phases.Display,
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl)
});
}, function(error) {
self.setState({
phase: self.Phases.Error
});
self.onError(error);
});
},
}

View File

@@ -0,0 +1,71 @@
/*
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 {
onFinished: function() {},
};
},
getInitialState: function() {
return {
busy: false,
errorString: null
}
},
componentWillMount: function() {
var cli = MatrixClientPeg.get();
this.setState({busy: true});
var self = this;
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
self.setState({
displayName: result.displayname,
busy: false
});
}, function(error) {
self.setState({
errorString: "Failed to fetch display name",
busy: false
});
});
},
changeDisplayname: function(new_displayname) {
this.setState({
busy: true,
errorString: null,
})
var self = this;
MatrixClientPeg.get().setDisplayName(new_displayname).then(function() {
self.setState({
busy: false,
displayName: new_displayname
});
}, function(error) {
self.setState({
busy: false,
errorString: "Failed to set display name"
});
});
},
}

View File

@@ -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()
})
});
},
}

View File

@@ -16,6 +16,13 @@ limitations under the License.
'use strict';
var MatrixClientPeg = require("../../MatrixClientPeg");
module.exports = {
shouldHighlight: function() {
var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent);
if (!actions || !actions.tweaks) { return false; }
return actions.tweaks.highlight;
}
};

View File

@@ -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);
}
};

View File

@@ -27,9 +27,9 @@ module.exports = {
var additionals = [];
if (content.info) {
if (content.info.mimetype && content.info.mimetype.length > 0) {
additionals.push(content.info.mimetype);
}
// if (content.info.mimetype && content.info.mimetype.length > 0) {
// additionals.push(content.info.mimetype);
// }
if (content.info.size) {
additionals.push(filesize(content.info.size));
}

View File

@@ -0,0 +1,324 @@
/*
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 self = this;
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
});
this.props.onFinished();
}
else {
self.setState({ creatingRoom: true });
MatrixClientPeg.get().createRoom({
invite: [this.props.member.userId],
preset: "private_chat"
}).done(function(res) {
self.setState({ creatingRoom: false });
dis.dispatch({
action: 'view_room',
room_id: res.room_id
});
self.props.onFinished();
}, function(err) {
self.setState({ creatingRoom: false });
console.error(
"Failed to create room: %s", JSON.stringify(err)
);
self.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,
creatingRoom: 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;
}
};

View File

@@ -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()
});
});
}
}
});
}
};

View File

@@ -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<sortedMembers.length; i++) {
var member = sortedMembers[i];
if (member.name && searchIndex < targetIndex) {
if (member.name.toLowerCase().indexOf(search[1].toLowerCase()) === 0) {
expansion = member.name;
searchIndex++;
}
}
}
if (searchIndex < targetIndex) { // then search raw mxids
for (var i=0; i<sortedMembers.length; i++) {
if (searchIndex >= 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;
}
}
};

View File

@@ -19,10 +19,5 @@ limitations under the License.
var MatrixClientPeg = require("../../MatrixClientPeg");
module.exports = {
shouldHighlight: function() {
var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent);
if (!actions || !actions.tweaks) { return false; }
return actions.tweaks.highlight;
}
};

View File

@@ -16,6 +16,103 @@ 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(e) {
dis.dispatch({
action: 'place_call',
type: e.shiftKey ? "screensharing" : "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
});
},
onMuteAudioClick: function() {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
if (!call) {
return;
}
var newState = !call.isMicrophoneMuted();
call.setMicrophoneMuted(newState);
this.setState({
audioMuted: newState
});
},
onMuteVideoClick: function() {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
if (!call) {
return;
}
var newState = !call.isLocalVideoMuted();
call.setLocalVideoMuted(newState);
this.setState({
videoMuted: newState
});
}
};

View File

@@ -14,8 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var React = require('react');
module.exports = {
};
propTypes: {
room: React.PropTypes.object.isRequired,
},
getInitialState: function() {
return {
power_levels_changed: false
};
}
};

View File

@@ -1,21 +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';
module.exports = {
};

View File

@@ -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() {

View File

@@ -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.
*/
'use strict';
module.exports = {
};

View File

@@ -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;
}
};

View File

@@ -0,0 +1,72 @@
/*
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());
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
// give a separate element for audio stream playback - both for voice calls
// and for the voice stream of screen captures
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
}
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";
}
}
};

View File

@@ -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
});
}
};

Some files were not shown because too many files have changed in this diff Show More