mirror of
https://github.com/element-hq/element-web
synced 2024-11-24 02:05:45 +03:00
Merge branch 'develop' into feature-composer-emoji
This commit is contained in:
commit
b7555f49ea
44 changed files with 1057 additions and 311 deletions
180
README.md
180
README.md
|
@ -3,65 +3,85 @@ matrix-react-sdk
|
||||||
|
|
||||||
This is a react-based SDK for inserting a Matrix chat/voip client into a web page.
|
This is a react-based SDK for inserting a Matrix chat/voip client into a web page.
|
||||||
|
|
||||||
This package provides the logic and 'controller' parts for the UI components. This
|
This package provides the React components needed to build a Matrix web client
|
||||||
forms one part of a complete matrix client, but it not useable in isolation. It
|
using React. It is not useable in isolation, and instead must must be used from
|
||||||
must be used from a 'skin'. A skin provides:
|
a 'skin'. A skin provides:
|
||||||
* The HTML for the UI components (in the form of React `render` methods)
|
* Customised implementations of presentation components.
|
||||||
* The CSS for this HTML
|
* Custom CSS
|
||||||
* The containing application
|
* The containing application
|
||||||
* Zero or more 'modules' containing non-UI functionality
|
* Zero or more 'modules' containing non-UI functionality
|
||||||
|
|
||||||
Skins are modules are exported from such a package in the `lib` directory.
|
**WARNING: As of July 2016, the skinning abstraction is broken due to rapid
|
||||||
`lib/skins` contains one directory per-skin, named after the skin, and the
|
development of `matrix-react-sdk` to meet the needs of Vector, the first app
|
||||||
`modules` directory contains modules as their javascript files.
|
to be built on top of the SDK** (https://github.com/vector-im/vector-web).
|
||||||
|
Right now `matrix-react-sdk` depends on some functionality from `vector-web`
|
||||||
|
(e.g. CSS), and `matrix-react-sdk` contains some Vector specific behaviour
|
||||||
|
(grep for 'vector'). This layering will be fixed asap once Vector development
|
||||||
|
has stabilised, but for now we do not advise trying to create new skins for
|
||||||
|
matrix-react-sdk until the layers are clearly separated again.
|
||||||
|
|
||||||
A basic skin is provided in the matrix-react-skin package. This also contains
|
In the interim, `vector-im/vector-web` and `matrix-org/matrix-react-sdk` should
|
||||||
a minimal application that instantiates the basic skin making a working matrix
|
be considered as a single project (for instance, matrix-react-sdk bugs
|
||||||
client.
|
are currently filed against vector-im/vector-web rather than this project).
|
||||||
|
|
||||||
You can use matrix-react-sdk directly, but to do this you would have to provide
|
Developer Guide
|
||||||
'views' for each UI component. To get started quickly, use matrix-react-skin.
|
===============
|
||||||
|
|
||||||
How to customise the SDK
|
Platform Targets:
|
||||||
========================
|
* Chrome, Firefox and Safari.
|
||||||
|
* Edge should also work, but we're not testing it proactively.
|
||||||
|
* WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox.
|
||||||
|
* Mobile Web is not currently a target platform - instead please use the native
|
||||||
|
iOS (https://github.com/matrix-org/matrix-ios-kit) and Android
|
||||||
|
(https://github.com/matrix-org/matrix-android-sdk) SDKs.
|
||||||
|
|
||||||
The SDK formerly used the 'atomic' design pattern as seen at http://patternlab.io to
|
All code lands on the `develop` branch - `master` is only used for stable releases.
|
||||||
encourage a very modular and reusable architecture, making it easy to
|
**Please file PRs against `develop`!!**
|
||||||
customise and use UI widgets independently of the rest of the SDK and your app.
|
|
||||||
|
|
||||||
So unfortunately at the moment this document does not describe how to customize your UI!
|
Please follow the standard Matrix contributor's guide:
|
||||||
|
https://github.com/matrix-org/synapse/tree/master/CONTRIBUTING.rst
|
||||||
|
|
||||||
###This is the old description for the atomic design pattern:
|
Please follow the Matrix JS/React code style as per:
|
||||||
|
https://github.com/matrix-org/matrix-react-sdk/tree/master/code_style.rst
|
||||||
|
|
||||||
In practice this means:
|
Whilst the layering separation between matrix-react-sdk and Vector is broken
|
||||||
|
(as of July 2016), code should be committed as follows:
|
||||||
|
* All new components: https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components
|
||||||
|
* Vector-specific components: https://github.com/vector-im/vector-web/tree/master/src/components
|
||||||
|
* In practice, `matrix-react-sdk` is still evolving so fast that the maintenance
|
||||||
|
burden of customising and overriding these components for Vector can seriously
|
||||||
|
impede development. So right now, there should be very few (if any) customisations for Vector.
|
||||||
|
* CSS for Matrix SDK components: https://github.com/vector-im/vector-web/tree/master/src/skins/vector/css/matrix-react-sdk
|
||||||
|
* CSS for Vector-specific overrides and components: https://github.com/vector-im/vector-web/tree/master/src/skins/vector/css/vector-web
|
||||||
|
|
||||||
* The UI of the app is strictly split up into a hierarchy of components.
|
React components in matrix-react-sdk are come in two different flavours:
|
||||||
|
'structures' and 'views'. Structures are stateful components which handle the
|
||||||
* Each component has its own:
|
more complicated business logic of the app, delegating their actual presentation
|
||||||
* View object defined as a React javascript class containing embedded
|
rendering to stateless 'view' components. For instance, the RoomView component
|
||||||
HTML expressed in React's JSX notation.
|
that orchestrates the act of visualising the contents of a given Matrix chat room
|
||||||
* CSS file, which defines the styling specific to that component.
|
tracks lots of state for its child components which it passes into them for visual
|
||||||
|
rendering via props.
|
||||||
* 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.
|
|
||||||
|
|
||||||
Good separation between the components is maintained by adopting various best
|
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:
|
practices that anyone working with the SDK needs to be be aware of and uphold:
|
||||||
|
|
||||||
* Views are named with upper camel case (e.g. molecules/MessageTile.js)
|
* Components are named with upper camel case (e.g. views/rooms/EventTile.js)
|
||||||
|
|
||||||
* The view's CSS file MUST have the same name (e.g. molecules/MessageTile.css)
|
* They are organised in a typically two-level hierarchy - first whether the
|
||||||
|
component is a view or a structure, and then a broad functional grouping
|
||||||
|
(e.g. 'rooms' here)
|
||||||
|
|
||||||
|
* After creating a new component you must run `npm run reskindex` to regenerate
|
||||||
|
the `component-index.js` for the SDK (used in future for skinning)
|
||||||
|
|
||||||
|
* The view's CSS file MUST have the same name (e.g. view/rooms/MessageTile.css).
|
||||||
|
CSS for matrix-react-sdk currently resides in
|
||||||
|
https://github.com/vector-im/vector-web/tree/master/src/skins/vector/css/matrix-react-sdk.
|
||||||
|
|
||||||
* Per-view CSS is optional - it could choose to inherit all its styling from
|
* 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 context of the rest of the app, although this is unusual for any but
|
||||||
the simplest atoms and molecules.
|
structural components (lacking presentation logic) and the simplest view
|
||||||
|
components.
|
||||||
|
|
||||||
* The view MUST *only* refer to the CSS rules defined in its own CSS file.
|
* The view MUST *only* refer to the CSS rules defined in its own CSS file.
|
||||||
'Stealing' styling information from other components (including parents)
|
'Stealing' styling information from other components (including parents)
|
||||||
|
@ -82,9 +102,10 @@ In practice this means:
|
||||||
|
|
||||||
* We deliberately use vanilla CSS 3.0 to avoid adding any more magic
|
* 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
|
dependencies into the mix than we already have. App developers are welcome
|
||||||
to use whatever floats their boat however.
|
to use whatever floats their boat however. In future we'll start using
|
||||||
|
css-next to pull in features like CSS variable support.
|
||||||
|
|
||||||
* The CSS for a component can however override the rules for child components.
|
* The CSS for a component can override the rules for child components.
|
||||||
For instance, .mx_RoomList .mx_RoomTile {} would be the selector to override
|
For instance, .mx_RoomList .mx_RoomTile {} would be the selector to override
|
||||||
styles of RoomTiles when viewed in the context of a RoomList view.
|
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
|
Overrides *must* be scoped to the View's CSS class - i.e. don't just define
|
||||||
|
@ -98,30 +119,36 @@ In practice this means:
|
||||||
generally not cool and stop the component from being reused easily in
|
generally not cool and stop the component from being reused easily in
|
||||||
different places.
|
different places.
|
||||||
|
|
||||||
* We don't use the atomify library itself, as React already provides most
|
Originally `matrix-react-sdk` followed the Atomic design pattern as per
|
||||||
of the modularity requirements it brings to the table.
|
http://patternlab.io to try to encourage a modular architecture. However, we
|
||||||
|
found that the grouping of components into atoms/molecules/organisms
|
||||||
|
made them harder to find relative to a functional split, and didn't emphasise
|
||||||
|
the distinction between 'structural' and 'view' components, so we backed away
|
||||||
|
from it.
|
||||||
|
|
||||||
With all this in mind, here's how you go about skinning the react SDK UI
|
Github Issues
|
||||||
components to embed a Matrix client into your app:
|
=============
|
||||||
|
|
||||||
* Create a new NPM project. Be sure to directly depend on react, (otherwise
|
All issues should be filed under https://github.com/vector-im/vector-web/issues
|
||||||
you can end up with two copies of react).
|
for now.
|
||||||
* 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
|
OUTDATED: To Create Your Own Skin
|
||||||
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
|
**This is ALL LIES currently, as skinning is currently broken - see the WARNING
|
||||||
from matrix-react-skin (which uses babel and webpack). You could use
|
section at the top of this readme.**
|
||||||
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
|
Skins are modules are exported from such a package in the `lib` directory.
|
||||||
the lib directory at the end of the build process, as well as any
|
`lib/skins` contains one directory per-skin, named after the skin, and the
|
||||||
packaging that you might do.
|
`modules` directory contains modules as their javascript files.
|
||||||
* 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
|
A basic skin is provided in the matrix-react-skin package. This also contains
|
||||||
import CSS from any skins that your skin inherts from.
|
a minimal application that instantiates the basic skin making a working matrix
|
||||||
|
client.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
To Create Your Own Skin
|
|
||||||
=======================
|
|
||||||
To actually change the look of a skin, you can create a base skin (which
|
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.
|
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
|
Note that derived skins are currently experimental: for example, the CSS
|
||||||
|
@ -145,3 +172,22 @@ Now you have the basis of a skin, you need to generate a skindex.json file. The
|
||||||
you add an npm script to run this, as in matrix-react-skin.
|
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.
|
For more specific detail on any of these steps, look at matrix-react-skin.
|
||||||
|
|
||||||
|
Alternative instructions:
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
|
162
code_style.md
Normal file
162
code_style.md
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
Matrix JavaScript/ECMAScript Style Guide
|
||||||
|
========================================
|
||||||
|
|
||||||
|
The intention of this guide is to make Matrix's JavaScript codebase clean,
|
||||||
|
consistent with other popular JavaScript styles and consistent with the rest of
|
||||||
|
the Matrix codebase. For reference, the Matrix Python style guide can be found
|
||||||
|
at https://github.com/matrix-org/synapse/blob/master/docs/code_style.rst
|
||||||
|
|
||||||
|
This document reflects how we would like Matrix JavaScript code to look, with
|
||||||
|
acknowledgement that a significant amount of code is written to older
|
||||||
|
standards.
|
||||||
|
|
||||||
|
Write applications in modern ECMAScript and use a transpiler where necessary to
|
||||||
|
target older platforms. When writing library code, consider carefully whether
|
||||||
|
to write in ES5 to allow all JavaScript application to use the code directly or
|
||||||
|
writing in modern ECMAScript and using a transpile step to generate the file
|
||||||
|
that applications can then include. There are significant benefits in being
|
||||||
|
able to use modern ECMAScript, although the tooling for doing so can be awkward
|
||||||
|
for library code, especially with regard to translating source maps and line
|
||||||
|
number throgh from the original code to the final application.
|
||||||
|
|
||||||
|
General Style
|
||||||
|
-------------
|
||||||
|
- 4 spaces to indent, for consistency with Matrix Python.
|
||||||
|
- 120 columns per line, but try to keep JavaScript code around the 80 column mark.
|
||||||
|
Inline JSX in particular can be nicer with more columns per line.
|
||||||
|
- No trailing whitespace at end of lines.
|
||||||
|
- Don't indent empty lines.
|
||||||
|
- One newline at the end of the file.
|
||||||
|
- Unix newlines, never `\r`
|
||||||
|
- Indent similar to our python code: break up long lines at logical boundaries,
|
||||||
|
more than one argument on a line is OK
|
||||||
|
- Use semicolons, for consistency with node.
|
||||||
|
- UpperCamelCase for class and type names
|
||||||
|
- lowerCamelCase for functions and variables.
|
||||||
|
- Single line ternary operators are fine.
|
||||||
|
- UPPER_CAMEL_CASE for constants
|
||||||
|
- Single quotes for strings by default, for consistency with most JavaScript styles:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
"bad" // Bad
|
||||||
|
'good' // Good
|
||||||
|
```
|
||||||
|
- Use parentheses or `\`` instead of '\\' for line continuation where ever possible
|
||||||
|
- Open braces on the same line (consistent with Node):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (x) {
|
||||||
|
console.log("I am a fish"); // Good
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x)
|
||||||
|
{
|
||||||
|
console.log("I am a fish"); // Bad
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Spaces after `if`, `for`, `else` etc, no space around the condition:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (x) {
|
||||||
|
console.log("I am a fish"); // Good
|
||||||
|
}
|
||||||
|
|
||||||
|
if(x) {
|
||||||
|
console.log("I am a fish"); // Bad
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( x ) {
|
||||||
|
console.log("I am a fish"); // Bad
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Declare one variable per var statement (consistent with Node). Unless they
|
||||||
|
are simple and closely related. If you put the next declaration on a new line,
|
||||||
|
treat yourself to another `var`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var key = "foo",
|
||||||
|
comparator = function(x, y) {
|
||||||
|
return x - y;
|
||||||
|
}; // Bad
|
||||||
|
|
||||||
|
var key = "foo";
|
||||||
|
var comparator = function(x, y) {
|
||||||
|
return x - y;
|
||||||
|
}; // Good
|
||||||
|
|
||||||
|
var x = 0, y = 0; // Fine
|
||||||
|
|
||||||
|
var x = 0;
|
||||||
|
var y = 0; // Also fine
|
||||||
|
```
|
||||||
|
- A single line `if` is fine, all others have braces. This prevents errors when adding to the code.:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (x) return true; // Fine
|
||||||
|
|
||||||
|
if (x) {
|
||||||
|
return true; // Also fine
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x)
|
||||||
|
return true; // Not fine
|
||||||
|
```
|
||||||
|
- Terminate all multi-line lists, object literals, imports and ideally function calls with commas (if using a transpiler). Note that trailing function commas require explicit configuration in babel at time of writing:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var mascots = [
|
||||||
|
"Patrick",
|
||||||
|
"Shirley",
|
||||||
|
"Colin",
|
||||||
|
"Susan",
|
||||||
|
"Sir Arthur David" // Bad
|
||||||
|
];
|
||||||
|
|
||||||
|
var mascots = [
|
||||||
|
"Patrick",
|
||||||
|
"Shirley",
|
||||||
|
"Colin",
|
||||||
|
"Susan",
|
||||||
|
"Sir Arthur David", // Good
|
||||||
|
];
|
||||||
|
```
|
||||||
|
- Use `null`, `undefined` etc consistently with node:
|
||||||
|
Boolean variables and functions should always be either true or false. Don't set it to 0 unless it's supposed to be a number.
|
||||||
|
When something is intentionally missing or removed, set it to null.
|
||||||
|
If returning a boolean, type coerce:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function hasThings() {
|
||||||
|
return !!length; // bad
|
||||||
|
return new Boolean(length); // REALLY bad
|
||||||
|
return Boolean(length); // good
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Don't set things to undefined. Reserve that value to mean "not yet set to anything."
|
||||||
|
Boolean objects are verboten.
|
||||||
|
- Use JSDoc
|
||||||
|
|
||||||
|
ECMAScript
|
||||||
|
----------
|
||||||
|
- Use `const` unless you need a re-assignable variable. This ensures things you don't want to be re-assigned can't be.
|
||||||
|
- Be careful migrating files to newer syntax.
|
||||||
|
- Don't mix `require` and `import` in the same file. Either stick to the old style or change them all.
|
||||||
|
- Likewise, don't mix things like class properties and `MyClass.prototype.MY_CONSTANT = 42;`
|
||||||
|
- Be careful mixing arrow functions and regular functions, eg. if one function in a promise chain is an
|
||||||
|
arrow function, they probably all should be.
|
||||||
|
- Apart from that, newer ES features should be used whenever the author deems them to be appropriate.
|
||||||
|
- Flow annotations are welcome and encouraged.
|
||||||
|
|
||||||
|
React
|
||||||
|
-----
|
||||||
|
- Use ES6 classes, although bear in mind a lot of code uses createClass.
|
||||||
|
- Pull out functions in props to the class, generally as specific event handlers:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<Foo onClick={function(ev) {doStuff();}}> // Bad
|
||||||
|
<Foo onClick={(ev) => {doStuff();}}> // Equally bad
|
||||||
|
<Foo onClick={this.doStuff}> // Better
|
||||||
|
<Foo onClick={this.onFooClick}> // Best, if onFooClick would do anything other than directly calling doStuff
|
||||||
|
```
|
||||||
|
- Think about whether your component really needs state: are you duplicating
|
||||||
|
information in component state that could be derived from the model?
|
12
package.json
12
package.json
|
@ -14,8 +14,8 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"reskindex": "reskindex -h header",
|
"reskindex": "reskindex -h header",
|
||||||
"build": "babel src -d lib --source-maps",
|
"build": "babel src -d lib --source-maps --stage 1",
|
||||||
"start": "babel src -w -d lib --source-maps",
|
"start": "babel src -w -d lib --source-maps --stage 1",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"lintall": "eslint src/ test/",
|
"lintall": "eslint src/ test/",
|
||||||
"clean": "rimraf lib",
|
"clean": "rimraf lib",
|
||||||
|
@ -42,10 +42,10 @@
|
||||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"q": "^1.4.1",
|
"q": "^1.4.1",
|
||||||
"react": "^15.0.1",
|
"react": "^15.2.1",
|
||||||
"react-addons-css-transition-group": "^15.1.0",
|
"react-addons-css-transition-group": "^15.2.1",
|
||||||
"react-dom": "^15.0.1",
|
"react-dom": "^15.2.1",
|
||||||
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e",
|
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#dbf0abf",
|
||||||
"sanitize-html": "^1.11.1",
|
"sanitize-html": "^1.11.1",
|
||||||
"velocity-vector": "vector-im/velocity#059e3b2",
|
"velocity-vector": "vector-im/velocity#059e3b2",
|
||||||
"whatwg-fetch": "^1.0.0"
|
"whatwg-fetch": "^1.0.0"
|
||||||
|
|
|
@ -38,11 +38,13 @@ class AddThreepid {
|
||||||
*/
|
*/
|
||||||
addEmailAddress(emailAddress, bind) {
|
addEmailAddress(emailAddress, bind) {
|
||||||
this.bind = bind;
|
this.bind = bind;
|
||||||
return MatrixClientPeg.get().requestEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
|
return MatrixClientPeg.get().requestAdd3pidEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
|
||||||
this.sessionId = res.sid;
|
this.sessionId = res.sid;
|
||||||
return res;
|
return res;
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
if (err.httpStatus) {
|
if (err.errcode == 'M_THREEPID_IN_USE') {
|
||||||
|
err.message = "This email address is already in use";
|
||||||
|
} else if (err.httpStatus) {
|
||||||
err.message = err.message + ` (Status ${err.httpStatus})`;
|
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
@ -52,6 +52,36 @@ function infoForImageFile(imageFile) {
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function infoForVideoFile(videoFile) {
|
||||||
|
var deferred = q.defer();
|
||||||
|
|
||||||
|
// Load the file into an html element
|
||||||
|
var video = document.createElement("video");
|
||||||
|
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
video.src = e.target.result;
|
||||||
|
|
||||||
|
// Once ready, returns its size
|
||||||
|
video.onloadedmetadata = function() {
|
||||||
|
deferred.resolve({
|
||||||
|
w: video.videoWidth,
|
||||||
|
h: video.videoHeight
|
||||||
|
});
|
||||||
|
};
|
||||||
|
video.onerror = function(e) {
|
||||||
|
deferred.reject(e);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
reader.onerror = function(e) {
|
||||||
|
deferred.reject(e);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(videoFile);
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ContentMessages {
|
class ContentMessages {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.inprogress = [];
|
this.inprogress = [];
|
||||||
|
@ -81,6 +111,12 @@ class ContentMessages {
|
||||||
} else if (file.type.indexOf('audio/') == 0) {
|
} else if (file.type.indexOf('audio/') == 0) {
|
||||||
content.msgtype = 'm.audio';
|
content.msgtype = 'm.audio';
|
||||||
def.resolve();
|
def.resolve();
|
||||||
|
} else if (file.type.indexOf('video/') == 0) {
|
||||||
|
content.msgtype = 'm.video';
|
||||||
|
infoForVideoFile(file).then(function (videoInfo) {
|
||||||
|
extend(content.info, videoInfo);
|
||||||
|
def.resolve();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
content.msgtype = 'm.file';
|
content.msgtype = 'm.file';
|
||||||
def.resolve();
|
def.resolve();
|
||||||
|
|
|
@ -186,7 +186,7 @@ module.exports = {
|
||||||
*
|
*
|
||||||
* highlights: optional list of words to highlight, ordered by longest word first
|
* highlights: optional list of words to highlight, ordered by longest word first
|
||||||
*
|
*
|
||||||
* opts.highlightLink: optional href to add to highlights
|
* opts.highlightLink: optional href to add to highlighted words
|
||||||
*/
|
*/
|
||||||
bodyToHtml: function(content, highlights, opts) {
|
bodyToHtml: function(content, highlights, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
|
@ -223,8 +223,10 @@ module.exports = {
|
||||||
let match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
let match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
||||||
let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
||||||
|
|
||||||
let className = classNames('markdown-body', {
|
const className = classNames({
|
||||||
'emoji-body': emojiBody,
|
'mx_EventTile_body': true,
|
||||||
|
'mx_EventTile_bigEmoji': emojiBody,
|
||||||
|
'markdown-body': isHtml,
|
||||||
});
|
});
|
||||||
return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} />;
|
return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} />;
|
||||||
},
|
},
|
||||||
|
|
|
@ -97,35 +97,20 @@ class MatrixClient {
|
||||||
|
|
||||||
// FIXME, XXX: this all seems very convoluted :(
|
// FIXME, XXX: this all seems very convoluted :(
|
||||||
//
|
//
|
||||||
// if we replace the singleton using URLs we bypass our createClientForPeg()
|
|
||||||
// global helper function... but if we replace it using
|
|
||||||
// an access_token we don't?
|
|
||||||
//
|
|
||||||
// Why do we have this peg wrapper rather than just MatrixClient.get()?
|
// Why do we have this peg wrapper rather than just MatrixClient.get()?
|
||||||
// Why do we name MatrixClient as MatrixClientPeg when we export it?
|
// Why do we name MatrixClient as MatrixClientPeg when we export it?
|
||||||
//
|
//
|
||||||
// -matthew
|
// -matthew
|
||||||
|
|
||||||
replaceUsingUrls(hs_url, is_url) {
|
replaceUsingUrls(hs_url, is_url) {
|
||||||
matrixClient = Matrix.createClient({
|
this.replaceClient(hs_url, is_url);
|
||||||
baseUrl: hs_url,
|
|
||||||
idBaseUrl: is_url
|
|
||||||
});
|
|
||||||
|
|
||||||
// XXX: factor this out with the localStorage setting in replaceUsingAccessToken
|
|
||||||
if (localStorage) {
|
|
||||||
try {
|
|
||||||
localStorage.setItem("mx_hs_url", hs_url);
|
|
||||||
localStorage.setItem("mx_is_url", is_url);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Error using local storage: can't persist HS/IS URLs!");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn("No local storage available: can't persist HS/IS URLs!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceUsingAccessToken(hs_url, is_url, user_id, access_token, isGuest) {
|
replaceUsingAccessToken(hs_url, is_url, user_id, access_token, isGuest) {
|
||||||
|
this.replaceClient(hs_url, is_url, user_id, access_token, isGuest);
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceClient(hs_url, is_url, user_id, access_token, isGuest) {
|
||||||
if (localStorage) {
|
if (localStorage) {
|
||||||
try {
|
try {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
|
|
@ -48,11 +48,13 @@ class PasswordReset {
|
||||||
*/
|
*/
|
||||||
resetPassword(emailAddress, newPassword) {
|
resetPassword(emailAddress, newPassword) {
|
||||||
this.password = newPassword;
|
this.password = newPassword;
|
||||||
return this.client.requestEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
|
return this.client.requestPasswordEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
|
||||||
this.sessionId = res.sid;
|
this.sessionId = res.sid;
|
||||||
return res;
|
return res;
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
if (err.httpStatus) {
|
if (err.errcode == 'M_THREEPID_NOT_FOUND') {
|
||||||
|
err.message = "This email address was not found";
|
||||||
|
} else if (err.httpStatus) {
|
||||||
err.message = err.message + ` (Status ${err.httpStatus})`;
|
err.message = err.message + ` (Status ${err.httpStatus})`;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
@ -152,7 +152,10 @@ class Register extends Signup {
|
||||||
console.log("Active flow => %s", JSON.stringify(flow));
|
console.log("Active flow => %s", JSON.stringify(flow));
|
||||||
var flowStage = self.firstUncompletedStage(flow);
|
var flowStage = self.firstUncompletedStage(flow);
|
||||||
if (flowStage != self.activeStage) {
|
if (flowStage != self.activeStage) {
|
||||||
return self.startStage(flowStage);
|
return self.startStage(flowStage).catch(function(err) {
|
||||||
|
self.setStep('START');
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -170,7 +170,7 @@ class EmailIdentityStage extends Stage {
|
||||||
encodeURIComponent(this.signupInstance.getServerData().session);
|
encodeURIComponent(this.signupInstance.getServerData().session);
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
return this.client.requestEmailToken(
|
return this.client.requestRegisterEmailToken(
|
||||||
this.signupInstance.email,
|
this.signupInstance.email,
|
||||||
this.clientSecret,
|
this.clientSecret,
|
||||||
1, // TODO: Multiple send attempts?
|
1, // TODO: Multiple send attempts?
|
||||||
|
@ -186,8 +186,8 @@ class EmailIdentityStage extends Stage {
|
||||||
var e = {
|
var e = {
|
||||||
isFatal: true
|
isFatal: true
|
||||||
};
|
};
|
||||||
if (error.errcode == 'THREEPID_IN_USE') {
|
if (error.errcode == 'M_THREEPID_IN_USE') {
|
||||||
e.message = "Email in use";
|
e.message = "This email address is already registered";
|
||||||
} else {
|
} else {
|
||||||
e.message = 'Unable to contact the given identity server';
|
e.message = 'Unable to contact the given identity server';
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
var Entry = require("./TabCompleteEntries").Entry;
|
|
||||||
|
import { Entry, MemberEntry, CommandEntry } from './TabCompleteEntries';
|
||||||
|
import SlashCommands from './SlashCommands';
|
||||||
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
|
||||||
const DELAY_TIME_MS = 1000;
|
const DELAY_TIME_MS = 1000;
|
||||||
const KEY_TAB = 9;
|
const KEY_TAB = 9;
|
||||||
|
@ -45,23 +48,39 @@ class TabComplete {
|
||||||
this.isFirstWord = false; // true if you tab-complete on the first word
|
this.isFirstWord = false; // true if you tab-complete on the first word
|
||||||
this.enterTabCompleteTimerId = null;
|
this.enterTabCompleteTimerId = null;
|
||||||
this.inPassiveMode = false;
|
this.inPassiveMode = false;
|
||||||
|
|
||||||
|
// Map tracking ordering of the room members.
|
||||||
|
// userId: integer, highest comes first.
|
||||||
|
this.memberTabOrder = {};
|
||||||
|
|
||||||
|
// monotonically increasing counter used for tracking ordering of members
|
||||||
|
this.memberOrderSeq = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Entry[]} completeList
|
* Call this when a a UI element representing a tab complete entry has been clicked
|
||||||
|
* @param {entry} The entry that was clicked
|
||||||
*/
|
*/
|
||||||
setCompletionList(completeList) {
|
onEntryClick(entry) {
|
||||||
this.list = completeList;
|
|
||||||
if (this.opts.onClickCompletes) {
|
if (this.opts.onClickCompletes) {
|
||||||
// assign onClick listeners for each entry to complete the text
|
this.completeTo(entry);
|
||||||
this.list.forEach((l) => {
|
|
||||||
l.onClick = () => {
|
|
||||||
this.completeTo(l);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadEntries(room) {
|
||||||
|
this._makeEntries(room);
|
||||||
|
this._initSorting(room);
|
||||||
|
this._sortEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMemberSpoke(member) {
|
||||||
|
if (this.memberTabOrder[member.userId] === undefined) {
|
||||||
|
this.list.push(new MemberEntry(member));
|
||||||
|
}
|
||||||
|
this.memberTabOrder[member.userId] = this.memberOrderSeq++;
|
||||||
|
this._sortEntries();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {DOMElement}
|
* @param {DOMElement}
|
||||||
*/
|
*/
|
||||||
|
@ -307,6 +326,54 @@ class TabComplete {
|
||||||
this.opts.onStateChange(this.completing);
|
this.opts.onStateChange(this.completing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_sortEntries() {
|
||||||
|
// largest comes first
|
||||||
|
const KIND_ORDER = {
|
||||||
|
command: 1,
|
||||||
|
member: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.list.sort((a, b) => {
|
||||||
|
const kindOrderDifference = KIND_ORDER[b.kind] - KIND_ORDER[a.kind];
|
||||||
|
if (kindOrderDifference != 0) {
|
||||||
|
return kindOrderDifference;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.kind == 'member') {
|
||||||
|
let orderA = this.memberTabOrder[a.member.userId];
|
||||||
|
let orderB = this.memberTabOrder[b.member.userId];
|
||||||
|
if (orderA === undefined) orderA = -1;
|
||||||
|
if (orderB === undefined) orderB = -1;
|
||||||
|
|
||||||
|
return orderB - orderA;
|
||||||
|
}
|
||||||
|
|
||||||
|
// anything else we have no ordering for
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_makeEntries(room) {
|
||||||
|
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
|
|
||||||
|
const members = room.getJoinedMembers().filter(function(member) {
|
||||||
|
if (member.userId !== myUserId) return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.list = MemberEntry.fromMemberList(members).concat(
|
||||||
|
CommandEntry.fromCommands(SlashCommands.getCommandList())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_initSorting(room) {
|
||||||
|
this.memberTabOrder = {};
|
||||||
|
this.memberOrderSeq = 0;
|
||||||
|
|
||||||
|
for (const ev of room.getLiveTimeline().getEvents()) {
|
||||||
|
this.memberTabOrder[ev.getSender()] = this.memberOrderSeq++;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = TabComplete;
|
module.exports = TabComplete;
|
||||||
|
|
|
@ -69,6 +69,7 @@ class Entry {
|
||||||
class CommandEntry extends Entry {
|
class CommandEntry extends Entry {
|
||||||
constructor(cmd, cmdWithArgs) {
|
constructor(cmd, cmdWithArgs) {
|
||||||
super(cmdWithArgs);
|
super(cmdWithArgs);
|
||||||
|
this.kind = 'command';
|
||||||
this.cmd = cmd;
|
this.cmd = cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,6 +96,7 @@ class MemberEntry extends Entry {
|
||||||
constructor(member) {
|
constructor(member) {
|
||||||
super(member.name || member.userId);
|
super(member.name || member.userId);
|
||||||
this.member = member;
|
this.member = member;
|
||||||
|
this.kind = 'member';
|
||||||
}
|
}
|
||||||
|
|
||||||
getImageJsx() {
|
getImageJsx() {
|
||||||
|
@ -114,24 +116,7 @@ class MemberEntry extends Entry {
|
||||||
}
|
}
|
||||||
|
|
||||||
MemberEntry.fromMemberList = function(members) {
|
MemberEntry.fromMemberList = function(members) {
|
||||||
return members.sort(function(a, b) {
|
return members.map(function(m) {
|
||||||
var userA = a.user;
|
|
||||||
var userB = b.user;
|
|
||||||
if (userA && !userB) {
|
|
||||||
return -1; // a comes first
|
|
||||||
}
|
|
||||||
else if (!userA && userB) {
|
|
||||||
return 1; // b comes first
|
|
||||||
}
|
|
||||||
else if (!userA && !userB) {
|
|
||||||
return 0; // don't care
|
|
||||||
}
|
|
||||||
else { // both User objects exist
|
|
||||||
var lastActiveAgoA = userA.lastActiveAgo || Number.MAX_SAFE_INTEGER;
|
|
||||||
var lastActiveAgoB = userB.lastActiveAgo || Number.MAX_SAFE_INTEGER;
|
|
||||||
return lastActiveAgoA - lastActiveAgoB;
|
|
||||||
}
|
|
||||||
}).map(function(m) {
|
|
||||||
return new MemberEntry(m);
|
return new MemberEntry(m);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,6 +113,35 @@ module.exports = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getUrlPreviewsDisabled: function() {
|
||||||
|
var event = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls");
|
||||||
|
return (event && event.getContent().disable);
|
||||||
|
},
|
||||||
|
|
||||||
|
setUrlPreviewsDisabled: function(disabled) {
|
||||||
|
// FIXME: handle errors
|
||||||
|
return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", {
|
||||||
|
disable: disabled
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getSyncedSettings: function() {
|
||||||
|
var event = MatrixClientPeg.get().getAccountData("im.vector.web.settings");
|
||||||
|
return event ? event.getContent() : {};
|
||||||
|
},
|
||||||
|
|
||||||
|
getSyncedSetting: function(type) {
|
||||||
|
var settings = this.getSyncedSettings();
|
||||||
|
return settings[type];
|
||||||
|
},
|
||||||
|
|
||||||
|
setSyncedSetting: function(type, value) {
|
||||||
|
var settings = this.getSyncedSettings();
|
||||||
|
settings[type] = value;
|
||||||
|
// FIXME: handle errors
|
||||||
|
return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings);
|
||||||
|
},
|
||||||
|
|
||||||
isFeatureEnabled: function(feature: string): boolean {
|
isFeatureEnabled: function(feature: string): boolean {
|
||||||
return localStorage.getItem(`mx_labs_feature_${feature}`) === 'true';
|
return localStorage.getItem(`mx_labs_feature_${feature}`) === 'true';
|
||||||
},
|
},
|
||||||
|
|
|
@ -74,6 +74,8 @@ module.exports.components['views.messages.TextualEvent'] = require('./components
|
||||||
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');
|
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');
|
||||||
module.exports.components['views.room_settings.AliasSettings'] = require('./components/views/room_settings/AliasSettings');
|
module.exports.components['views.room_settings.AliasSettings'] = require('./components/views/room_settings/AliasSettings');
|
||||||
module.exports.components['views.room_settings.ColorSettings'] = require('./components/views/room_settings/ColorSettings');
|
module.exports.components['views.room_settings.ColorSettings'] = require('./components/views/room_settings/ColorSettings');
|
||||||
|
module.exports.components['views.room_settings.UrlPreviewSettings'] = require('./components/views/room_settings/UrlPreviewSettings');
|
||||||
|
module.exports.components['views.rooms.Autocomplete'] = require('./components/views/rooms/Autocomplete');
|
||||||
module.exports.components['views.rooms.AuxPanel'] = require('./components/views/rooms/AuxPanel');
|
module.exports.components['views.rooms.AuxPanel'] = require('./components/views/rooms/AuxPanel');
|
||||||
module.exports.components['views.rooms.EntityTile'] = require('./components/views/rooms/EntityTile');
|
module.exports.components['views.rooms.EntityTile'] = require('./components/views/rooms/EntityTile');
|
||||||
module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile');
|
module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile');
|
||||||
|
|
|
@ -390,7 +390,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// FIXME: controller shouldn't be loading a view :(
|
// FIXME: controller shouldn't be loading a view :(
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
var modal = Modal.createDialog(Loader);
|
var modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||||
|
|
||||||
d.then(function() {
|
d.then(function() {
|
||||||
modal.close();
|
modal.close();
|
||||||
|
|
|
@ -44,6 +44,9 @@ module.exports = React.createClass({
|
||||||
// ID of an event to highlight. If undefined, no event will be highlighted.
|
// ID of an event to highlight. If undefined, no event will be highlighted.
|
||||||
highlightedEventId: React.PropTypes.string,
|
highlightedEventId: React.PropTypes.string,
|
||||||
|
|
||||||
|
// Should we show URL Previews
|
||||||
|
showUrlPreview: React.PropTypes.bool,
|
||||||
|
|
||||||
// event after which we should show a read marker
|
// event after which we should show a read marker
|
||||||
readMarkerEventId: React.PropTypes.string,
|
readMarkerEventId: React.PropTypes.string,
|
||||||
|
|
||||||
|
@ -365,6 +368,7 @@ module.exports = React.createClass({
|
||||||
onWidgetLoad={this._onWidgetLoad}
|
onWidgetLoad={this._onWidgetLoad}
|
||||||
readReceipts={readReceipts}
|
readReceipts={readReceipts}
|
||||||
readReceiptMap={this._readReceiptMap}
|
readReceiptMap={this._readReceiptMap}
|
||||||
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
checkUnmounting={this._isUnmounting}
|
checkUnmounting={this._isUnmounting}
|
||||||
eventSendStatus={mxEv.status}
|
eventSendStatus={mxEv.status}
|
||||||
last={last} isSelectedEvent={highlight}/>
|
last={last} isSelectedEvent={highlight}/>
|
||||||
|
|
|
@ -26,9 +26,9 @@ module.exports = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// the room this statusbar is representing.
|
// the room this statusbar is representing.
|
||||||
room: React.PropTypes.object.isRequired,
|
room: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
// a list of TabCompleteEntries.Entry objects
|
// a TabComplete object
|
||||||
tabCompleteEntries: React.PropTypes.array,
|
tabComplete: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
// the number of messages which have arrived since we've been scrolled up
|
// the number of messages which have arrived since we've been scrolled up
|
||||||
numUnreadMessages: React.PropTypes.number,
|
numUnreadMessages: React.PropTypes.number,
|
||||||
|
@ -208,11 +208,11 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.tabCompleteEntries) {
|
if (this.props.tabComplete.isTabCompleting()) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_tabCompleteBar">
|
<div className="mx_RoomStatusBar_tabCompleteBar">
|
||||||
<div className="mx_RoomStatusBar_tabCompleteWrapper">
|
<div className="mx_RoomStatusBar_tabCompleteWrapper">
|
||||||
<TabCompleteBar entries={this.props.tabCompleteEntries} />
|
<TabCompleteBar tabComplete={this.props.tabComplete} />
|
||||||
<div className="mx_RoomStatusBar_tabCompleteEol" title="->|">
|
<div className="mx_RoomStatusBar_tabCompleteEol" title="->|">
|
||||||
<TintableSvg src="img/eol.svg" width="22" height="16"/>
|
<TintableSvg src="img/eol.svg" width="22" height="16"/>
|
||||||
Auto-complete
|
Auto-complete
|
||||||
|
@ -233,7 +233,7 @@ module.exports = React.createClass({
|
||||||
<a className="mx_RoomStatusBar_resend_link"
|
<a className="mx_RoomStatusBar_resend_link"
|
||||||
onClick={ this.props.onResendAllClick }>
|
onClick={ this.props.onResendAllClick }>
|
||||||
Resend all
|
Resend all
|
||||||
</a> or <a
|
</a> or <a
|
||||||
className="mx_RoomStatusBar_resend_link"
|
className="mx_RoomStatusBar_resend_link"
|
||||||
onClick={ this.props.onCancelAllClick }>
|
onClick={ this.props.onCancelAllClick }>
|
||||||
cancel all
|
cancel all
|
||||||
|
@ -247,7 +247,7 @@ module.exports = React.createClass({
|
||||||
// unread count trumps who is typing since the unread count is only
|
// unread count trumps who is typing since the unread count is only
|
||||||
// set when you've scrolled up
|
// set when you've scrolled up
|
||||||
if (this.props.numUnreadMessages) {
|
if (this.props.numUnreadMessages) {
|
||||||
var unreadMsgs = this.props.numUnreadMessages + " new message" +
|
var unreadMsgs = this.props.numUnreadMessages + " new message" +
|
||||||
(this.props.numUnreadMessages > 1 ? "s" : "");
|
(this.props.numUnreadMessages > 1 ? "s" : "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -291,5 +291,5 @@ module.exports = React.createClass({
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -31,10 +31,7 @@ var Modal = require("../../Modal");
|
||||||
var sdk = require('../../index');
|
var sdk = require('../../index');
|
||||||
var CallHandler = require('../../CallHandler');
|
var CallHandler = require('../../CallHandler');
|
||||||
var TabComplete = require("../../TabComplete");
|
var TabComplete = require("../../TabComplete");
|
||||||
var MemberEntry = require("../../TabCompleteEntries").MemberEntry;
|
|
||||||
var CommandEntry = require("../../TabCompleteEntries").CommandEntry;
|
|
||||||
var Resend = require("../../Resend");
|
var Resend = require("../../Resend");
|
||||||
var SlashCommands = require("../../SlashCommands");
|
|
||||||
var dis = require("../../dispatcher");
|
var dis = require("../../dispatcher");
|
||||||
var Tinter = require("../../Tinter");
|
var Tinter = require("../../Tinter");
|
||||||
var rate_limited_func = require('../../ratelimitedfunc');
|
var rate_limited_func = require('../../ratelimitedfunc');
|
||||||
|
@ -141,6 +138,7 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||||
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
|
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
|
||||||
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
|
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
|
||||||
|
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||||
|
|
||||||
this.tabComplete = new TabComplete({
|
this.tabComplete = new TabComplete({
|
||||||
allowLooping: false,
|
allowLooping: false,
|
||||||
|
@ -204,6 +202,9 @@ module.exports = React.createClass({
|
||||||
user_is_in_room = this.state.room.hasMembershipState(
|
user_is_in_room = this.state.room.hasMembershipState(
|
||||||
MatrixClientPeg.get().credentials.userId, 'join'
|
MatrixClientPeg.get().credentials.userId, 'join'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this._updateAutoComplete();
|
||||||
|
this.tabComplete.loadEntries(this.state.room);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user_is_in_room && this.state.roomId) {
|
if (!user_is_in_room && this.state.roomId) {
|
||||||
|
@ -267,6 +268,7 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData);
|
MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData);
|
||||||
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||||
|
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.removeEventListener('resize', this.onResize);
|
window.removeEventListener('resize', this.onResize);
|
||||||
|
@ -338,6 +340,10 @@ module.exports = React.createClass({
|
||||||
// ignore events for other rooms
|
// ignore events for other rooms
|
||||||
if (!this.state.room || room.roomId != this.state.room.roomId) return;
|
if (!this.state.room || room.roomId != this.state.room.roomId) return;
|
||||||
|
|
||||||
|
if (ev.getType() === "org.matrix.room.preview_urls") {
|
||||||
|
this._updatePreviewUrlVisibility(room);
|
||||||
|
}
|
||||||
|
|
||||||
// ignore anything but real-time updates at the end of the room:
|
// ignore anything but real-time updates at the end of the room:
|
||||||
// updates from pagination will happen when the paginate completes.
|
// updates from pagination will happen when the paginate completes.
|
||||||
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
||||||
|
@ -357,12 +363,21 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update the tab complete list as it depends on who most recently spoke,
|
||||||
|
// and that has probably just changed
|
||||||
|
if (ev.sender) {
|
||||||
|
this.tabComplete.onMemberSpoke(ev.sender);
|
||||||
|
// nb. we don't need to update the new autocomplete here since
|
||||||
|
// its results are currently ordered purely by search score.
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// called when state.room is first initialised (either at initial load,
|
// called when state.room is first initialised (either at initial load,
|
||||||
// after a successful peek, or after we join the room).
|
// after a successful peek, or after we join the room).
|
||||||
_onRoomLoaded: function(room) {
|
_onRoomLoaded: function(room) {
|
||||||
this._calculatePeekRules(room);
|
this._calculatePeekRules(room);
|
||||||
|
this._updatePreviewUrlVisibility(room);
|
||||||
},
|
},
|
||||||
|
|
||||||
_calculatePeekRules: function(room) {
|
_calculatePeekRules: function(room) {
|
||||||
|
@ -381,6 +396,42 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_updatePreviewUrlVisibility: function(room) {
|
||||||
|
// console.log("_updatePreviewUrlVisibility");
|
||||||
|
|
||||||
|
// check our per-room overrides
|
||||||
|
var roomPreviewUrls = room.getAccountData("org.matrix.room.preview_urls");
|
||||||
|
if (roomPreviewUrls && roomPreviewUrls.getContent().disable !== undefined) {
|
||||||
|
this.setState({
|
||||||
|
showUrlPreview: !roomPreviewUrls.getContent().disable
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check our global disable override
|
||||||
|
var userRoomPreviewUrls = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls");
|
||||||
|
if (userRoomPreviewUrls && userRoomPreviewUrls.getContent().disable) {
|
||||||
|
this.setState({
|
||||||
|
showUrlPreview: false
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the room state event
|
||||||
|
var roomStatePreviewUrls = room.currentState.getStateEvents('org.matrix.room.preview_urls', '');
|
||||||
|
if (roomStatePreviewUrls && roomStatePreviewUrls.getContent().disable) {
|
||||||
|
this.setState({
|
||||||
|
showUrlPreview: false
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, we assume they're on.
|
||||||
|
this.setState({
|
||||||
|
showUrlPreview: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onRoom: function(room) {
|
onRoom: function(room) {
|
||||||
// This event is fired when the room is 'stored' by the JS SDK, which
|
// This event is fired when the room is 'stored' by the JS SDK, which
|
||||||
// means it's now a fully-fledged room object ready to be used, so
|
// means it's now a fully-fledged room object ready to be used, so
|
||||||
|
@ -411,14 +462,23 @@ module.exports = React.createClass({
|
||||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomAccountData: function(room, event) {
|
onAccountData: function(event) {
|
||||||
if (room.roomId == this.props.roomId) {
|
if (event.getType() === "org.matrix.preview_urls" && this.state.room) {
|
||||||
if (event.getType === "org.matrix.room.color_scheme") {
|
this._updatePreviewUrlVisibility(this.state.room);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onRoomAccountData: function(event, room) {
|
||||||
|
if (room.roomId == this.state.roomId) {
|
||||||
|
if (event.getType() === "org.matrix.room.color_scheme") {
|
||||||
var color_scheme = event.getContent();
|
var color_scheme = event.getContent();
|
||||||
// XXX: we should validate the event
|
// XXX: we should validate the event
|
||||||
console.log("Tinter.tint from onRoomAccountData");
|
console.log("Tinter.tint from onRoomAccountData");
|
||||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||||
}
|
}
|
||||||
|
else if (event.getType() === "org.matrix.room.preview_urls") {
|
||||||
|
this._updatePreviewUrlVisibility(room);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -434,7 +494,8 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
// a member state changed in this room, refresh the tab complete list
|
// a member state changed in this room, refresh the tab complete list
|
||||||
this._updateTabCompleteList();
|
this.tabComplete.loadEntries(this.state.room);
|
||||||
|
this._updateAutoComplete();
|
||||||
|
|
||||||
// if we are now a member of the room, where we were not before, that
|
// if we are now a member of the room, where we were not before, that
|
||||||
// means we have finished joining a room we were previously peeking
|
// means we have finished joining a room we were previously peeking
|
||||||
|
@ -499,8 +560,6 @@ module.exports = React.createClass({
|
||||||
window.addEventListener('resize', this.onResize);
|
window.addEventListener('resize', this.onResize);
|
||||||
this.onResize();
|
this.onResize();
|
||||||
|
|
||||||
this._updateTabCompleteList();
|
|
||||||
|
|
||||||
// XXX: EVIL HACK to autofocus inviting on empty rooms.
|
// XXX: EVIL HACK to autofocus inviting on empty rooms.
|
||||||
// We use the setTimeout to avoid racing with focus_composer.
|
// We use the setTimeout to avoid racing with focus_composer.
|
||||||
if (this.state.room &&
|
if (this.state.room &&
|
||||||
|
@ -518,24 +577,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_updateTabCompleteList: function() {
|
|
||||||
var cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
if (!this.state.room) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var members = this.state.room.getJoinedMembers().filter(function(member) {
|
|
||||||
if (member.userId !== cli.credentials.userId) return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
UserProvider.getInstance().setUserList(members);
|
|
||||||
this.tabComplete.setCompletionList(
|
|
||||||
MemberEntry.fromMemberList(members).concat(
|
|
||||||
CommandEntry.fromCommands(SlashCommands.getCommandList())
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
componentDidUpdate: function() {
|
||||||
if (this.refs.roomView) {
|
if (this.refs.roomView) {
|
||||||
var roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
var roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
||||||
|
@ -1260,6 +1301,14 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_updateAutoComplete: function() {
|
||||||
|
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
|
const members = this.state.room.getJoinedMembers().filter(function(member) {
|
||||||
|
if (member.userId !== myUserId) return true;
|
||||||
|
});
|
||||||
|
UserProvider.getInstance().setUserList(members);
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
||||||
var MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
var MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||||
|
@ -1373,12 +1422,10 @@ module.exports = React.createClass({
|
||||||
statusBar = <UploadBar room={this.state.room} />
|
statusBar = <UploadBar room={this.state.room} />
|
||||||
} else if (!this.state.searchResults) {
|
} else if (!this.state.searchResults) {
|
||||||
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
|
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
|
||||||
var tabEntries = this.tabComplete.isTabCompleting() ?
|
|
||||||
this.tabComplete.peek(6) : null;
|
|
||||||
|
|
||||||
statusBar = <RoomStatusBar
|
statusBar = <RoomStatusBar
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
tabCompleteEntries={tabEntries}
|
tabComplete={this.tabComplete}
|
||||||
numUnreadMessages={this.state.numUnreadMessages}
|
numUnreadMessages={this.state.numUnreadMessages}
|
||||||
hasUnsentMessages={this.state.hasUnsentMessages}
|
hasUnsentMessages={this.state.hasUnsentMessages}
|
||||||
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
|
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
|
||||||
|
@ -1511,6 +1558,8 @@ module.exports = React.createClass({
|
||||||
hideMessagePanel = true;
|
hideMessagePanel = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
||||||
|
|
||||||
var messagePanel = (
|
var messagePanel = (
|
||||||
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
|
@ -1520,6 +1569,7 @@ module.exports = React.createClass({
|
||||||
eventPixelOffset={this.props.eventPixelOffset}
|
eventPixelOffset={this.props.eventPixelOffset}
|
||||||
onScroll={ this.onMessageListScroll }
|
onScroll={ this.onMessageListScroll }
|
||||||
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
|
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
|
||||||
|
showUrlPreview = { this.state.showUrlPreview }
|
||||||
opacity={ this.props.opacity }
|
opacity={ this.props.opacity }
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,9 @@ var TimelinePanel = React.createClass({
|
||||||
// half way down the viewport.
|
// half way down the viewport.
|
||||||
eventPixelOffset: React.PropTypes.number,
|
eventPixelOffset: React.PropTypes.number,
|
||||||
|
|
||||||
|
// Should we show URL Previews
|
||||||
|
showUrlPreview: React.PropTypes.bool,
|
||||||
|
|
||||||
// callback which is called when the panel is scrolled.
|
// callback which is called when the panel is scrolled.
|
||||||
onScroll: React.PropTypes.func,
|
onScroll: React.PropTypes.func,
|
||||||
|
|
||||||
|
@ -934,6 +937,7 @@ var TimelinePanel = React.createClass({
|
||||||
readMarkerEventId={ this.state.readMarkerEventId }
|
readMarkerEventId={ this.state.readMarkerEventId }
|
||||||
readMarkerVisible={ this.state.readMarkerVisible }
|
readMarkerVisible={ this.state.readMarkerVisible }
|
||||||
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
||||||
|
showUrlPreview = { this.props.showUrlPreview }
|
||||||
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
||||||
stickyBottom={ stickyBottom }
|
stickyBottom={ stickyBottom }
|
||||||
onScroll={ this.onMessageListScroll }
|
onScroll={ this.onMessageListScroll }
|
||||||
|
|
|
@ -214,9 +214,10 @@ module.exports = React.createClass({
|
||||||
onFinished: this.onEmailDialogFinished,
|
onFinished: this.onEmailDialogFinished,
|
||||||
});
|
});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
|
this.setState({email_add_pending: false});
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Unable to add email address",
|
title: "Unable to add email address",
|
||||||
description: err.toString()
|
description: err.message
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ReactDOM.findDOMNode(this.refs.add_threepid_input).blur();
|
ReactDOM.findDOMNode(this.refs.add_threepid_input).blur();
|
||||||
|
@ -261,6 +262,63 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_renderUserInterfaceSettings: function() {
|
||||||
|
var client = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
var settingsLabels = [
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
id: 'alwaysShowTimestamps',
|
||||||
|
label: 'Always show message timestamps',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'showTwelveHourTimestamps',
|
||||||
|
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'useCompactLayout',
|
||||||
|
label: 'Use compact timeline layout',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'useFixedWidthFont',
|
||||||
|
label: 'Use fixed width font',
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
];
|
||||||
|
|
||||||
|
var syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>User Interface</h3>
|
||||||
|
<div className="mx_UserSettings_section">
|
||||||
|
<div className="mx_UserSettings_toggle">
|
||||||
|
<input id="urlPreviewsDisabled"
|
||||||
|
type="checkbox"
|
||||||
|
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
|
||||||
|
onChange={ e => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
|
||||||
|
/>
|
||||||
|
<label htmlFor="urlPreviewsDisabled">
|
||||||
|
Disable inline URL previews by default
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ settingsLabels.forEach( setting => {
|
||||||
|
<div className="mx_UserSettings_toggle">
|
||||||
|
<input id={ setting.id }
|
||||||
|
type="checkbox"
|
||||||
|
defaultChecked={ syncedSettings[setting.id] }
|
||||||
|
onChange={ e => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
|
||||||
|
/>
|
||||||
|
<label htmlFor={ setting.id }>
|
||||||
|
{ settings.label }
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
_renderDeviceInfo: function() {
|
_renderDeviceInfo: function() {
|
||||||
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -378,7 +436,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
this._renderLabs = function () {
|
this._renderLabs = function () {
|
||||||
let features = LABS_FEATURES.map(feature => (
|
let features = LABS_FEATURES.map(feature => (
|
||||||
<div key={feature.id}>
|
<div key={feature.id} className="mx_UserSettings_toggle">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={feature.id}
|
id={feature.id}
|
||||||
|
@ -452,6 +510,8 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
{notification_area}
|
{notification_area}
|
||||||
|
|
||||||
|
{this._renderUserInterfaceSettings()}
|
||||||
|
|
||||||
{this._renderDeviceInfo()}
|
{this._renderDeviceInfo()}
|
||||||
|
|
||||||
{this._renderLabs()}
|
{this._renderLabs()}
|
||||||
|
|
|
@ -54,6 +54,16 @@ module.exports = React.createClass({
|
||||||
return {
|
return {
|
||||||
busy: false,
|
busy: false,
|
||||||
errorText: null,
|
errorText: null,
|
||||||
|
// We remember the values entered by the user because
|
||||||
|
// the registration form will be unmounted during the
|
||||||
|
// course of registration, but if there's an error we
|
||||||
|
// want to bring back the registration form with the
|
||||||
|
// values the user entered still in it. We can keep
|
||||||
|
// them in this component's state since this component
|
||||||
|
// persist for the duration of the registration process.
|
||||||
|
formVals: {
|
||||||
|
email: this.props.email,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -108,7 +118,8 @@ module.exports = React.createClass({
|
||||||
var self = this;
|
var self = this;
|
||||||
this.setState({
|
this.setState({
|
||||||
errorText: "",
|
errorText: "",
|
||||||
busy: true
|
busy: true,
|
||||||
|
formVals: formVals,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (formVals.username !== this.props.username) {
|
if (formVals.username !== this.props.username) {
|
||||||
|
@ -228,11 +239,15 @@ module.exports = React.createClass({
|
||||||
break; // NOP
|
break; // NOP
|
||||||
case "Register.START":
|
case "Register.START":
|
||||||
case "Register.STEP_m.login.dummy":
|
case "Register.STEP_m.login.dummy":
|
||||||
|
// NB. Our 'username' prop is specifically for upgrading
|
||||||
|
// a guest account
|
||||||
registerStep = (
|
registerStep = (
|
||||||
<RegistrationForm
|
<RegistrationForm
|
||||||
showEmail={true}
|
showEmail={true}
|
||||||
defaultUsername={this.props.username}
|
defaultUsername={this.state.formVals.username}
|
||||||
defaultEmail={this.props.email}
|
defaultEmail={this.state.formVals.email}
|
||||||
|
defaultPassword={this.state.formVals.password}
|
||||||
|
guestUsername={this.props.username}
|
||||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||||
onError={this.onFormValidationFailed}
|
onError={this.onFormValidationFailed}
|
||||||
onRegisterClick={this.onFormSubmit} />
|
onRegisterClick={this.onFormSubmit} />
|
||||||
|
|
|
@ -59,7 +59,7 @@ module.exports = React.createClass({
|
||||||
{this.props.description}
|
{this.props.description}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this.props.onFinished} autoFocus={this.props.focus}>
|
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}>
|
||||||
{this.props.button}
|
{this.props.button}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -46,7 +46,7 @@ module.exports = React.createClass({
|
||||||
Sign out?
|
Sign out?
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons" onKeyDown={ this.onKeyDown }>
|
<div className="mx_Dialog_buttons" onKeyDown={ this.onKeyDown }>
|
||||||
<button autoFocus onClick={this.logOut}>Sign Out</button>
|
<button className="mx_Dialog_primary" autoFocus onClick={this.logOut}>Sign Out</button>
|
||||||
<button onClick={this.cancelPrompt}>Cancel</button>
|
<button onClick={this.cancelPrompt}>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -63,7 +63,7 @@ module.exports = React.createClass({
|
||||||
{this.props.description}
|
{this.props.description}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this.props.onFinished} autoFocus={true}>
|
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={true}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button onClick={this.onRegisterClicked}>
|
<button onClick={this.onRegisterClicked}>
|
||||||
|
|
|
@ -56,7 +56,7 @@ module.exports = React.createClass({
|
||||||
{this.props.description}
|
{this.props.description}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this.onOk} autoFocus={this.props.focus}>
|
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
|
||||||
{this.props.button}
|
{this.props.button}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,7 @@ module.exports = React.createClass({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<input type="submit" value="Set" />
|
<input className="mx_Dialog_primary" type="submit" value="Set" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -86,7 +86,7 @@ module.exports = React.createClass({
|
||||||
<button onClick={this.onCancel}>
|
<button onClick={this.onCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button onClick={this.onOk}>
|
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
||||||
{this.props.button}
|
{this.props.button}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -35,8 +35,16 @@ module.exports = React.createClass({
|
||||||
displayName: 'RegistrationForm',
|
displayName: 'RegistrationForm',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
// Values pre-filled in the input boxes when the component loads
|
||||||
defaultEmail: React.PropTypes.string,
|
defaultEmail: React.PropTypes.string,
|
||||||
defaultUsername: React.PropTypes.string,
|
defaultUsername: React.PropTypes.string,
|
||||||
|
defaultPassword: React.PropTypes.string,
|
||||||
|
|
||||||
|
// A username that will be used if no username is entered.
|
||||||
|
// Specifying this param will also warn the user that entering
|
||||||
|
// a different username will cause a fresh account to be generated.
|
||||||
|
guestUsername: React.PropTypes.string,
|
||||||
|
|
||||||
showEmail: React.PropTypes.bool,
|
showEmail: React.PropTypes.bool,
|
||||||
minPasswordLength: React.PropTypes.number,
|
minPasswordLength: React.PropTypes.number,
|
||||||
onError: React.PropTypes.func,
|
onError: React.PropTypes.func,
|
||||||
|
@ -55,10 +63,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
email: this.props.defaultEmail,
|
|
||||||
username: null,
|
|
||||||
password: null,
|
|
||||||
passwordConfirm: null,
|
|
||||||
fieldValid: {}
|
fieldValid: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -103,7 +107,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_doSubmit: function() {
|
_doSubmit: function() {
|
||||||
var promise = this.props.onRegisterClick({
|
var promise = this.props.onRegisterClick({
|
||||||
username: this.refs.username.value.trim() || this.props.defaultUsername,
|
username: this.refs.username.value.trim() || this.props.guestUsername,
|
||||||
password: this.refs.password.value.trim(),
|
password: this.refs.password.value.trim(),
|
||||||
email: this.refs.email.value.trim()
|
email: this.refs.email.value.trim()
|
||||||
});
|
});
|
||||||
|
@ -144,7 +148,7 @@ module.exports = React.createClass({
|
||||||
break;
|
break;
|
||||||
case FIELD_USERNAME:
|
case FIELD_USERNAME:
|
||||||
// XXX: SPEC-1
|
// XXX: SPEC-1
|
||||||
var username = this.refs.username.value.trim() || this.props.defaultUsername;
|
var username = this.refs.username.value.trim() || this.props.guestUsername;
|
||||||
if (encodeURIComponent(username) != username) {
|
if (encodeURIComponent(username) != username) {
|
||||||
this.markFieldValid(
|
this.markFieldValid(
|
||||||
field_id,
|
field_id,
|
||||||
|
@ -225,7 +229,7 @@ module.exports = React.createClass({
|
||||||
emailSection = (
|
emailSection = (
|
||||||
<input className="mx_Login_field" type="text" ref="email"
|
<input className="mx_Login_field" type="text" ref="email"
|
||||||
autoFocus={true} placeholder="Email address (optional)"
|
autoFocus={true} placeholder="Email address (optional)"
|
||||||
defaultValue={this.state.email}
|
defaultValue={this.props.defaultEmail}
|
||||||
style={this._styleField(FIELD_EMAIL)}
|
style={this._styleField(FIELD_EMAIL)}
|
||||||
onBlur={function() {self.validateField(FIELD_EMAIL)}} />
|
onBlur={function() {self.validateField(FIELD_EMAIL)}} />
|
||||||
);
|
);
|
||||||
|
@ -237,8 +241,8 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
var placeholderUserName = "User name";
|
var placeholderUserName = "User name";
|
||||||
if (this.props.defaultUsername) {
|
if (this.props.guestUsername) {
|
||||||
placeholderUserName += " (default: " + this.props.defaultUsername + ")"
|
placeholderUserName += " (default: " + this.props.guestUsername + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -247,23 +251,23 @@ module.exports = React.createClass({
|
||||||
{emailSection}
|
{emailSection}
|
||||||
<br />
|
<br />
|
||||||
<input className="mx_Login_field" type="text" ref="username"
|
<input className="mx_Login_field" type="text" ref="username"
|
||||||
placeholder={ placeholderUserName } defaultValue={this.state.username}
|
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
||||||
style={this._styleField(FIELD_USERNAME)}
|
style={this._styleField(FIELD_USERNAME)}
|
||||||
onBlur={function() {self.validateField(FIELD_USERNAME)}} />
|
onBlur={function() {self.validateField(FIELD_USERNAME)}} />
|
||||||
<br />
|
<br />
|
||||||
{ this.props.defaultUsername ?
|
{ this.props.guestUsername ?
|
||||||
<div className="mx_Login_fieldLabel">Setting a user name will create a fresh account</div> : null
|
<div className="mx_Login_fieldLabel">Setting a user name will create a fresh account</div> : null
|
||||||
}
|
}
|
||||||
<input className="mx_Login_field" type="password" ref="password"
|
<input className="mx_Login_field" type="password" ref="password"
|
||||||
style={this._styleField(FIELD_PASSWORD)}
|
style={this._styleField(FIELD_PASSWORD)}
|
||||||
onBlur={function() {self.validateField(FIELD_PASSWORD)}}
|
onBlur={function() {self.validateField(FIELD_PASSWORD)}}
|
||||||
placeholder="Password" defaultValue={this.state.password} />
|
placeholder="Password" defaultValue={this.props.defaultPassword} />
|
||||||
<br />
|
<br />
|
||||||
<input className="mx_Login_field" type="password" ref="passwordConfirm"
|
<input className="mx_Login_field" type="password" ref="passwordConfirm"
|
||||||
placeholder="Confirm password"
|
placeholder="Confirm password"
|
||||||
style={this._styleField(FIELD_PASSWORD_CONFIRM)}
|
style={this._styleField(FIELD_PASSWORD_CONFIRM)}
|
||||||
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM)}}
|
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM)}}
|
||||||
defaultValue={this.state.passwordConfirm} />
|
defaultValue={this.props.defaultPassword} />
|
||||||
<br />
|
<br />
|
||||||
{registerButton}
|
{registerButton}
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -34,7 +34,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
if (fullWidth < thumbWidth && fullHeight < thumbHeight) {
|
if (fullWidth < thumbWidth && fullHeight < thumbHeight) {
|
||||||
// no scaling needs to be applied
|
// no scaling needs to be applied
|
||||||
return fullHeight;
|
return 1;
|
||||||
}
|
}
|
||||||
var widthMulti = thumbWidth / fullWidth;
|
var widthMulti = thumbWidth / fullWidth;
|
||||||
var heightMulti = thumbHeight / fullHeight;
|
var heightMulti = thumbHeight / fullHeight;
|
||||||
|
|
|
@ -38,6 +38,9 @@ module.exports = React.createClass({
|
||||||
/* link URL for the highlights */
|
/* link URL for the highlights */
|
||||||
highlightLink: React.PropTypes.string,
|
highlightLink: React.PropTypes.string,
|
||||||
|
|
||||||
|
/* should show URL previews for this event */
|
||||||
|
showUrlPreview: React.PropTypes.bool,
|
||||||
|
|
||||||
/* callback called when dynamic content in events are loaded */
|
/* callback called when dynamic content in events are loaded */
|
||||||
onWidgetLoad: React.PropTypes.func,
|
onWidgetLoad: React.PropTypes.func,
|
||||||
},
|
},
|
||||||
|
@ -71,6 +74,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
return <BodyType ref="body" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
|
return <BodyType ref="body" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
|
||||||
highlightLink={this.props.highlightLink}
|
highlightLink={this.props.highlightLink}
|
||||||
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
onWidgetLoad={this.props.onWidgetLoad} />;
|
onWidgetLoad={this.props.onWidgetLoad} />;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -39,6 +39,9 @@ module.exports = React.createClass({
|
||||||
/* link URL for the highlights */
|
/* link URL for the highlights */
|
||||||
highlightLink: React.PropTypes.string,
|
highlightLink: React.PropTypes.string,
|
||||||
|
|
||||||
|
/* should show URL previews for this event */
|
||||||
|
showUrlPreview: React.PropTypes.bool,
|
||||||
|
|
||||||
/* callback for when our widget has loaded */
|
/* callback for when our widget has loaded */
|
||||||
onWidgetLoad: React.PropTypes.func,
|
onWidgetLoad: React.PropTypes.func,
|
||||||
},
|
},
|
||||||
|
@ -56,34 +59,47 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
linkifyElement(this.refs.content, linkifyMatrix.options);
|
linkifyElement(this.refs.content, linkifyMatrix.options);
|
||||||
|
this.calculateUrlPreview();
|
||||||
var links = this.findLinks(this.refs.content.children);
|
|
||||||
if (links.length) {
|
|
||||||
this.setState({ links: links.map((link)=>{
|
|
||||||
return link.getAttribute("href");
|
|
||||||
})});
|
|
||||||
|
|
||||||
// lazy-load the hidden state of the preview widget from localstorage
|
|
||||||
if (global.localStorage) {
|
|
||||||
var hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
|
|
||||||
this.setState({ widgetHidden: hidden });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
|
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
|
||||||
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this));
|
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentDidUpdate: function() {
|
||||||
|
this.calculateUrlPreview();
|
||||||
|
},
|
||||||
|
|
||||||
shouldComponentUpdate: function(nextProps, nextState) {
|
shouldComponentUpdate: function(nextProps, nextState) {
|
||||||
|
//console.log("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||||
|
|
||||||
// exploit that events are immutable :)
|
// exploit that events are immutable :)
|
||||||
// ...and that .links is only ever set in componentDidMount and never changes
|
|
||||||
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
|
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
|
||||||
nextProps.highlights !== this.props.highlights ||
|
nextProps.highlights !== this.props.highlights ||
|
||||||
nextProps.highlightLink !== this.props.highlightLink ||
|
nextProps.highlightLink !== this.props.highlightLink ||
|
||||||
|
nextProps.showUrlPreview !== this.props.showUrlPreview ||
|
||||||
nextState.links !== this.state.links ||
|
nextState.links !== this.state.links ||
|
||||||
nextState.widgetHidden !== this.state.widgetHidden);
|
nextState.widgetHidden !== this.state.widgetHidden);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
calculateUrlPreview: function() {
|
||||||
|
//console.log("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||||
|
|
||||||
|
if (this.props.showUrlPreview && !this.state.links.length) {
|
||||||
|
var links = this.findLinks(this.refs.content.children);
|
||||||
|
if (links.length) {
|
||||||
|
this.setState({ links: links.map((link)=>{
|
||||||
|
return link.getAttribute("href");
|
||||||
|
})});
|
||||||
|
|
||||||
|
// lazy-load the hidden state of the preview widget from localstorage
|
||||||
|
if (global.localStorage) {
|
||||||
|
var hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
|
||||||
|
this.setState({ widgetHidden: hidden });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
findLinks: function(nodes) {
|
findLinks: function(nodes) {
|
||||||
var links = [];
|
var links = [];
|
||||||
for (var i = 0; i < nodes.length; i++) {
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
@ -163,12 +179,14 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
var mxEvent = this.props.mxEvent;
|
var mxEvent = this.props.mxEvent;
|
||||||
var content = mxEvent.getContent();
|
var content = mxEvent.getContent();
|
||||||
var body = HtmlUtils.bodyToHtml(content, this.props.highlights,
|
var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {});
|
||||||
{highlightLink: this.props.highlightLink});
|
|
||||||
|
|
||||||
|
if (this.props.highlightLink) {
|
||||||
|
body = <a href={ this.props.highlightLink }>{ body }</a>;
|
||||||
|
}
|
||||||
|
|
||||||
var widgets;
|
var widgets;
|
||||||
if (this.state.links.length && !this.state.widgetHidden) {
|
if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) {
|
||||||
var LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget');
|
var LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget');
|
||||||
widgets = this.state.links.map((link)=>{
|
widgets = this.state.links.map((link)=>{
|
||||||
return <LinkPreviewWidget
|
return <LinkPreviewWidget
|
||||||
|
|
|
@ -83,13 +83,11 @@ module.exports = React.createClass({
|
||||||
alias: this.state.canonicalAlias
|
alias: this.state.canonicalAlias
|
||||||
}, ""
|
}, ""
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// save new aliases for m.room.aliases
|
// save new aliases for m.room.aliases
|
||||||
var aliasOperations = this.getAliasOperations();
|
var aliasOperations = this.getAliasOperations();
|
||||||
var promises = [];
|
|
||||||
for (var i = 0; i < aliasOperations.length; i++) {
|
for (var i = 0; i < aliasOperations.length; i++) {
|
||||||
var alias_operation = aliasOperations[i];
|
var alias_operation = aliasOperations[i];
|
||||||
console.log("alias %s %s", alias_operation.place, alias_operation.val);
|
console.log("alias %s %s", alias_operation.place, alias_operation.val);
|
||||||
|
@ -301,7 +299,7 @@ module.exports = React.createClass({
|
||||||
<div className="mx_RoomSettings_addAlias">
|
<div className="mx_RoomSettings_addAlias">
|
||||||
<img src="img/plus.svg" width="14" height="14" alt="Add"
|
<img src="img/plus.svg" width="14" height="14" alt="Add"
|
||||||
onClick={ self.onAliasAdded.bind(self, undefined) }/>
|
onClick={ self.onAliasAdded.bind(self, undefined) }/>
|
||||||
</div>
|
</div>
|
||||||
</div> : ""
|
</div> : ""
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -57,7 +57,7 @@ module.exports = React.createClass({
|
||||||
data.primary_color = scheme.primary_color;
|
data.primary_color = scheme.primary_color;
|
||||||
data.secondary_color = scheme.secondary_color;
|
data.secondary_color = scheme.secondary_color;
|
||||||
data.index = this._getColorIndex(data);
|
data.index = this._getColorIndex(data);
|
||||||
|
|
||||||
if (data.index === -1) {
|
if (data.index === -1) {
|
||||||
// append the unrecognised colours to our palette
|
// append the unrecognised colours to our palette
|
||||||
data.index = ROOM_COLORS.length;
|
data.index = ROOM_COLORS.length;
|
||||||
|
|
157
src/components/views/room_settings/UrlPreviewSettings.js
Normal file
157
src/components/views/room_settings/UrlPreviewSettings.js
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 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 q = require("q");
|
||||||
|
var React = require('react');
|
||||||
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
var sdk = require("../../../index");
|
||||||
|
var Modal = require("../../../Modal");
|
||||||
|
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'UrlPreviewSettings',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
room: React.PropTypes.object,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
var cli = MatrixClientPeg.get();
|
||||||
|
var roomState = this.props.room.currentState;
|
||||||
|
|
||||||
|
var roomPreviewUrls = this.props.room.currentState.getStateEvents('org.matrix.room.preview_urls', '');
|
||||||
|
var userPreviewUrls = this.props.room.getAccountData("org.matrix.room.preview_urls");
|
||||||
|
|
||||||
|
return {
|
||||||
|
globalDisableUrlPreview: (roomPreviewUrls && roomPreviewUrls.getContent().disable) || false,
|
||||||
|
userDisableUrlPreview: (userPreviewUrls && (userPreviewUrls.getContent().disable === true)) || false,
|
||||||
|
userEnableUrlPreview: (userPreviewUrls && (userPreviewUrls.getContent().disable === false)) || false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.originalState = Object.assign({}, this.state);
|
||||||
|
},
|
||||||
|
|
||||||
|
saveSettings: function() {
|
||||||
|
var promises = [];
|
||||||
|
|
||||||
|
if (this.state.globalDisableUrlPreview !== this.originalState.globalDisableUrlPreview) {
|
||||||
|
console.log("UrlPreviewSettings: Updating room's preview_urls state event");
|
||||||
|
promises.push(
|
||||||
|
MatrixClientPeg.get().sendStateEvent(
|
||||||
|
this.props.room.roomId, "org.matrix.room.preview_urls", {
|
||||||
|
disable: this.state.globalDisableUrlPreview
|
||||||
|
}, ""
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = undefined;
|
||||||
|
if (this.state.userDisableUrlPreview !== this.originalState.userDisableUrlPreview) {
|
||||||
|
console.log("UrlPreviewSettings: Disabling user's per-room preview_urls");
|
||||||
|
content = this.state.userDisableUrlPreview ? { disable : true } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.userEnableUrlPreview !== this.originalState.userEnableUrlPreview) {
|
||||||
|
console.log("UrlPreviewSettings: Enabling user's per-room preview_urls");
|
||||||
|
if (!content || content.disable === undefined) {
|
||||||
|
content = this.state.userEnableUrlPreview ? { disable : false } : {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
promises.push(
|
||||||
|
MatrixClientPeg.get().setRoomAccountData(
|
||||||
|
this.props.room.roomId, "org.matrix.room.preview_urls", content
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("UrlPreviewSettings: saveSettings: " + JSON.stringify(promises));
|
||||||
|
|
||||||
|
return promises;
|
||||||
|
},
|
||||||
|
|
||||||
|
onGlobalDisableUrlPreviewChange: function() {
|
||||||
|
this.setState({
|
||||||
|
globalDisableUrlPreview: this.refs.globalDisableUrlPreview.checked ? true : false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onUserEnableUrlPreviewChange: function() {
|
||||||
|
this.setState({
|
||||||
|
userDisableUrlPreview: false,
|
||||||
|
userEnableUrlPreview: this.refs.userEnableUrlPreview.checked ? true : false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onUserDisableUrlPreviewChange: function() {
|
||||||
|
this.setState({
|
||||||
|
userDisableUrlPreview: this.refs.userDisableUrlPreview.checked ? true : false,
|
||||||
|
userEnableUrlPreview: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var self = this;
|
||||||
|
var roomState = this.props.room.currentState;
|
||||||
|
var cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
var maySetRoomPreviewUrls = roomState.mayClientSendStateEvent('org.matrix.room.preview_urls', cli);
|
||||||
|
var disableRoomPreviewUrls;
|
||||||
|
if (maySetRoomPreviewUrls) {
|
||||||
|
disableRoomPreviewUrls =
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ref="globalDisableUrlPreview"
|
||||||
|
onChange={ this.onGlobalDisableUrlPreviewChange }
|
||||||
|
checked={ this.state.globalDisableUrlPreview } />
|
||||||
|
Disable URL previews by default for participants in this room
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
disableRoomPreviewUrls =
|
||||||
|
<label>
|
||||||
|
URL previews are { this.state.globalDisableUrlPreview ? "disabled" : "enabled" } by default for participants in this room.
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomSettings_toggles">
|
||||||
|
<h3>URL Previews</h3>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
You have <a href="#/settings">{ UserSettingsStore.getUrlPreviewsDisabled() ? 'disabled' : 'enabled' }</a> URL previews by default.
|
||||||
|
</label>
|
||||||
|
{ disableRoomPreviewUrls }
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ref="userEnableUrlPreview"
|
||||||
|
onChange={ this.onUserEnableUrlPreviewChange }
|
||||||
|
checked={ this.state.userEnableUrlPreview } />
|
||||||
|
Enable URL previews for this room (affects only you)
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ref="userDisableUrlPreview"
|
||||||
|
onChange={ this.onUserDisableUrlPreviewChange }
|
||||||
|
checked={ this.state.userDisableUrlPreview } />
|
||||||
|
Disable URL previews for this room (affects only you)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
|
@ -29,6 +29,23 @@ var PRESENCE_CLASS = {
|
||||||
"unavailable": "mx_EntityTile_unavailable"
|
"unavailable": "mx_EntityTile_unavailable"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function presenceClassForMember(presenceState, lastActiveAgo) {
|
||||||
|
// offline is split into two categories depending on whether we have
|
||||||
|
// a last_active_ago for them.
|
||||||
|
if (presenceState == 'offline') {
|
||||||
|
if (lastActiveAgo) {
|
||||||
|
return PRESENCE_CLASS['offline'] + '_beenactive';
|
||||||
|
} else {
|
||||||
|
return PRESENCE_CLASS['offline'] + '_neveractive';
|
||||||
|
}
|
||||||
|
} else if (presenceState) {
|
||||||
|
return PRESENCE_CLASS[presenceState];
|
||||||
|
} else {
|
||||||
|
return PRESENCE_CLASS['offline'] + '_neveractive';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'EntityTile',
|
displayName: 'EntityTile',
|
||||||
|
|
||||||
|
@ -79,7 +96,10 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var presenceClass = PRESENCE_CLASS[this.props.presenceState] || "mx_EntityTile_offline";
|
const presenceClass = presenceClassForMember(
|
||||||
|
this.props.presenceState, this.props.presenceLastActiveAgo
|
||||||
|
);
|
||||||
|
|
||||||
var mainClassName = "mx_EntityTile ";
|
var mainClassName = "mx_EntityTile ";
|
||||||
mainClassName += presenceClass + (this.props.className ? (" " + this.props.className) : "");
|
mainClassName += presenceClass + (this.props.className ? (" " + this.props.className) : "");
|
||||||
var nameEl;
|
var nameEl;
|
||||||
|
|
|
@ -101,6 +101,9 @@ module.exports = React.createClass({
|
||||||
/* link URL for the highlights */
|
/* link URL for the highlights */
|
||||||
highlightLink: React.PropTypes.string,
|
highlightLink: React.PropTypes.string,
|
||||||
|
|
||||||
|
/* should show URL previews for this event */
|
||||||
|
showUrlPreview: React.PropTypes.bool,
|
||||||
|
|
||||||
/* is this the focused event */
|
/* is this the focused event */
|
||||||
isSelectedEvent: React.PropTypes.bool,
|
isSelectedEvent: React.PropTypes.bool,
|
||||||
|
|
||||||
|
@ -359,6 +362,8 @@ module.exports = React.createClass({
|
||||||
var SenderProfile = sdk.getComponent('messages.SenderProfile');
|
var SenderProfile = sdk.getComponent('messages.SenderProfile');
|
||||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
|
|
||||||
|
//console.log("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||||
|
|
||||||
var content = this.props.mxEvent.getContent();
|
var content = this.props.mxEvent.getContent();
|
||||||
var msgtype = content.msgtype;
|
var msgtype = content.msgtype;
|
||||||
|
|
||||||
|
@ -420,6 +425,7 @@ module.exports = React.createClass({
|
||||||
<div className="mx_EventTile_line">
|
<div className="mx_EventTile_line">
|
||||||
<EventTileType ref="tile" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
|
<EventTileType ref="tile" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
|
||||||
highlightLink={this.props.highlightLink}
|
highlightLink={this.props.highlightLink}
|
||||||
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
onWidgetLoad={this.props.onWidgetLoad} />
|
onWidgetLoad={this.props.onWidgetLoad} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -37,17 +37,14 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
var cli = MatrixClientPeg.get();
|
||||||
|
cli.on("RoomState.members", this.onRoomStateMember);
|
||||||
|
|
||||||
this._emailEntity = null;
|
this._emailEntity = null;
|
||||||
// Load the complete user list for inviting new users
|
|
||||||
// TODO: Keep this list bleeding-edge up-to-date. Practically speaking,
|
// we have to update the list whenever membership changes
|
||||||
// it will do for now not being updated as random new users join different
|
// particularly to avoid bug https://github.com/vector-im/vector-web/issues/1813
|
||||||
// rooms as this list will be reloaded every room swap.
|
this._updateList();
|
||||||
if (this._room) {
|
|
||||||
this._userList = MatrixClientPeg.get().getUsers().filter((u) => {
|
|
||||||
return !this._room.hasMembershipState(u.userId, "join");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -55,6 +52,28 @@ module.exports = React.createClass({
|
||||||
this.onSearchQueryChanged('');
|
this.onSearchQueryChanged('');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
var cli = MatrixClientPeg.get();
|
||||||
|
if (cli) {
|
||||||
|
cli.removeListener("RoomState.members", this.onRoomStateMember);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateList: function() {
|
||||||
|
this._room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||||
|
// Load the complete user list for inviting new users
|
||||||
|
if (this._room) {
|
||||||
|
this._userList = MatrixClientPeg.get().getUsers().filter((u) => {
|
||||||
|
return (!this._room.hasMembershipState(u.userId, "join") &&
|
||||||
|
!this._room.hasMembershipState(u.userId, "invite"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onRoomStateMember: function(ev, state, member) {
|
||||||
|
this._updateList();
|
||||||
|
},
|
||||||
|
|
||||||
onInvite: function(ev) {
|
onInvite: function(ev) {
|
||||||
this.props.onInvite(this._input);
|
this.props.onInvite(this._input);
|
||||||
},
|
},
|
||||||
|
|
|
@ -61,12 +61,16 @@ module.exports = React.createClass({
|
||||||
updating: 0,
|
updating: 0,
|
||||||
devicesLoading: true,
|
devicesLoading: true,
|
||||||
devices: null,
|
devices: null,
|
||||||
|
existingOneToOneRoomId: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._cancelDeviceList = null;
|
this._cancelDeviceList = null;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
existingOneToOneRoomId: this.getExistingOneToOneRoomId()
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -90,6 +94,44 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getExistingOneToOneRoomId: function() {
|
||||||
|
var self = this;
|
||||||
|
var rooms = MatrixClientPeg.get().getRooms();
|
||||||
|
var userIds = [
|
||||||
|
this.props.member.userId,
|
||||||
|
MatrixClientPeg.get().credentials.userId
|
||||||
|
];
|
||||||
|
var existingRoomId;
|
||||||
|
|
||||||
|
// roomId can be null here because of a hack in MatrixChat.onUserClick where we
|
||||||
|
// abuse this to view users rather than room members.
|
||||||
|
var currentMembers;
|
||||||
|
if (this.props.member.roomId) {
|
||||||
|
var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId);
|
||||||
|
currentMembers = currentRoom.getJoinedMembers();
|
||||||
|
}
|
||||||
|
|
||||||
|
// reuse the first private 1:1 we find
|
||||||
|
existingRoomId = null;
|
||||||
|
|
||||||
|
for (var i = 0; i < rooms.length; i++) {
|
||||||
|
// don't try to reuse public 1:1 rooms
|
||||||
|
var join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", '');
|
||||||
|
if (join_rules && join_rules.getContent().join_rule === 'public') continue;
|
||||||
|
|
||||||
|
var members = rooms[i].getJoinedMembers();
|
||||||
|
if (members.length === 2 &&
|
||||||
|
userIds.indexOf(members[0].userId) !== -1 &&
|
||||||
|
userIds.indexOf(members[1].userId) !== -1)
|
||||||
|
{
|
||||||
|
existingRoomId = rooms[i].roomId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingRoomId;
|
||||||
|
},
|
||||||
|
|
||||||
onDeviceVerificationChanged: function(userId, device) {
|
onDeviceVerificationChanged: function(userId, device) {
|
||||||
if (userId == this.props.member.userId) {
|
if (userId == this.props.member.userId) {
|
||||||
// no need to re-download the whole thing; just update our copy of
|
// no need to re-download the whole thing; just update our copy of
|
||||||
|
@ -349,66 +391,29 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onChatClick: function() {
|
onChatClick: function() {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
|
||||||
|
// TODO: keep existingOneToOneRoomId updated if we see any room member changes anywhere
|
||||||
|
|
||||||
|
var useExistingOneToOneRoom = this.state.existingOneToOneRoomId && (this.state.existingOneToOneRoomId !== this.props.member.roomId);
|
||||||
|
|
||||||
// check if there are any existing rooms with just us and them (1:1)
|
// 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.
|
// If so, just view that room. If not, create a private room with them.
|
||||||
var self = this;
|
if (this.state.existingOneToOneRoomId) {
|
||||||
var rooms = MatrixClientPeg.get().getRooms();
|
|
||||||
var userIds = [
|
|
||||||
this.props.member.userId,
|
|
||||||
MatrixClientPeg.get().credentials.userId
|
|
||||||
];
|
|
||||||
var existingRoomId;
|
|
||||||
|
|
||||||
// roomId can be null here because of a hack in MatrixChat.onUserClick where we
|
|
||||||
// abuse this to view users rather than room members.
|
|
||||||
var currentMembers;
|
|
||||||
if (this.props.member.roomId) {
|
|
||||||
var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId);
|
|
||||||
currentMembers = currentRoom.getJoinedMembers();
|
|
||||||
}
|
|
||||||
// if we're currently in a 1:1 with this user, start a new chat
|
|
||||||
if (currentMembers && currentMembers.length === 2 &&
|
|
||||||
userIds.indexOf(currentMembers[0].userId) !== -1 &&
|
|
||||||
userIds.indexOf(currentMembers[1].userId) !== -1)
|
|
||||||
{
|
|
||||||
existingRoomId = null;
|
|
||||||
}
|
|
||||||
// otherwise reuse the first private 1:1 we find
|
|
||||||
else {
|
|
||||||
existingRoomId = null;
|
|
||||||
|
|
||||||
for (var i = 0; i < rooms.length; i++) {
|
|
||||||
// don't try to reuse public 1:1 rooms
|
|
||||||
var join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", '');
|
|
||||||
if (join_rules && join_rules.getContent().join_rule === 'public') continue;
|
|
||||||
|
|
||||||
var members = rooms[i].getJoinedMembers();
|
|
||||||
if (members.length === 2 &&
|
|
||||||
userIds.indexOf(members[0].userId) !== -1 &&
|
|
||||||
userIds.indexOf(members[1].userId) !== -1)
|
|
||||||
{
|
|
||||||
existingRoomId = rooms[i].roomId;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingRoomId) {
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: existingRoomId
|
room_id: this.state.existingOneToOneRoomId,
|
||||||
});
|
});
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
self.setState({ updating: self.state.updating + 1 });
|
this.setState({ updating: this.state.updating + 1 });
|
||||||
createRoom({
|
createRoom({
|
||||||
createOpts: {
|
createOpts: {
|
||||||
invite: [this.props.member.userId],
|
invite: [this.props.member.userId],
|
||||||
},
|
},
|
||||||
}).finally(function() {
|
}).finally(() => {
|
||||||
self.props.onFinished();
|
this.props.onFinished();
|
||||||
self.setState({ updating: self.state.updating - 1 });
|
this.setState({ updating: this.state.updating - 1 });
|
||||||
}).done();
|
}).done();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -553,7 +558,22 @@ module.exports = React.createClass({
|
||||||
if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) {
|
if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) {
|
||||||
// FIXME: we're referring to a vector component from react-sdk
|
// FIXME: we're referring to a vector component from react-sdk
|
||||||
var BottomLeftMenuTile = sdk.getComponent('rooms.BottomLeftMenuTile');
|
var BottomLeftMenuTile = sdk.getComponent('rooms.BottomLeftMenuTile');
|
||||||
startChat = <BottomLeftMenuTile collapsed={ false } img="img/create-big.svg" label="Start chat" onClick={ this.onChatClick }/>
|
|
||||||
|
var label;
|
||||||
|
if (this.state.existingOneToOneRoomId) {
|
||||||
|
if (this.state.existingOneToOneRoomId == this.props.member.roomId) {
|
||||||
|
label = "Start new direct chat";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
label = "Go to direct chat";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
label = "Start direct chat";
|
||||||
|
}
|
||||||
|
|
||||||
|
startChat = <BottomLeftMenuTile collapsed={ false } img="img/create-big.svg"
|
||||||
|
label={ label } onClick={ this.onChatClick }/>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.updating) {
|
if (this.state.updating) {
|
||||||
|
|
|
@ -54,7 +54,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
this.memberDict = this.getMemberDict();
|
this.memberDict = this.getMemberDict();
|
||||||
|
|
||||||
state.members = this.roomMembers(INITIAL_LOAD_NUM_MEMBERS);
|
state.members = this.roomMembers();
|
||||||
return state;
|
return state;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -64,7 +64,10 @@ module.exports = React.createClass({
|
||||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||||
cli.on("RoomState.events", this.onRoomStateEvent);
|
cli.on("RoomState.events", this.onRoomStateEvent);
|
||||||
cli.on("Room", this.onRoom); // invites
|
cli.on("Room", this.onRoom); // invites
|
||||||
cli.on("User.presence", this.onUserPresence);
|
// We listen for changes to the lastPresenceTs which is essentially
|
||||||
|
// listening for all presence events (we display most of not all of
|
||||||
|
// the information contained in presence events).
|
||||||
|
cli.on("User.lastPresenceTs", this.onUserLastPresenceTs);
|
||||||
// cli.on("Room.timeline", this.onRoomTimeline);
|
// cli.on("Room.timeline", this.onRoomTimeline);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -75,24 +78,11 @@ module.exports = React.createClass({
|
||||||
cli.removeListener("RoomMember.name", this.onRoomMemberName);
|
cli.removeListener("RoomMember.name", this.onRoomMemberName);
|
||||||
cli.removeListener("RoomState.events", this.onRoomStateEvent);
|
cli.removeListener("RoomState.events", this.onRoomStateEvent);
|
||||||
cli.removeListener("Room", this.onRoom);
|
cli.removeListener("Room", this.onRoom);
|
||||||
cli.removeListener("User.presence", this.onUserPresence);
|
cli.removeListener("User.lastPresenceTs", this.onUserLastPresenceTs);
|
||||||
// cli.removeListener("Room.timeline", this.onRoomTimeline);
|
// cli.removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// Lazy-load in more than the first N members
|
|
||||||
setTimeout(function() {
|
|
||||||
if (!self.isMounted()) return;
|
|
||||||
// lazy load to prevent it blocking the first render
|
|
||||||
self.setState({
|
|
||||||
members: self.roomMembers()
|
|
||||||
});
|
|
||||||
}, 50);
|
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
||||||
// ignore anything but real-time updates at the end of the room:
|
// ignore anything but real-time updates at the end of the room:
|
||||||
|
@ -121,7 +111,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
|
|
||||||
onUserPresence(event, user) {
|
onUserLastPresenceTs(event, user) {
|
||||||
// Attach a SINGLE listener for global presence changes then locate the
|
// Attach a SINGLE listener for global presence changes then locate the
|
||||||
// member tile and re-render it. This is more efficient than every tile
|
// member tile and re-render it. This is more efficient than every tile
|
||||||
// evar attaching their own listener.
|
// evar attaching their own listener.
|
||||||
|
@ -325,7 +315,7 @@ module.exports = React.createClass({
|
||||||
return all_members;
|
return all_members;
|
||||||
},
|
},
|
||||||
|
|
||||||
roomMembers: function(limit) {
|
roomMembers: function() {
|
||||||
var all_members = this.memberDict || {};
|
var all_members = this.memberDict || {};
|
||||||
var all_user_ids = Object.keys(all_members);
|
var all_user_ids = Object.keys(all_members);
|
||||||
var ConferenceHandler = CallHandler.getConferenceHandler();
|
var ConferenceHandler = CallHandler.getConferenceHandler();
|
||||||
|
@ -334,7 +324,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var to_display = [];
|
var to_display = [];
|
||||||
var count = 0;
|
var count = 0;
|
||||||
for (var i = 0; i < all_user_ids.length && (limit === undefined || count < limit); ++i) {
|
for (var i = 0; i < all_user_ids.length; ++i) {
|
||||||
var user_id = all_user_ids[i];
|
var user_id = all_user_ids[i];
|
||||||
var m = all_members[user_id];
|
var m = all_members[user_id];
|
||||||
|
|
||||||
|
@ -442,9 +432,16 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var memberList = self.state.members.filter(function(userId) {
|
var memberList = self.state.members.filter(function(userId) {
|
||||||
var m = self.memberDict[userId];
|
var m = self.memberDict[userId];
|
||||||
if (query && m.name.toLowerCase().indexOf(query) === -1) {
|
|
||||||
return false;
|
if (query) {
|
||||||
|
const matchesName = m.name.toLowerCase().indexOf(query) !== -1;
|
||||||
|
const matchesId = m.userId.toLowerCase().indexOf(query) !== -1;
|
||||||
|
|
||||||
|
if (!matchesName && !matchesId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.membership == membership;
|
return m.membership == membership;
|
||||||
}).map(function(userId) {
|
}).map(function(userId) {
|
||||||
var m = self.memberDict[userId];
|
var m = self.memberDict[userId];
|
||||||
|
|
|
@ -65,7 +65,12 @@ module.exports = React.createClass({
|
||||||
tags_changed: false,
|
tags_changed: false,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
areNotifsMuted: areNotifsMuted,
|
areNotifsMuted: areNotifsMuted,
|
||||||
isRoomPublished: false, // loaded async in componentWillMount
|
// isRoomPublished is loaded async in componentWillMount so when the component
|
||||||
|
// inits, the saved value will always be undefined, however getInitialState()
|
||||||
|
// is also called from the saving code so we must return the correct value here
|
||||||
|
// if we have it (although this could race if the user saves before we load whether
|
||||||
|
// the room is published or not).
|
||||||
|
isRoomPublished: this._originalIsRoomPublished,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -211,10 +216,13 @@ module.exports = React.createClass({
|
||||||
// color scheme
|
// color scheme
|
||||||
promises.push(this.saveColor());
|
promises.push(this.saveColor());
|
||||||
|
|
||||||
|
// url preview settings
|
||||||
|
promises.push(this.saveUrlPreviewSettings());
|
||||||
|
|
||||||
// encryption
|
// encryption
|
||||||
promises.push(this.saveEncryption());
|
promises.push(this.saveEncryption());
|
||||||
|
|
||||||
console.log("Performing %s operations", promises.length);
|
console.log("Performing %s operations: %s", promises.length, JSON.stringify(promises));
|
||||||
return q.allSettled(promises);
|
return q.allSettled(promises);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -228,6 +236,11 @@ module.exports = React.createClass({
|
||||||
return this.refs.color_settings.saveSettings();
|
return this.refs.color_settings.saveSettings();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
saveUrlPreviewSettings: function() {
|
||||||
|
if (!this.refs.url_preview_settings) { return q(); }
|
||||||
|
return this.refs.url_preview_settings.saveSettings();
|
||||||
|
},
|
||||||
|
|
||||||
saveEncryption: function () {
|
saveEncryption: function () {
|
||||||
if (!this.refs.encrypt) { return q(); }
|
if (!this.refs.encrypt) { return q(); }
|
||||||
|
|
||||||
|
@ -422,6 +435,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var AliasSettings = sdk.getComponent("room_settings.AliasSettings");
|
var AliasSettings = sdk.getComponent("room_settings.AliasSettings");
|
||||||
var ColorSettings = sdk.getComponent("room_settings.ColorSettings");
|
var ColorSettings = sdk.getComponent("room_settings.ColorSettings");
|
||||||
|
var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
||||||
var EditableText = sdk.getComponent('elements.EditableText');
|
var EditableText = sdk.getComponent('elements.EditableText');
|
||||||
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||||
|
|
||||||
|
@ -654,6 +668,8 @@ module.exports = React.createClass({
|
||||||
canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')}
|
canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')}
|
||||||
aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} />
|
aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} />
|
||||||
|
|
||||||
|
<UrlPreviewSettings ref="url_preview_settings" room={this.props.room} />
|
||||||
|
|
||||||
<h3>Permissions</h3>
|
<h3>Permissions</h3>
|
||||||
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
|
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
|
||||||
<div className="mx_RoomSettings_powerLevel">
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
|
|
|
@ -43,7 +43,10 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return( { hover : false });
|
return({
|
||||||
|
hover : false,
|
||||||
|
badgeHover : false,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onClick: function() {
|
onClick: function() {
|
||||||
|
@ -61,6 +64,14 @@ module.exports = React.createClass({
|
||||||
this.setState( { hover : false });
|
this.setState( { hover : false });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
badgeOnMouseEnter: function() {
|
||||||
|
this.setState( { badgeHover : true } );
|
||||||
|
},
|
||||||
|
|
||||||
|
badgeOnMouseLeave: function() {
|
||||||
|
this.setState( { badgeHover : false } );
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
var me = this.props.room.currentState.members[myUserId];
|
var me = this.props.room.currentState.members[myUserId];
|
||||||
|
@ -83,9 +94,25 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||||
var badge;
|
var badge;
|
||||||
if (this.props.highlight || notificationCount > 0) {
|
var badgeContent;
|
||||||
badge = <div className="mx_RoomTile_badge">{ notificationCount ? notificationCount : '!' }</div>;
|
var badgeClasses;
|
||||||
|
|
||||||
|
if (this.state.badgeHover) {
|
||||||
|
badgeContent = "\u00B7\u00B7\u00B7";
|
||||||
|
} else if (this.props.highlight || notificationCount > 0) {
|
||||||
|
badgeContent = notificationCount ? notificationCount : '!';
|
||||||
|
} else {
|
||||||
|
badgeContent = '\u200B';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.highlight || notificationCount > 0) {
|
||||||
|
badgeClasses = "mx_RoomTile_badge";
|
||||||
|
} else {
|
||||||
|
badgeClasses = "mx_RoomTile_badge mx_RoomTile_badge_no_unread";
|
||||||
|
}
|
||||||
|
|
||||||
|
badge = <div className={ badgeClasses } onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}>{ badgeContent }</div>;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
if (this.props.highlight) {
|
if (this.props.highlight) {
|
||||||
badge = <div className="mx_RoomTile_badge">!</div>;
|
badge = <div className="mx_RoomTile_badge">!</div>;
|
||||||
|
|
|
@ -24,17 +24,17 @@ module.exports = React.createClass({
|
||||||
displayName: 'TabCompleteBar',
|
displayName: 'TabCompleteBar',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
entries: React.PropTypes.array.isRequired
|
tabComplete: React.PropTypes.object.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<div className="mx_TabCompleteBar">
|
<div className="mx_TabCompleteBar">
|
||||||
{this.props.entries.map(function(entry, i) {
|
{this.props.tabComplete.peek(6).map((entry, i) => {
|
||||||
return (
|
return (
|
||||||
<div key={entry.getKey() || i + ""}
|
<div key={entry.getKey() || i + ""}
|
||||||
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
|
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
|
||||||
onClick={entry.onClick.bind(entry)} >
|
onClick={this.props.tabComplete.onEntryClick.bind(this.props.tabComplete, entry)} >
|
||||||
{entry.getImageJsx()}
|
{entry.getImageJsx()}
|
||||||
<span className="mx_TabCompleteBar_text">
|
<span className="mx_TabCompleteBar_text">
|
||||||
{entry.getText()}
|
{entry.getText()}
|
||||||
|
|
|
@ -64,11 +64,15 @@ function createRoom(opts) {
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
var modal = Modal.createDialog(Loader);
|
var modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||||
|
|
||||||
return client.createRoom(createOpts).finally(function() {
|
return client.createRoom(createOpts).finally(function() {
|
||||||
modal.close();
|
modal.close();
|
||||||
}).then(function(res) {
|
}).then(function(res) {
|
||||||
|
// NB createRoom doesn't block on the client seeing the echo that the
|
||||||
|
// room has been created, so we race here with the client knowing that
|
||||||
|
// the room exists, causing things like
|
||||||
|
// https://github.com/vector-im/vector-web/issues/1813
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: res.room_id
|
room_id: res.room_id
|
||||||
|
|
|
@ -210,7 +210,7 @@ describe('TimelinePanel', function() {
|
||||||
var N_EVENTS = 600;
|
var N_EVENTS = 600;
|
||||||
|
|
||||||
// sadly, loading all those events takes a while
|
// sadly, loading all those events takes a while
|
||||||
this.timeout(N_EVENTS * 20);
|
this.timeout(N_EVENTS * 30);
|
||||||
|
|
||||||
// client.getRoom is called a /lot/ in this test, so replace
|
// client.getRoom is called a /lot/ in this test, so replace
|
||||||
// sinon's spy with a fast noop.
|
// sinon's spy with a fast noop.
|
||||||
|
@ -271,6 +271,8 @@ describe('TimelinePanel', function() {
|
||||||
|
|
||||||
// we should now be able to scroll down, and paginate in the other
|
// we should now be able to scroll down, and paginate in the other
|
||||||
// direction.
|
// direction.
|
||||||
|
console.log("scrollingDiv.scrollTop is " + scrollingDiv.scrollTop);
|
||||||
|
console.log("Going to set it to " + scrollingDiv.scrollHeight);
|
||||||
scrollingDiv.scrollTop = scrollingDiv.scrollHeight;
|
scrollingDiv.scrollTop = scrollingDiv.scrollHeight;
|
||||||
return awaitScroll();
|
return awaitScroll();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
|
Loading…
Reference in a new issue