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
265 changed files with 5254 additions and 7896 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,2 @@
node_modules
vector/bundle.*
lib

View File

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

View File

@@ -1,9 +0,0 @@
Vector is written mainly by the Vector team, building upon the Matrix React
SDK. Vector also welcomes external contributions. Third party contributors
include:
* Nolan Darilek (https://github.com/ndarilek)
Accessibility and semantic markup contributions
* https://github.com/neko259
Improved scrollbar CSS

View File

@@ -1,31 +0,0 @@
Changes in vector v0.1.2 (2015-10-28)
======================================
* Support Room Avatars
* Fullscreen video calls
* Mute mic in VoIP calls
* Fix bug with multiple desktop notifications
* Context menu on messages
* Better hover-over on member list
* Support CAS auth
* Many other bug fixes
Changes in vector v0.1.1 (2015-08-10)
======================================
* Support logging in with an email address
* Use the Vector identity server
* Fix a bug where the client was not stopped properly on logout
* Fix bugs where field values would be forgotten if login or registration failed
* Improve URL bar navigation
* Add explanatory help text on advanced server options
* Fix a bug which caused execptions on malformed VoIP invitations
* Remove superfluous scrollbars on Firefox
* Numerous CSS fixes
* Improved accessibility
* Support command-click / middle click to open image in a new tab
* Improved room directory
* Fix display of text with many combining unicode points
Changes in vector v0.1.0 (2015-08-10)
======================================
Initial release

View File

@@ -1,4 +0,0 @@
Contributing code to Vector
===========================
Vector follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst

170
README.md
View File

@@ -1,58 +1,142 @@
Vector/Web
==========
matrix-react-sdk
================
Vector is a Matrix web client built using the Matrix React SDK (https://github.com/matrix-org/matrix-react-sdk).
This is a react-based SDK for inserting a Matrix chat/voip client into a web page.
Getting started
===============
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/vector-im/vector-web.git`
3. Switch to the SDK directory: `cd vector-web`
4. Install the prerequisites: `npm install`
5. Start the development builder and a testing server: `npm start`
6. Wait a few seconds for the initial build to finish.
7. Open http://127.0.0.1:8080/ in your browser to see your newly built Vector.
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.
With `npm start`, any changes you make to the source files will cause a rebuild so
your changes will show up when you refresh.
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.
For production use, run `npm run build` to build all the necessary files
into the `vector` directory and run your own server.
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.
Development
===========
How to customise the SDK
========================
For simple tweaks, you can work on any of the source files within Vector with the
setup above, and your changes will cause an instant rebuild.
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.
In practice this means:
However, all serious development on Vector happens on the `develop` branch. This typically
depends on the `develop` snapshot versions of `matrix-react-sdk` and `matrix-js-sdk`
too, which isn't expressed in Vector's `package.json`. To do this, check out
the `develop` branches of these libraries and then use `npm link` to tell Vector
about them:
* The UI of the app is strictly split up into a hierarchy of components.
* Each component has its own:
* View object defined as a React javascript class containing embedded
HTML expressed in React's JSX notation.
* CSS file, which defines the styling specific to that component.
* Components are loosely grouped into the 5 levels outlined by atomic design:
* atoms: fundamental building blocks (e.g. a timestamp tag)
* molecules: "group of atoms which functions together as a unit"
(e.g. a message in a chat timeline)
* organisms: "groups of molecules (and atoms) which form a distinct section
of a UI" (e.g. a view of a chat room)
* templates: "a reusable configuration of organisms" - used to combine and
style organisms into a well-defined global look and feel
* pages: specific instances of templates.
1. `git clone git@github.com:matrix-org/matrix-react-sdk.git`
2. `cd matrix-react-sdk`
3. `git checkout develop`
4. `npm install`
5. `npm start` (to start the dev rebuilder)
6. `cd ../vector-web`
7. Link the react sdk package into the example:
`npm link path/to/your/react/sdk`
Good separation between the components is maintained by adopting various best
practices that anyone working with the SDK needs to be be aware of and uphold:
Similarly, you may need to `npm link path/to/your/js/sdk` in your `matrix-react-sdk`
directory.
* Views are named with upper camel case (e.g. molecules/MessageTile.js)
If you add or remove any components from the Vector skin, you will need to rebuild
the skin's index by running, `npm run reskindex`.
* The view's CSS file MUST have the same name (e.g. molecules/MessageTile.css)
You may need to run `npm i source-map-loader` in matrix-js-sdk if you get errors
about "Cannot resolve module 'source-map-loader'" due to shortcomings in webpack.
* Per-view CSS is optional - it could choose to inherit all its styling from
the context of the rest of the app, although this is unusual for any but
the simplest atoms and molecules.
Deployment
==========
* The view MUST *only* refer to the CSS rules defined in its own CSS file.
'Stealing' styling information from other components (including parents)
is not cool, as it breaks the independence of the components.
Just run `npm build` and then mount the `vector` directory on your webserver to
actually serve up the app, which is entirely static content.
* CSS classes are named with an app-specific namespacing prefix to try to avoid
CSS collisions. The base skin shipped by Matrix.org with the matrix-react-sdk
uses the naming prefix "mx_". A company called Yoyodyne Inc might use a
prefix like "yy_" for its app-specific classes.
* CSS classes use upper camel case when they describe React components - e.g.
.mx_MessageTile is the selector for the CSS applied to a MessageTile view.
* CSS classes for DOM elements within a view which aren't components are named
by appending a lower camel case identifier to the view's class name - e.g.
.mx_MessageTile_randomDiv is how you'd name the class of an arbitrary div
within the MessageTile view.
* We deliberately use vanilla CSS 3.0 to avoid adding any more magic
dependencies into the mix than we already have. App developers are welcome
to use whatever floats their boat however.
* The CSS for a component can however override the rules for child components.
For instance, .mx_RoomList .mx_RoomTile {} would be the selector to override
styles of RoomTiles when viewed in the context of a RoomList view.
Overrides *must* be scoped to the View's CSS class - i.e. don't just define
.mx_RoomTile {} in RoomList.css - only RoomTile.css is allowed to define its
own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override
only to the context of RoomList views. N.B. overrides should be relatively
rare as in general CSS inheritence should be enough.
* Components should render only within the bounding box of their outermost DOM
element. Page-absolute positioning and negative CSS margins and similar are
generally not cool and stop the component from being reused easily in
different places.
* We don't use the atomify library itself, as React already provides most
of the modularity requirements it brings to the table.
With all this in mind, here's how you go about skinning the react SDK UI
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 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.
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,4 +0,0 @@
{
"default_hs_url": "https://matrix.org",
"default_is_url": "https://vector.im"
}

View File

@@ -1,52 +0,0 @@
# VoIP Conferencing
This is a draft proposal for a naive voice/video conferencing implementation for
Matrix clients. There are many possible conferencing architectures possible for
Matrix (Multipoint Conferencing Unit (MCU); Stream Forwarding Unit (SFU); Peer-
to-Peer mesh (P2P), etc; events shared in the group room; events shared 1:1;
possibly even out-of-band signalling).
This is a starting point for a naive MCU implementation which could provide one
possible Matrix-wide solution in future, which retains backwards compatibility
with standard 1:1 calling.
* A client chooses to initiate a conference for a given room by starting a
voice or video call with a 'conference focus' user. This is a virtual user
(typically Application Service) which implements a conferencing bridge. It
isn't defined how the client discovers or selects this user.
* The conference focus user MUST join the room in which the client has
initiated the conference - this may require the client to invite the
conference focus user to the room, depending on the room's `join_rules`. The
conference focus user needs to be in the room to let the bridge eject users
from the conference who have left the room in which it was initiated, and aid
discovery of the conference by other users in the room. The bridge
identifies the room to join based on the user ID by which it was invited.
The format of this identifier is implementation dependent for now.
* If a client leaves the group chat room, they MUST be ejected from the
conference. If a client leaves the 1:1 room with the conference focus user,
they SHOULD be ejected from the conference.
* For now, rooms can contain multiple conference focus users - it's left to
user or client implementation to select which to converge on. In future this
could be mediated using a state event (e.g. `im.vector.call.mcu`), but we
can't do that right now as by default normal users can't set arbitrary state
events on a room.
* To participate in the conference, other clients initiates a standard 1:1
voice or video call to the conference focus user.
* For best UX, clients SHOULD show the ongoing voice/video call in the UI
context of the group room rather than 1:1 with the focus user. If a client
recognises a conference user present in the room, it MAY chose to highlight
this in the UI (e.g. with a "conference ongoing" notification, to aid
discovery). Clients MAY hide the 1:1 room with the focus user (although in
future this room could be used for floor control or other direct
communication with the conference focus)
* When all users have left the conference, the 'conference focus' user SHOULD
leave the room.
* If a conference focus user joins a room but does not receive a 1:1 voice or
video call, it SHOULD time out after a period of time and leave the room.

View File

@@ -1,48 +1,41 @@
{
"name": "vector-web",
"version": "0.1.2",
"description": "Vector webapp",
"name": "matrix-react-sdk",
"version": "0.0.2",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
"type": "git",
"url": "https://github.com/vector-im/vector-web"
"url": "https://github.com/matrix-org/matrix-react-sdk"
},
"license": "Apache-2.0",
"style": "bundle.css",
"main": "lib/index.js",
"bin": {
"reskindex": "./reskindex.js"
},
"scripts": {
"reskindex": "reskindex vector -h src/skins/vector/header",
"build:css": "catw \"src/skins/vector/css/**/*.css\" -o vector/bundle.css -c uglifycss --no-watch",
"build:compile": "babel --source-maps -d lib src",
"build:bundle": "NODE_ENV=production webpack -p lib/vector/index.js vector/bundle.js",
"build": "npm run build:css && npm run build:compile && npm run build:bundle",
"start:js": "webpack -w src/vector/index.js vector/bundle.js",
"start:skins:css": "catw \"src/skins/vector/css/**/*.css\" -o vector/bundle.css",
"//cache": "Note the -c 1 below due to https://code.google.com/p/chromium/issues/detail?id=508270",
"start": "parallelshell \"npm run start:js\" \"npm run start:skins:css\" \"http-server -c 1 vector\"",
"clean": "rimraf lib vector/bundle.css vector/bundle.js vector/bundle.js.map",
"prepublish": "npm run build:css && npm run build:compile"
"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",
"flux": "^2.0.3",
"glob": "^5.0.14",
"linkifyjs": "^2.0.0-beta.4",
"matrix-js-sdk": "^0.3.0",
"matrix-react-sdk": "^0.0.2",
"optimist": "^0.6.1",
"q": "^1.4.1",
"react": "^0.13.3",
"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": {
"babel": "^5.8.23",
"babel-core": "^5.8.25",
"babel-loader": "^5.3.2",
"catw": "^1.0.1",
"http-server": "^0.8.4",
"json-loader": "^0.5.3",
"parallelshell": "^1.2.0",
"rimraf": "^2.4.3",
"source-map-loader": "^0.1.5",
"uglifycss": "0.0.15"
"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,53 +0,0 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
module.exports = {
avatarUrlForMember: function(member, width, height, resizeMethod) {
var url = member.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
width,
height,
resizeMethod
);
if (!url) {
// member can be null here currently since on invites, the JS SDK
// does not have enough info to build a RoomMember object for
// the inviter.
url = this.defaultAvatarUrlForString(member ? member.userId : '');
}
return url;
},
defaultAvatarUrlForString: function(s) {
var total = 0;
for (var i = 0; i < s.length; ++i) {
total += s.charCodeAt(i);
}
switch (total % 3) {
case 0:
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9QjNbxSKP4eagAFnTseHFErFYLBaLxWKxWCwWi8Vi8cX4CzAABSwCRWJw31gAAAAASUVORK5CYII=";
case 1:
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9chOaxgCP4eagAFk9seHFErFYLBaLxWKxWCwWi8Vi8cX4CzAAtKMCks/JG8MAAAAASUVORK5CYII=";
case 2:
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9YzNayQCP4eagADldseHFErFYLBaLxWKxWCwWi8Vi8cX4CzAAyiACeHwPiu4AAAAASUVORK5CYII=";
}
}
}

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

@@ -14,18 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ServerConfig {
margin-top: 7px;
'use strict';
var url = require ('url');
function getServiceUrl() {
var parsedUrl = url.parse(window.location.href);
return parsedUrl.protocol + "//" + parsedUrl.host + parsedUrl.pathname;
}
.mx_ServerConfig .mx_Login_field {
margin-top: 4px;
margin-bottom: 5px;
}
.mx_ServerConfig_help:link {
opacity: 0.8;
font-size: 14px;
font-weight: 300;
color: #4a4a4a;
}
module.exports = {
getServiceUrl: getServiceUrl
};

86
src/ContentMessages.js Normal file
View File

@@ -0,0 +1,86 @@
/*
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 q = require('q');
var extend = require('./extend');
function infoForImageFile(imageFile) {
var deferred = q.defer();
// Load the file into an html element
var img = document.createElement("img");
var reader = new FileReader();
reader.onload = function(e) {
img.src = e.target.result;
// Once ready, returns its size
img.onload = function() {
deferred.resolve({
w: img.width,
h: img.height
});
};
img.onerror = function(e) {
deferred.reject(e);
};
};
reader.onerror = function(e) {
deferred.reject(e);
};
reader.readAsDataURL(imageFile);
return deferred.promise;
}
function sendContentToRoom(file, roomId, matrixClient) {
var content = {
body: file.name,
info: {
size: file.size,
}
};
// 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';
infoForImageFile(file).then(function(imageInfo) {
extend(content.info, imageInfo);
def.resolve();
});
} else {
content.msgtype = 'm.file';
def.resolve();
}
return def.promise.then(function() {
return matrixClient.uploadContent(file);
}).then(function(url) {
content.url = url;
return matrixClient.sendMessage(roomId, content);
});
}
module.exports = {
sendContentToRoom: sendContentToRoom
};

View File

@@ -1,81 +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');
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
// pass in a custom control as the actual body.
module.exports = {
ContextualMenuContainerId: "mx_ContextualMenu_Container",
getOrCreateContainer: function() {
var container = document.getElementById(this.ContextualMenuContainerId);
if (!container) {
container = document.createElement("div");
container.id = this.ContextualMenuContainerId;
document.body.appendChild(container);
}
return container;
},
createMenu: function (Element, props) {
var self = this;
var closeMenu = function() {
React.unmountComponentAtNode(self.getOrCreateContainer());
if (props && props.onFinished) props.onFinished.apply(null, arguments);
};
var position = {
top: props.top - 20,
};
var chevron = null;
if (props.left) {
chevron = <img className="mx_ContextualMenu_chevron_left" src="img/chevron-left.png" width="9" height="16" />
position.left = props.left + 8;
} else {
chevron = <img className="mx_ContextualMenu_chevron_right" src="img/chevron-right.png" width="9" height="16" />
position.right = props.right + 8;
}
var className = 'mx_ContextualMenu_wrapper';
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the menu from a button click!
var menu = (
<div className={className}>
<div className="mx_ContextualMenu" style={position}>
{chevron}
<Element {...props} onFinished={closeMenu}/>
</div>
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
</div>
);
React.render(menu, this.getOrCreateContainer());
return {close: closeMenu};
},
};

105
src/MatrixClientPeg.js Normal file
View File

@@ -0,0 +1,105 @@
/*
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';
// A thing that holds your Matrix Client
var Matrix = require("matrix-js-sdk");
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,
idBaseUrl: is_url,
accessToken: access_token,
userId: user_id
};
if (localStorage) {
opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
opts.deviceId = deviceId();
}
matrixClient = Matrix.createClient(opts);
}
if (localStorage) {
var hs_url = localStorage.getItem("mx_hs_url");
var is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
var access_token = localStorage.getItem("mx_access_token");
var user_id = localStorage.getItem("mx_user_id");
if (access_token && user_id && hs_url) {
createClient(hs_url, is_url, user_id, access_token);
}
}
class MatrixClient {
get() {
return matrixClient;
}
unset() {
matrixClient = null;
}
replaceUsingUrls(hs_url, is_url) {
matrixClient = Matrix.createClient({
baseUrl: hs_url,
idBaseUrl: is_url
});
}
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);
localStorage.setItem("mx_access_token", access_token);
} catch (e) {
console.warn("Error using local storage: can't persist session!");
}
} else {
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

@@ -16,19 +16,21 @@ limitations under the License.
'use strict';
var React = require('react');
module.exports = React.createClass({
displayName: 'Spinner',
render: function() {
var w = this.props.w || 32;
var h = this.props.h || 32;
var imgClass = this.props.imgClassName || "";
return (
<div>
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
</div>
);
function tsOfNewestEvent(room) {
if (room.timeline.length) {
return room.timeline[room.timeline.length - 1].getTs();
}
});
else {
return Number.MAX_SAFE_INTEGER;
}
}
function mostRecentActivityFirst(roomList) {
return roomList.sort(function(a,b) {
return tsOfNewestEvent(b) - tsOfNewestEvent(a);
});
}
module.exports = {
mostRecentActivityFirst: mostRecentActivityFirst
};

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

@@ -0,0 +1,88 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var React = require('react');
module.exports = {
propTypes: {
onValueChanged: React.PropTypes.func,
initialValue: React.PropTypes.string,
label: React.PropTypes.string,
placeHolder: React.PropTypes.string,
},
Phases: {
Display: "display",
Edit: "edit",
},
getDefaultProps: function() {
return {
onValueChanged: function() {},
initialValue: '',
label: 'Click to set',
placeholder: '',
};
},
getInitialState: function() {
return {
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, shouldSubmit, suppressListener) {
var self = this;
this.setState({
value: val,
phase: this.Phases.Display,
}, function() {
if (!suppressListener) {
self.onValueChanged(shouldSubmit);
}
});
},
edit: function() {
this.setState({
phase: this.Phases.Edit,
});
},
cancelEdit: function() {
this.setState({
phase: this.Phases.Display,
});
this.onValueChanged(false);
},
onValueChanged: function(shouldSubmit) {
this.props.onValueChanged(this.state.value, shouldSubmit);
},
};

View File

@@ -0,0 +1,58 @@
/*
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 sdk = require('../../index');
var dis = require("../../dispatcher");
module.exports = {
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
onAction: function(payload) {
if (payload.action !== "notifier_enabled") {
return;
}
this.forceUpdate();
},
enabled: function() {
var Notifier = sdk.getComponent('organisms.Notifier');
return Notifier.isEnabled();
},
onClick: function() {
var Notifier = sdk.getComponent('organisms.Notifier');
var self = this;
if (!Notifier.supportsDesktopNotifications()) {
return;
}
if (!Notifier.isEnabled()) {
Notifier.setEnabled(true, function() {
self.forceUpdate();
});
} else {
Notifier.setEnabled(false);
}
this.forceUpdate();
},
};

View File

@@ -14,12 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ProgressBar {
height: 5px;
border: 1px solid black;
}
'use strict';
.mx_ProgressBar_fill {
height: 100%;
background-color: #000;
}
var dis = require("../../dispatcher");
module.exports = {
onClick: function() {
dis.dispatch({
action: 'logout'
});
},
};

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

@@ -14,5 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_MessageTimestamp {
}
'use strict';
var dis = require("../../dispatcher");
module.exports = {
onClick: function() {
dis.dispatch({
action: 'view_user_settings'
});
},
};

View File

@@ -18,15 +18,18 @@ limitations under the License.
var React = require('react');
module.exports = React.createClass({
displayName: 'UnknownMessageTile',
render: function() {
var content = this.props.mxEvent.getContent();
return (
<span className="mx_UnknownMessageTile">
{content.body}
</span>
);
module.exports = {
propTypes: {
onCreateRoom: React.PropTypes.func,
},
});
getDefaultProps: function() {
return {
onCreateRoom: function() {},
};
},
onClick: function() {
this.props.onCreateRoom();
},
};

View File

@@ -18,18 +18,23 @@ limitations under the License.
var React = require('react');
var CasLoginController = require('matrix-react-sdk/lib/controllers/organisms/CasLogin');
var Presets = {
PrivateChat: "private_chat",
PublicChat: "public_chat",
Custom: "custom",
};
module.exports = React.createClass({
displayName: 'CasLogin',
mixins: [CasLoginController],
render: function() {
return (
<div>
<button onClick={this.onCasClicked}>Sign in with CAS</button>
</div>
);
module.exports = {
propTypes: {
onChange: React.PropTypes.func,
preset: React.PropTypes.string
},
});
Presets: Presets,
getDefaultProps: function() {
return {
onChange: function() {},
};
},
};

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

@@ -18,15 +18,24 @@ limitations under the License.
var React = require('react');
var LogoutButtonController = require('matrix-react-sdk/lib/controllers/atoms/LogoutButton')
module.exports = {
propTypes: {
default_name: React.PropTypes.string
},
module.exports = React.createClass({
displayName: 'LogoutButton',
mixins: [LogoutButtonController],
getDefaultProps: function() {
return {
default_name: '',
};
},
render: function() {
return (
<button className="mx_LogoutButton" onClick={this.onClick}>Sign out</button>
);
}
});
getInitialState: function() {
return {
room_name: this.props.default_name,
}
},
getName: function() {
return this.state.room_name;
},
};

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

@@ -14,24 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_MImageTile_thumbnail {
/*
background-color: #fff;
border: 2px solid #fff;
border-radius: 1px;
*/
}
'use strict';
.mx_MImageTile_download {
color: #80cef4;
cursor: pointer;
}
var MatrixClientPeg = require("../../MatrixClientPeg");
.mx_MImageTile_download a {
color: #80cef4;
text-decoration: none;
}
module.exports = {
shouldHighlight: function() {
var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent);
if (!actions || !actions.tweaks) { return false; }
return actions.tweaks.highlight;
}
};
.mx_MImageTile_download img {
padding-right: 8px;
}

View File

@@ -16,19 +16,15 @@ limitations under the License.
'use strict';
var React = require('react');
var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../linkify-matrix');
module.exports = React.createClass({
displayName: 'ViewSource',
linkifyMatrix(linkify);
render: function() {
return (
<div className="mx_ViewSource">
<pre>
{JSON.stringify(this.props.mxEvent.event, null, 2)}
</pre>
</div>
);
module.exports = {
componentDidMount: function() {
linkifyElement(this.refs.content.getDOMNode(), linkifyMatrix.options);
}
});
};

View File

@@ -0,0 +1,44 @@
/*
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 filesize = require('filesize');
module.exports = {
presentableTextForFile: function(content) {
var linkText = 'Attachment';
if (content.body && content.body.length > 0) {
linkText = content.body;
}
var additionals = [];
if (content.info) {
// if (content.info.mimetype && content.info.mimetype.length > 0) {
// additionals.push(content.info.mimetype);
// }
if (content.info.size) {
additionals.push(filesize(content.info.size));
}
}
if (additionals.length > 0) {
linkText += ' (' + additionals.join(', ') + ')';
}
return linkText;
}
};

View File

@@ -14,21 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_MatrixToolbar {
text-align: center;
background-color: #ff0064;
color: #fff;
font-weight: bold;
padding: 6px;
}
'use strict';
.mx_MatrixToolbar button {
margin-left: 12px;
}
var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../linkify-matrix.js');
linkifyMatrix(linkify);
.mx_MatrixToolbar_close {
float: right;
margin-top: 3px;
margin-right: 12px;
cursor: pointer;
}
module.exports = {
componentDidMount: function() {
linkifyElement(this.refs.content.getDOMNode(), linkifyMatrix.options);
}
};

View File

@@ -0,0 +1,30 @@
/*
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 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

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

@@ -0,0 +1,58 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var dis = require("../../dispatcher");
var Modal = require("../../Modal");
var sdk = require('../../index.js');
var Loader = require("react-loader");
var MatrixClientPeg = require("../../MatrixClientPeg");
module.exports = {
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

@@ -0,0 +1,413 @@
/*
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 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) {
switch (payload.action) {
case 'focus_composer':
this.refs.textarea.getDOMNode().focus();
break;
}
},
onKeyDown: function (ev) {
if (ev.keyCode === KeyCode.ENTER) {
var input = this.refs.textarea.getDOMNode().value;
if (input.length === 0) {
ev.preventDefault();
return;
}
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

@@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_MemberAvatar {
z-index: 20;
border-radius: 20px;
background-color: #dbdbdb;
}
'use strict';
var MatrixClientPeg = require("../../MatrixClientPeg");
module.exports = {
};

View File

@@ -18,14 +18,9 @@ limitations under the License.
var React = require('react');
module.exports = React.createClass({
displayName: 'VideoFeed',
render: function() {
return (
<video>
</video>
);
module.exports = {
propTypes: {
value: React.PropTypes.number,
max: React.PropTypes.number
},
});
};

View File

@@ -0,0 +1,118 @@
/*
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';
/*
* 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

@@ -0,0 +1,29 @@
/*
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: {
room: React.PropTypes.object.isRequired,
},
getInitialState: function() {
return {
power_levels_changed: false
};
}
};

View File

@@ -14,18 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RoomList {
}
'use strict';
.mx_RoomList_recents {
margin-top: -12px;
display: table;
table-layout: fixed;
width: 100%;
}
var dis = require("../../dispatcher");
.mx_RoomList h2 {
padding-left: 16px;
padding-right: 16px;
padding-bottom: 10px;
}
module.exports = {
onClick: function() {
dis.dispatch({
action: 'view_room',
room_id: this.props.room.roomId
});
},
};

View File

@@ -0,0 +1,64 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var React = require("react");
module.exports = {
propTypes: {
onHsUrlChanged: React.PropTypes.func,
onIsUrlChanged: React.PropTypes.func,
default_hs_url: React.PropTypes.string,
default_is_url: React.PropTypes.string
},
getDefaultProps: function() {
return {
onHsUrlChanged: function() {},
onIsUrlChanged: function() {},
defaultHsUrl: 'https://matrix.org/',
defaultIsUrl: 'https://matrix.org/'
};
},
getInitialState: function() {
return {
hs_url: this.props.defaultHsUrl,
is_url: this.props.defaultIsUrl,
}
},
hsChanged: function(ev) {
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}, function() {
this.props.onIsUrlChanged(this.state.is_url);
});
},
getHsUrl: function() {
return this.state.hs_url;
},
getIsUrl: function() {
return this.state.is_url;
},
};

View File

@@ -18,26 +18,28 @@ limitations under the License.
var React = require('react');
var TextForEvent = require('matrix-react-sdk/lib/TextForEvent');
module.exports = {
propTypes: {
onChange: React.PropTypes.func,
selected_users: React.PropTypes.arrayOf(React.PropTypes.string),
},
module.exports = React.createClass({
displayName: 'EventAsTextTile',
getDefaultProps: function() {
return {
onChange: function() {},
selected: [],
};
},
statics: {
needsSenderProfile: function() {
return false;
addUser: function(user_id) {
if (this.props.selected_users.indexOf(user_id == -1)) {
this.props.onChange(this.props.selected_users.concat([user_id]));
}
},
render: function() {
var text = TextForEvent.textForEvent(this.props.mxEvent);
if (text == null || text.length == 0) return null;
return (
<div className="mx_EventAsTextTile">
{TextForEvent.textForEvent(this.props.mxEvent)}
</div>
);
removeUser: function(user_id) {
this.props.onChange(this.props.selected_users.filter(function(e) {
return e != user_id;
}));
},
});
};

View File

@@ -14,12 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var dis = require("matrix-react-sdk/lib/dispatcher");
var CallHandler = require("matrix-react-sdk/lib/CallHandler");
var MatrixClientPeg = require("matrix-react-sdk/lib/MatrixClientPeg");
var VectorConferenceHandler = require('../../../modules/VectorConferenceHandler');
var dis = require("../../../dispatcher");
var CallHandler = require("../../../CallHandler");
/*
* State vars:
@@ -27,30 +23,14 @@ var VectorConferenceHandler = require('../../../modules/VectorConferenceHandler'
*
* Props:
* this.props.room = Room (JS SDK)
*
* Internal state:
* this._trackedRoom = (either from props.room or programatically set)
*/
module.exports = {
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this._trackedRoom = null;
if (this.props.room) {
this._trackedRoom = this.props.room;
this.showCall(this._trackedRoom.roomId);
}
else {
var call = CallHandler.getAnyActiveCall();
if (call) {
console.log(
"Global CallView is now tracking active call in room %s",
call.roomId
);
this._trackedRoom = MatrixClientPeg.get().getRoom(call.roomId);
this.showCall(call.roomId);
}
this.showCall(this.props.room.roomId);
}
},
@@ -59,19 +39,19 @@ module.exports = {
},
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) {
// 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.getCallForRoom(roomId) ||
VectorConferenceHandler.getConferenceCallForRoom(roomId)
);
var call = CallHandler.getCall(roomId);
if (call) {
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
@@ -80,17 +60,12 @@ module.exports = {
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
}
if (call && call.type === "video" && call.state !== 'ended') {
// if this call is a conf call, don't display local video as the
// conference will have us in it
this.getVideoView().getLocalVideoElement().style.display = (
call.confUserId ? "none" : "initial"
);
this.getVideoView().getLocalVideoElement().style.display = "initial";
this.getVideoView().getRemoteVideoElement().style.display = "initial";
}
else {
this.getVideoView().getLocalVideoElement().style.display = "none";
this.getVideoView().getRemoteVideoElement().style.display = "none";
dis.dispatch({action: 'video_fullscreen', fullscreen: false});
}
}
};

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

View File

@@ -0,0 +1,36 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var MatrixClientPeg = require("../../MatrixClientPeg");
var Cas = require("../../CasLogic");
module.exports = {
onCasClicked: function(ev) {
var serviceRedirectUrl = Cas.getServiceUrl() + "#/login/cas";
var self = this;
MatrixClientPeg.get().getCasServer().done(function(data) {
var serverUrl = data.serverUrl + "/login?service=" + encodeURIComponent(serviceRedirectUrl);
window.location.href = serverUrl;
}, function(error) {
self.setStep("stage_m.login.cas");
self.setState({errorText: 'Login failed.'});
});
},
};

View File

@@ -0,0 +1,135 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var React = require("react");
var MatrixClientPeg = require("../../MatrixClientPeg");
var PresetValues = require('../atoms/create_room/Presets').Presets;
var q = require('q');
var encryption = require("../../encryption");
module.exports = {
propTypes: {
onRoomCreated: React.PropTypes.func,
},
phases: {
CONFIG: "CONFIG", // We're waiting for user to configure and hit create.
CREATING: "CREATING", // We're sending the request.
CREATED: "CREATED", // We successfully created the room.
ERROR: "ERROR", // There was an error while trying to create room.
},
getDefaultProps: function() {
return {
onRoomCreated: function() {},
};
},
getInitialState: function() {
return {
phase: this.phases.CONFIG,
error_string: "",
is_private: true,
share_history: false,
default_preset: PresetValues.PrivateChat,
topic: '',
room_name: '',
invited_users: [],
};
},
onCreateRoom: function() {
var options = {};
if (this.state.room_name) {
options.name = this.state.room_name;
}
if (this.state.topic) {
options.topic = this.state.topic;
}
if (this.state.preset) {
if (this.state.preset != PresetValues.Custom) {
options.preset = this.state.preset;
} else {
options.initial_state = [
{
type: "m.room.join_rules",
content: {
"join_rules": this.state.is_private ? "invite" : "public"
}
},
{
type: "m.room.history_visibility",
content: {
"history_visibility": this.state.share_history ? "shared" : "invited"
}
},
];
}
}
options.invite = this.state.invited_users;
var alias = this.getAliasLocalpart();
if (alias) {
options.room_alias_name = alias;
}
var cli = MatrixClientPeg.get();
if (!cli) {
// TODO: Error.
console.error("Cannot create room: No matrix client.");
return;
}
var deferred = cli.createRoom(options);
var response;
if (this.state.encrypt) {
deferred = deferred.then(function(res) {
response = res;
return encryption.enableEncryption(
cli, response.roomId, options.invite
);
}).then(function() {
return q(response) }
);
}
this.setState({
phase: this.phases.CREATING,
});
var self = this;
deferred.then(function (resp) {
self.setState({
phase: self.phases.CREATED,
});
self.props.onRoomCreated(resp.room_id);
}, function(err) {
self.setState({
phase: self.phases.ERROR,
error_string: err.toString(),
});
});
}
};

View File

@@ -14,23 +14,23 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var React = require("react");
var React = require('react');
var MTextTileController = require('matrix-react-sdk/lib/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>
);
module.exports = {
propTypes: {
title: React.PropTypes.string,
description: React.PropTypes.string,
button: React.PropTypes.string,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired,
},
});
getDefaultProps: function() {
return {
title: "Error",
description: "An error has occurred.",
button: "OK",
focus: true,
};
},
};

View File

@@ -14,26 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_VideoView {
width: 100%;
position: relative;
z-index: 30;
}
var dis = require("../../dispatcher");
.mx_VideoView video {
width: 100%;
}
module.exports = {
logOut: function() {
dis.dispatch({action: 'logout'});
if (this.props.onFinished) {
this.props.onFinished();
}
},
.mx_VideoView_remoteVideoFeed {
width: 100%;
background-color: #000;
z-index: 50;
}
cancelPrompt: function() {
if (this.props.onFinished) {
this.props.onFinished();
}
}
};
.mx_VideoView_localVideoFeed {
width: 20%;
position: absolute;
left: 16px;
bottom: 28px;
z-index: 100;
}

View File

@@ -0,0 +1,182 @@
/*
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 Modal = require("../../Modal");
var sdk = require('../../index');
var INITIAL_LOAD_NUM_MEMBERS = 50;
module.exports = {
getInitialState: function() {
if (!this.props.roomId) return { members: [] };
var cli = MatrixClientPeg.get();
var room = cli.getRoom(this.props.roomId);
if (!room) return { members: [] };
this.memberDict = this.getMemberDict();
var members = this.roomMembers(INITIAL_LOAD_NUM_MEMBERS);
return {
members: members
};
},
componentWillMount: function() {
var cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember);
cli.on("Room", this.onRoom); // invites
},
componentWillUnmount: function() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room", this.onRoom);
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn);
}
},
componentDidMount: function() {
var self = this;
// Lazy-load in more than the first N members
setTimeout(function() {
if (!self.isMounted()) return;
self.setState({
members: self.roomMembers()
});
}, 50);
// Attach a SINGLE listener for global presence changes then locate the
// member tile and re-render it. This is more efficient than every tile
// evar attaching their own listener.
function updateUserState(event, user) {
// XXX: evil hack to track the age of this presence info.
// this should be removed once syjs-28 is resolved in the JS SDK itself.
user.lastPresenceTs = Date.now();
var tile = self.refs[user.userId];
if (tile) {
self._updateList(); // reorder the membership list
}
}
// FIXME: we should probably also reset 'lastActiveAgo' to zero whenever
// we see a typing notif from a user, as we don't get presence updates for those.
MatrixClientPeg.get().on("User.presence", updateUserState);
this.userPresenceFn = updateUserState;
},
// Remember to set 'key' on a MemberList to the ID of the room it's for
/*componentWillReceiveProps: function(newProps) {
},*/
onRoom: function(room) {
if (room.roomId !== this.props.roomId) {
return;
}
// We listen for room events because when we accept an invite
// we need to wait till the room is fully populated with state
// before refreshing the member list else we get a stale list.
this._updateList();
},
onRoomStateMember: function(ev, state, member) {
this._updateList();
},
_updateList: function() {
this.memberDict = this.getMemberDict();
var self = this;
this.setState({
members: self.roomMembers()
});
},
onInvite: function(inputText) {
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
var self = this;
// sanity check the input
inputText = inputText.trim(); // react requires es5-shim so we know trim() exists
if (inputText[0] !== '@' || inputText.indexOf(":") === -1) {
console.error("Bad user ID to invite: %s", inputText);
Modal.createDialog(ErrorDialog, {
title: "Invite Error",
description: "Malformed user ID. Should look like '@localpart:domain'"
});
return;
}
self.setState({
inviting: true
});
console.log("Invite %s to %s", inputText, this.props.roomId);
MatrixClientPeg.get().invite(this.props.roomId, inputText).done(
function(res) {
console.log("Invited");
self.setState({
inviting: false
});
}, function(err) {
console.error("Failed to invite: %s", JSON.stringify(err));
Modal.createDialog(ErrorDialog, {
title: "Server error whilst inviting",
description: err.message
});
self.setState({
inviting: false
});
});
},
getMemberDict: function() {
if (!this.props.roomId) return {};
var cli = MatrixClientPeg.get();
var room = cli.getRoom(this.props.roomId);
if (!room) return {};
var all_members = room.currentState.members;
// XXX: evil hack until SYJS-28 is fixed
Object.keys(all_members).map(function(userId) {
if (all_members[userId].user && !all_members[userId].user.lastPresenceTs) {
all_members[userId].user.lastPresenceTs = Date.now();
}
});
return all_members;
},
roomMembers: function(limit) {
var all_members = this.memberDict || {};
var all_user_ids = Object.keys(all_members);
if (this.memberSort) all_user_ids.sort(this.memberSort);
var to_display = [];
var count = 0;
for (var i = 0; i < all_user_ids.length && (limit === undefined || count < limit); ++i) {
var user_id = all_user_ids[i];
var m = all_members[user_id];
if (m.membership == 'join' || m.membership == 'invite') {
to_display.push(user_id);
++count;
}
}
return to_display;
}
};

View File

@@ -0,0 +1,124 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher");
/*
* Dispatches:
* {
* action: "notifier_enabled",
* value: boolean
* }
*/
module.exports = {
start: function() {
this.boundOnRoomTimeline = this.onRoomTimeline.bind(this);
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
this.state = { 'toolbarHidden' : false };
},
stop: function() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
}
},
supportsDesktopNotifications: function() {
return !!global.Notification;
},
havePermission: function() {
if (!this.supportsDesktopNotifications()) return false;
return global.Notification.permission == 'granted';
},
setEnabled: function(enable, callback) {
if(enable) {
if (!this.havePermission()) {
global.Notification.requestPermission(function() {
if (callback) {
callback();
dis.dispatch({
action: "notifier_enabled",
value: true
});
}
});
}
if (!global.localStorage) return;
global.localStorage.setItem('notifications_enabled', 'true');
if (this.havePermission) {
dis.dispatch({
action: "notifier_enabled",
value: true
});
}
}
else {
if (!global.localStorage) return;
global.localStorage.setItem('notifications_enabled', 'false');
dis.dispatch({
action: "notifier_enabled",
value: false
});
}
this.setToolbarHidden(false);
},
isEnabled: function() {
if (!this.havePermission()) return false;
if (!global.localStorage) return true;
var enabled = global.localStorage.getItem('notifications_enabled');
if (enabled === null) return true;
return enabled === 'true';
},
setToolbarHidden: function(hidden) {
this.state.toolbarHidden = hidden;
dis.dispatch({
action: "notifier_enabled",
value: this.isEnabled()
});
},
isToolbarHidden: function() {
return this.state.toolbarHidden;
},
onRoomTimeline: function(ev, room, toStartOfTimeline) {
if (toStartOfTimeline) return;
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return;
if (!this.isEnabled()) {
return;
}
var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
if (actions && actions.notify) {
this.displayNotification(ev, room);
}
}
};

View File

@@ -0,0 +1,36 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require("react");
module.exports = {
propTypes: {
title: React.PropTypes.string,
description: React.PropTypes.string,
button: React.PropTypes.string,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired,
},
getDefaultProps: function() {
return {
title: "",
description: "",
button: "OK",
focus: true,
};
},
};

View File

@@ -17,15 +17,10 @@ limitations under the License.
'use strict';
var React = require("react");
var MatrixClientPeg = require("matrix-react-sdk/lib/MatrixClientPeg");
var RoomListSorter = require("matrix-react-sdk/lib/RoomListSorter");
var dis = require("matrix-react-sdk/lib/dispatcher");
var MatrixClientPeg = require("../../MatrixClientPeg");
var RoomListSorter = require("../../RoomListSorter");
var sdk = require('matrix-react-sdk');
var VectorConferenceHandler = require("../../modules/VectorConferenceHandler");
var CallHandler = require("matrix-react-sdk/lib/CallHandler");
var HIDE_CONFERENCE_CHANS = true;
var sdk = require('../../index');
module.exports = {
componentWillMount: function() {
@@ -34,7 +29,6 @@ module.exports = {
cli.on("Room.timeline", this.onRoomTimeline);
cli.on("Room.name", this.onRoomName);
cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName);
var rooms = this.getRoomList();
this.setState({
@@ -43,38 +37,16 @@ module.exports = {
});
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
},
onAction: function(payload) {
switch (payload.action) {
// listen for call state changes to prod the render method, which
// may hide the global CallView if the call it is tracking is dead
case 'call_state':
this._recheckCallElement(this.props.selectedRoom);
break;
case 'view_tooltip':
this.tooltip = payload.tooltip;
this._repositionTooltip();
if (this.tooltip) this.tooltip.style.display = 'block';
break
}
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room", this.onRoom);
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
}
},
componentWillReceiveProps: function(newProps) {
this.state.activityMap[newProps.selectedRoom] = undefined;
this._recheckCallElement(newProps.selectedRoom);
this.setState({
activityMap: this.state.activityMap
});
@@ -114,14 +86,9 @@ module.exports = {
},
onRoomStateEvents: function(ev, state) {
setTimeout(this.refreshRoomList, 0);
this.refreshRoomList();
},
onRoomMemberName: function(ev, member) {
setTimeout(this.refreshRoomList, 0);
},
refreshRoomList: function() {
var rooms = this.getRoomList();
this.setState({
@@ -132,63 +99,26 @@ module.exports = {
getRoomList: function() {
return RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms().filter(function(room) {
var me = room.getMember(MatrixClientPeg.get().credentials.userId);
var shouldShowRoom = (
me && (me.membership == "join" || me.membership == "invite")
);
// hiding conf rooms only ever toggles shouldShowRoom to false
if (shouldShowRoom && HIDE_CONFERENCE_CHANS) {
// we want to hide the 1:1 conf<->user room and not the group chat
var joinedMembers = room.getJoinedMembers();
if (joinedMembers.length === 2) {
var otherMember = joinedMembers.filter(function(m) {
return m.userId !== me.userId
})[0];
if (VectorConferenceHandler.isConferenceUser(otherMember)) {
// console.log("Hiding conference 1:1 room %s", room.roomId);
shouldShowRoom = false;
}
}
}
return shouldShowRoom;
var member = room.getMember(MatrixClientPeg.get().credentials.userId);
return member && (member.membership == "join" || member.membership == "invite");
})
);
},
_recheckCallElement: function(selectedRoomId) {
// if we aren't viewing a room with an ongoing call, but there is an
// active call, show the call element - we need to do this to make
// audio/video not crap out
var activeCall = CallHandler.getAnyActiveCall();
var callForRoom = CallHandler.getCallForRoom(selectedRoomId);
var showCall = (activeCall && !callForRoom);
this.setState({
show_call_element: showCall
});
},
_repositionTooltip: function(e) {
if (this.tooltip && this.tooltip.parentElement) {
var scroll = this.getDOMNode();
this.tooltip.style.top = (scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - scroll.scrollTop) + "px";
}
},
makeRoomTiles: function() {
var RoomTile = sdk.getComponent('molecules.RoomTile');
var self = this;
var RoomTile = sdk.getComponent("molecules.RoomTile");
return this.state.roomList.map(function(room) {
var selected = room.roomId == self.props.selectedRoom;
return (
<RoomTile
room={room}
key={room.roomId}
collapsed={self.props.collapsed}
selected={selected}
unread={self.state.activityMap[room.roomId] === 1}
highlight={self.state.activityMap[room.roomId] === 2}
/>
);
});
}
},
};

View File

@@ -14,17 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var MatrixClientPeg = require("matrix-react-sdk/lib/MatrixClientPeg");
var MatrixClientPeg = require("../../MatrixClientPeg");
var React = require("react");
var q = require("q");
var ContentMessages = require("matrix-react-sdk/lib//ContentMessages");
var WhoIsTyping = require("matrix-react-sdk/lib/WhoIsTyping");
var Modal = require("matrix-react-sdk/lib/Modal");
var sdk = require('matrix-react-sdk/lib/index');
var CallHandler = require('matrix-react-sdk/lib/CallHandler');
var VectorConferenceHandler = require('../../modules/VectorConferenceHandler');
var ContentMessages = require("../../ContentMessages");
var WhoIsTyping = require("../../WhoIsTyping");
var Modal = require("../../Modal");
var sdk = require('../../index');
var dis = require("matrix-react-sdk/lib/dispatcher");
var dis = require("../../dispatcher");
var PAGINATE_SIZE = 20;
var INITIAL_SIZE = 20;
@@ -46,7 +44,6 @@ module.exports = {
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Room.name", this.onRoomName);
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
this.atBottom = true;
},
@@ -63,7 +60,6 @@ module.exports = {
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping);
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
}
},
@@ -81,20 +77,15 @@ module.exports = {
this.forceUpdate();
break;
case 'call_state':
if (CallHandler.getCallForRoom(this.props.roomId)) {
// Call state has changed so we may be loading video elements
// which will obscure the message log.
// scroll to bottom
var messageWrapper = this.refs.messageWrapper;
if (messageWrapper) {
messageWrapper = messageWrapper.getDOMNode();
messageWrapper.scrollTop = messageWrapper.scrollHeight;
}
if (this.props.roomId !== payload.room_id) {
break;
}
// scroll to bottom
var messageWrapper = this.refs.messageWrapper;
if (messageWrapper) {
messageWrapper = messageWrapper.getDOMNode();
messageWrapper.scrollTop = messageWrapper.scrollHeight;
}
// possibly remove the conf call notification if we're now in
// the conf
this._updateConfCallNotification();
break;
}
},
@@ -162,36 +153,6 @@ module.exports = {
this.forceUpdate();
},
onRoomStateMember: function(ev, state, member) {
if (member.roomId !== this.props.roomId ||
member.userId !== VectorConferenceHandler.getConferenceUserIdForRoom(member.roomId)) {
return;
}
this._updateConfCallNotification();
},
_updateConfCallNotification: function() {
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (!room) return;
var confMember = room.getMember(
VectorConferenceHandler.getConferenceUserIdForRoom(this.props.roomId)
);
if (!confMember) {
return;
}
var confCall = VectorConferenceHandler.getConferenceCallForRoom(confMember.roomId);
// A conf call notification should be displayed if there is an ongoing
// conf call but this cilent isn't a part of it.
this.setState({
displayConfCallNotification: (
(!confCall || confCall.call_state === "ended") &&
confMember.membership === "join"
)
});
},
componentDidMount: function() {
if (this.refs.messageWrapper) {
var messageWrapper = this.refs.messageWrapper.getDOMNode();
@@ -205,8 +166,6 @@ module.exports = {
this.fillSpace();
}
this._updateConfCallNotification();
},
componentDidUpdate: function() {
@@ -357,8 +316,6 @@ module.exports = {
},
getEventTiles: function() {
var DateSeparator = sdk.getComponent('molecules.DateSeparator');
var ret = [];
var count = 0;
@@ -373,7 +330,6 @@ module.exports = {
var continuation = false;
var last = false;
var dateSeparator = null;
if (i == this.state.room.timeline.length - 1) {
last = true;
}
@@ -388,27 +344,10 @@ module.exports = {
{
continuation = true;
}
var ts0 = this.state.room.timeline[i - 1].getTs();
var ts1 = this.state.room.timeline[i].getTs();
if (new Date(ts0).toDateString() !== new Date(ts1).toDateString()) {
dateSeparator = <DateSeparator key={ts1} ts={ts1}/>;
continuation = false;
}
}
if (i === 1) { // n.b. 1, not 0, as the 0th event is an m.room.create and so doesn't show on the timeline
var ts1 = this.state.room.timeline[i].getTs();
dateSeparator = <li key={ts1}><DateSeparator ts={ts1}/></li>;
continuation = false;
}
ret.unshift(
<li key={mxEv.getId()}><EventTile mxEvent={mxEv} continuation={continuation} last={last}/></li>
);
if (dateSeparator) {
ret.unshift(dateSeparator);
}
++count;
}
return ret;

View File

@@ -0,0 +1,54 @@
/*
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 q = require('q');
var version = require('../../../package.json').version;
module.exports = {
Phases: {
Loading: "loading",
Display: "display",
},
getInitialState: function() {
return {
avatarUrl: null,
threePids: [],
clientVersion: version,
phase: this.Phases.Loading,
};
},
componentWillMount: function() {
var self = this;
var cli = MatrixClientPeg.get();
var profile_d = cli.getProfileInfo(cli.credentials.userId);
var threepid_d = cli.getThreePids();
q.all([profile_d, threepid_d]).then(
function(resps) {
self.setState({
avatarUrl: resps[0].avatar_url,
threepids: resps[1].threepids,
phase: self.Phases.Display,
});
},
function(err) { console.err(err); }
);
}
}

View File

@@ -0,0 +1,419 @@
/*
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 RoomListSorter = require("../../RoomListSorter");
var Presence = require("../../Presence");
var dis = require("../../dispatcher");
var sdk = require('../../index');
var MatrixTools = require('../../MatrixTools');
var linkifyMatrix = require("../../linkify-matrix");
var Cas = require("../../CasLogic");
module.exports = {
PageTypes: {
RoomView: "room_view",
UserSettings: "user_settings",
CreateRoom: "create_room",
RoomDirectory: "room_directory",
},
AuxPanel: {
RoomSettings: "room_settings",
},
getInitialState: function() {
var s = {
logged_in: !!(MatrixClientPeg.get() && MatrixClientPeg.get().credentials),
collapse_lhs: false,
collapse_rhs: false,
ready: false,
};
if (s.logged_in) {
if (MatrixClientPeg.get().getRooms().length) {
s.page_type = this.PageTypes.RoomView;
} else {
s.page_type = this.PageTypes.RoomDirectory;
}
}
return s;
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
if (this.state.logged_in) {
this.startMatrixClient();
}
this.focusComposer = false;
document.addEventListener("keydown", this.onKeyDown);
window.addEventListener("focus", this.onFocus);
if (this.state.logged_in) {
this.notifyNewScreen('');
} else {
this.notifyNewScreen('login');
}
// this can technically be done anywhere but doing this here keeps all
// the routing url path logic together.
if (this.onAliasClick) {
linkifyMatrix.onAliasClick = this.onAliasClick;
}
if (this.onUserClick) {
linkifyMatrix.onUserClick = this.onUserClick;
}
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
document.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("focus", this.onFocus);
},
componentDidUpdate: function() {
if (this.focusComposer) {
dis.dispatch({action: 'focus_composer'});
this.focusComposer = false;
}
},
onAction: function(payload) {
var roomIndexDelta = 1;
var Notifier = sdk.getComponent('organisms.Notifier');
var self = this;
switch (payload.action) {
case 'logout':
if (window.localStorage) {
window.localStorage.clear();
}
Notifier.stop();
Presence.stop();
MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().removeAllListeners();
MatrixClientPeg.unset();
this.notifyNewScreen('login');
this.replaceState({
logged_in: false,
ready: false
});
break;
case 'start_registration':
if (this.state.logged_in) return;
var newState = payload.params || {};
newState.screen = 'register';
if (
payload.params &&
payload.params.client_secret &&
payload.params.session_id &&
payload.params.hs_url &&
payload.params.is_url &&
payload.params.sid
) {
newState.register_client_secret = payload.params.client_secret;
newState.register_session_id = payload.params.session_id;
newState.register_hs_url = payload.params.hs_url;
newState.register_is_url = payload.params.is_url;
newState.register_id_sid = payload.params.sid;
}
this.replaceState(newState);
this.notifyNewScreen('register');
break;
case 'start_login':
if (this.state.logged_in) return;
this.replaceState({
screen: 'login'
});
this.notifyNewScreen('login');
break;
case 'cas_login':
if (this.state.logged_in) return;
var self = this;
var client = MatrixClientPeg.get();
var serviceUrl = Cas.getServiceUrl();
client.loginWithCas(payload.params.ticket, serviceUrl).done(function(data) {
MatrixClientPeg.replaceUsingAccessToken(
client.getHomeserverUrl(), client.getIdentityServerUrl(),
data.user_id, data.access_token
);
self.setState({
screen: undefined,
logged_in: true
});
self.startMatrixClient();
self.notifyNewScreen('');
}, function(error) {
self.notifyNewScreen('login');
self.setState({errorText: 'Login failed.'});
});
break;
case 'view_room':
this.focusComposer = true;
var newState = {
currentRoom: payload.room_id,
page_type: this.PageTypes.RoomView,
};
if (this.sdkReady) {
// if the SDK is not ready yet, remember what room
// we're supposed to be on but don't notify about
// the new screen yet (we won't be showing it yet)
// The normal case where this happens is navigating
// to the room in the URL bar on page load.
var presentedId = payload.room_id;
var room = MatrixClientPeg.get().getRoom(payload.room_id);
if (room) {
var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
if (theAlias) presentedId = theAlias;
}
this.notifyNewScreen('room/'+presentedId);
newState.ready = true;
}
this.setState(newState);
break;
case 'view_prev_room':
roomIndexDelta = -1;
case 'view_next_room':
var allRooms = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms()
);
var roomIndex = -1;
for (var i = 0; i < allRooms.length; ++i) {
if (allRooms[i].roomId == this.state.currentRoom) {
roomIndex = i;
break;
}
}
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
if (roomIndex < 0) roomIndex = allRooms.length - 1;
this.focusComposer = true;
this.setState({
currentRoom: allRooms[roomIndex].roomId
});
this.notifyNewScreen('room/'+allRooms[roomIndex].roomId);
break;
case 'view_indexed_room':
var allRooms = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms()
);
var roomIndex = payload.roomIndex;
if (allRooms[roomIndex]) {
this.focusComposer = true;
this.setState({
currentRoom: allRooms[roomIndex].roomId
});
this.notifyNewScreen('room/'+allRooms[roomIndex].roomId);
}
break;
case 'view_room_alias':
var foundRoom = MatrixTools.getRoomForAlias(
MatrixClientPeg.get().getRooms(), payload.room_alias
);
if (foundRoom) {
dis.dispatch({
action: 'view_room',
room_id: foundRoom.roomId
});
return;
}
// resolve the alias and *then* view it
MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).done(
function(result) {
dis.dispatch({
action: 'view_room',
room_id: result.room_id
});
});
break;
case 'view_user_settings':
this.setState({
page_type: this.PageTypes.UserSettings,
});
break;
case 'view_create_room':
this.setState({
page_type: this.PageTypes.CreateRoom,
});
break;
case 'view_room_directory':
this.setState({
page_type: this.PageTypes.RoomDirectory,
});
break;
case 'notifier_enabled':
this.forceUpdate();
break;
case 'hide_left_panel':
this.setState({
collapse_lhs: true,
});
break;
case 'show_left_panel':
this.setState({
collapse_lhs: false,
});
break;
case 'hide_right_panel':
this.setState({
collapse_rhs: true,
});
break;
case 'show_right_panel':
this.setState({
collapse_rhs: false,
});
break;
}
},
onLoggedIn: function() {
this.setState({
screen: undefined,
logged_in: true
});
this.startMatrixClient();
this.notifyNewScreen('');
},
startMatrixClient: function() {
var Notifier = sdk.getComponent('organisms.Notifier');
var cli = MatrixClientPeg.get();
var self = this;
cli.on('syncComplete', function() {
self.sdkReady = true;
if (self.starting_room_alias) {
dis.dispatch({
action: 'view_room_alias',
room_alias: self.starting_room_alias
});
delete self.starting_room_alias;
} else {
if (!self.state.currentRoom) {
var firstRoom = null;
if (cli.getRooms() && cli.getRooms().length) {
firstRoom = RoomListSorter.mostRecentActivityFirst(
cli.getRooms()
)[0].roomId;
self.setState({ready: true, currentRoom: firstRoom, page_type: self.PageTypes.RoomView});
} else {
self.setState({ready: true, page_type: self.PageTypes.RoomDirectory});
}
} else {
self.setState({ready: true, page_type: self.PageTypes.RoomView});
}
// we notifyNewScreen now because now the room will actually be displayed,
// and (mostly) now we can get the correct alias.
var presentedId = self.state.currentRoom;
var room = MatrixClientPeg.get().getRoom(self.state.currentRoom);
if (room) {
var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
if (theAlias) presentedId = theAlias;
}
self.notifyNewScreen('room/'+presentedId);
dis.dispatch({action: 'focus_composer'});
}
});
cli.on('Call.incoming', function(call) {
dis.dispatch({
action: 'incoming_call',
call: call
});
});
Notifier.start();
Presence.start();
cli.startClient();
},
onKeyDown: function(ev) {
if (ev.altKey) {
if (ev.ctrlKey && ev.keyCode > 48 && ev.keyCode < 58) {
dis.dispatch({
action: 'view_indexed_room',
roomIndex: ev.keyCode - 49,
});
ev.stopPropagation();
ev.preventDefault();
return;
}
switch (ev.keyCode) {
case 38:
dis.dispatch({action: 'view_prev_room'});
ev.stopPropagation();
ev.preventDefault();
break;
case 40:
dis.dispatch({action: 'view_next_room'});
ev.stopPropagation();
ev.preventDefault();
break;
}
}
},
onFocus: function(ev) {
dis.dispatch({action: 'focus_composer'});
},
showScreen: function(screen, params) {
if (screen == 'register') {
dis.dispatch({
action: 'start_registration',
params: params
});
} else if (screen == 'login') {
dis.dispatch({
action: 'start_login',
params: params
});
} else if (screen == 'cas_login') {
dis.dispatch({
action: 'cas_login',
params: params
});
} else if (screen.indexOf('room/') == 0) {
var roomString = screen.split('/')[1];
if (roomString[0] == '#') {
if (this.state.logged_in) {
dis.dispatch({
action: 'view_room_alias',
room_alias: roomString
});
} else {
// Okay, we'll take you here soon...
this.starting_room_alias = roomString;
// ...but you're still going to have to log in.
this.notifyNewScreen('login');
}
} else {
dis.dispatch({
action: 'view_room',
room_id: roomString
});
}
}
},
notifyNewScreen: function(screen) {
if (this.props.onNewScreen) {
this.props.onNewScreen(screen);
}
}
};

View File

@@ -0,0 +1,115 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher");
module.exports = {
getInitialState: function() {
return {
step: 'choose_hs',
busy: false,
currentStep: 0,
totalSteps: 1
};
},
setStep: function(step) {
this.setState({ step: step, busy: false });
},
onHSChosen: function() {
MatrixClientPeg.replaceUsingUrls(
this.getHsUrl(),
this.getIsUrl()
);
this.setState({
hs_url: this.getHsUrl(),
is_url: this.getIsUrl(),
});
this.setStep("fetch_stages");
var cli = MatrixClientPeg.get();
this.setState({
busy: true,
errorText: "",
});
var self = this;
cli.loginFlows().done(function(result) {
self.setState({
flows: result.flows,
currentStep: 1,
totalSteps: result.flows.length+1
});
self.setStep('stage_'+result.flows[0].type);
}, function(error) {
self.setStep("choose_hs");
self.setState({errorText: 'Unable to contact the given Home Server'});
});
},
onUserPassEntered: function(ev) {
ev.preventDefault();
this.setState({
busy: true,
errorText: "",
});
var self = this;
var formVals = this.getFormVals();
var loginParams = {
password: formVals.password
};
if (formVals.username.indexOf('@') > 0) {
loginParams.medium = 'email';
loginParams.address = formVals.username;
} else {
loginParams.user = formVals.username;
}
MatrixClientPeg.get().login('m.login.password', loginParams).done(function(data) {
MatrixClientPeg.replaceUsingAccessToken(
self.state.hs_url, self.state.is_url,
data.user_id, data.access_token
);
if (self.props.onLoggedIn) {
self.props.onLoggedIn();
}
}, function(error) {
self.setStep("stage_m.login.password");
if (error.httpStatus == 400 && loginParams.medium) {
self.setState({errorText: 'This Home Server does not support login using email address.'});
}
else if (error.httpStatus === 403) {
self.setState({errorText: 'Incorrect username and/or password.'});
}
else {
self.setState({
errorText: 'There was a problem logging in. (HTTP ' + error.httpStatus + ")"
});
}
});
},
showRegister: function(ev) {
ev.preventDefault();
dis.dispatch({
action: 'start_registration'
});
}
};

View File

@@ -14,45 +14,326 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher");
var extend = require('matrix-react-sdk/lib/extend');
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var BaseRegisterController = require('matrix-react-sdk/lib/controllers/templates/Register.js');
module.exports = {
FieldErrors: {
PasswordMismatch: 'PasswordMismatch',
TooShort: 'TooShort',
Missing: 'Missing',
InUse: 'InUse',
Length: 'Length'
},
var RegisterController = {};
extend(RegisterController, BaseRegisterController);
getInitialState: function() {
return {
step: 'initial',
busy: false,
currentStep: 0,
totalSteps: 1
};
},
RegisterController.onRegistered = function(user_id, access_token) {
MatrixClientPeg.replaceUsingAccessToken(
this.state.hs_url, this.state.is_url, user_id, access_token
);
componentWillMount: function() {
this.savedParams = {
email: '',
username: '',
password: '',
confirmPassword: ''
};
this.readNewProps();
},
this.setState({
step: 'profile',
busy: true
});
componentWillReceiveProps: function() {
this.readNewProps();
},
var self = this;
var cli = MatrixClientPeg.get();
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
self.setState({
avatarUrl: result.avatar_url,
busy: false
readNewProps: function() {
if (this.props.clientSecret && this.props.hsUrl &&
this.props.isUrl && this.props.sessionId &&
this.props.idSid) {
this.authSessionId = this.props.sessionId;
MatrixClientPeg.replaceUsingUrls(
this.props.hsUrl,
this.props.isUrl
);
this.setState({
hs_url: this.props.hsUrl,
is_url: this.props.isUrl
});
this.savedParams = {client_secret: this.props.clientSecret};
this.setState({busy: true});
var isLocation = document.createElement('a');
isLocation.href = this.props.isUrl;
var auth = {
type: 'm.login.email.identity',
threepid_creds: {
sid: this.props.idSid,
client_secret: this.savedParams.client_secret,
id_server: isLocation.host
}
};
this.tryRegister(auth);
}
},
componentDidUpdate: function() {
// Just putting a script tag into the returned jsx doesn't work, annoyingly,
// so we do this instead.
if (this.refs.recaptchaContainer) {
var scriptTag = document.createElement('script');
window.mx_on_recaptcha_loaded = this.onCaptchaLoaded;
scriptTag.setAttribute('src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit");
this.refs.recaptchaContainer.getDOMNode().appendChild(scriptTag);
}
},
setStep: function(step) {
this.setState({ step: step, errorText: '', busy: false });
},
getSupportedStageTypes: function() {
return ['m.login.email.identity', 'm.login.recaptcha'];
},
chooseFlow: function(flows) {
// this is fairly simple right now
var supportedTypes = this.getSupportedStageTypes();
var emailFlow = null;
var otherFlow = null;
for (var flowI = 0; flowI < flows.length; ++flowI) {
var flow = flows[flowI];
var flowHasEmail = false;
var flowSupported = true;
for (var stageI = 0; stageI < flow.stages.length; ++stageI) {
var stage = flow.stages[stageI];
if (supportedTypes.indexOf(stage) == -1) {
flowSupported = false;
}
if (stage == 'm.login.email.identity') {
flowHasEmail = true;
}
}
if (flowSupported) {
if (flowHasEmail) {
emailFlow = flow;
} else {
otherFlow = flow;
}
}
}
if (
this.savedParams.email != '' ||
this.completedStages.indexOf('m.login.email.identity') > -1
) {
return emailFlow;
} else {
return otherFlow;
}
},
firstUncompletedStageIndex: function(flow) {
if (this.completedStages === undefined) return 0;
for (var i = 0; i < flow.stages.length; ++i) {
if (this.completedStages.indexOf(flow.stages[i]) == -1) {
return i;
}
}
},
numCompletedStages: function(flow) {
if (this.completedStages === undefined) return 0;
var nCompleted = 0;
for (var i = 0; i < flow.stages.length; ++i) {
if (this.completedStages.indexOf(flow.stages[i]) > -1) {
++nCompleted;
}
}
return nCompleted;
},
onInitialStageSubmit: function(ev) {
ev.preventDefault();
var formVals = this.getRegFormVals();
this.savedParams = formVals;
var badFields = {};
if (formVals.password != formVals.confirmPassword) {
badFields.confirmPassword = this.FieldErrors.PasswordMismatch;
}
if (formVals.password == '') {
badFields.password = this.FieldErrors.Missing;
} else if (formVals.password.length < 6) {
badFields.password = this.FieldErrors.Length;
}
if (formVals.username == '') {
badFields.username = this.FieldErrors.Missing;
}
if (Object.keys(badFields).length > 0) {
this.onBadFields(badFields);
return;
}
MatrixClientPeg.replaceUsingUrls(
this.getHsUrl(),
this.getIsUrl()
);
this.setState({
hs_url: this.getHsUrl(),
is_url: this.getIsUrl()
});
this.setState({busy: true});
this.tryRegister();
},
startStage: function(stageName) {
var self = this;
this.setStep('stage_'+stageName);
switch(stageName) {
case 'm.login.email.identity':
self.setState({
busy: true
});
var cli = MatrixClientPeg.get();
this.savedParams.client_secret = cli.generateClientSecret();
this.savedParams.send_attempt = 1;
var nextLink = this.props.registrationUrl +
'?client_secret=' +
encodeURIComponent(this.savedParams.client_secret) +
"&hs_url=" +
encodeURIComponent(this.state.hs_url) +
"&is_url=" +
encodeURIComponent(this.state.is_url) +
"&session_id=" +
encodeURIComponent(this.authSessionId);
cli.requestEmailToken(
this.savedParams.email,
this.savedParams.client_secret,
this.savedParams.send_attempt,
nextLink
).done(function(response) {
self.setState({
busy: false,
});
self.setStep('stage_m.login.email.identity');
}, function(error) {
self.setStep('initial');
var newState = {busy: false};
if (error.errcode == 'THREEPID_IN_USE') {
self.onBadFields({email: self.FieldErrors.InUse});
} else {
newState.errorText = 'Unable to contact the given Home Server';
}
self.setState(newState);
});
break;
case 'm.login.recaptcha':
if (!this.authParams || !this.authParams['m.login.recaptcha'].public_key) {
this.setState({
errorText: "This server has not supplied enough information for Recaptcha authentication"
});
}
break;
}
},
onRegistered: function(user_id, access_token) {
MatrixClientPeg.replaceUsingAccessToken(
this.state.hs_url, this.state.is_url, user_id, access_token
);
if (this.props.onLoggedIn) {
this.props.onLoggedIn();
}
},
onCaptchaLoaded: function() {
if (this.refs.recaptchaContainer) {
var sitekey = this.authParams['m.login.recaptcha'].public_key;
global.grecaptcha.render('mx_recaptcha', {
'sitekey': sitekey,
'callback': this.onCaptchaDone
});
}
},
onCaptchaDone: function(captcha_response) {
this.tryRegister({
type: 'm.login.recaptcha',
response: captcha_response
});
},
function(err) {
console.err(err);
self.setState({
busy: false
});
});
};
RegisterController.onAccountReady = function() {
if (this.props.onLoggedIn) {
this.props.onLoggedIn();
tryRegister: function(auth) {
var self = this;
MatrixClientPeg.get().register(
this.savedParams.username,
this.savedParams.password,
this.authSessionId,
auth
).done(function(result) {
self.onRegistered(result.user_id, result.access_token);
}, function(error) {
if (error.httpStatus == 401 && error.data.flows) {
self.authParams = error.data.params;
self.authSessionId = error.data.session;
self.completedStages = error.data.completed || [];
var flow = self.chooseFlow(error.data.flows);
var flowStage = self.firstUncompletedStageIndex(flow);
var numDone = self.numCompletedStages(flow);
self.setState({
busy: false,
flows: flow,
currentStep: 1+numDone,
totalSteps: flow.stages.length+1,
flowStage: flowStage
});
self.startStage(flow.stages[flowStage]);
} else {
self.setStep("initial");
var newState = {
busy: false,
errorText: "Unable to contact the given Home Server"
};
if (error.name == 'M_USER_IN_USE') {
delete newState.errorText;
self.onBadFields({
username: self.FieldErrors.InUse
});
} else if (error.httpStatus == 401) {
newState.errorText = "Authorisation failed!";
} else if (error.httpStatus >= 400 && error.httpStatus < 500) {
newState.errorText = "Registration failed!";
} else if (error.httpStatus >= 500 && error.httpStatus < 600) {
newState.errorText = "Server error during registration!";
} else if (error.name == "M_MISSING_PARAM") {
// The HS hasn't remembered the login params from
// the first try when the login email was sent.
newState.errorText = "This home server does not support resuming registration.";
}
self.setState(newState);
}
});
},
showLogin: function(ev) {
ev.preventDefault();
dis.dispatch({
action: 'start_login'
});
}
};
module.exports = RegisterController;

47
src/dispatcher.js Normal file
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.
*/
'use strict';
var flux = require("flux");
class MatrixDispatcher extends flux.Dispatcher {
/**
* @param {Object} payload Required. The payload to dispatch.
* Must contain at least an 'action' key.
* @param {boolean} sync Optional. Pass true to dispatch
* synchronously. This is useful for anything triggering
* an operation that the browser requires user interaction
* for.
*/
dispatch(payload, sync) {
if (sync) {
super.dispatch(payload);
} else {
// Unless the caller explicitly asked for us to dispatch synchronously,
// we always set a timeout to do this: The flux dispatcher complains
// if you dispatch from within a dispatch, so rather than action
// handlers having to worry about not calling anything that might
// then dispatch, we just do dispatches asynchronously.
setTimeout(super.dispatch.bind(this, payload), 0);
}
}
};
if (global.mxDispatcher === undefined) {
global.mxDispatcher = new MatrixDispatcher();
}
module.exports = global.mxDispatcher;

38
src/encryption.js Normal file
View File

@@ -0,0 +1,38 @@
/*
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.
*/
function enableEncyption(client, roomId, members) {
members = members.slice(0);
members.push(client.credentials.userId);
// TODO: Check the keys actually match what keys the user has.
// TODO: Don't redownload keys each time.
return client.downloadKeys(members, "forceDownload").then(function(res) {
return client.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
members: members,
});
})
}
function disableEncryption(client, roomId) {
return client.disableRoomEncryption(roomId);
}
module.exports = {
enableEncryption: enableEncyption,
disableEncryption: disableEncryption,
}

View File

@@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_MTextTile {
white-space: pre-wrap;
}
'use strict';
module.exports = function(dest, src) {
for (var i in src) {
if (src.hasOwnProperty(i)) {
dest[i] = src[i];
}
}
return dest;
}

35
src/index.js Normal file
View File

@@ -0,0 +1,35 @@
/*
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 Skinner = require('./Skinner');
var Modulator = require('./Modulator');
module.exports.loadSkin = function(skinObject) {
Skinner.load(skinObject);
};
module.exports.loadModule = function(moduleObject) {
Modulator.loadModule(moduleObject);
};
module.exports.resetSkin = function() {
Skinner.reset();
};
module.exports.getComponent = function(componentName) {
return Skinner.getComponent(componentName);
};

120
src/linkify-matrix.js Normal file
View File

@@ -0,0 +1,120 @@
/*
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.
*/
function matrixLinkify(linkify) {
// Text tokens
var TT = linkify.scanner.TOKENS;
// Multi tokens
var MT = linkify.parser.TOKENS;
var MultiToken = MT.Base;
var S_START = linkify.parser.start;
var ROOMALIAS = function(value) {
MultiToken.call(this, value);
this.type = 'roomalias';
this.isLink = true;
};
ROOMALIAS.prototype = new MultiToken();
var S_HASH = new linkify.parser.State();
var S_HASH_NAME = new linkify.parser.State();
var S_HASH_NAME_COLON = new linkify.parser.State();
var S_HASH_NAME_COLON_DOMAIN = new linkify.parser.State();
var S_HASH_NAME_COLON_DOMAIN_DOT = new linkify.parser.State();
var S_ROOMALIAS = new linkify.parser.State(ROOMALIAS);
var roomname_tokens = [
TT.DOT,
TT.PLUS,
TT.NUM,
TT.DOMAIN,
TT.TLD
];
S_START.on(TT.POUND, S_HASH);
S_HASH.on(roomname_tokens, S_HASH_NAME);
S_HASH_NAME.on(roomname_tokens, S_HASH_NAME);
S_HASH_NAME.on(TT.DOMAIN, S_HASH_NAME);
S_HASH_NAME.on(TT.COLON, S_HASH_NAME_COLON);
S_HASH_NAME_COLON.on(TT.DOMAIN, S_HASH_NAME_COLON_DOMAIN);
S_HASH_NAME_COLON_DOMAIN.on(TT.DOT, S_HASH_NAME_COLON_DOMAIN_DOT);
S_HASH_NAME_COLON_DOMAIN_DOT.on(TT.DOMAIN, S_HASH_NAME_COLON_DOMAIN);
S_HASH_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_ROOMALIAS);
var USERID = function(value) {
MultiToken.call(this, value);
this.type = 'userid';
this.isLink = true;
};
USERID.prototype = new MultiToken();
var S_AT = new linkify.parser.State();
var S_AT_NAME = new linkify.parser.State();
var S_AT_NAME_COLON = new linkify.parser.State();
var S_AT_NAME_COLON_DOMAIN = new linkify.parser.State();
var S_AT_NAME_COLON_DOMAIN_DOT = new linkify.parser.State();
var S_USERID = new linkify.parser.State(USERID);
var username_tokens = [
TT.DOT,
TT.PLUS,
TT.NUM,
TT.DOMAIN,
TT.TLD
];
S_START.on(TT.AT, S_AT);
S_AT.on(username_tokens, S_AT_NAME);
S_AT_NAME.on(username_tokens, S_AT_NAME);
S_AT_NAME.on(TT.DOMAIN, S_AT_NAME);
S_AT_NAME.on(TT.COLON, S_AT_NAME_COLON);
S_AT_NAME_COLON.on(TT.DOMAIN, S_AT_NAME_COLON_DOMAIN);
S_AT_NAME_COLON_DOMAIN.on(TT.DOT, S_AT_NAME_COLON_DOMAIN_DOT);
S_AT_NAME_COLON_DOMAIN_DOT.on(TT.DOMAIN, S_AT_NAME_COLON_DOMAIN);
S_AT_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_USERID);
}
matrixLinkify.onUserClick = function(e, userId) { e.preventDefault(); };
matrixLinkify.onAliasClick = function(e, roomAlias) { e.preventDefault(); };
matrixLinkify.options = {
events: function (href, type) {
switch (type) {
case "userid":
return {
click: function(e) {
matrixLinkify.onUserClick(e, href);
}
};
case "roomalias":
return {
click: function(e) {
matrixLinkify.onAliasClick(e, href);
}
};
}
}
};
module.exports = matrixLinkify;

View File

@@ -1,134 +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 q = require("q");
var Matrix = require("matrix-js-sdk");
var Room = Matrix.Room;
var CallHandler = require('matrix-react-sdk/lib/CallHandler');
// FIXME: This currently forces Vector to try to hit the matrix.org AS for conferencing.
// This is bad because it prevents people running their own ASes from being used.
// This isn't permanent and will be customisable in the future: see the proposal
// at docs/conferencing.md for more info.
var USER_PREFIX = "fs_";
var DOMAIN = "matrix.org";
function ConferenceCall(matrixClient, groupChatRoomId) {
this.client = matrixClient;
this.groupRoomId = groupChatRoomId;
this.confUserId = module.exports.getConferenceUserIdForRoom(this.groupRoomId);
}
ConferenceCall.prototype.setup = function() {
var self = this;
return this._joinConferenceUser().then(function() {
return self._getConferenceUserRoom();
}).then(function(room) {
// return a call for *this* room to be placed. We also tack on
// confUserId to speed up lookups (else we'd need to loop every room
// looking for a 1:1 room with this conf user ID!)
var call = Matrix.createNewMatrixCall(self.client, room.roomId);
call.confUserId = self.confUserId;
return call;
});
};
ConferenceCall.prototype._joinConferenceUser = function() {
// Make sure the conference user is in the group chat room
var groupRoom = this.client.getRoom(this.groupRoomId);
if (!groupRoom) {
return q.reject("Bad group room ID");
}
var member = groupRoom.getMember(this.confUserId);
if (member && member.membership === "join") {
return q();
}
return this.client.invite(this.groupRoomId, this.confUserId);
};
ConferenceCall.prototype._getConferenceUserRoom = function() {
// Use an existing 1:1 with the conference user; else make one
var rooms = this.client.getRooms();
var confRoom = null;
for (var i = 0; i < rooms.length; i++) {
var confUser = rooms[i].getMember(this.confUserId);
if (confUser && confUser.membership === "join" &&
rooms[i].getJoinedMembers().length === 2) {
confRoom = rooms[i];
break;
}
}
if (confRoom) {
return q(confRoom);
}
return this.client.createRoom({
preset: "private_chat",
invite: [this.confUserId]
}).then(function(res) {
return new Room(res.room_id);
});
};
/**
* Check if this room member is in fact a conference bot.
* @param {RoomMember} The room member to check
* @return {boolean} True if it is a conference bot.
*/
module.exports.isConferenceUser = function(roomMember) {
if (roomMember.userId.indexOf("@" + USER_PREFIX) !== 0) {
return false;
}
var base64part = roomMember.userId.split(":")[0].substring(1 + USER_PREFIX.length);
if (base64part) {
var decoded = new Buffer(base64part, "base64").toString();
// ! $STUFF : $STUFF
return /^!.+:.+/.test(decoded);
}
return false;
};
module.exports.getConferenceUserIdForRoom = function(roomId) {
// abuse browserify's core node Buffer support (strip padding ='s)
var base64RoomId = new Buffer(roomId).toString("base64").replace(/=/g, "");
return "@" + USER_PREFIX + base64RoomId + ":" + DOMAIN;
};
module.exports.createNewMatrixCall = function(client, roomId) {
var confCall = new ConferenceCall(
client, roomId
);
return confCall.setup();
};
module.exports.getConferenceCallForRoom = function(roomId) {
// search for a conference 1:1 call for this group chat room ID
var activeCall = CallHandler.getAnyActiveCall();
if (activeCall && activeCall.confUserId) {
var thisRoomConfUserId = module.exports.getConferenceUserIdForRoom(
roomId
);
if (thisRoomConfUserId === activeCall.confUserId) {
return activeCall;
}
}
return null;
};
module.exports.ConferenceCall = ConferenceCall;
module.exports.slot = 'conference';

View File

@@ -1,171 +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.
*/
html {
/* hack to stop overscroll bounce on OSX and iOS.
N.B. Breaks things when we have legitimate horizontal overscroll */
height: 100%;
overflow: hidden;
}
body {
font-family: 'Lato', Helvetica, Arial, Sans-Serif;
font-size: 16px;
color: #454545;
border: 0px;
margin: 0px;
}
div.error {
color: red;
}
h2 {
color: #80cef4;
font-weight: 400;
font-size: 20px;
margin-top: 16px;
margin-bottom: 16px;
}
a:hover,
a:link,
a:visited {
color: #80CEF4;
}
.mx_ContextualMenu_background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 1.0;
z-index: 2000;
}
.mx_ContextualMenu {
border: 1px solid #a9dbf4;
border-radius: 8px;
background-color: #fff;
color: #747474;
position: fixed;
z-index: 2001;
padding: 6px;
}
.mx_ContextualMenu_chevron_right {
padding: 12px;
position: absolute;
right: -21px;
top: 0px;
}
.mx_ContextualMenu_chevron_left {
padding: 12px;
position: absolute;
left: -21px;
top: 0px;
}
.mx_ContextualMenu_field {
padding: 3px 6px 3px 6px;
cursor: pointer;
}
.mx_ContextualMenu_spinner {
display: block;
margin: 0 auto;
}
.mx_Dialog_background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
opacity: 0.2;
z-index: 2000;
}
.mx_Dialog_wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
-webkit-justify-content: center;
justify-content: center;
}
.mx_Dialog {
background-color: #fff;
color: #747474;
text-align: center;
z-index: 2010;
font-weight: 300;
font-size: 16px;
position: relative;
border-radius: 8px;
max-width: 75%;
}
.mx_ImageView {
margin: 6px;
/* hack: flexbox bug? */
margin-bottom: 4px;
}
.mx_Dialog_content {
margin: 24px;
}
.mx_Dialog_buttons {
padding-bottom: 24px;
}
.mx_Dialog button {
border: 0px;
height: 36px;
border-radius: 36px;
font-weight: 400;
font-size: 16px;
color: #fff;
background-color: #80cef4;
margin-left: 8px;
margin-right: 8px;
padding-left: 1em;
padding-right: 1em;
}
.mx_ErrorDialogTitle,
.mx_QuestionDialogTitle {
min-height: 16px;
padding: 12px;
border-bottom: 1px solid #a9dbf4;
font-weight: bold;
font-size: 20px;
line-height: 1.4;
}

View File

@@ -1,10 +0,0 @@
.mx_RoomDropTarget,
.mx_RoomList_favourites_label,
.mx_RoomList_archive_label,
.mx_RoomHeader_search,
.mx_RoomSettings_encrypt,
.mx_CreateRoom_encrypt,
.mx_RightPanel_filebutton
{
display: none !important;
}

View File

@@ -1,112 +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_EventTile {
max-width: 100%;
clear: both;
margin-top: 32px;
margin-left: 56px;
}
.mx_EventTile_avatar {
padding-left: 12px;
padding-right: 12px;
margin-left: -64px;
margin-top: -7px;
float: left;
}
.mx_EventTile_avatar img {
background-color: #dbdbdb;
border-radius: 20px;
border: 0px;
}
.mx_EventTile_continuation {
margin-top: 8px ! important;
}
.mx_EventTile .mx_SenderProfile {
color: #454545;
opacity: 0.5;
font-size: 14px;
margin-bottom: 4px;
display: block;
}
.mx_EventTile .mx_MessageTimestamp {
color: #454545;
opacity: 0.5;
font-size: 14px;
float: right;
}
.mx_EventTile_content {
padding-right: 100px;
display: block;
}
.mx_EventTile_notice .mx_MessageTile_content {
opacity: 0.5;
}
.mx_EventTile_sending {
color: #ddd;
}
.mx_EventTile_notSent {
color: #f11;
}
.mx_EventTile_highlight {
color: #FF0064;
}
.mx_EventTile_msgOption {
float: right;
}
.mx_MessageTimestamp {
display: none;
}
.mx_EventTile_last .mx_MessageTimestamp {
display: block;
}
.mx_EventTile:hover .mx_MessageTimestamp {
display: block;
}
.mx_EventTile_editButton {
float: right;
display: none;
border: 0px;
outline: none;
margin-right: 3px;
}
.mx_EventTile:hover .mx_EventTile_editButton {
display: inline-block;
}
.mx_EventTile.menu .mx_EventTile_editButton {
display: inline-block;
}
.mx_EventTile.menu .mx_MessageTimestamp {
display: inline-block;
}

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_MNoticeTile {
opacity: 0.5;
}

View File

@@ -1,16 +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.
*/

View File

@@ -1,134 +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_MemberTile {
display: table-row;
height: 49px;
position: relative;
}
.mx_MemberTile_avatar {
display: table-cell;
padding-left: 14px;
padding-right: 12px;
padding-top: 3px;
padding-bottom: 3px;
vertical-align: middle;
width: 40px;
height: 40px;
position: relative;
}
.mx_MemberTile_inviteTile {
cursor: pointer;
}
.mx_MemberTile_inviteEditing {
display: initial ! important;
}
.mx_MemberTile_inviteEditing .mx_MemberTile_avatar {
display: none;
}
.mx_MemberTile_inviteEditing .mx_MemberTile_name {
width: 200px;
}
.mx_MemberTile_inviteEditing .mx_MemberTile_name input {
border-radius: 3px;
border: 1px solid #c7c7c7;
font-weight: 300;
font-size: 14px;
padding: 9px;
margin-top: 6px;
margin-left: 14px;
}
.mx_MemberTile_power {
position: absolute;
width: 48px;
height: 48px;
left: 10px;
top: -1px;
}
.mx_MemberTile_name {
display: table-cell;
vertical-align: middle;
overflow: hidden;
text-overflow: ellipsis;
}
.mx_MemberTile_details {
display: table-cell;
padding-right: 14px;
vertical-align: middle;
}
.mx_MemberTile_hover {
background-color: #f0f0f0;
font-size: 12px;
color: #747474;
}
.mx_MemberTile_userId {
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
}
.mx_MemberTile_leave {
cursor: pointer;
margin-top: 8px;
margin-right: -4px;
margin-left: 6px;
float: right;
}
/*
.mx_MemberTile_nameWrapper {
display: table-cell;
vertical-align: middle;
overflow: hidden;
text-overflow: ellipsis;
}
.mx_MemberTile_nameSpan {
}
*/
.mx_MemberTile_unavailable .mx_MemberTile_avatar,
.mx_MemberTile_unavailable .mx_MemberTile_name,
.mx_MemberTile_unavailable .mx_MemberTile_nameSpan
{
opacity: 0.66;
}
.mx_MemberTile_offline .mx_MemberTile_avatar,
.mx_MemberTile_offline .mx_MemberTile_name,
.mx_MemberTile_offline .mx_MemberTile_nameSpan
{
opacity: 0.25;
}
.mx_MemberTile_zalgo {
font-family: Helvetica, Arial, Sans-Serif;
}
.mx_MemberTile:hover .mx_MessageTimestamp {
display: block;
}

View File

@@ -1,84 +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_wrapper {
max-width: 720px;
height: 50px;
vertical-align: middle;
margin: auto;
background-color: #fff;
border-radius: 25px;
border: 1px solid #a9dbf4;
}
.mx_MessageComposer_row {
display: table-row;
width: 100%;
height: 50px;
}
.mx_MessageComposer .mx_MessageComposer_avatar {
display: table-cell;
padding-left: 5px;
padding-right: 10px;
height: 50px;
}
.mx_MessageComposer .mx_MessageComposer_avatar img {
margin-top: 5px;
border-radius: 20px;
background-color: #dbdbdb;
}
.mx_MessageComposer_input {
display: table-cell;
width: 100%;
vertical-align: middle;
height: 50px;
}
.mx_MessageComposer_input textarea {
font-size: 15px;
width: 100%;
height: 1.2em;
padding-top: 0.7em;
padding-bottom: 0.7em;
border: 0px;
resize: none;
outline: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
/* needed for FF */
font-family: 'Lato', Helvetica, Arial, Sans-Serif;
}
/* hack for FF as vertical alignment of custom placeholder text is broken */
.mx_MessageComposer_input textarea::-moz-placeholder {
line-height: 100%;
}
.mx_MessageComposer_upload {
display: table-cell;
vertical-align: middle;
padding-right: 15px;
cursor: pointer;
}
.mx_MessageComposer_upload img {
margin-top: 5px;
}

View File

@@ -1,27 +0,0 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RoomDropTarget {
font-size: 14px;
text-align: center;
margin-left: 8px;
margin-right: 8px;
padding-top: 16px;
padding-bottom: 16px;
background-color: #fbfbfb;
border: 1px dashed #d7d7d7;
border-radius: 8px;
}

View File

@@ -1,173 +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 {
}
.mx_RoomHeader_wrapper {
max-width: 720px;
margin: auto;
height: 88px;
border-bottom: 1px solid #a8dbf3;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
}
.mx_RoomHeader_leftRow {
height: 48px;
margin-top: 18px;
-webkit-box-ordinal-group: 1;
-moz-box-ordinal-group: 1;
-ms-flex-order: 1;
-webkit-order: 1;
order: 1;
-webkit-flex: 1;
flex: 1;
}
.mx_RoomHeader_textButton {
height: 48px;
margin-top: 18px;
background-color: #80cef4;
border-radius: 48px;
margin-right: 8px;
color: #fff;
line-height: 48px;
text-align: center;
-webkit-box-ordinal-group: 2;
-moz-box-ordinal-group: 2;
-ms-flex-order: 2;
-webkit-order: 2;
order: 2;
cursor: pointer;
/*
-webkit-flex: 0 0 90px;
flex: 0 0 90px;
*/
padding-left: 12px;
padding-right: 12px;
}
.mx_RoomHeader_rightRow {
height: 48px;
margin-top: 18px;
background-color: #fff;
border-radius: 48px;
border: 1px solid #a9dbf4;
-webkit-box-ordinal-group: 3;
-moz-box-ordinal-group: 3;
-ms-flex-order: 3;
-webkit-order: 3;
order: 3;
}
.mx_RoomHeader_info {
display: table-cell;
height: 48px;
vertical-align: middle;
}
.mx_RoomHeader_simpleHeader {
line-height: 88px;
color: #80cef4;
font-weight: 400;
font-size: 20px;
overflow: hidden;
text-overflow: ellipsis;
}
.mx_RoomHeader_name {
vertical-align: middle;
height: 28px;
color: #80cef4;
font-weight: 400;
font-size: 20px;
padding-left: 16px;
padding-right: 16px;
text-overflow: ellipsis;
}
.mx_RoomHeader_nameEditing {
padding-left: 16px;
padding-right: 16px;
margin-top: -5px;
}
.mx_RoomHeader_name input, .mx_RoomHeader_nameInput {
border-radius: 3px;
width: 260px;
border: 1px solid #c7c7c7;
font-weight: 300;
font-size: 14px;
padding: 9px;
}
.mx_RoomHeader_nameInput {
margin-top: 6px;
}
.mx_RoomHeader_topic {
vertical-align: bottom;
float: left;
max-height: 38px;
color: #70b5d7;
font-weight: 300;
padding-left: 16px;
padding-right: 16px;
overflow: hidden;
text-overflow: ellipsis;
}
.mx_RoomHeader_avatar {
display: table-cell;
width: 48px;
height: 50px;
vertical-align: middle;
}
.mx_RoomHeader_avatar img {
border-radius: 24px;
}
.mx_RoomHeader_button {
height: 48px;
display: table-cell;
vertical-align: middle;
padding-left: 8px;
padding-right: 8px;
}
.mx_RoomHeader_button img {
cursor: pointer;
}
.mx_RoomHeader_voipButton {
display: table-cell;
}
.mx_RoomHeader_voipButtons {
margin-top: 18px;
}

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.
*/
.mx_RoomSettings {
max-height: 250px;
padding-top: 12px;
}
.mx_RoomSettings_settings {
display: table;
margin: 5px 0;
}
.mx_RoomSettings_settings > div {
display: table-row;
}
.mx_RoomSettings_settings > div > * {
display: table-cell;
margin: 0 10px;
}
.mx_RoomSettings input,
.mx_RoomSettings textarea {
border-radius: 3px;
border: 1px solid #c7c7c7;
font-weight: 300;
font-size: 14px;
padding: 9px;
margin-top: 6px;
}
.mx_RoomSettings_description {
width: 330px;
}
.mx_RoomSettings_buttons {
text-align: right;
margin-bottom: 16px;
}
.mx_RoomSettings_button {
display: inline;
border: 0px;
height: 36px;
border-radius: 36px;
font-weight: 400;
font-size: 16px;
color: #fff;
background-color: #80cef4;
width: auto;
margin: auto;
padding: 6px;
padding-left: 1em;
padding-right: 1em;
}

View File

@@ -1,104 +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 {
cursor: pointer;
display: table-row;
color: #818794;
}
.mx_RoomTile_avatar {
display: table-cell;
padding-right: 10px;
padding-top: 3px;
padding-bottom: 3px;
padding-left: 10px;
vertical-align: middle;
width: 36px;
height: 36px;
position: relative;
}
.mx_RoomTile_avatar img {
border-radius: 20px;
background-color: #dbdbdb;
}
.mx_RoomTile_name {
display: table-cell;
vertical-align: middle;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 16px;
}
.collapsed .mx_RoomTile_name {
display: none;
}
/*
.mx_RoomTile_nameBadge {
display: table;
width: 100%;
height: 50px;
}
.mx_RoomTile_badgeCell {
display: table-cell;
vertical-align: middle;
width: 26px;
}
.mx_RoomTile_badge {
background-color: #80cef4;
color: #fff;
border-radius: 26px;
font-weight: 400;
font-size: 14px;
line-height: 28px;
width: 26px;
height: 26px;
text-align: center;
}
*/
.mx_RoomTile_badge {
background-color: #ff0064;
border: 3px solid #fff;
border-radius: 16px;
width: 9px;
height: 9px;
position: absolute;
right: 9px;
bottom: 3px;
}
.mx_RoomTile_unread,
.mx_RoomTile_highlight,
.mx_RoomTile_invited
{
font-weight: bold;
color: #000;
}
.mx_RoomTile_selected {
background-color: #f3f8fa;
color: #80cef4;
font-weight: bold;
}
.mx_RoomTile:hover {
}

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.
*/
.mx_RoomTooltip {
display: none;
position: fixed;
border: 1px solid #a9dbf4;
border-radius: 8px;
background-color: #fff;
z-index: 1000;
margin-top: 6px;
left: 64px;
padding: 6px;
}
.mx_RoomTooltip_chevron {
position: absolute;
left: -9px;
top: 8px;
}

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_zalgo {
font-family: Helvetica, Arial, Sans-Serif;
display: table-row ! important;
}

View File

@@ -1,15 +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.
*/

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.
*/
.mx_IncomingCallBox {
text-align: center;
border: 1px solid #a9dbf4;
border-radius: 8px;
background-color: #fff;
position: absolute;
z-index: 1000;
left: 235px;
top: 155px;
padding: 6px;
}
.mx_IncomingCallBox_chevron {
padding: 12px;
position: absolute;
left: -21px;
top: 0px;
}
.mx_IncomingCallBox_title {
padding: 6px;
font-weight: bold;
}
.mx_IncomingCallBox_buttons {
display: table-row;
}
.mx_IncomingCallBox_buttons_cell {
vertical-align: middle;
display: table-cell;
padding: 6px;
width: 50%;
}
.mx_IncomingCallBox_buttons_decline,
.mx_IncomingCallBox_buttons_accept {
vertical-align: middle;
width: 80px;
height: 36px;
line-height: 36px;
border-radius: 36px;
color: #fff;
}
.mx_IncomingCallBox_buttons_decline {
background-color: #f48080;
}
.mx_IncomingCallBox_buttons_accept {
background-color: #80f480;
}

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.
*/
.mx_CreateRoom {
width: 720px;
margin-left: auto;
margin-right: auto;
color: #4a4a4a;
}
.mx_CreateRoom input,
.mx_CreateRoom textarea {
border-radius: 3px;
border: 1px solid #c7c7c7;
font-weight: 300;
font-size: 14px;
padding: 9px;
margin-top: 6px;
}
.mx_CreateRoom_description {
width: 330px;
}

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.
*/
.mx_LeftPanel {
position: relative;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
flex-direction: column;
-webkit-flex-direction: column;
}
.mx_LeftPanel_hideButton {
position: absolute;
top: 10px;
right: 0px;
padding: 8px;
cursor: pointer;
}
.mx_LeftPanel .mx_RoomList {
-webkit-box-ordinal-group: 1;
-moz-box-ordinal-group: 1;
-ms-flex-order: 1;
-webkit-order: 1;
order: 1;
overflow-y: auto;
-webkit-flex: 1 1 0;
flex: 1 1 0;
}
.mx_LeftPanel .mx_BottomLeftMenu {
-webkit-box-ordinal-group: 3;
-moz-box-ordinal-group: 3;
-ms-flex-order: 3;
-webkit-order: 3;
order: 3;
-webkit-flex: 0 0 170px;
flex: 0 0 170px;
border-top: 1px solid #f3f8fa;
}
.mx_LeftPanel .mx_BottomLeftMenu .mx_RoomTile {
color: #378bb4;
}
.mx_LeftPanel .mx_BottomLeftMenu .mx_BottomLeftMenu_options {
margin-top: 12px;
width: 100%;
}

View File

@@ -1,60 +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_MemberList {
height: 100%;
margin-bottom: 100px;
padding: 8px;
-webkit-flex: 1;
flex: 1;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
flex-direction: column;
-webkit-flex-direction: column;
}
.mx_MemberList_chevron {
position: absolute;
right: 35px;
margin-top: -15px;
}
.mx_MemberList_border {
border: 1px solid #a9dbf4;
overflow-y: auto;
border-radius: 8px;
background-color: #fff;
order: 1;
-webkit-flex: 1 1 0;
flex: 1 1 0px;
}
.mx_MemberList_wrapper {
display: table;
table-layout: fixed;
width: 100%;
}
.mx_MemberList h2 {
margin: 14px;
}

View File

@@ -1,67 +0,0 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RightPanel {
position: relative;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
flex-direction: column;
-webkit-flex-direction: column;
}
.mx_RightPanel_header {
-webkit-box-ordinal-group: 1;
-moz-box-ordinal-group: 1;
-ms-flex-order: 1;
-webkit-order: 1;
order: 1;
-webkit-flex: 0 0 66px;
flex: 0 0 66px;
}
/** Fixme - factor this out with the main header **/
.mx_RightPanel_headerButtonGroup {
margin-top: 18px;
height: 48px;
float: right;
background-color: #fff;
border-radius: 48px;
border: 1px solid #a9dbf4;
margin-right: 22px;
}
.mx_RightPanel_headerButton {
cursor: pointer;
height: 48px;
display: table-cell;
vertical-align: middle;
padding-left: 8px;
padding-right: 8px;
}
.mx_RightPanel .mx_MemberList {
-webkit-box-ordinal-group: 2;
-moz-box-ordinal-group: 2;
-ms-flex-order: 2;
-webkit-order: 2;
order: 2;
}

View File

@@ -1,99 +0,0 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RoomDirectory {
width: 720px;
margin-left: auto;
margin-right: auto;
margin-bottom: 12px;
color: #4a4a4a;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
flex-direction: column;
-webkit-flex-direction: column;
}
.mx_RoomDirectory_list {
-webkit-flex: 1;
flex: 1;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
flex-direction: column;
-webkit-flex-direction: column;
}
.mx_RoomDirectory_input {
margin: auto;
border-radius: 3px;
border: 1px solid #c7c7c7;
font-weight: 300;
font-size: 14px;
padding: 9px;
margin-top: 12px;
margin-bottom: 12px;
}
.mx_RoomDirectory_tableWrapper {
overflow-y: auto;
-webkit-flex: 1 1 0;
flex: 1 1 0;
}
.mx_RoomDirectory_table {
width: 100%;
text-align: left;
table-layout: fixed;
}
.mx_RoomDirectory_table th {
font-weight: 400;
font-size: 12px;
}
.mx_RoomDirectory_table tbody {
cursor: pointer;
}
.mx_RoomDirectory_table td {
font-weight: 300;
font-size: 16px;
overflow-x: hidden;
text-overflow: ellipsis;
}
.mx_RoomDirectory_table .mx_RoomDirectory_name {
font-weight: 400;
}
.mx_RoomDirectory_table .mx_RoomDirectory_topic {
font-weight: 400;
font-size: 12px;
}
.mx_RoomDirectory_table td,
.mx_RoomDirectory_table th, {
padding: 6px;
}

View File

@@ -1,255 +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;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
width: 100%;
flex-direction: column;
-webkit-flex-direction: column;
}
.mx_RoomView .mx_RoomHeader {
-webkit-box-ordinal-group: 1;
-moz-box-ordinal-group: 1;
-ms-flex-order: 1;
-webkit-order: 1;
order: 1;
-webkit-flex: 0 0 88px;
flex: 0 0 88px;
}
.mx_RoomView_fileDropTarget {
min-width: 0px;
max-width: 720px;
width: 100%;
font-size: 20px;
text-align: center;
pointer-events: none;
padding-left: 12px;
padding-right: 12px;
margin-left: -12px;
-webkit-border-top-left-radius: 10px;
-webkit-border-top-right-radius: 10px;
-moz-border-radius-topleft: 10px;
-moz-border-radius-topright: 10px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
background-color: rgba(255, 255, 255, 0.9);
border: 2px dashed #80cef4;
border-bottom: none;
position: absolute;
top: 88px;
bottom: 0px;
z-index: 3000;
}
.mx_RoomView_fileDropTargetLabel {
top: 50%;
width: 100%;
margin-top: -50px;
position: absolute;
}
.mx_RoomView_auxPanel {
-webkit-box-ordinal-group: 2;
-moz-box-ordinal-group: 2;
-ms-flex-order: 2;
-webkit-order: 2;
order: 2;
min-width: 0px;
max-width: 720px;
width: 100%;
margin: auto;
overflow: auto;
border-bottom: 1px solid #a8dbf3;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
.mx_RoomView_messagePanel {
-webkit-box-ordinal-group: 3;
-moz-box-ordinal-group: 3;
-ms-flex-order: 3;
-webkit-order: 3;
order: 3;
-webkit-flex: 1 1 0;
flex: 1 1 0;
width: 100%;
overflow-y: auto;
}
.mx_RoomView_messageListWrapper {
max-width: 720px;
margin: auto;
}
.mx_RoomView_MessageList {
width: 100%;
list-style-type: none;
padding: 0px;
}
.mx_RoomView_MessageList li {
clear: both;
}
.mx_RoomView_MessageList h2 {
clear: both;
margin-top: 32px;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid #a8dbf3;
}
.mx_RoomView_invitePrompt {
-webkit-box-ordinal-group: 2;
-moz-box-ordinal-group: 2;
-ms-flex-order: 2;
-webkit-order: 2;
order: 2;
min-width: 0px;
max-width: 720px;
width: 100%;
margin: auto;
margin-top: 12px;
margin-bottom: 12px;
}
.mx_RoomView_statusArea {
-webkit-box-ordinal-group: 4;
-moz-box-ordinal-group: 4;
-ms-flex-order: 4;
-webkit-order: 4;
order: 4;
width: 100%;
-webkit-flex: 0 0 58px;
flex: 0 0 58px;
}
.mx_RoomView_statusAreaBox {
max-width: 720px;
margin: auto;
border-top: 1px solid #a8dbf3;
}
.mx_RoomView_unreadMessagesBar {
margin-top: 13px;
color: #fff;
font-weight: bold;
background-color: #ff0064;
border-radius: 30px;
height: 30px;
line-height: 30px;
cursor: pointer;
}
.mx_RoomView_unreadMessagesBar img {
padding-left: 22px;
padding-right: 22px;
}
.mx_RoomView_typingBar {
margin-top: 17px;
margin-left: 56px;
color: #818794;
}
.mx_RoomView_typingBar img {
padding-left: 12px;
padding-right: 12px;
margin-left: -64px;
margin-top: -7px;
float: left;
}
.mx_RoomView .mx_MessageComposer {
-webkit-box-ordinal-group: 5;
-moz-box-ordinal-group: 5;
-ms-flex-order: 5;
-webkit-order: 5;
order: 5;
width: 100%;
-webkit-flex: 0 0 63px;
flex: 0 0 63px;
margin-right: 2px;
}
.mx_RoomView_uploadProgressOuter {
width: 100%;
background-color: rgba(169, 219, 244, 0.5);
height: 4px;
}
.mx_RoomView_uploadProgressInner {
background-color: #80cef4;
height: 4px;
}
.mx_RoomView_uploadFilename {
margin-top: 15px;
margin-left: 56px;
}
.mx_RoomView_uploadIcon {
float: left;
margin-top: 6px;
margin-left: 5px;
}
.mx_RoomView_uploadCancel {
float: right;
margin-top: 6px;
margin-right: 10px;
}
.mx_RoomView_uploadBytes {
float: right;
opacity: 0.5;
margin-top: 15px;
margin-right: 10px;
}
.mx_RoomView_ongoingConfCallNotification {
width: 100%;
text-align: center;
background-color: #ff0064;
color: #fff;
font-weight: bold;
padding: 6px;
}

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.
*/
.mx_UserSettings {
width: 720px;
margin-left: auto;
margin-right: auto;
}

View File

@@ -1,3 +0,0 @@
.mx_ViewSource pre {
text-align: left;
}

View File

@@ -1,125 +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_wrapper {
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
flex-direction: column;
-webkit-flex-direction: column;
width: 100%;
height: 100%;
}
.mx_MatrixToolbar {
-webkit-box-ordinal-group: 1;
-moz-box-ordinal-group: 1;
-ms-flex-order: 1;
-webkit-order: 1;
order: 1;
height: 21px;
}
.mx_MatrixChat_toolbarShowing {
height: auto;
}
.mx_MatrixChat {
width: 100%;
height: 100%;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-box-ordinal-group: 2;
-moz-box-ordinal-group: 2;
-ms-flex-order: 2;
-webkit-order: 2;
order: 2;
-webkit-flex: 1;
flex: 1;
}
.mx_MatrixChat .mx_LeftPanel {
-webkit-box-ordinal-group: 1;
-moz-box-ordinal-group: 1;
-ms-flex-order: 1;
-webkit-order: 1;
order: 1;
-webkit-flex: 0 0 230px;
flex: 0 0 230px;
}
.mx_MatrixChat .mx_LeftPanel.collapsed {
-webkit-flex: 0 0 60px;
flex: 0 0 60px;
}
.mx_MatrixChat .mx_MatrixChat_middlePanel {
-webkit-box-ordinal-group: 2;
-moz-box-ordinal-group: 2;
-ms-flex-order: 2;
-webkit-order: 2;
order: 2;
padding-left: 12px;
padding-right: 12px;
background-color: #f3f8fa;
-webkit-flex: 1;
flex: 1;
/* XXX: Hack: apparently if you try to nest a flex-box
* within a non-flex-box within a flex-box, the height
* of the innermost element gets miscalculated if the
* parents are both auto.
* Ideally we'd launch straight into the RoomView at this
* point, but instead we fudge it and make the middlePanel
* flex itself.
*/
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
}
.mx_MatrixChat .mx_RightPanel {
-webkit-box-ordinal-group: 3;
-moz-box-ordinal-group: 3;
-ms-flex-order: 3;
-webkit-order: 3;
order: 3;
background-color: #f3f8fa;
-webkit-flex: 0 0 230px;
flex: 0 0 230px;
}
.mx_MatrixChat .mx_RightPanel.collapsed {
-webkit-flex: 0 0 72px;
flex: 0 0 72px;
}

View File

@@ -1,110 +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: 100%;
height: 100%;
}
.mx_Login h2 {
color: #4a4a4a;
font-weight: 300;
margin-top: 32px;
margin-bottom: 20px;
}
.mx_Login_box {
width: 300px;
margin: auto;
padding-top: 100px;
}
.mx_Login_logo {
text-align: center;
}
.mx_Login_field {
width: 100%;
border-radius: 3px;
border: 1px solid #c7c7c7;
font-weight: 300;
font-size: 14px;
padding: 9px;
margin-bottom: 14px;
}
.mx_Login_submit {
margin-top: 35px;
margin-bottom: 24px;
width: 100%;
border-radius: 40px;
height: 40px;
border: 0px;
background-color: #76cfa6;
font-size: 16px;
color: #fff;
}
.mx_Login_label {
font-size: 14px;
opacity: 0.8;
}
.mx_Login_checkbox {
margin-right: 10px;
}
.mx_Login_create {
display: block;
text-align: center;
width: 100%;
font-size: 14px;
opacity: 0.8;
}
.mx_Login_create:link {
color: #4a4a4a;
}
.mx_Login_links {
display: block;
text-align: center;
width: 100%;
font-size: 14px;
opacity: 0.8;
}
.mx_Login_links a:link {
color: #4a4a4a;
}
.mx_Login_loader {
position: absolute;
left: 50%;
margin-top: 12px;
}
.mx_Login_error {
color: #ff2020;
font-weight: bold;
text-align: center;
/*
height: 24px;
*/
margin-top: 12px;
margin-bottom: 12px;
}

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