mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 05:04:57 +08:00
merge in develop
This commit is contained in:
commit
90e5ab2ca3
109
.eslintrc
Normal file
109
.eslintrc
Normal file
@ -0,0 +1,109 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react",
|
||||
"flowtype"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
"impliedStrict": true
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"amd": true,
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:react/recommended"],
|
||||
"rules": {
|
||||
"no-undef": ["warn"],
|
||||
"global-strict": ["off"],
|
||||
"no-extra-semi": ["warn"],
|
||||
"no-underscore-dangle": ["off"],
|
||||
"no-console": ["off"],
|
||||
"no-unused-vars": ["off"],
|
||||
"no-trailing-spaces": ["warn", {
|
||||
"skipBlankLines": true
|
||||
}],
|
||||
"no-unreachable": ["warn"],
|
||||
"no-spaced-func": ["warn"],
|
||||
"no-new-func": ["error"],
|
||||
"no-new-wrappers": ["error"],
|
||||
"no-invalid-regexp": ["error"],
|
||||
"no-extra-bind": ["error"],
|
||||
"no-magic-numbers": ["error", {
|
||||
"ignore": [-1, 0, 1], // usually used in array/string indexing
|
||||
"ignoreArrayIndexes": true,
|
||||
"enforceConst": true,
|
||||
"detectObjects": true
|
||||
}],
|
||||
"consistent-return": ["error"],
|
||||
"valid-jsdoc": ["error"],
|
||||
"no-use-before-define": ["error"],
|
||||
"camelcase": ["warn"],
|
||||
"array-callback-return": ["error"],
|
||||
"dot-location": ["warn", "property"],
|
||||
"guard-for-in": ["error"],
|
||||
"no-useless-call": ["warn"],
|
||||
"no-useless-escape": ["warn"],
|
||||
"no-useless-concat": ["warn"],
|
||||
"brace-style": ["warn", "1tbs"],
|
||||
"comma-style": ["warn", "last"],
|
||||
"space-before-function-paren": ["warn", "never"],
|
||||
"space-before-blocks": ["warn", "always"],
|
||||
"keyword-spacing": ["warn", {
|
||||
"before": true,
|
||||
"after": true
|
||||
}],
|
||||
|
||||
// dangling commas required, but only for multiline objects/arrays
|
||||
"comma-dangle": ["warn", "always-multiline"],
|
||||
// always === instead of ==, unless dealing with null/undefined
|
||||
"eqeqeq": ["error", "smart"],
|
||||
// always use curly braces, even with single statements
|
||||
"curly": ["error", "all"],
|
||||
// phasing out var in favour of let/const is a good idea
|
||||
"no-var": ["warn"],
|
||||
// always require semicolons
|
||||
"semi": ["error", "always"],
|
||||
// prefer rest and spread over the Old Ways
|
||||
"prefer-spread": ["warn"],
|
||||
"prefer-rest-params": ["warn"],
|
||||
|
||||
/** react **/
|
||||
|
||||
// bind or arrow function in props causes performance issues
|
||||
"react/jsx-no-bind": ["error"],
|
||||
"react/jsx-key": ["error"],
|
||||
"react/prefer-stateless-function": ["warn"],
|
||||
"react/sort-comp": ["warn"],
|
||||
|
||||
/** flowtype **/
|
||||
"flowtype/require-parameter-type": 1,
|
||||
"flowtype/require-return-type": [
|
||||
1,
|
||||
"always",
|
||||
{
|
||||
"annotateUndefined": "never"
|
||||
}
|
||||
],
|
||||
"flowtype/space-after-type-colon": [
|
||||
1,
|
||||
"always"
|
||||
],
|
||||
"flowtype/space-before-type-colon": [
|
||||
1,
|
||||
"never"
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"flowtype": {
|
||||
"onlyFilesWithFlowAnnotation": true
|
||||
}
|
||||
}
|
||||
}
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
npm-debug.log
|
||||
|
||||
/node_modules
|
||||
/lib
|
||||
|
||||
|
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 package provides the logic and 'controller' parts for the UI components. This
|
||||
forms one part of a complete matrix client, but it not useable in isolation. It
|
||||
must be used from a 'skin'. A skin provides:
|
||||
* The HTML for the UI components (in the form of React `render` methods)
|
||||
* The CSS for this HTML
|
||||
* The containing application
|
||||
This package provides the React components needed to build a Matrix web client
|
||||
using React. It is not useable in isolation, and instead must must be used from
|
||||
a 'skin'. A skin provides:
|
||||
* Customised implementations of presentation components.
|
||||
* Custom CSS
|
||||
* The containing application
|
||||
* Zero or more 'modules' containing non-UI functionality
|
||||
|
||||
Skins are modules are exported from such a package in the `lib` directory.
|
||||
`lib/skins` contains one directory per-skin, named after the skin, and the
|
||||
`modules` directory contains modules as their javascript files.
|
||||
**WARNING: As of July 2016, the skinning abstraction is broken due to rapid
|
||||
development of `matrix-react-sdk` to meet the needs of Vector, the first app
|
||||
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
|
||||
a minimal application that instantiates the basic skin making a working matrix
|
||||
client.
|
||||
In the interim, `vector-im/vector-web` and `matrix-org/matrix-react-sdk` should
|
||||
be considered as a single project (for instance, matrix-react-sdk bugs
|
||||
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
|
||||
'views' for each UI component. To get started quickly, use matrix-react-skin.
|
||||
Developer Guide
|
||||
===============
|
||||
|
||||
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
|
||||
encourage a very modular and reusable architecture, making it easy to
|
||||
customise and use UI widgets independently of the rest of the SDK and your app.
|
||||
All code lands on the `develop` branch - `master` is only used for stable releases.
|
||||
**Please file PRs against `develop`!!**
|
||||
|
||||
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.
|
||||
|
||||
* Each component has its own:
|
||||
* View object defined as a React javascript class containing embedded
|
||||
HTML expressed in React's JSX notation.
|
||||
* CSS file, which defines the styling specific to that component.
|
||||
|
||||
* Components are loosely grouped into the 5 levels outlined by atomic design:
|
||||
* atoms: fundamental building blocks (e.g. a timestamp tag)
|
||||
* molecules: "group of atoms which functions together as a unit"
|
||||
(e.g. a message in a chat timeline)
|
||||
* organisms: "groups of molecules (and atoms) which form a distinct section
|
||||
of a UI" (e.g. a view of a chat room)
|
||||
* templates: "a reusable configuration of organisms" - used to combine and
|
||||
style organisms into a well-defined global look and feel
|
||||
* pages: specific instances of templates.
|
||||
React components in matrix-react-sdk are come in two different flavours:
|
||||
'structures' and 'views'. Structures are stateful components which handle the
|
||||
more complicated business logic of the app, delegating their actual presentation
|
||||
rendering to stateless 'view' components. For instance, the RoomView component
|
||||
that orchestrates the act of visualising the contents of a given Matrix chat room
|
||||
tracks lots of state for its child components which it passes into them for visual
|
||||
rendering via props.
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
* 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
|
||||
the context of the rest of the app, although this is unusual for any but
|
||||
the simplest atoms and molecules.
|
||||
the context of the rest of the app, although this is unusual for any but
|
||||
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.
|
||||
'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
|
||||
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
|
||||
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
|
||||
@ -98,30 +119,36 @@ In practice this means:
|
||||
generally not cool and stop the component from being reused easily in
|
||||
different places.
|
||||
|
||||
* We don't use the atomify library itself, as React already provides most
|
||||
of the modularity requirements it brings to the table.
|
||||
Originally `matrix-react-sdk` followed the Atomic design pattern as per
|
||||
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
|
||||
components to embed a Matrix client into your app:
|
||||
Github Issues
|
||||
=============
|
||||
|
||||
* 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.
|
||||
All issues should be filed under https://github.com/vector-im/vector-web/issues
|
||||
for now.
|
||||
|
||||
OUTDATED: To Create Your Own Skin
|
||||
=================================
|
||||
|
||||
**This is ALL LIES currently, as skinning is currently broken - see the WARNING
|
||||
section at the top of this readme.**
|
||||
|
||||
Skins are modules are exported from such a package in the `lib` directory.
|
||||
`lib/skins` contains one directory per-skin, named after the skin, and the
|
||||
`modules` directory contains modules as their javascript files.
|
||||
|
||||
A basic skin is provided in the matrix-react-skin package. This also contains
|
||||
a minimal application that instantiates the basic skin making a working matrix
|
||||
client.
|
||||
|
||||
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
|
||||
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
|
||||
@ -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.
|
||||
|
||||
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?
|
@ -2,6 +2,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
export KARMAFLAGS="--no-colors"
|
||||
export NVM_DIR="/home/jenkins/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||
nvm use 4
|
||||
@ -14,6 +15,9 @@ npm install
|
||||
# run the mocha tests
|
||||
npm run test
|
||||
|
||||
# run eslint
|
||||
npm run lint -- -f checkstyle -o eslint.xml || true
|
||||
|
||||
# delete the old tarball, if it exists
|
||||
rm -f matrix-react-sdk-*.tgz
|
||||
|
||||
|
@ -171,5 +171,6 @@ module.exports = function (config) {
|
||||
},
|
||||
devtool: 'inline-source-map',
|
||||
},
|
||||
browserNoActivityTimeout: 15000,
|
||||
});
|
||||
};
|
||||
|
29
package.json
29
package.json
@ -13,13 +13,15 @@
|
||||
"reskindex": "./reskindex.js"
|
||||
},
|
||||
"scripts": {
|
||||
"reskindex": "reskindex -h header",
|
||||
"build": "babel src -d lib --source-maps",
|
||||
"start": "babel src -w -d lib --source-maps",
|
||||
"reskindex": "./reskindex.js -h header",
|
||||
"build": "babel src -d lib --source-maps --stage 1",
|
||||
"start": "babel src -w -d lib --source-maps --stage 1",
|
||||
"lint": "eslint src/",
|
||||
"lintall": "eslint src/ test/",
|
||||
"clean": "rimraf lib",
|
||||
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt",
|
||||
"test": "karma start --browsers PhantomJS",
|
||||
"test-multi": "karma start --single-run=false"
|
||||
"test": "karma start $KARMAFLAGS --browsers PhantomJS",
|
||||
"test-multi": "karma start $KARMAFLAGS --single-run=false"
|
||||
},
|
||||
"dependencies": {
|
||||
"browser-request": "^0.3.3",
|
||||
@ -28,21 +30,26 @@
|
||||
"draft-js-export-html": "^0.2.2",
|
||||
"draft-js-export-markdown": "^0.2.0",
|
||||
"draft-js-import-markdown": "^0.1.6",
|
||||
"emojione": "^2.2.2",
|
||||
"favico.js": "^0.3.10",
|
||||
"filesize": "^3.1.2",
|
||||
"flux": "^2.0.3",
|
||||
"fuse.js": "^2.2.0",
|
||||
"glob": "^5.0.14",
|
||||
"highlight.js": "^8.9.1",
|
||||
"linkifyjs": "^2.0.0-beta.4",
|
||||
"lodash": "^4.13.1",
|
||||
"marked": "^0.3.5",
|
||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
||||
"optimist": "^0.6.1",
|
||||
"q": "^1.4.1",
|
||||
"react": "^15.0.1",
|
||||
"react-dom": "^15.0.1",
|
||||
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e",
|
||||
"react": "^15.2.1",
|
||||
"react-addons-css-transition-group": "^15.2.1",
|
||||
"react-dom": "^15.2.1",
|
||||
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
||||
"sanitize-html": "^1.11.1",
|
||||
"velocity-vector": "vector-im/velocity#059e3b2"
|
||||
"velocity-vector": "vector-im/velocity#059e3b2",
|
||||
"whatwg-fetch": "^1.0.0"
|
||||
},
|
||||
"//babelversion": [
|
||||
"brief experiments with babel6 seems to show that it generates source ",
|
||||
@ -52,8 +59,12 @@
|
||||
"devDependencies": {
|
||||
"babel": "^5.8.23",
|
||||
"babel-core": "^5.8.38",
|
||||
"babel-eslint": "^6.1.0",
|
||||
"babel-loader": "^5.4.0",
|
||||
"babel-polyfill": "^6.5.0",
|
||||
"eslint": "^2.13.1",
|
||||
"eslint-plugin-flowtype": "^2.3.0",
|
||||
"eslint-plugin-react": "^5.2.2",
|
||||
"expect": "^1.16.0",
|
||||
"json-loader": "^0.5.3",
|
||||
"karma": "^0.13.22",
|
||||
|
@ -38,11 +38,13 @@ class AddThreepid {
|
||||
*/
|
||||
addEmailAddress(emailAddress, 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;
|
||||
return res;
|
||||
}, 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})`;
|
||||
}
|
||||
throw err;
|
||||
|
@ -181,11 +181,11 @@ function _onAction(payload) {
|
||||
console.error("Unknown conf call type: %s", payload.type);
|
||||
}
|
||||
}
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
switch (payload.action) {
|
||||
case 'place_call':
|
||||
if (module.exports.getAnyActiveCall()) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Existing Call",
|
||||
description: "You are already in a call."
|
||||
@ -195,6 +195,7 @@ function _onAction(payload) {
|
||||
|
||||
// if the runtime env doesn't do VoIP, whine.
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "VoIP is unsupported",
|
||||
description: "You cannot place VoIP calls in this browser."
|
||||
@ -210,7 +211,7 @@ function _onAction(payload) {
|
||||
|
||||
var members = room.getJoinedMembers();
|
||||
if (members.length <= 1) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
description: "You cannot place a call with yourself."
|
||||
});
|
||||
@ -236,23 +237,37 @@ function _onAction(payload) {
|
||||
case 'place_conference_call':
|
||||
console.log("Place conference call in %s", payload.room_id);
|
||||
if (!ConferenceHandler) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
description: "Conference calls are not supported in this client"
|
||||
});
|
||||
}
|
||||
else if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "VoIP is unsupported",
|
||||
description: "You cannot place VoIP calls in this browser."
|
||||
});
|
||||
}
|
||||
else {
|
||||
ConferenceHandler.createNewMatrixCall(
|
||||
MatrixClientPeg.get(), payload.room_id
|
||||
).done(function(call) {
|
||||
placeCall(call);
|
||||
}, function(err) {
|
||||
console.error("Failed to setup conference call: %s", err);
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Warning!",
|
||||
description: "Conference calling in Vector is in development and may not be reliable.",
|
||||
onFinished: confirm=>{
|
||||
if (confirm) {
|
||||
ConferenceHandler.createNewMatrixCall(
|
||||
MatrixClientPeg.get(), payload.room_id
|
||||
).done(function(call) {
|
||||
placeCall(call);
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to set up conference call",
|
||||
description: "Conference call failed: " + err,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
@ -52,6 +52,36 @@ function infoForImageFile(imageFile) {
|
||||
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 {
|
||||
constructor() {
|
||||
this.inprogress = [];
|
||||
@ -74,13 +104,25 @@ class ContentMessages {
|
||||
var def = q.defer();
|
||||
if (file.type.indexOf('image/') == 0) {
|
||||
content.msgtype = 'm.image';
|
||||
infoForImageFile(file).then(function (imageInfo) {
|
||||
infoForImageFile(file).then(imageInfo=>{
|
||||
extend(content.info, imageInfo);
|
||||
def.resolve();
|
||||
}, error=>{
|
||||
content.msgtype = 'm.file';
|
||||
def.resolve();
|
||||
});
|
||||
} else if (file.type.indexOf('audio/') == 0) {
|
||||
content.msgtype = 'm.audio';
|
||||
def.resolve();
|
||||
} else if (file.type.indexOf('video/') == 0) {
|
||||
content.msgtype = 'm.video';
|
||||
infoForVideoFile(file).then(videoInfo=>{
|
||||
extend(content.info, videoInfo);
|
||||
def.resolve();
|
||||
}, error=>{
|
||||
content.msgtype = 'm.file';
|
||||
def.resolve();
|
||||
});
|
||||
} else {
|
||||
content.msgtype = 'm.file';
|
||||
def.resolve();
|
||||
|
@ -1,51 +0,0 @@
|
||||
/*
|
||||
Copyright 2015 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
const IS_GUEST_KEY = "matrix-is-guest";
|
||||
|
||||
class GuestAccess {
|
||||
|
||||
constructor(localStorage) {
|
||||
this.localStorage = localStorage;
|
||||
try {
|
||||
this._isGuest = localStorage.getItem(IS_GUEST_KEY) === "true";
|
||||
}
|
||||
catch (e) {} // don't care
|
||||
}
|
||||
|
||||
setPeekedRoom(roomId) {
|
||||
// we purposefully do not persist this to local storage as peeking is
|
||||
// entirely transient.
|
||||
this._peekedRoomId = roomId;
|
||||
}
|
||||
|
||||
getPeekedRoom() {
|
||||
return this._peekedRoomId;
|
||||
}
|
||||
|
||||
isGuest() {
|
||||
return this._isGuest;
|
||||
}
|
||||
|
||||
markAsGuest(isGuest) {
|
||||
try {
|
||||
this.localStorage.setItem(IS_GUEST_KEY, JSON.stringify(isGuest));
|
||||
} catch (e) {} // ignore. If they don't do LS, they'll just get a new account.
|
||||
this._isGuest = isGuest;
|
||||
this._peekedRoomId = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuestAccess;
|
@ -20,6 +20,11 @@ var React = require('react');
|
||||
var sanitizeHtml = require('sanitize-html');
|
||||
var highlight = require('highlight.js');
|
||||
var linkifyMatrix = require('./linkify-matrix');
|
||||
import escape from 'lodash/escape';
|
||||
import emojione from 'emojione';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
||||
|
||||
var sanitizeHtmlParams = {
|
||||
allowedTags: [
|
||||
@ -28,7 +33,7 @@ var sanitizeHtmlParams = {
|
||||
// deliberately no h1/h2 to stop people shouting.
|
||||
'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
||||
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre'
|
||||
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'img',
|
||||
],
|
||||
allowedAttributes: {
|
||||
// custom ones first:
|
||||
@ -42,7 +47,9 @@ var sanitizeHtmlParams = {
|
||||
selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ],
|
||||
// URL schemes we permit
|
||||
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
|
||||
allowedSchemesByTag: {},
|
||||
allowedSchemesByTag: {
|
||||
img: [ 'data' ],
|
||||
},
|
||||
|
||||
transformTags: { // custom to matrix
|
||||
// add blank targets to all hyperlinks except vector URLs
|
||||
@ -179,46 +186,49 @@ module.exports = {
|
||||
*
|
||||
* 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) {
|
||||
opts = opts || {};
|
||||
|
||||
var isHtml = (content.format === "org.matrix.custom.html");
|
||||
let body = isHtml ? content.formatted_body : escape(content.body);
|
||||
|
||||
var safeBody;
|
||||
if (isHtml) {
|
||||
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
|
||||
// to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
|
||||
// are interrupted by HTML tags (not that we did before) - e.g. foo<span/>bar won't get highlighted
|
||||
// by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
|
||||
try {
|
||||
if (highlights && highlights.length > 0) {
|
||||
var highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
|
||||
var safeHighlights = highlights.map(function(highlight) {
|
||||
return sanitizeHtml(highlight, sanitizeHtmlParams);
|
||||
});
|
||||
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
|
||||
sanitizeHtmlParams.textFilter = function(safeText) {
|
||||
return highlighter.applyHighlights(safeText, safeHighlights).join('');
|
||||
};
|
||||
}
|
||||
safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
|
||||
}
|
||||
finally {
|
||||
delete sanitizeHtmlParams.textFilter;
|
||||
}
|
||||
return <span className="markdown-body" dangerouslySetInnerHTML={{ __html: safeBody }} />;
|
||||
} else {
|
||||
safeBody = content.body;
|
||||
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
|
||||
// to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
|
||||
// are interrupted by HTML tags (not that we did before) - e.g. foo<span/>bar won't get highlighted
|
||||
// by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
|
||||
try {
|
||||
if (highlights && highlights.length > 0) {
|
||||
var highlighter = new TextHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
|
||||
return highlighter.applyHighlights(safeBody, highlights);
|
||||
}
|
||||
else {
|
||||
return safeBody;
|
||||
var highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
|
||||
var safeHighlights = highlights.map(function(highlight) {
|
||||
return sanitizeHtml(highlight, sanitizeHtmlParams);
|
||||
});
|
||||
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
|
||||
sanitizeHtmlParams.textFilter = function(safeText) {
|
||||
return highlighter.applyHighlights(safeText, safeHighlights).join('');
|
||||
};
|
||||
}
|
||||
safeBody = sanitizeHtml(body, sanitizeHtmlParams);
|
||||
emojione.imageType = 'svg';
|
||||
safeBody = emojione.unicodeToImage(safeBody);
|
||||
}
|
||||
finally {
|
||||
delete sanitizeHtmlParams.textFilter;
|
||||
}
|
||||
|
||||
EMOJI_REGEX.lastIndex = 0;
|
||||
let contentBodyTrimmed = content.body.trim();
|
||||
let match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
||||
let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
||||
|
||||
const className = classNames({
|
||||
'mx_EventTile_body': true,
|
||||
'mx_EventTile_bigEmoji': emojiBody,
|
||||
'markdown-body': isHtml,
|
||||
});
|
||||
return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} />;
|
||||
},
|
||||
|
||||
highlightDom: function(element) {
|
||||
@ -228,5 +238,11 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
emojifyText: function(text) {
|
||||
emojione.imageType = 'svg';
|
||||
return {
|
||||
__html: emojione.unicodeToImage(escape(text)),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
110
src/Lifecycle.js
Normal file
110
src/Lifecycle.js
Normal file
@ -0,0 +1,110 @@
|
||||
/*
|
||||
Copyright 2015, 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.
|
||||
*/
|
||||
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import Notifier from './Notifier'
|
||||
import UserActivity from './UserActivity';
|
||||
import Presence from './Presence';
|
||||
import dis from './dispatcher';
|
||||
|
||||
/**
|
||||
* Transitions to a logged-in state using the given credentials
|
||||
* @param {MatrixClientCreds} credentials The credentials to use
|
||||
*/
|
||||
function setLoggedIn(credentials) {
|
||||
credentials.guest = Boolean(credentials.guest);
|
||||
console.log("onLoggedIn => %s (guest=%s)", credentials.userId, credentials.guest);
|
||||
MatrixClientPeg.replaceUsingCreds(credentials);
|
||||
|
||||
dis.dispatch({action: 'on_logged_in'});
|
||||
|
||||
startMatrixClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the current session out and transitions to the logged-out state
|
||||
*/
|
||||
function logout() {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
// logout doesn't work for guest sessions
|
||||
// Also we sometimes want to re-log in a guest session
|
||||
// if we abort the login
|
||||
_onLoggedOut();
|
||||
return;
|
||||
}
|
||||
|
||||
return MatrixClientPeg.get().logout().then(_onLoggedOut,
|
||||
(err) => {
|
||||
// Just throwing an error here is going to be very unhelpful
|
||||
// if you're trying to log out because your server's down and
|
||||
// you want to log into a different server, so just forget the
|
||||
// access token. It's annoying that this will leave the access
|
||||
// token still valid, but we should fix this by having access
|
||||
// tokens expire (and if you really think you've been compromised,
|
||||
// change your password).
|
||||
console.log("Failed to call logout API: token will not be invalidated");
|
||||
_onLoggedOut();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the matrix client and all other react-sdk services that
|
||||
* listen for events while a session is logged in.
|
||||
*/
|
||||
function startMatrixClient() {
|
||||
// dispatch this before starting the matrix client: it's used
|
||||
// to add listeners for the 'sync' event so otherwise we'd have
|
||||
// a race condition (and we need to dispatch synchronously for this
|
||||
// to work).
|
||||
dis.dispatch({action: 'will_start_client'}, true);
|
||||
|
||||
Notifier.start();
|
||||
UserActivity.start();
|
||||
Presence.start();
|
||||
|
||||
MatrixClientPeg.start();
|
||||
}
|
||||
|
||||
function _onLoggedOut() {
|
||||
if (window.localStorage) {
|
||||
const hsUrl = window.localStorage.getItem("mx_hs_url");
|
||||
const isUrl = window.localStorage.getItem("mx_is_url");
|
||||
window.localStorage.clear();
|
||||
// preserve our HS & IS URLs for convenience
|
||||
// N.B. we cache them in hsUrl/isUrl and can't really inline them
|
||||
// as getCurrentHsUrl() may call through to localStorage.
|
||||
if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl);
|
||||
if (isUrl) window.localStorage.setItem("mx_is_url", isUrl);
|
||||
}
|
||||
_stopMatrixClient();
|
||||
|
||||
dis.dispatch({action: 'on_logged_out'});
|
||||
}
|
||||
|
||||
// stop all the background processes related to the current client
|
||||
function _stopMatrixClient() {
|
||||
Notifier.stop();
|
||||
UserActivity.stop();
|
||||
Presence.stop();
|
||||
MatrixClientPeg.get().stopClient();
|
||||
MatrixClientPeg.get().removeAllListeners();
|
||||
MatrixClientPeg.unset();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setLoggedIn, logout, startMatrixClient
|
||||
};
|
@ -16,13 +16,10 @@ limitations under the License.
|
||||
|
||||
'use strict';
|
||||
|
||||
// A thing that holds your Matrix Client
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
var GuestAccess = require("./GuestAccess");
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import utils from 'matrix-js-sdk/lib/utils';
|
||||
|
||||
var matrixClient = null;
|
||||
|
||||
var localStorage = window.localStorage;
|
||||
const localStorage = window.localStorage;
|
||||
|
||||
function deviceId() {
|
||||
// XXX: is Math.random()'s deterministicity a problem here?
|
||||
@ -35,97 +32,71 @@ function deviceId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
function createClientForPeg(hs_url, is_url, user_id, access_token, guestAccess) {
|
||||
var opts = {
|
||||
baseUrl: hs_url,
|
||||
idBaseUrl: is_url,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
timelineSupport: true,
|
||||
};
|
||||
|
||||
if (localStorage) {
|
||||
opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
|
||||
opts.deviceId = deviceId();
|
||||
}
|
||||
|
||||
matrixClient = Matrix.createClient(opts);
|
||||
|
||||
// we're going to add eventlisteners for each matrix event tile, so the
|
||||
// potential number of event listeners is quite high.
|
||||
matrixClient.setMaxListeners(500);
|
||||
|
||||
if (guestAccess) {
|
||||
console.log("Guest: %s", guestAccess.isGuest());
|
||||
matrixClient.setGuest(guestAccess.isGuest());
|
||||
var peekedRoomId = guestAccess.getPeekedRoom();
|
||||
if (peekedRoomId) {
|
||||
console.log("Peeking in room %s", peekedRoomId);
|
||||
matrixClient.peekInRoom(peekedRoomId);
|
||||
}
|
||||
}
|
||||
interface MatrixClientCreds {
|
||||
homeserverUrl: string,
|
||||
identityServerUrl: string,
|
||||
userId: string,
|
||||
accessToken: string,
|
||||
guest: boolean,
|
||||
}
|
||||
|
||||
if (localStorage) {
|
||||
var hs_url = localStorage.getItem("mx_hs_url");
|
||||
var is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
||||
var access_token = localStorage.getItem("mx_access_token");
|
||||
var user_id = localStorage.getItem("mx_user_id");
|
||||
var guestAccess = new GuestAccess(localStorage);
|
||||
if (access_token && user_id && hs_url) {
|
||||
console.log("Restoring session for %s", user_id);
|
||||
createClientForPeg(hs_url, is_url, user_id, access_token, guestAccess);
|
||||
}
|
||||
else {
|
||||
console.log("Session not found.");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Wrapper object for handling the js-sdk Matrix Client object in the react-sdk
|
||||
* Handles the creation/initialisation of client objects.
|
||||
* This module provides a singleton instance of this class so the 'current'
|
||||
* Matrix Client object is available easily.
|
||||
*/
|
||||
class MatrixClientPeg {
|
||||
constructor() {
|
||||
this.matrixClient = null;
|
||||
|
||||
class MatrixClient {
|
||||
|
||||
constructor(guestAccess) {
|
||||
this.guestAccess = guestAccess;
|
||||
// These are the default options used when when the
|
||||
// client is started in 'start'. These can be altered
|
||||
// at any time up to after the 'will_start_client'
|
||||
// event is finished processing.
|
||||
this.opts = {
|
||||
initialSyncLimit: 20,
|
||||
};
|
||||
}
|
||||
|
||||
get() {
|
||||
return matrixClient;
|
||||
get(): MatrixClient {
|
||||
return this.matrixClient;
|
||||
}
|
||||
|
||||
unset() {
|
||||
matrixClient = null;
|
||||
this.matrixClient = null;
|
||||
}
|
||||
|
||||
// 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 name MatrixClient as MatrixClientPeg when we export it?
|
||||
//
|
||||
// -matthew
|
||||
|
||||
/**
|
||||
* Replace this MatrixClientPeg's client with a client instance that has
|
||||
* Home Server / Identity Server URLs but no credentials
|
||||
*/
|
||||
replaceUsingUrls(hs_url, is_url) {
|
||||
matrixClient = Matrix.createClient({
|
||||
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!");
|
||||
}
|
||||
this._replaceClient(hs_url, is_url);
|
||||
}
|
||||
|
||||
replaceUsingAccessToken(hs_url, is_url, user_id, access_token, isGuest) {
|
||||
/**
|
||||
* Replace this MatrixClientPeg's client with a client instance that has
|
||||
* Home Server / Identity Server URLs and active credentials
|
||||
*/
|
||||
replaceUsingCreds(creds: MatrixClientCreds) {
|
||||
this._replaceClient(
|
||||
creds.homeserverUrl,
|
||||
creds.identityServerUrl,
|
||||
creds.userId,
|
||||
creds.accessToken,
|
||||
creds.guest,
|
||||
);
|
||||
}
|
||||
|
||||
start() {
|
||||
const opts = utils.deepCopy(this.opts);
|
||||
// the react sdk doesn't work without this, so don't allow
|
||||
opts.pendingEventOrdering = "detached";
|
||||
this.get().startClient(opts);
|
||||
}
|
||||
|
||||
_replaceClient(hs_url, is_url, user_id, access_token, isGuest) {
|
||||
if (localStorage) {
|
||||
try {
|
||||
localStorage.clear();
|
||||
@ -133,15 +104,19 @@ class MatrixClient {
|
||||
console.warn("Error clearing local storage", e);
|
||||
}
|
||||
}
|
||||
this.guestAccess.markAsGuest(Boolean(isGuest));
|
||||
createClientForPeg(hs_url, is_url, user_id, access_token, this.guestAccess);
|
||||
this._createClient(hs_url, is_url, user_id, access_token, isGuest);
|
||||
|
||||
if (localStorage) {
|
||||
try {
|
||||
localStorage.setItem("mx_hs_url", hs_url);
|
||||
localStorage.setItem("mx_is_url", is_url);
|
||||
localStorage.setItem("mx_user_id", user_id);
|
||||
localStorage.setItem("mx_access_token", access_token);
|
||||
console.log("Session persisted for %s", user_id);
|
||||
|
||||
if (user_id !== undefined && access_token !== undefined) {
|
||||
localStorage.setItem("mx_user_id", user_id);
|
||||
localStorage.setItem("mx_access_token", access_token);
|
||||
localStorage.setItem("mx_is_guest", JSON.stringify(isGuest));
|
||||
console.log("Session persisted for %s", user_id);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Error using local storage: can't persist session!", e);
|
||||
}
|
||||
@ -149,9 +124,68 @@ class MatrixClient {
|
||||
console.warn("No local storage available: can't persist session!");
|
||||
}
|
||||
}
|
||||
|
||||
getCredentials(): MatrixClientCreds {
|
||||
return {
|
||||
homeserverUrl: this.matrixClient.baseUrl,
|
||||
identityServerUrl: this.matrixClient.idBaseUrl,
|
||||
userId: this.matrixClient.credentials.userId,
|
||||
accessToken: this.matrixClient.getAccessToken(),
|
||||
guest: this.matrixClient.isGuest(),
|
||||
};
|
||||
}
|
||||
|
||||
tryRestore() {
|
||||
if (localStorage) {
|
||||
const hs_url = localStorage.getItem("mx_hs_url");
|
||||
const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
||||
const access_token = localStorage.getItem("mx_access_token");
|
||||
const user_id = localStorage.getItem("mx_user_id");
|
||||
|
||||
let is_guest;
|
||||
if (localStorage.getItem("mx_is_guest") !== null) {
|
||||
is_guest = localStorage.getItem("mx_is_guest") === "true";
|
||||
} else {
|
||||
// legacy key name
|
||||
is_guest = localStorage.getItem("matrix-is-guest") === "true";
|
||||
}
|
||||
|
||||
if (access_token && user_id && hs_url) {
|
||||
console.log("Restoring session for %s", user_id);
|
||||
this._createClient(hs_url, is_url, user_id, access_token);
|
||||
this.matrixClient.setGuest(is_guest);
|
||||
} else {
|
||||
console.log("Session not found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_createClient(hs_url, is_url, user_id, access_token, isGuest) {
|
||||
var opts = {
|
||||
baseUrl: hs_url,
|
||||
idBaseUrl: is_url,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
timelineSupport: true,
|
||||
};
|
||||
|
||||
if (localStorage) {
|
||||
opts.sessionStore = new Matrix.WebStorageSessionStore(localStorage);
|
||||
opts.deviceId = deviceId();
|
||||
}
|
||||
|
||||
this.matrixClient = Matrix.createClient(opts);
|
||||
|
||||
// we're going to add eventlisteners for each matrix event tile, so the
|
||||
// potential number of event listeners is quite high.
|
||||
this.matrixClient.setMaxListeners(500);
|
||||
|
||||
this.matrixClient.setGuest(Boolean(isGuest));
|
||||
}
|
||||
}
|
||||
|
||||
if (!global.mxMatrixClient) {
|
||||
global.mxMatrixClient = new MatrixClient(new GuestAccess(localStorage));
|
||||
if (!global.mxMatrixClientPeg) {
|
||||
global.mxMatrixClientPeg = new MatrixClientPeg();
|
||||
global.mxMatrixClientPeg.tryRestore();
|
||||
}
|
||||
module.exports = global.mxMatrixClient;
|
||||
module.exports = global.mxMatrixClientPeg;
|
||||
|
@ -24,30 +24,5 @@ module.exports = {
|
||||
getDisplayAliasForRoom: function(room) {
|
||||
return room.getCanonicalAlias() || room.getAliases()[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a list of room objects, return the room which has the given alias,
|
||||
* else null.
|
||||
*/
|
||||
getRoomForAlias: function(rooms, room_alias) {
|
||||
var room;
|
||||
for (var i = 0; i < rooms.length; i++) {
|
||||
var aliasEvents = rooms[i].currentState.getStateEvents(
|
||||
"m.room.aliases"
|
||||
);
|
||||
for (var j = 0; j < aliasEvents.length; j++) {
|
||||
var aliases = aliasEvents[j].getContent().aliases || [];
|
||||
for (var k = 0; k < aliases.length; k++) {
|
||||
if (aliases[k] === room_alias) {
|
||||
room = rooms[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (room) { break; }
|
||||
}
|
||||
if (room) { break; }
|
||||
}
|
||||
return room || null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,11 +48,13 @@ class PasswordReset {
|
||||
*/
|
||||
resetPassword(emailAddress, 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;
|
||||
return res;
|
||||
}, 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})`;
|
||||
}
|
||||
throw err;
|
||||
|
170
src/RichText.js
170
src/RichText.js
@ -1,16 +1,22 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Editor,
|
||||
EditorState,
|
||||
Modifier,
|
||||
ContentState,
|
||||
ContentBlock,
|
||||
convertFromHTML,
|
||||
DefaultDraftBlockRenderMap,
|
||||
DefaultDraftInlineStyle,
|
||||
CompositeDecorator
|
||||
CompositeDecorator,
|
||||
SelectionState,
|
||||
Entity,
|
||||
} from 'draft-js';
|
||||
import * as sdk from './index';
|
||||
import * as emojione from 'emojione';
|
||||
|
||||
const BLOCK_RENDER_MAP = DefaultDraftBlockRenderMap.set('unstyled', {
|
||||
element: 'span'
|
||||
element: 'span',
|
||||
/*
|
||||
draft uses <div> by default which we don't really like, so we're using <span>
|
||||
this is probably not a good idea since <span> is not a block level element but
|
||||
@ -23,17 +29,18 @@ const STYLES = {
|
||||
CODE: 'code',
|
||||
ITALIC: 'em',
|
||||
STRIKETHROUGH: 's',
|
||||
UNDERLINE: 'u'
|
||||
UNDERLINE: 'u',
|
||||
};
|
||||
|
||||
const MARKDOWN_REGEX = {
|
||||
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
|
||||
ITALIC: /([\*_])([\w\s]+?)\1/g,
|
||||
BOLD: /([\*_])\1([\w\s]+?)\1\1/g
|
||||
BOLD: /([\*_])\1([\w\s]+?)\1\1/g,
|
||||
};
|
||||
|
||||
const USERNAME_REGEX = /@\S+:\S+/g;
|
||||
const ROOM_REGEX = /#\S+:\S+/g;
|
||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g');
|
||||
|
||||
export function contentStateToHTML(contentState: ContentState): string {
|
||||
return contentState.getBlockMap().map((block) => {
|
||||
@ -60,7 +67,7 @@ export function contentStateToHTML(contentState: ContentState): string {
|
||||
let result = `<${elem}>${content.join('')}</${elem}>`;
|
||||
|
||||
// dirty hack because we don't want block level tags by default, but breaks
|
||||
if(elem === 'span')
|
||||
if (elem === 'span')
|
||||
result += '<br />';
|
||||
return result;
|
||||
}).join('');
|
||||
@ -70,6 +77,48 @@ export function HTMLtoContentState(html: string): ContentState {
|
||||
return ContentState.createFromBlockArray(convertFromHTML(html));
|
||||
}
|
||||
|
||||
function unicodeToEmojiUri(str) {
|
||||
let replaceWith, unicode, alt;
|
||||
if ((!emojione.unicodeAlt) || (emojione.sprites)) {
|
||||
// if we are using the shortname as the alt tag then we need a reversed array to map unicode code point to shortnames
|
||||
let mappedUnicode = emojione.mapUnicodeToShort();
|
||||
}
|
||||
|
||||
str = str.replace(emojione.regUnicode, function(unicodeChar) {
|
||||
if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) {
|
||||
// if the unicodeChar doesnt exist just return the entire match
|
||||
return unicodeChar;
|
||||
} else {
|
||||
// get the unicode codepoint from the actual char
|
||||
unicode = emojione.jsEscapeMap[unicodeChar];
|
||||
return emojione.imagePathSVG+unicode+'.svg'+emojione.cacheBustParam;
|
||||
}
|
||||
});
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
// Workaround for https://github.com/facebook/draft-js/issues/414
|
||||
let emojiDecorator = {
|
||||
strategy: (contentBlock, callback) => {
|
||||
findWithRegex(EMOJI_REGEX, contentBlock, callback);
|
||||
},
|
||||
component: (props) => {
|
||||
let uri = unicodeToEmojiUri(props.children[0].props.text);
|
||||
let shortname = emojione.toShort(props.children[0].props.text);
|
||||
let style = {
|
||||
display: 'inline-block',
|
||||
width: '1em',
|
||||
maxHeight: '1em',
|
||||
background: `url(${uri})`,
|
||||
backgroundSize: 'contain',
|
||||
backgroundPosition: 'center center',
|
||||
overflow: 'hidden',
|
||||
};
|
||||
return (<span title={shortname} style={style}><span style={{opacity: 0}}>{props.children}</span></span>);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a composite decorator which has access to provided scope.
|
||||
*/
|
||||
@ -85,9 +134,10 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
|
||||
// unused until we make these decorators immutable (autocomplete needed)
|
||||
let name = member ? member.name : null;
|
||||
let avatar = member ? <MemberAvatar member={member} width={16} height={16}/> : null;
|
||||
return <span className="mx_UserPill">{avatar} {props.children}</span>;
|
||||
return <span className="mx_UserPill">{avatar}{props.children}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
let roomDecorator = {
|
||||
strategy: (contentBlock, callback) => {
|
||||
findWithRegex(ROOM_REGEX, contentBlock, callback);
|
||||
@ -97,7 +147,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
|
||||
}
|
||||
};
|
||||
|
||||
return [usernameDecorator, roomDecorator];
|
||||
return [usernameDecorator, roomDecorator, emojiDecorator];
|
||||
}
|
||||
|
||||
export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||
@ -123,6 +173,7 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||
</a>
|
||||
)
|
||||
});
|
||||
markdownDecorators.push(emojiDecorator);
|
||||
|
||||
return markdownDecorators;
|
||||
}
|
||||
@ -153,7 +204,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
|
||||
text = "";
|
||||
|
||||
|
||||
for(let currentKey = startKey;
|
||||
for (let currentKey = startKey;
|
||||
currentKey && currentKey !== endKey;
|
||||
currentKey = contentState.getKeyAfter(currentKey)) {
|
||||
let blockText = getText(currentKey);
|
||||
@ -168,3 +219,106 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
|
||||
|
||||
return Modifier.replaceText(contentState, rangeToReplace, modifyFn(text), inlineStyle, entityKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the plaintext offsets of the given SelectionState.
|
||||
* Note that this inherently means we make assumptions about what that means (no separator between ContentBlocks, etc)
|
||||
* Used by autocomplete to show completions when the current selection lies within, or at the edges of a command.
|
||||
*/
|
||||
export function selectionStateToTextOffsets(selectionState: SelectionState,
|
||||
contentBlocks: Array<ContentBlock>): {start: number, end: number} {
|
||||
let offset = 0, start = 0, end = 0;
|
||||
for (let block of contentBlocks) {
|
||||
if (selectionState.getStartKey() === block.getKey()) {
|
||||
start = offset + selectionState.getStartOffset();
|
||||
}
|
||||
if (selectionState.getEndKey() === block.getKey()) {
|
||||
end = offset + selectionState.getEndOffset();
|
||||
break;
|
||||
}
|
||||
offset += block.getLength();
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
};
|
||||
}
|
||||
|
||||
export function textOffsetsToSelectionState({start, end}: {start: number, end: number},
|
||||
contentBlocks: Array<ContentBlock>): SelectionState {
|
||||
let selectionState = SelectionState.createEmpty();
|
||||
|
||||
for (let block of contentBlocks) {
|
||||
let blockLength = block.getLength();
|
||||
|
||||
if (start !== -1 && start < blockLength) {
|
||||
selectionState = selectionState.merge({
|
||||
anchorKey: block.getKey(),
|
||||
anchorOffset: start,
|
||||
});
|
||||
start = -1;
|
||||
} else {
|
||||
start -= blockLength;
|
||||
}
|
||||
|
||||
if (end !== -1 && end <= blockLength) {
|
||||
selectionState = selectionState.merge({
|
||||
focusKey: block.getKey(),
|
||||
focusOffset: end,
|
||||
});
|
||||
end = -1;
|
||||
} else {
|
||||
end -= blockLength;
|
||||
}
|
||||
}
|
||||
|
||||
return selectionState;
|
||||
}
|
||||
|
||||
// modified version of https://github.com/draft-js-plugins/draft-js-plugins/blob/master/draft-js-emoji-plugin/src/modifiers/attachImmutableEntitiesToEmojis.js
|
||||
export function attachImmutableEntitiesToEmoji(editorState: EditorState): EditorState {
|
||||
const contentState = editorState.getCurrentContent();
|
||||
const blocks = contentState.getBlockMap();
|
||||
let newContentState = contentState;
|
||||
|
||||
blocks.forEach((block) => {
|
||||
const plainText = block.getText();
|
||||
|
||||
const addEntityToEmoji = (start, end) => {
|
||||
const existingEntityKey = block.getEntityAt(start);
|
||||
if (existingEntityKey) {
|
||||
// avoid manipulation in case the emoji already has an entity
|
||||
const entity = Entity.get(existingEntityKey);
|
||||
if (entity && entity.get('type') === 'emoji') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const selection = SelectionState.createEmpty(block.getKey())
|
||||
.set('anchorOffset', start)
|
||||
.set('focusOffset', end);
|
||||
const emojiText = plainText.substring(start, end);
|
||||
const entityKey = Entity.create('emoji', 'IMMUTABLE', { emojiUnicode: emojiText });
|
||||
newContentState = Modifier.replaceText(
|
||||
newContentState,
|
||||
selection,
|
||||
emojiText,
|
||||
null,
|
||||
entityKey,
|
||||
);
|
||||
};
|
||||
|
||||
findWithRegex(EMOJI_REGEX, block, addEntityToEmoji);
|
||||
});
|
||||
|
||||
if (!newContentState.equals(contentState)) {
|
||||
return EditorState.push(
|
||||
editorState,
|
||||
newContentState,
|
||||
'convert-to-immutable-emojis',
|
||||
);
|
||||
}
|
||||
|
||||
return editorState;
|
||||
}
|
||||
|
@ -152,7 +152,10 @@ class Register extends Signup {
|
||||
console.log("Active flow => %s", JSON.stringify(flow));
|
||||
var flowStage = self.firstUncompletedStage(flow);
|
||||
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);
|
||||
|
||||
var self = this;
|
||||
return this.client.requestEmailToken(
|
||||
return this.client.requestRegisterEmailToken(
|
||||
this.signupInstance.email,
|
||||
this.clientSecret,
|
||||
1, // TODO: Multiple send attempts?
|
||||
@ -186,8 +186,8 @@ class EmailIdentityStage extends Stage {
|
||||
var e = {
|
||||
isFatal: true
|
||||
};
|
||||
if (error.errcode == 'THREEPID_IN_USE') {
|
||||
e.message = "Email in use";
|
||||
if (error.errcode == 'M_THREEPID_IN_USE') {
|
||||
e.message = "This email address is already registered";
|
||||
} else {
|
||||
e.message = 'Unable to contact the given identity server';
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||
var MatrixTools = require("./MatrixTools");
|
||||
var dis = require("./dispatcher");
|
||||
var encryption = require("./encryption");
|
||||
var Tinter = require("./Tinter");
|
||||
|
||||
|
||||
@ -82,32 +81,13 @@ var commands = {
|
||||
return success(
|
||||
MatrixClientPeg.get().setRoomAccountData(
|
||||
room_id, "org.matrix.room.color_scheme", colorScheme
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
}),
|
||||
|
||||
encrypt: new Command("encrypt", "<on|off>", function(room_id, args) {
|
||||
if (args == "on") {
|
||||
var client = MatrixClientPeg.get();
|
||||
var members = client.getRoom(room_id).currentState.members;
|
||||
var user_ids = Object.keys(members);
|
||||
return success(
|
||||
encryption.enableEncryption(client, room_id, user_ids)
|
||||
);
|
||||
}
|
||||
if (args == "off") {
|
||||
var client = MatrixClientPeg.get();
|
||||
return success(
|
||||
encryption.disableEncryption(client, room_id)
|
||||
);
|
||||
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
}),
|
||||
|
||||
// Change the room topic
|
||||
topic: new Command("topic", "<topic>", function(room_id, args) {
|
||||
if (args) {
|
||||
@ -132,46 +112,25 @@ var commands = {
|
||||
}),
|
||||
|
||||
// Join a room
|
||||
join: new Command("join", "<room_alias>", function(room_id, args) {
|
||||
join: new Command("join", "#alias:domain", function(room_id, args) {
|
||||
if (args) {
|
||||
var matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
var room_alias = matches[1];
|
||||
if (room_alias[0] !== '#') {
|
||||
return reject("Usage: /join #alias:domain");
|
||||
return reject(this.getUsage());
|
||||
}
|
||||
if (!room_alias.match(/:/)) {
|
||||
room_alias += ':' + MatrixClientPeg.get().getDomain();
|
||||
}
|
||||
|
||||
// Try to find a room with this alias
|
||||
// XXX: do we need to do this? Doesn't the JS SDK suppress duplicate attempts to join the same room?
|
||||
var foundRoom = MatrixTools.getRoomForAlias(
|
||||
MatrixClientPeg.get().getRooms(),
|
||||
room_alias
|
||||
);
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_alias: room_alias,
|
||||
auto_join: true,
|
||||
});
|
||||
|
||||
if (foundRoom) { // we've already joined this room, view it if it's not archived.
|
||||
var me = foundRoom.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (me && me.membership !== "leave") {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: foundRoom.roomId
|
||||
});
|
||||
return success();
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise attempt to join this alias.
|
||||
return success(
|
||||
MatrixClientPeg.get().joinRoom(room_alias).then(
|
||||
function(room) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: room.roomId
|
||||
});
|
||||
})
|
||||
);
|
||||
return success();
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
|
@ -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
|
||||
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 KEY_TAB = 9;
|
||||
@ -45,23 +48,39 @@ class TabComplete {
|
||||
this.isFirstWord = false; // true if you tab-complete on the first word
|
||||
this.enterTabCompleteTimerId = null;
|
||||
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) {
|
||||
this.list = completeList;
|
||||
onEntryClick(entry) {
|
||||
if (this.opts.onClickCompletes) {
|
||||
// assign onClick listeners for each entry to complete the text
|
||||
this.list.forEach((l) => {
|
||||
l.onClick = () => {
|
||||
this.completeTo(l);
|
||||
}
|
||||
});
|
||||
this.completeTo(entry);
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
*/
|
||||
@ -307,6 +326,54 @@ class TabComplete {
|
||||
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;
|
||||
|
@ -69,6 +69,7 @@ class Entry {
|
||||
class CommandEntry extends Entry {
|
||||
constructor(cmd, cmdWithArgs) {
|
||||
super(cmdWithArgs);
|
||||
this.kind = 'command';
|
||||
this.cmd = cmd;
|
||||
}
|
||||
|
||||
@ -95,6 +96,7 @@ class MemberEntry extends Entry {
|
||||
constructor(member) {
|
||||
super(member.name || member.userId);
|
||||
this.member = member;
|
||||
this.kind = 'member';
|
||||
}
|
||||
|
||||
getImageJsx() {
|
||||
@ -114,24 +116,7 @@ class MemberEntry extends Entry {
|
||||
}
|
||||
|
||||
MemberEntry.fromMemberList = function(members) {
|
||||
return members.sort(function(a, b) {
|
||||
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 members.map(function(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 {
|
||||
return localStorage.getItem(`mx_labs_feature_${feature}`) === 'true';
|
||||
},
|
||||
|
@ -18,6 +18,19 @@ module.exports = React.createClass({
|
||||
|
||||
// optional transition information for changing existing children
|
||||
transition: React.PropTypes.object,
|
||||
|
||||
// a list of state objects to apply to each child node in turn
|
||||
startStyles: React.PropTypes.array,
|
||||
|
||||
// a list of transition options from the corresponding startStyle
|
||||
enterTransitionOpts: React.PropTypes.array,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
startStyles: [],
|
||||
enterTransitionOpts: [],
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
@ -56,56 +69,71 @@ module.exports = React.createClass({
|
||||
}
|
||||
self.children[c.key] = old;
|
||||
} else {
|
||||
// new element. If it has a startStyle, use that as the style and go through
|
||||
// new element. If we have a startStyle, use that as the style and go through
|
||||
// the enter animations
|
||||
var newProps = {
|
||||
ref: self.collectNode.bind(self, c.key)
|
||||
};
|
||||
if (c.props.startStyle && Object.keys(c.props.startStyle).length) {
|
||||
var startStyle = c.props.startStyle;
|
||||
if (Array.isArray(startStyle)) {
|
||||
startStyle = startStyle[0];
|
||||
}
|
||||
newProps._restingStyle = c.props.style;
|
||||
var newProps = {};
|
||||
var restingStyle = c.props.style;
|
||||
|
||||
var startStyles = self.props.startStyles;
|
||||
if (startStyles.length > 0) {
|
||||
var startStyle = startStyles[0]
|
||||
newProps.style = startStyle;
|
||||
//console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
|
||||
// apply the enter animations once it's mounted
|
||||
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
|
||||
}
|
||||
|
||||
newProps.ref = (n => self._collectNode(
|
||||
c.key, n, restingStyle
|
||||
));
|
||||
|
||||
self.children[c.key] = React.cloneElement(c, newProps);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
collectNode: function(k, node) {
|
||||
/**
|
||||
* called when a child element is mounted/unmounted
|
||||
*
|
||||
* @param {string} k key of the child
|
||||
* @param {null|Object} node On mount: React node. On unmount: null
|
||||
* @param {Object} restingStyle final style
|
||||
*/
|
||||
_collectNode: function(k, node, restingStyle) {
|
||||
if (
|
||||
node &&
|
||||
this.nodes[k] === undefined &&
|
||||
node.props.startStyle &&
|
||||
Object.keys(node.props.startStyle).length
|
||||
this.props.startStyles.length > 0
|
||||
) {
|
||||
var startStyles = this.props.startStyles;
|
||||
var transitionOpts = this.props.enterTransitionOpts;
|
||||
var domNode = ReactDom.findDOMNode(node);
|
||||
var startStyles = node.props.startStyle;
|
||||
var transitionOpts = node.props.enterTransitionOpts;
|
||||
if (!Array.isArray(startStyles)) {
|
||||
startStyles = [ startStyles ];
|
||||
transitionOpts = [ transitionOpts ];
|
||||
}
|
||||
// start from startStyle 1: 0 is the one we gave it
|
||||
// to start with, so now we animate 1 etc.
|
||||
for (var i = 1; i < startStyles.length; ++i) {
|
||||
Velocity(domNode, startStyles[i], transitionOpts[i-1]);
|
||||
//console.log("start: "+JSON.stringify(startStyles[i]));
|
||||
/*
|
||||
console.log("start:",
|
||||
JSON.stringify(transitionOpts[i-1]),
|
||||
"->",
|
||||
JSON.stringify(startStyles[i]),
|
||||
);
|
||||
*/
|
||||
}
|
||||
|
||||
// and then we animate to the resting state
|
||||
Velocity(domNode, node.props._restingStyle,
|
||||
Velocity(domNode, restingStyle,
|
||||
transitionOpts[i-1])
|
||||
.then(() => {
|
||||
// once we've reached the resting state, hide the element if
|
||||
// appropriate
|
||||
domNode.style.visibility = node.props._restingStyle.visibility;
|
||||
domNode.style.visibility = restingStyle.visibility;
|
||||
});
|
||||
|
||||
//console.log("enter: "+JSON.stringify(node.props._restingStyle));
|
||||
/*
|
||||
console.log("enter:",
|
||||
JSON.stringify(transitionOpts[i-1]),
|
||||
"->",
|
||||
JSON.stringify(restingStyle));
|
||||
*/
|
||||
} else if (node === null) {
|
||||
// Velocity stores data on elements using the jQuery .data()
|
||||
// method, and assumes you'll be using jQuery's .remove() to
|
||||
|
54
src/autocomplete/AutocompleteProvider.js
Normal file
54
src/autocomplete/AutocompleteProvider.js
Normal file
@ -0,0 +1,54 @@
|
||||
import Q from 'q';
|
||||
|
||||
export default class AutocompleteProvider {
|
||||
constructor(commandRegex?: RegExp, fuseOpts?: any) {
|
||||
if(commandRegex) {
|
||||
if(!commandRegex.global) {
|
||||
throw new Error('commandRegex must have global flag set');
|
||||
}
|
||||
this.commandRegex = commandRegex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Of the matched commands in the query, returns the first that contains or is contained by the selection, or null.
|
||||
*/
|
||||
getCurrentCommand(query: string, selection: {start: number, end: number}): ?Array<string> {
|
||||
if (this.commandRegex == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.commandRegex.lastIndex = 0;
|
||||
|
||||
let match;
|
||||
while ((match = this.commandRegex.exec(query)) != null) {
|
||||
let matchStart = match.index,
|
||||
matchEnd = matchStart + match[0].length;
|
||||
|
||||
if (selection.start <= matchEnd && selection.end >= matchStart) {
|
||||
return {
|
||||
command: match,
|
||||
range: {
|
||||
start: matchStart,
|
||||
end: matchEnd,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
command: null,
|
||||
range: {
|
||||
start: -1,
|
||||
end: -1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
return Q.when([]);
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return 'Default Provider';
|
||||
}
|
||||
}
|
22
src/autocomplete/Autocompleter.js
Normal file
22
src/autocomplete/Autocompleter.js
Normal file
@ -0,0 +1,22 @@
|
||||
import CommandProvider from './CommandProvider';
|
||||
import DuckDuckGoProvider from './DuckDuckGoProvider';
|
||||
import RoomProvider from './RoomProvider';
|
||||
import UserProvider from './UserProvider';
|
||||
import EmojiProvider from './EmojiProvider';
|
||||
|
||||
const PROVIDERS = [
|
||||
UserProvider,
|
||||
CommandProvider,
|
||||
DuckDuckGoProvider,
|
||||
RoomProvider,
|
||||
EmojiProvider,
|
||||
].map(completer => completer.getInstance());
|
||||
|
||||
export function getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
return PROVIDERS.map(provider => {
|
||||
return {
|
||||
completions: provider.getCompletions(query, selection),
|
||||
provider,
|
||||
};
|
||||
});
|
||||
}
|
86
src/autocomplete/CommandProvider.js
Normal file
86
src/autocomplete/CommandProvider.js
Normal file
@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import Q from 'q';
|
||||
import Fuse from 'fuse.js';
|
||||
import {TextualCompletion} from './Components';
|
||||
|
||||
const COMMANDS = [
|
||||
{
|
||||
command: '/me',
|
||||
args: '<message>',
|
||||
description: 'Displays action',
|
||||
},
|
||||
{
|
||||
command: '/ban',
|
||||
args: '<user-id> [reason]',
|
||||
description: 'Bans user with given id',
|
||||
},
|
||||
{
|
||||
command: '/deop',
|
||||
args: '<user-id>',
|
||||
description: 'Deops user with given id',
|
||||
},
|
||||
{
|
||||
command: '/invite',
|
||||
args: '<user-id>',
|
||||
description: 'Invites user with given id to current room'
|
||||
},
|
||||
{
|
||||
command: '/join',
|
||||
args: '<room-alias>',
|
||||
description: 'Joins room with given alias',
|
||||
},
|
||||
{
|
||||
command: '/kick',
|
||||
args: '<user-id> [reason]',
|
||||
description: 'Kicks user with given id',
|
||||
},
|
||||
{
|
||||
command: '/nick',
|
||||
args: '<display-name>',
|
||||
description: 'Changes your display nickname',
|
||||
},
|
||||
];
|
||||
|
||||
let COMMAND_RE = /(^\/\w*)/g;
|
||||
|
||||
let instance = null;
|
||||
|
||||
export default class CommandProvider extends AutocompleteProvider {
|
||||
constructor() {
|
||||
super(COMMAND_RE);
|
||||
this.fuse = new Fuse(COMMANDS, {
|
||||
keys: ['command', 'args', 'description'],
|
||||
});
|
||||
}
|
||||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
let completions = [];
|
||||
let {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (command) {
|
||||
completions = this.fuse.search(command[0]).map(result => {
|
||||
return {
|
||||
completion: result.command + ' ',
|
||||
component: (<TextualCompletion
|
||||
title={result.command}
|
||||
subtitle={result.args}
|
||||
description={result.description}
|
||||
/>),
|
||||
range,
|
||||
};
|
||||
});
|
||||
}
|
||||
return Q.when(completions);
|
||||
}
|
||||
|
||||
getName() {
|
||||
return 'Commands';
|
||||
}
|
||||
|
||||
static getInstance(): CommandProvider {
|
||||
if (instance == null)
|
||||
instance = new CommandProvider();
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
19
src/autocomplete/Components.js
Normal file
19
src/autocomplete/Components.js
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
export function TextualCompletion({
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
}: {
|
||||
title: ?string,
|
||||
subtitle: ?string,
|
||||
description: ?string
|
||||
}) {
|
||||
return (
|
||||
<div style={{width: '100%'}}>
|
||||
<span>{title}</span>
|
||||
<em>{subtitle}</em>
|
||||
<span style={{color: 'gray', float: 'right'}}>{description}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
90
src/autocomplete/DuckDuckGoProvider.js
Normal file
90
src/autocomplete/DuckDuckGoProvider.js
Normal file
@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import Q from 'q';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
import {TextualCompletion} from './Components';
|
||||
|
||||
const DDG_REGEX = /\/ddg\s+(.+)$/g;
|
||||
const REFERRER = 'vector';
|
||||
|
||||
let instance = null;
|
||||
|
||||
export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||
constructor() {
|
||||
super(DDG_REGEX);
|
||||
}
|
||||
|
||||
static getQueryUri(query: String) {
|
||||
return `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}`
|
||||
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;
|
||||
}
|
||||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
let {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (!query || !command) {
|
||||
return Q.when([]);
|
||||
}
|
||||
|
||||
return fetch(DuckDuckGoProvider.getQueryUri(command[1]), {
|
||||
method: 'GET',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
let results = json.Results.map(result => {
|
||||
return {
|
||||
completion: result.Text,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={result.Text}
|
||||
description={result.Result} />
|
||||
),
|
||||
range,
|
||||
};
|
||||
});
|
||||
if (json.Answer) {
|
||||
results.unshift({
|
||||
completion: json.Answer,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={json.Answer}
|
||||
description={json.AnswerType} />
|
||||
),
|
||||
range,
|
||||
});
|
||||
}
|
||||
if (json.RelatedTopics && json.RelatedTopics.length > 0) {
|
||||
results.unshift({
|
||||
completion: json.RelatedTopics[0].Text,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={json.RelatedTopics[0].Text} />
|
||||
),
|
||||
range,
|
||||
});
|
||||
}
|
||||
if (json.AbstractText) {
|
||||
results.unshift({
|
||||
completion: json.AbstractText,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={json.AbstractText} />
|
||||
),
|
||||
range,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
});
|
||||
}
|
||||
|
||||
getName() {
|
||||
return 'Results from DuckDuckGo';
|
||||
}
|
||||
|
||||
static getInstance(): DuckDuckGoProvider {
|
||||
if (instance == null) {
|
||||
instance = new DuckDuckGoProvider();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
48
src/autocomplete/EmojiProvider.js
Normal file
48
src/autocomplete/EmojiProvider.js
Normal file
@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import Q from 'q';
|
||||
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
const EMOJI_REGEX = /:\w*:?/g;
|
||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
||||
|
||||
let instance = null;
|
||||
|
||||
export default class EmojiProvider extends AutocompleteProvider {
|
||||
constructor() {
|
||||
super(EMOJI_REGEX);
|
||||
this.fuse = new Fuse(EMOJI_SHORTNAMES);
|
||||
}
|
||||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
let completions = [];
|
||||
let {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (command) {
|
||||
completions = this.fuse.search(command[0]).map(result => {
|
||||
let shortname = EMOJI_SHORTNAMES[result];
|
||||
let imageHTML = shortnameToImage(shortname);
|
||||
return {
|
||||
completion: shortnameToUnicode(shortname),
|
||||
component: (
|
||||
<div className="mx_Autocomplete_Completion">
|
||||
<span dangerouslySetInnerHTML={{__html: imageHTML}}></span> {shortname}
|
||||
</div>
|
||||
),
|
||||
range,
|
||||
};
|
||||
}).slice(0, 4);
|
||||
}
|
||||
return Q.when(completions);
|
||||
}
|
||||
|
||||
getName() {
|
||||
return 'Emoji';
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (instance == null)
|
||||
instance = new EmojiProvider();
|
||||
return instance;
|
||||
}
|
||||
}
|
62
src/autocomplete/RoomProvider.js
Normal file
62
src/autocomplete/RoomProvider.js
Normal file
@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import Q from 'q';
|
||||
import MatrixClientPeg from '../MatrixClientPeg';
|
||||
import Fuse from 'fuse.js';
|
||||
import {TextualCompletion} from './Components';
|
||||
import {getDisplayAliasForRoom} from '../MatrixTools';
|
||||
|
||||
const ROOM_REGEX = /(?=#)([^\s]*)/g;
|
||||
|
||||
let instance = null;
|
||||
|
||||
export default class RoomProvider extends AutocompleteProvider {
|
||||
constructor() {
|
||||
super(ROOM_REGEX, {
|
||||
keys: ['displayName', 'userId'],
|
||||
});
|
||||
this.fuse = new Fuse([], {
|
||||
keys: ['name', 'roomId', 'aliases'],
|
||||
});
|
||||
}
|
||||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
let client = MatrixClientPeg.get();
|
||||
let completions = [];
|
||||
const {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (command) {
|
||||
// the only reason we need to do this is because Fuse only matches on properties
|
||||
this.fuse.set(client.getRooms().filter(room => !!room).map(room => {
|
||||
return {
|
||||
room: room,
|
||||
name: room.name,
|
||||
roomId: room.roomId,
|
||||
aliases: room.getAliases(),
|
||||
};
|
||||
}));
|
||||
completions = this.fuse.search(command[0]).map(room => {
|
||||
let displayAlias = getDisplayAliasForRoom(room.room) || room.roomId;
|
||||
return {
|
||||
completion: displayAlias,
|
||||
component: (
|
||||
<TextualCompletion title={room.name} description={displayAlias} />
|
||||
),
|
||||
range,
|
||||
};
|
||||
}).slice(0, 4);
|
||||
}
|
||||
return Q.when(completions);
|
||||
}
|
||||
|
||||
getName() {
|
||||
return 'Rooms';
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new RoomProvider();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
57
src/autocomplete/UserProvider.js
Normal file
57
src/autocomplete/UserProvider.js
Normal file
@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import Q from 'q';
|
||||
import Fuse from 'fuse.js';
|
||||
import {TextualCompletion} from './Components';
|
||||
|
||||
const USER_REGEX = /@[^\s]*/g;
|
||||
|
||||
let instance = null;
|
||||
|
||||
export default class UserProvider extends AutocompleteProvider {
|
||||
constructor() {
|
||||
super(USER_REGEX, {
|
||||
keys: ['name', 'userId'],
|
||||
});
|
||||
this.users = [];
|
||||
this.fuse = new Fuse([], {
|
||||
keys: ['name', 'userId'],
|
||||
});
|
||||
}
|
||||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
let completions = [];
|
||||
let {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (command) {
|
||||
this.fuse.set(this.users);
|
||||
completions = this.fuse.search(command[0]).map(user => {
|
||||
const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
|
||||
return {
|
||||
completion: user.userId,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={displayName}
|
||||
description={user.userId} />
|
||||
),
|
||||
range
|
||||
};
|
||||
}).slice(0, 4);
|
||||
}
|
||||
return Q.when(completions);
|
||||
}
|
||||
|
||||
getName() {
|
||||
return 'Users';
|
||||
}
|
||||
|
||||
setUserList(users) {
|
||||
this.users = users;
|
||||
}
|
||||
|
||||
static getInstance(): UserProvider {
|
||||
if (instance == null) {
|
||||
instance = new UserProvider();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
module.exports.components = {};
|
||||
module.exports.components['structures.ContextualMenu'] = require('./components/structures/ContextualMenu');
|
||||
module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom');
|
||||
module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat');
|
||||
module.exports.components['structures.MessagePanel'] = require('./components/structures/MessagePanel');
|
||||
@ -51,6 +52,7 @@ module.exports.components['views.dialogs.QuestionDialog'] = require('./component
|
||||
module.exports.components['views.dialogs.SetDisplayNameDialog'] = require('./components/views/dialogs/SetDisplayNameDialog');
|
||||
module.exports.components['views.dialogs.TextInputDialog'] = require('./components/views/dialogs/TextInputDialog');
|
||||
module.exports.components['views.elements.EditableText'] = require('./components/views/elements/EditableText');
|
||||
module.exports.components['views.elements.EditableTextContainer'] = require('./components/views/elements/EditableTextContainer');
|
||||
module.exports.components['views.elements.PowerSelector'] = require('./components/views/elements/PowerSelector');
|
||||
module.exports.components['views.elements.ProgressBar'] = require('./components/views/elements/ProgressBar');
|
||||
module.exports.components['views.elements.TintableSvg'] = require('./components/views/elements/TintableSvg');
|
||||
@ -74,6 +76,8 @@ module.exports.components['views.messages.TextualEvent'] = require('./components
|
||||
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.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.EntityTile'] = require('./components/views/rooms/EntityTile');
|
||||
module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile');
|
||||
@ -104,6 +108,8 @@ module.exports.components['views.rooms.UserTile'] = require('./components/views/
|
||||
module.exports.components['views.settings.ChangeAvatar'] = require('./components/views/settings/ChangeAvatar');
|
||||
module.exports.components['views.settings.ChangeDisplayName'] = require('./components/views/settings/ChangeDisplayName');
|
||||
module.exports.components['views.settings.ChangePassword'] = require('./components/views/settings/ChangePassword');
|
||||
module.exports.components['views.settings.DevicesPanel'] = require('./components/views/settings/DevicesPanel');
|
||||
module.exports.components['views.settings.DevicesPanelEntry'] = require('./components/views/settings/DevicesPanelEntry');
|
||||
module.exports.components['views.settings.EnableNotificationsButton'] = require('./components/views/settings/EnableNotificationsButton');
|
||||
module.exports.components['views.voip.CallView'] = require('./components/views/voip/CallView');
|
||||
module.exports.components['views.voip.IncomingCallBox'] = require('./components/views/voip/IncomingCallBox');
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
|
||||
'use strict';
|
||||
|
||||
var classNames = require('classnames');
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
|
||||
@ -27,6 +28,12 @@ var ReactDOM = require('react-dom');
|
||||
module.exports = {
|
||||
ContextualMenuContainerId: "mx_ContextualMenu_Container",
|
||||
|
||||
propTypes: {
|
||||
menuWidth: React.PropTypes.number,
|
||||
menuHeight: React.PropTypes.number,
|
||||
chevronOffset: React.PropTypes.number,
|
||||
},
|
||||
|
||||
getOrCreateContainer: function() {
|
||||
var container = document.getElementById(this.ContextualMenuContainerId);
|
||||
|
||||
@ -45,29 +52,50 @@ module.exports = {
|
||||
var closeMenu = function() {
|
||||
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
|
||||
|
||||
if (props && props.onFinished) props.onFinished.apply(null, arguments);
|
||||
if (props && props.onFinished) {
|
||||
props.onFinished.apply(null, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
var position = {
|
||||
top: props.top - 20,
|
||||
top: props.top,
|
||||
};
|
||||
|
||||
var chevronOffset = {
|
||||
top: props.chevronOffset,
|
||||
}
|
||||
|
||||
var chevron = null;
|
||||
if (props.left) {
|
||||
chevron = <img className="mx_ContextualMenu_chevron_left" src="img/chevron-left.png" width="9" height="16" />
|
||||
position.left = props.left + 8;
|
||||
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>
|
||||
position.left = props.left;
|
||||
} else {
|
||||
chevron = <img className="mx_ContextualMenu_chevron_right" src="img/chevron-right.png" width="9" height="16" />
|
||||
position.right = props.right + 8;
|
||||
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>
|
||||
position.right = props.right;
|
||||
}
|
||||
|
||||
var className = 'mx_ContextualMenu_wrapper';
|
||||
|
||||
var menuClasses = classNames({
|
||||
'mx_ContextualMenu': true,
|
||||
'mx_ContextualMenu_left': props.left,
|
||||
'mx_ContextualMenu_right': !props.left,
|
||||
});
|
||||
|
||||
var menuSize = {};
|
||||
if (props.menuWidth) {
|
||||
menuSize.width = props.menuWidth;
|
||||
}
|
||||
|
||||
if (props.menuHeight) {
|
||||
menuSize.height = props.menuHeight;
|
||||
}
|
||||
|
||||
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
|
||||
// property set here so you can't close the menu from a button click!
|
||||
var menu = (
|
||||
<div className={className}>
|
||||
<div className="mx_ContextualMenu" style={position}>
|
||||
<div className={className} style={position}>
|
||||
<div className={menuClasses} style={menuSize}>
|
||||
{chevron}
|
||||
<Element {...props} onFinished={closeMenu}/>
|
||||
</div>
|
@ -24,7 +24,6 @@ var PresetValues = {
|
||||
Custom: "custom",
|
||||
};
|
||||
var q = require('q');
|
||||
var encryption = require("../../encryption");
|
||||
var sdk = require('../../index');
|
||||
|
||||
module.exports = React.createClass({
|
||||
@ -108,17 +107,8 @@ module.exports = React.createClass({
|
||||
|
||||
var deferred = cli.createRoom(options);
|
||||
|
||||
var response;
|
||||
|
||||
if (this.state.encrypt) {
|
||||
deferred = deferred.then(function(res) {
|
||||
response = res;
|
||||
return encryption.enableEncryption(
|
||||
cli, response.room_id, options.invite
|
||||
);
|
||||
}).then(function() {
|
||||
return q(response) }
|
||||
);
|
||||
// TODO
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -21,7 +21,7 @@ var Favico = require('favico.js');
|
||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
var SdkConfig = require("../../SdkConfig");
|
||||
var Notifier = require("../../Notifier");
|
||||
var ContextualMenu = require("../../ContextualMenu");
|
||||
var ContextualMenu = require("./ContextualMenu");
|
||||
var RoomListSorter = require("../../RoomListSorter");
|
||||
var UserActivity = require("../../UserActivity");
|
||||
var Presence = require("../../Presence");
|
||||
@ -37,6 +37,7 @@ var sdk = require('../../index');
|
||||
var MatrixTools = require('../../MatrixTools');
|
||||
var linkifyMatrix = require("../../linkify-matrix");
|
||||
var KeyCode = require('../../KeyCode');
|
||||
var Lifecycle = require('../../Lifecycle');
|
||||
|
||||
var createRoom = require("../../createRoom");
|
||||
|
||||
@ -109,10 +110,14 @@ module.exports = React.createClass({
|
||||
return window.localStorage.getItem("mx_hs_url");
|
||||
}
|
||||
else {
|
||||
return this.props.config.default_hs_url || "https://matrix.org";
|
||||
return this.getDefaultHsUrl();
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultHsUrl() {
|
||||
return this.props.config.default_hs_url || "https://matrix.org";
|
||||
},
|
||||
|
||||
getFallbackHsUrl: function() {
|
||||
return this.props.config.fallback_hs_url;
|
||||
},
|
||||
@ -127,16 +132,31 @@ module.exports = React.createClass({
|
||||
return window.localStorage.getItem("mx_is_url");
|
||||
}
|
||||
else {
|
||||
return this.props.config.default_is_url || "https://vector.im"
|
||||
return this.getDefaultIsUrl();
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultIsUrl() {
|
||||
return this.props.config.default_is_url || "https://vector.im";
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
SdkConfig.put(this.props.config);
|
||||
this.favicon = new Favico({animation: 'none'});
|
||||
|
||||
// Stashed guest credentials if the user logs out
|
||||
// whilst logged in as a guest user (so they can change
|
||||
// their mind & log back in)
|
||||
this.guestCreds = null;
|
||||
|
||||
if (this.props.config.sync_timeline_limit) {
|
||||
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
let clientStarted = false;
|
||||
|
||||
this._autoRegisterAsGuest = false;
|
||||
if (this.props.enableGuest) {
|
||||
if (!this.getCurrentHsUrl()) {
|
||||
@ -150,13 +170,14 @@ module.exports = React.createClass({
|
||||
this.props.startingQueryParams.guest_access_token)
|
||||
{
|
||||
this._autoRegisterAsGuest = false;
|
||||
this.onLoggedIn({
|
||||
Lifecycle.setLoggedIn({
|
||||
userId: this.props.startingQueryParams.guest_user_id,
|
||||
accessToken: this.props.startingQueryParams.guest_access_token,
|
||||
homeserverUrl: this.props.config.default_hs_url,
|
||||
identityServerUrl: this.props.config.default_is_url,
|
||||
homeserverUrl: this.getDefaultHsUrl(),
|
||||
identityServerUrl: this.getDefaultIsUrl(),
|
||||
guest: true
|
||||
});
|
||||
clientStarted = true;
|
||||
}
|
||||
else {
|
||||
this._autoRegisterAsGuest = true;
|
||||
@ -168,7 +189,9 @@ module.exports = React.createClass({
|
||||
// Don't auto-register as a guest. This applies if you refresh the page on a
|
||||
// logged in client THEN hit the Sign Out button.
|
||||
this._autoRegisterAsGuest = false;
|
||||
this.startMatrixClient();
|
||||
if (!clientStarted) {
|
||||
Lifecycle.startMatrixClient();
|
||||
}
|
||||
}
|
||||
this.focusComposer = false;
|
||||
// scrollStateMap is a map from room id to the scroll state returned by
|
||||
@ -223,7 +246,7 @@ module.exports = React.createClass({
|
||||
MatrixClientPeg.get().registerGuest().done(function(creds) {
|
||||
console.log("Registered as guest: %s", creds.user_id);
|
||||
self._setAutoRegisterAsGuest(false);
|
||||
self.onLoggedIn({
|
||||
Lifecycle.setLoggedIn({
|
||||
userId: creds.user_id,
|
||||
accessToken: creds.access_token,
|
||||
homeserverUrl: hsUrl,
|
||||
@ -254,34 +277,10 @@ module.exports = React.createClass({
|
||||
var self = this;
|
||||
switch (payload.action) {
|
||||
case 'logout':
|
||||
var guestCreds;
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
guestCreds = { // stash our guest creds so we can backout if needed
|
||||
userId: MatrixClientPeg.get().credentials.userId,
|
||||
accessToken: MatrixClientPeg.get().getAccessToken(),
|
||||
homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(),
|
||||
identityServerUrl: MatrixClientPeg.get().getIdentityServerUrl(),
|
||||
guest: true
|
||||
}
|
||||
this.guestCreds = MatrixClientPeg.getCredentials();
|
||||
}
|
||||
|
||||
if (window.localStorage) {
|
||||
var hsUrl = this.getCurrentHsUrl();
|
||||
var isUrl = this.getCurrentIsUrl();
|
||||
window.localStorage.clear();
|
||||
// preserve our HS & IS URLs for convenience
|
||||
// N.B. we cache them in hsUrl/isUrl and can't really inline them
|
||||
// as getCurrentHsUrl() may call through to localStorage.
|
||||
window.localStorage.setItem("mx_hs_url", hsUrl);
|
||||
window.localStorage.setItem("mx_is_url", isUrl);
|
||||
}
|
||||
this._stopMatrixClient();
|
||||
this.notifyNewScreen('login');
|
||||
this.replaceState({
|
||||
logged_in: false,
|
||||
ready: false,
|
||||
guestCreds: guestCreds,
|
||||
});
|
||||
Lifecycle.logout();
|
||||
break;
|
||||
case 'start_registration':
|
||||
var newState = payload.params || {};
|
||||
@ -307,7 +306,6 @@ module.exports = React.createClass({
|
||||
if (this.state.logged_in) return;
|
||||
this.replaceState({
|
||||
screen: 'login',
|
||||
guestCreds: this.state.guestCreds,
|
||||
});
|
||||
this.notifyNewScreen('login');
|
||||
break;
|
||||
@ -317,17 +315,12 @@ module.exports = React.createClass({
|
||||
});
|
||||
break;
|
||||
case 'start_upgrade_registration':
|
||||
// stash our guest creds so we can backout if needed
|
||||
this.guestCreds = MatrixClientPeg.getCredentials();
|
||||
this.replaceState({
|
||||
screen: "register",
|
||||
upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(),
|
||||
guestAccessToken: MatrixClientPeg.get().getAccessToken(),
|
||||
guestCreds: { // stash our guest creds so we can backout if needed
|
||||
userId: MatrixClientPeg.get().credentials.userId,
|
||||
accessToken: MatrixClientPeg.get().getAccessToken(),
|
||||
homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(),
|
||||
identityServerUrl: MatrixClientPeg.get().getIdentityServerUrl(),
|
||||
guest: true
|
||||
}
|
||||
});
|
||||
this.notifyNewScreen('register');
|
||||
break;
|
||||
@ -349,10 +342,13 @@ module.exports = React.createClass({
|
||||
|
||||
var client = MatrixClientPeg.get();
|
||||
client.loginWithToken(payload.params.loginToken).done(function(data) {
|
||||
MatrixClientPeg.replaceUsingAccessToken(
|
||||
client.getHomeserverUrl(), client.getIdentityServerUrl(),
|
||||
data.user_id, data.access_token
|
||||
);
|
||||
MatrixClientPeg.replaceUsingCreds({
|
||||
homeserverUrl: client.getHomeserverUrl(),
|
||||
identityServerUrl: client.getIdentityServerUrl(),
|
||||
userId: data.user_id,
|
||||
accessToken: data.access_token,
|
||||
guest: false,
|
||||
});
|
||||
self.setState({
|
||||
screen: undefined,
|
||||
logged_in: true
|
||||
@ -384,7 +380,7 @@ module.exports = React.createClass({
|
||||
|
||||
// FIXME: controller shouldn't be loading a view :(
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
var modal = Modal.createDialog(Loader);
|
||||
var modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||
|
||||
d.then(function() {
|
||||
modal.close();
|
||||
@ -405,10 +401,7 @@ module.exports = React.createClass({
|
||||
// known to be in (eg. user clicks on a room in the recents panel), supply the ID
|
||||
// If the user is clicking on a room in the context of the alias being presented
|
||||
// to them, supply the room alias. If both are supplied, the room ID will be ignored.
|
||||
this._viewRoom(
|
||||
payload.room_id, payload.room_alias, payload.show_settings, payload.event_id,
|
||||
payload.third_party_invite, payload.oob_data
|
||||
);
|
||||
this._viewRoom(payload);
|
||||
break;
|
||||
case 'view_prev_room':
|
||||
roomIndexDelta = -1;
|
||||
@ -425,7 +418,7 @@ module.exports = React.createClass({
|
||||
}
|
||||
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
|
||||
if (roomIndex < 0) roomIndex = allRooms.length - 1;
|
||||
this._viewRoom(allRooms[roomIndex].roomId);
|
||||
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
|
||||
break;
|
||||
case 'view_indexed_room':
|
||||
var allRooms = RoomListSorter.mostRecentActivityFirst(
|
||||
@ -433,7 +426,7 @@ module.exports = React.createClass({
|
||||
);
|
||||
var roomIndex = payload.roomIndex;
|
||||
if (allRooms[roomIndex]) {
|
||||
this._viewRoom(allRooms[roomIndex].roomId);
|
||||
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
|
||||
}
|
||||
break;
|
||||
case 'view_user_settings':
|
||||
@ -479,6 +472,15 @@ module.exports = React.createClass({
|
||||
middleOpacity: payload.middleOpacity,
|
||||
});
|
||||
break;
|
||||
case 'on_logged_in':
|
||||
this._onLoggedIn();
|
||||
break;
|
||||
case 'on_logged_out':
|
||||
this._onLoggedOut();
|
||||
break;
|
||||
case 'will_start_client':
|
||||
this._onWillStartClient();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
@ -493,39 +495,45 @@ module.exports = React.createClass({
|
||||
|
||||
// switch view to the given room
|
||||
//
|
||||
// eventId is optional and will cause a switch to the context of that
|
||||
// particular event.
|
||||
// @param {Object} thirdPartyInvite Object containing data about the third party
|
||||
// @param {Object} room_info Object containing data about the room to be joined
|
||||
// @param {string=} room_info.room_id ID of the room to join. One of room_id or room_alias must be given.
|
||||
// @param {string=} room_info.room_alias Alias of the room to join. One of room_id or room_alias must be given.
|
||||
// @param {boolean=} room_info.auto_join If true, automatically attempt to join the room if not already a member.
|
||||
// @param {boolean=} room_info.show_settings Makes RoomView show the room settings dialog.
|
||||
// @param {string=} room_info.event_id ID of the event in this room to show: this will cause a switch to the
|
||||
// context of that particular event.
|
||||
// @param {Object=} room_info.third_party_invite Object containing data about the third party
|
||||
// we received to join the room, if any.
|
||||
// @param {string} thirdPartyInvite.inviteSignUrl 3pid invite sign URL
|
||||
// @param {string} thirdPartyInvite.invitedwithEmail The email address the invite was sent to
|
||||
// @param {Object} oob_data Object of additional data about the room
|
||||
// @param {string=} room_info.third_party_invite.inviteSignUrl 3pid invite sign URL
|
||||
// @param {string=} room_info.third_party_invite.invitedEmail The email address the invite was sent to
|
||||
// @param {Object=} room_info.oob_data Object of additional data about the room
|
||||
// that has been passed out-of-band (eg.
|
||||
// room name and avatar from an invite email)
|
||||
_viewRoom: function(roomId, roomAlias, showSettings, eventId, thirdPartyInvite, oob_data) {
|
||||
_viewRoom: function(room_info) {
|
||||
// before we switch room, record the scroll state of the current room
|
||||
this._updateScrollMap();
|
||||
|
||||
this.focusComposer = true;
|
||||
|
||||
var newState = {
|
||||
initialEventId: eventId,
|
||||
highlightedEventId: eventId,
|
||||
initialEventId: room_info.event_id,
|
||||
highlightedEventId: room_info.event_id,
|
||||
initialEventPixelOffset: undefined,
|
||||
page_type: this.PageTypes.RoomView,
|
||||
thirdPartyInvite: thirdPartyInvite,
|
||||
roomOobData: oob_data,
|
||||
currentRoomAlias: roomAlias,
|
||||
thirdPartyInvite: room_info.third_party_invite,
|
||||
roomOobData: room_info.oob_data,
|
||||
currentRoomAlias: room_info.room_alias,
|
||||
autoJoin: room_info.auto_join,
|
||||
};
|
||||
|
||||
if (!roomAlias) {
|
||||
newState.currentRoomId = roomId;
|
||||
if (!room_info.room_alias) {
|
||||
newState.currentRoomId = room_info.room_id;
|
||||
}
|
||||
|
||||
// if we aren't given an explicit event id, look for one in the
|
||||
// scrollStateMap.
|
||||
if (!eventId) {
|
||||
var scrollState = this.scrollStateMap[roomId];
|
||||
if (!room_info.event_id) {
|
||||
var scrollState = this.scrollStateMap[room_info.room_id];
|
||||
if (scrollState) {
|
||||
newState.initialEventId = scrollState.focussedEvent;
|
||||
newState.initialEventPixelOffset = scrollState.pixelOffset;
|
||||
@ -538,8 +546,8 @@ module.exports = React.createClass({
|
||||
// the new screen yet (we won't be showing it yet)
|
||||
// The normal case where this happens is navigating
|
||||
// to the room in the URL bar on page load.
|
||||
var presentedId = roomAlias || roomId;
|
||||
var room = MatrixClientPeg.get().getRoom(roomId);
|
||||
var presentedId = room_info.room_alias || room_info.room_id;
|
||||
var room = MatrixClientPeg.get().getRoom(room_info.room_id);
|
||||
if (room) {
|
||||
var theAlias = MatrixTools.getDisplayAliasForRoom(room);
|
||||
if (theAlias) presentedId = theAlias;
|
||||
@ -555,15 +563,15 @@ module.exports = React.createClass({
|
||||
// Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
}
|
||||
|
||||
if (eventId) {
|
||||
presentedId += "/"+eventId;
|
||||
if (room_info.event_id) {
|
||||
presentedId += "/"+room_info.event_id;
|
||||
}
|
||||
this.notifyNewScreen('room/'+presentedId);
|
||||
newState.ready = true;
|
||||
}
|
||||
this.setState(newState);
|
||||
|
||||
if (this.refs.roomView && showSettings) {
|
||||
if (this.refs.roomView && room_info.showSettings) {
|
||||
this.refs.roomView.showSettings(true);
|
||||
}
|
||||
},
|
||||
@ -583,23 +591,36 @@ module.exports = React.createClass({
|
||||
this.scrollStateMap[roomId] = state;
|
||||
},
|
||||
|
||||
onLoggedIn: function(credentials) {
|
||||
credentials.guest = Boolean(credentials.guest);
|
||||
console.log("onLoggedIn => %s (guest=%s)", credentials.userId, credentials.guest);
|
||||
MatrixClientPeg.replaceUsingAccessToken(
|
||||
credentials.homeserverUrl, credentials.identityServerUrl,
|
||||
credentials.userId, credentials.accessToken, credentials.guest
|
||||
);
|
||||
/**
|
||||
* Called when a new logged in session has started
|
||||
*/
|
||||
_onLoggedIn: function(credentials) {
|
||||
this.guestCreds = null;
|
||||
this.notifyNewScreen('');
|
||||
this.setState({
|
||||
screen: undefined,
|
||||
logged_in: true
|
||||
logged_in: true,
|
||||
});
|
||||
this.startMatrixClient();
|
||||
this.notifyNewScreen('');
|
||||
},
|
||||
|
||||
startMatrixClient: function() {
|
||||
/**
|
||||
* Called when the session is logged out
|
||||
*/
|
||||
_onLoggedOut: function() {
|
||||
this.notifyNewScreen('login');
|
||||
this.replaceState({
|
||||
logged_in: false,
|
||||
ready: false,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Called just before the matrix client is started
|
||||
* (useful for setting listeners)
|
||||
*/
|
||||
_onWillStartClient() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
|
||||
var self = this;
|
||||
cli.on('sync', function(state, prevState) {
|
||||
self.updateFavicon(state, prevState);
|
||||
@ -666,13 +687,6 @@ module.exports = React.createClass({
|
||||
action: 'logout'
|
||||
});
|
||||
});
|
||||
Notifier.start();
|
||||
UserActivity.start();
|
||||
Presence.start();
|
||||
cli.startClient({
|
||||
pendingEventOrdering: "detached",
|
||||
initialSyncLimit: this.props.config.sync_timeline_limit || 20,
|
||||
});
|
||||
},
|
||||
|
||||
// stop all the background processes related to the current client
|
||||
@ -910,12 +924,14 @@ module.exports = React.createClass({
|
||||
|
||||
onReturnToGuestClick: function() {
|
||||
// reanimate our guest login
|
||||
this.onLoggedIn(this.state.guestCreds);
|
||||
this.setState({ guestCreds: null });
|
||||
if (this.guestCreds) {
|
||||
Lifecycle.setLoggedIn(this.guestCreds);
|
||||
this.guestCreds = null;
|
||||
}
|
||||
},
|
||||
|
||||
onRegistered: function(credentials) {
|
||||
this.onLoggedIn(credentials);
|
||||
Lifecycle.setLoggedIn(credentials);
|
||||
// do post-registration stuff
|
||||
// This now goes straight to user settings
|
||||
// We use _setPage since if we wait for
|
||||
@ -1032,6 +1048,7 @@ module.exports = React.createClass({
|
||||
<RoomView
|
||||
ref="roomView"
|
||||
roomAddress={this.state.currentRoomAlias || this.state.currentRoomId}
|
||||
autoJoin={this.state.autoJoin}
|
||||
onRoomIdResolved={this.onRoomIdResolved}
|
||||
eventId={this.state.initialEventId}
|
||||
thirdPartyInvite={this.state.thirdPartyInvite}
|
||||
@ -1111,8 +1128,8 @@ module.exports = React.createClass({
|
||||
email={this.props.startingQueryParams.email}
|
||||
username={this.state.upgradeUsername}
|
||||
guestAccessToken={this.state.guestAccessToken}
|
||||
defaultHsUrl={this.props.config.default_hs_url}
|
||||
defaultIsUrl={this.props.config.default_is_url}
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
defaultIsUrl={this.getDefaultIsUrl()}
|
||||
brand={this.props.config.brand}
|
||||
customHsUrl={this.getCurrentHsUrl()}
|
||||
customIsUrl={this.getCurrentIsUrl()}
|
||||
@ -1120,14 +1137,14 @@ module.exports = React.createClass({
|
||||
onLoggedIn={this.onRegistered}
|
||||
onLoginClick={this.onLoginClick}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
onCancelClick={ this.state.guestCreds ? this.onReturnToGuestClick : null }
|
||||
onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.screen == 'forgot_password') {
|
||||
return (
|
||||
<ForgotPassword
|
||||
defaultHsUrl={this.props.config.default_hs_url}
|
||||
defaultIsUrl={this.props.config.default_is_url}
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
defaultIsUrl={this.getDefaultIsUrl()}
|
||||
customHsUrl={this.getCurrentHsUrl()}
|
||||
customIsUrl={this.getCurrentIsUrl()}
|
||||
onComplete={this.onLoginClick}
|
||||
@ -1136,16 +1153,16 @@ module.exports = React.createClass({
|
||||
} else {
|
||||
return (
|
||||
<Login
|
||||
onLoggedIn={this.onLoggedIn}
|
||||
onLoggedIn={Lifecycle.setLoggedIn}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
defaultHsUrl={this.props.config.default_hs_url}
|
||||
defaultIsUrl={this.props.config.default_is_url}
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
defaultIsUrl={this.getDefaultIsUrl()}
|
||||
customHsUrl={this.getCurrentHsUrl()}
|
||||
customIsUrl={this.getCurrentIsUrl()}
|
||||
fallbackHsUrl={this.getFallbackHsUrl()}
|
||||
onForgotPasswordClick={this.onForgotPasswordClick}
|
||||
onLoginAsGuestClick={this.props.enableGuest && this.props.config && this.props.config.default_hs_url ? this._registerAsGuest.bind(this, true) : undefined}
|
||||
onCancelClick={ this.state.guestCreds ? this.onReturnToGuestClick : null }
|
||||
onLoginAsGuestClick={this.props.enableGuest && this.props.config && this._registerAsGuest.bind(this, true)}
|
||||
onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -44,6 +44,9 @@ module.exports = React.createClass({
|
||||
// ID of an event to highlight. If undefined, no event will be highlighted.
|
||||
highlightedEventId: React.PropTypes.string,
|
||||
|
||||
// Should we show URL Previews
|
||||
showUrlPreview: React.PropTypes.bool,
|
||||
|
||||
// event after which we should show a read marker
|
||||
readMarkerEventId: React.PropTypes.string,
|
||||
|
||||
@ -365,6 +368,7 @@ module.exports = React.createClass({
|
||||
onWidgetLoad={this._onWidgetLoad}
|
||||
readReceipts={readReceipts}
|
||||
readReceiptMap={this._readReceiptMap}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
checkUnmounting={this._isUnmounting}
|
||||
eventSendStatus={mxEv.status}
|
||||
last={last} isSelectedEvent={highlight}/>
|
||||
|
@ -26,9 +26,9 @@ module.exports = React.createClass({
|
||||
propTypes: {
|
||||
// the room this statusbar is representing.
|
||||
room: React.PropTypes.object.isRequired,
|
||||
|
||||
// a list of TabCompleteEntries.Entry objects
|
||||
tabCompleteEntries: React.PropTypes.array,
|
||||
|
||||
// a TabComplete object
|
||||
tabComplete: React.PropTypes.object.isRequired,
|
||||
|
||||
// the number of messages which have arrived since we've been scrolled up
|
||||
numUnreadMessages: React.PropTypes.number,
|
||||
@ -208,11 +208,11 @@ module.exports = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.tabCompleteEntries) {
|
||||
if (this.props.tabComplete.isTabCompleting()) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_tabCompleteBar">
|
||||
<div className="mx_RoomStatusBar_tabCompleteWrapper">
|
||||
<TabCompleteBar entries={this.props.tabCompleteEntries} />
|
||||
<TabCompleteBar tabComplete={this.props.tabComplete} />
|
||||
<div className="mx_RoomStatusBar_tabCompleteEol" title="->|">
|
||||
<TintableSvg src="img/eol.svg" width="22" height="16"/>
|
||||
Auto-complete
|
||||
@ -233,7 +233,7 @@ module.exports = React.createClass({
|
||||
<a className="mx_RoomStatusBar_resend_link"
|
||||
onClick={ this.props.onResendAllClick }>
|
||||
Resend all
|
||||
</a> or <a
|
||||
</a> or <a
|
||||
className="mx_RoomStatusBar_resend_link"
|
||||
onClick={ this.props.onCancelAllClick }>
|
||||
cancel all
|
||||
@ -247,7 +247,7 @@ module.exports = React.createClass({
|
||||
// unread count trumps who is typing since the unread count is only
|
||||
// set when you've scrolled up
|
||||
if (this.props.numUnreadMessages) {
|
||||
var unreadMsgs = this.props.numUnreadMessages + " new message" +
|
||||
var unreadMsgs = this.props.numUnreadMessages + " new message" +
|
||||
(this.props.numUnreadMessages > 1 ? "s" : "");
|
||||
|
||||
return (
|
||||
@ -291,5 +291,5 @@ module.exports = React.createClass({
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -31,16 +31,15 @@ var Modal = require("../../Modal");
|
||||
var sdk = require('../../index');
|
||||
var CallHandler = require('../../CallHandler');
|
||||
var TabComplete = require("../../TabComplete");
|
||||
var MemberEntry = require("../../TabCompleteEntries").MemberEntry;
|
||||
var CommandEntry = require("../../TabCompleteEntries").CommandEntry;
|
||||
var Resend = require("../../Resend");
|
||||
var SlashCommands = require("../../SlashCommands");
|
||||
var dis = require("../../dispatcher");
|
||||
var Tinter = require("../../Tinter");
|
||||
var rate_limited_func = require('../../ratelimitedfunc');
|
||||
var ObjectUtils = require('../../ObjectUtils');
|
||||
var MatrixTools = require('../../MatrixTools');
|
||||
|
||||
import UserProvider from '../../autocomplete/UserProvider';
|
||||
|
||||
var DEBUG = false;
|
||||
|
||||
if (DEBUG) {
|
||||
@ -117,6 +116,11 @@ module.exports = React.createClass({
|
||||
guestsCanJoin: false,
|
||||
canPeek: false,
|
||||
|
||||
// error object, as from the matrix client/server API
|
||||
// If we failed to load information about the room,
|
||||
// store the error here.
|
||||
roomLoadError: null,
|
||||
|
||||
// this is true if we are fully scrolled-down, and are looking at
|
||||
// the end of the live timeline. It has the effect of hiding the
|
||||
// 'scroll to bottom' knob, among a couple of other things.
|
||||
@ -134,6 +138,7 @@ module.exports = React.createClass({
|
||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
|
||||
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
|
||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||
|
||||
this.tabComplete = new TabComplete({
|
||||
allowLooping: false,
|
||||
@ -159,10 +164,11 @@ module.exports = React.createClass({
|
||||
roomId: result.room_id,
|
||||
roomLoading: !room,
|
||||
hasUnsentMessages: this._hasUnsentMessages(room),
|
||||
}, this._updatePeeking);
|
||||
}, this._onHaveRoom);
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
roomLoading: false,
|
||||
roomLoadError: err,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
@ -172,11 +178,11 @@ module.exports = React.createClass({
|
||||
room: room,
|
||||
roomLoading: !room,
|
||||
hasUnsentMessages: this._hasUnsentMessages(room),
|
||||
}, this._updatePeeking);
|
||||
}, this._onHaveRoom);
|
||||
}
|
||||
},
|
||||
|
||||
_updatePeeking: function() {
|
||||
_onHaveRoom: function() {
|
||||
// if this is an unknown room then we're in one of three states:
|
||||
// - This is a room we can peek into (search engine) (we can /peek)
|
||||
// - This is a room we can publicly join or were invited to. (we can /join)
|
||||
@ -187,29 +193,47 @@ module.exports = React.createClass({
|
||||
// Note that peeking works by room ID and room ID only, as opposed to joining
|
||||
// which must be by alias or invite wherever possible (peeking currently does
|
||||
// not work over federation).
|
||||
if (!this.state.room && this.state.roomId) {
|
||||
console.log("Attempting to peek into room %s", this.state.roomId);
|
||||
|
||||
MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => {
|
||||
this.setState({
|
||||
room: room,
|
||||
roomLoading: false,
|
||||
});
|
||||
this._onRoomLoaded(room);
|
||||
}, (err) => {
|
||||
// This won't necessarily be a MatrixError, but we duck-type
|
||||
// here and say if it's got an 'errcode' key with the right value,
|
||||
// it means we can't peek.
|
||||
if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") {
|
||||
// This is fine: the room just isn't peekable (we assume).
|
||||
// NB. We peek if we are not in the room, although if we try to peek into
|
||||
// a room in which we have a member event (ie. we've left) synapse will just
|
||||
// send us the same data as we get in the sync (ie. the last events we saw).
|
||||
var user_is_in_room = null;
|
||||
if (this.state.room) {
|
||||
user_is_in_room = this.state.room.hasMembershipState(
|
||||
MatrixClientPeg.get().credentials.userId, 'join'
|
||||
);
|
||||
|
||||
this._updateAutoComplete();
|
||||
this.tabComplete.loadEntries(this.state.room);
|
||||
}
|
||||
|
||||
if (!user_is_in_room && this.state.roomId) {
|
||||
if (this.props.autoJoin) {
|
||||
this.onJoinButtonClicked();
|
||||
} else if (this.state.roomId) {
|
||||
console.log("Attempting to peek into room %s", this.state.roomId);
|
||||
|
||||
MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => {
|
||||
this.setState({
|
||||
room: room,
|
||||
roomLoading: false,
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}).done();
|
||||
} else if (this.state.room) {
|
||||
this._onRoomLoaded(room);
|
||||
}, (err) => {
|
||||
// This won't necessarily be a MatrixError, but we duck-type
|
||||
// here and say if it's got an 'errcode' key with the right value,
|
||||
// it means we can't peek.
|
||||
if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") {
|
||||
// This is fine: the room just isn't peekable (we assume).
|
||||
this.setState({
|
||||
roomLoading: false,
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}).done();
|
||||
}
|
||||
} else if (user_is_in_room) {
|
||||
MatrixClientPeg.get().stopPeeking();
|
||||
this._onRoomLoaded(this.state.room);
|
||||
}
|
||||
@ -244,6 +268,7 @@ module.exports = React.createClass({
|
||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData);
|
||||
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
}
|
||||
|
||||
window.removeEventListener('resize', this.onResize);
|
||||
@ -315,6 +340,10 @@ module.exports = React.createClass({
|
||||
// ignore events for other rooms
|
||||
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:
|
||||
// updates from pagination will happen when the paginate completes.
|
||||
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
||||
@ -334,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,
|
||||
// after a successful peek, or after we join the room).
|
||||
_onRoomLoaded: function(room) {
|
||||
this._calculatePeekRules(room);
|
||||
this._updatePreviewUrlVisibility(room);
|
||||
},
|
||||
|
||||
_calculatePeekRules: function(room) {
|
||||
@ -358,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) {
|
||||
// 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
|
||||
@ -388,14 +462,23 @@ module.exports = React.createClass({
|
||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
},
|
||||
|
||||
onRoomAccountData: function(room, event) {
|
||||
if (room.roomId == this.props.roomId) {
|
||||
if (event.getType === "org.matrix.room.color_scheme") {
|
||||
onAccountData: function(event) {
|
||||
if (event.getType() === "org.matrix.preview_urls" && this.state.room) {
|
||||
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();
|
||||
// XXX: we should validate the event
|
||||
console.log("Tinter.tint from onRoomAccountData");
|
||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
}
|
||||
else if (event.getType() === "org.matrix.room.preview_urls") {
|
||||
this._updatePreviewUrlVisibility(room);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -410,8 +493,20 @@ module.exports = React.createClass({
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.ConferenceHandler &&
|
||||
member.userId === this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) {
|
||||
this._updateConfCallNotification();
|
||||
}
|
||||
|
||||
this._updateRoomMembers();
|
||||
},
|
||||
|
||||
// rate limited because a power level change will emit an event for every
|
||||
// member in the room.
|
||||
_updateRoomMembers: new rate_limited_func(function() {
|
||||
// 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
|
||||
// means we have finished joining a room we were previously peeking
|
||||
@ -422,12 +517,7 @@ module.exports = React.createClass({
|
||||
joining: false
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.ConferenceHandler &&
|
||||
member.userId === this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) {
|
||||
this._updateConfCallNotification();
|
||||
}
|
||||
},
|
||||
}, 500),
|
||||
|
||||
_hasUnsentMessages: function(room) {
|
||||
return this._getUnsentMessages(room).length > 0;
|
||||
@ -476,8 +566,6 @@ module.exports = React.createClass({
|
||||
window.addEventListener('resize', this.onResize);
|
||||
this.onResize();
|
||||
|
||||
this._updateTabCompleteList();
|
||||
|
||||
// XXX: EVIL HACK to autofocus inviting on empty rooms.
|
||||
// We use the setTimeout to avoid racing with focus_composer.
|
||||
if (this.state.room &&
|
||||
@ -495,22 +583,6 @@ module.exports = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
_updateTabCompleteList: new rate_limited_func(function() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
|
||||
if (!this.state.room || !this.tabComplete) {
|
||||
return;
|
||||
}
|
||||
var members = this.state.room.getJoinedMembers().filter(function(member) {
|
||||
if (member.userId !== cli.credentials.userId) return true;
|
||||
});
|
||||
this.tabComplete.setCompletionList(
|
||||
MemberEntry.fromMemberList(members).concat(
|
||||
CommandEntry.fromCommands(SlashCommands.getCommandList())
|
||||
)
|
||||
);
|
||||
}, 500),
|
||||
|
||||
componentDidUpdate: function() {
|
||||
if (this.refs.roomView) {
|
||||
var roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
||||
@ -992,7 +1064,7 @@ module.exports = React.createClass({
|
||||
this.setState({
|
||||
rejecting: true
|
||||
});
|
||||
MatrixClientPeg.get().leave(this.props.roomAddress).done(function() {
|
||||
MatrixClientPeg.get().leave(this.state.roomId).done(function() {
|
||||
dis.dispatch({ action: 'view_next_room' });
|
||||
self.setState({
|
||||
rejecting: false
|
||||
@ -1235,6 +1307,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() {
|
||||
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
||||
var MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||
@ -1267,6 +1347,7 @@ module.exports = React.createClass({
|
||||
|
||||
// We have no room object for this room, only the ID.
|
||||
// We've got to this room by following a link, possibly a third party invite.
|
||||
var room_alias = this.props.roomAddress[0] == '#' ? this.props.roomAddress : null;
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<RoomHeader ref="header"
|
||||
@ -1277,7 +1358,8 @@ module.exports = React.createClass({
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
|
||||
onRejectClick={ this.onRejectThreepidInviteButtonClicked }
|
||||
canJoin={ true } canPreview={ false }
|
||||
canPreview={ false } error={ this.state.roomLoadError }
|
||||
roomAlias={room_alias}
|
||||
spinner={this.state.joining}
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
@ -1315,7 +1397,7 @@ module.exports = React.createClass({
|
||||
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
|
||||
onRejectClick={ this.onRejectButtonClicked }
|
||||
inviterName={ inviterName }
|
||||
canJoin={ true } canPreview={ false }
|
||||
canPreview={ false }
|
||||
spinner={this.state.joining}
|
||||
room={this.state.room}
|
||||
/>
|
||||
@ -1346,12 +1428,10 @@ module.exports = React.createClass({
|
||||
statusBar = <UploadBar room={this.state.room} />
|
||||
} else if (!this.state.searchResults) {
|
||||
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
|
||||
var tabEntries = this.tabComplete.isTabCompleting() ?
|
||||
this.tabComplete.peek(6) : null;
|
||||
|
||||
statusBar = <RoomStatusBar
|
||||
room={this.state.room}
|
||||
tabCompleteEntries={tabEntries}
|
||||
tabComplete={this.tabComplete}
|
||||
numUnreadMessages={this.state.numUnreadMessages}
|
||||
hasUnsentMessages={this.state.hasUnsentMessages}
|
||||
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
|
||||
@ -1385,7 +1465,7 @@ module.exports = React.createClass({
|
||||
invitedEmail = this.props.thirdPartyInvite.invitedEmail;
|
||||
}
|
||||
aux = (
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} canJoin={true}
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
spinner={this.state.joining}
|
||||
inviterName={inviterName}
|
||||
@ -1484,6 +1564,8 @@ module.exports = React.createClass({
|
||||
hideMessagePanel = true;
|
||||
}
|
||||
|
||||
// console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
||||
|
||||
var messagePanel = (
|
||||
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
||||
room={this.state.room}
|
||||
@ -1493,6 +1575,7 @@ module.exports = React.createClass({
|
||||
eventPixelOffset={this.props.eventPixelOffset}
|
||||
onScroll={ this.onMessageListScroll }
|
||||
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
|
||||
showUrlPreview = { this.state.showUrlPreview }
|
||||
opacity={ this.props.opacity }
|
||||
/>);
|
||||
|
||||
|
@ -540,7 +540,6 @@ module.exports = React.createClass({
|
||||
// it's not obvious why we have a separate div and ol anyway.
|
||||
return (<GeminiScrollbar autoshow={true} ref="geminiPanel"
|
||||
onScroll={this.onScroll} onResize={this.onResize}
|
||||
relayoutOnUpdate={false}
|
||||
className={this.props.className} style={this.props.style}>
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
|
||||
|
@ -71,6 +71,9 @@ var TimelinePanel = React.createClass({
|
||||
// half way down the viewport.
|
||||
eventPixelOffset: React.PropTypes.number,
|
||||
|
||||
// Should we show URL Previews
|
||||
showUrlPreview: React.PropTypes.bool,
|
||||
|
||||
// callback which is called when the panel is scrolled.
|
||||
onScroll: React.PropTypes.func,
|
||||
|
||||
@ -934,6 +937,7 @@ var TimelinePanel = React.createClass({
|
||||
readMarkerEventId={ this.state.readMarkerEventId }
|
||||
readMarkerVisible={ this.state.readMarkerVisible }
|
||||
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
||||
showUrlPreview = { this.props.showUrlPreview }
|
||||
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
||||
stickyBottom={ stickyBottom }
|
||||
onScroll={ this.onMessageListScroll }
|
||||
|
@ -214,9 +214,10 @@ module.exports = React.createClass({
|
||||
onFinished: this.onEmailDialogFinished,
|
||||
});
|
||||
}, (err) => {
|
||||
this.setState({email_add_pending: false});
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Unable to add email address",
|
||||
description: err.toString()
|
||||
description: err.message
|
||||
});
|
||||
});
|
||||
ReactDOM.findDOMNode(this.refs.add_threepid_input).blur();
|
||||
@ -261,7 +262,64 @@ module.exports = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
_renderDeviceInfo: function() {
|
||||
_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>
|
||||
);
|
||||
},
|
||||
|
||||
_renderCryptoInfo: function() {
|
||||
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
||||
return null;
|
||||
}
|
||||
@ -282,6 +340,45 @@ module.exports = React.createClass({
|
||||
);
|
||||
},
|
||||
|
||||
_renderDevicesPanel: function() {
|
||||
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
||||
return null;
|
||||
}
|
||||
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
||||
return (
|
||||
<div>
|
||||
<h3>Devices</h3>
|
||||
<DevicesPanel className="mx_UserSettings_section" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_renderLabs: function () {
|
||||
let features = LABS_FEATURES.map(feature => (
|
||||
<div key={feature.id} className="mx_UserSettings_toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={feature.id}
|
||||
name={feature.id}
|
||||
defaultChecked={UserSettingsStore.isFeatureEnabled(feature.id)}
|
||||
onChange={e => {
|
||||
UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked);
|
||||
this.forceUpdate();
|
||||
}}/>
|
||||
<label htmlFor={feature.id}>{feature.name}</label>
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div>
|
||||
<h3>Labs</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
<p>These are experimental features that may break in unexpected ways. Use with caution.</p>
|
||||
{features}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
@ -302,6 +399,7 @@ module.exports = React.createClass({
|
||||
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||
var Notifications = sdk.getComponent("settings.Notifications");
|
||||
var EditableText = sdk.getComponent('elements.EditableText');
|
||||
|
||||
var avatarUrl = (
|
||||
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
|
||||
);
|
||||
@ -376,36 +474,11 @@ module.exports = React.createClass({
|
||||
</div>);
|
||||
}
|
||||
|
||||
this._renderLabs = function () {
|
||||
let features = LABS_FEATURES.map(feature => (
|
||||
<div key={feature.id}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={feature.id}
|
||||
name={feature.id}
|
||||
defaultChecked={UserSettingsStore.isFeatureEnabled(feature.id)}
|
||||
onChange={e => UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked)} />
|
||||
<label htmlFor={feature.id}>{feature.name}</label>
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div>
|
||||
<h3>Labs</h3>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<p>These are experimental features that may break in unexpected ways. Use with caution.</p>
|
||||
{features}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx_UserSettings">
|
||||
<SimpleRoomHeader title="Settings" onCancelClick={ this.props.onClose }/>
|
||||
|
||||
<GeminiScrollbar className="mx_UserSettings_body"
|
||||
relayoutOnUpdate={false}
|
||||
autoshow={true}>
|
||||
|
||||
<h3>Profile</h3>
|
||||
@ -452,9 +525,10 @@ module.exports = React.createClass({
|
||||
|
||||
{notification_area}
|
||||
|
||||
{this._renderDeviceInfo()}
|
||||
|
||||
{this._renderUserInterfaceSettings()}
|
||||
{this._renderLabs()}
|
||||
{this._renderDevicesPanel()}
|
||||
{this._renderCryptoInfo()}
|
||||
|
||||
<h3>Advanced</h3>
|
||||
|
||||
|
@ -232,7 +232,9 @@ module.exports = React.createClass({displayName: 'Login',
|
||||
<div className="mx_Login_box">
|
||||
<LoginHeader />
|
||||
<div>
|
||||
<h2>Sign in</h2>
|
||||
<h2>Sign in
|
||||
{ loader }
|
||||
</h2>
|
||||
{ this.componentForStep(this._getCurrentFlowStep()) }
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
@ -244,7 +246,6 @@ module.exports = React.createClass({displayName: 'Login',
|
||||
onIsUrlChanged={this.onIsUrlChanged}
|
||||
delayTimeMs={1000}/>
|
||||
<div className="mx_Login_error">
|
||||
{ loader }
|
||||
{ this.state.errorText }
|
||||
</div>
|
||||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||
|
@ -54,6 +54,16 @@ module.exports = React.createClass({
|
||||
return {
|
||||
busy: false,
|
||||
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;
|
||||
this.setState({
|
||||
errorText: "",
|
||||
busy: true
|
||||
busy: true,
|
||||
formVals: formVals,
|
||||
});
|
||||
|
||||
if (formVals.username !== this.props.username) {
|
||||
@ -228,11 +239,15 @@ module.exports = React.createClass({
|
||||
break; // NOP
|
||||
case "Register.START":
|
||||
case "Register.STEP_m.login.dummy":
|
||||
// NB. Our 'username' prop is specifically for upgrading
|
||||
// a guest account
|
||||
registerStep = (
|
||||
<RegistrationForm
|
||||
showEmail={true}
|
||||
defaultUsername={this.props.username}
|
||||
defaultEmail={this.props.email}
|
||||
defaultUsername={this.state.formVals.username}
|
||||
defaultEmail={this.state.formVals.email}
|
||||
defaultPassword={this.state.formVals.password}
|
||||
guestUsername={this.props.username}
|
||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||
onError={this.onFormValidationFailed}
|
||||
onRegisterClick={this.onFormSubmit} />
|
||||
|
@ -18,6 +18,7 @@ limitations under the License.
|
||||
|
||||
var React = require('react');
|
||||
var AvatarLogic = require("../../../Avatar");
|
||||
import {emojifyText} from '../../../HtmlUtils';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'BaseAvatar',
|
||||
@ -132,32 +133,36 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var name = this.props.name;
|
||||
|
||||
var imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||
|
||||
const {
|
||||
name, idName, title, url, urls, width, height, resizeMethod,
|
||||
defaultToInitialLetter,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
if (imageUrl === this.state.defaultImageUrl) {
|
||||
var initialLetter = this._getInitialLetter(this.props.name);
|
||||
var initialLetter = emojifyText(this._getInitialLetter(name));
|
||||
return (
|
||||
<span className="mx_BaseAvatar" {...this.props}>
|
||||
<span className="mx_BaseAvatar" {...otherProps}>
|
||||
<span className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||
style={{ fontSize: (this.props.width * 0.65) + "px",
|
||||
width: this.props.width + "px",
|
||||
lineHeight: this.props.height + "px" }}>
|
||||
{ initialLetter }
|
||||
style={{ fontSize: (width * 0.65) + "px",
|
||||
width: width + "px",
|
||||
lineHeight: height + "px" }}
|
||||
dangerouslySetInnerHTML={initialLetter}>
|
||||
</span>
|
||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||
alt="" title={this.props.title} onError={this.onError}
|
||||
width={this.props.width} height={this.props.height} />
|
||||
alt="" title={title} onError={this.onError}
|
||||
width={width} height={height} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
|
||||
onError={this.onError}
|
||||
width={this.props.width} height={this.props.height}
|
||||
title={this.props.title} alt=""
|
||||
{...this.props} />
|
||||
width={width} height={height}
|
||||
title={title} alt=""
|
||||
{...otherProps} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -59,9 +59,12 @@ module.exports = React.createClass({
|
||||
|
||||
render: function() {
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
var {member, ...otherProps} = this.props;
|
||||
|
||||
return (
|
||||
<BaseAvatar {...this.props} name={this.state.name} title={this.state.title}
|
||||
idName={this.props.member.userId} url={this.state.imageUrl} />
|
||||
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
|
||||
idName={member.userId} url={this.state.imageUrl} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -126,11 +126,13 @@ module.exports = React.createClass({
|
||||
render: function() {
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
var roomName = this.props.room ? this.props.room.name : this.props.oobData.name;
|
||||
var {room, oobData, ...otherProps} = this.props;
|
||||
|
||||
var roomName = room ? room.name : oobData.name;
|
||||
|
||||
return (
|
||||
<BaseAvatar {...this.props} name={roomName}
|
||||
idName={this.props.room ? this.props.room.roomId : null}
|
||||
<BaseAvatar {...otherProps} name={roomName}
|
||||
idName={room ? room.roomId : null}
|
||||
urls={this.state.urls} />
|
||||
);
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ module.exports = React.createClass({
|
||||
{this.props.description}
|
||||
</div>
|
||||
<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}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -46,7 +46,7 @@ module.exports = React.createClass({
|
||||
Sign out?
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -63,7 +63,7 @@ module.exports = React.createClass({
|
||||
{this.props.description}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.props.onFinished} autoFocus={true}>
|
||||
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={true}>
|
||||
Cancel
|
||||
</button>
|
||||
<button onClick={this.onRegisterClicked}>
|
||||
|
@ -56,7 +56,7 @@ module.exports = React.createClass({
|
||||
{this.props.description}
|
||||
</div>
|
||||
<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}
|
||||
</button>
|
||||
|
||||
|
@ -76,7 +76,7 @@ module.exports = React.createClass({
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<input type="submit" value="Set" />
|
||||
<input className="mx_Dialog_primary" type="submit" value="Set" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -86,7 +86,7 @@ module.exports = React.createClass({
|
||||
<button onClick={this.onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button onClick={this.onOk}>
|
||||
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
||||
{this.props.button}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -49,6 +49,8 @@ module.exports = React.createClass({
|
||||
label: '',
|
||||
placeholder: '',
|
||||
editable: true,
|
||||
className: "mx_EditableText",
|
||||
placeholderClassName: "mx_EditableText_placeholder",
|
||||
};
|
||||
},
|
||||
|
||||
@ -92,7 +94,7 @@ module.exports = React.createClass({
|
||||
this.refs.editable_div.textContent = this.value;
|
||||
this.refs.editable_div.setAttribute("class", this.props.className);
|
||||
this.placeholder = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
@ -101,7 +103,7 @@ module.exports = React.createClass({
|
||||
|
||||
setValue: function(value) {
|
||||
this.value = value;
|
||||
this.showPlaceholder(!this.value);
|
||||
this.showPlaceholder(!this.value);
|
||||
},
|
||||
|
||||
edit: function() {
|
||||
@ -125,7 +127,7 @@ module.exports = React.createClass({
|
||||
|
||||
onKeyDown: function(ev) {
|
||||
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||
|
||||
|
||||
if (this.placeholder) {
|
||||
this.showPlaceholder(false);
|
||||
}
|
||||
@ -173,7 +175,7 @@ module.exports = React.createClass({
|
||||
var range = document.createRange();
|
||||
range.setStart(node, 0);
|
||||
range.setEnd(node, node.length);
|
||||
|
||||
|
||||
var sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
|
147
src/components/views/elements/EditableTextContainer.js
Normal file
147
src/components/views/elements/EditableTextContainer.js
Normal file
@ -0,0 +1,147 @@
|
||||
/*
|
||||
Copyright 2015, 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import q from 'q';
|
||||
|
||||
/**
|
||||
* A component which wraps an EditableText, with a spinner while updates take
|
||||
* place.
|
||||
*
|
||||
* Parent components should supply an 'onSubmit' callback which returns a
|
||||
* promise; a spinner is shown until the promise resolves.
|
||||
*
|
||||
* The parent can also supply a 'getIntialValue' callback, which works in a
|
||||
* similarly asynchronous way. If this is not provided, the initial value is
|
||||
* taken from the 'initialValue' property.
|
||||
*/
|
||||
export default class EditableTextContainer extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._unmounted = false;
|
||||
this.state = {
|
||||
busy: false,
|
||||
errorString: null,
|
||||
value: props.initialValue,
|
||||
};
|
||||
this._onValueChanged = this._onValueChanged.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (this.props.getInitialValue === undefined) {
|
||||
// use whatever was given in the initialValue property.
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({busy: true});
|
||||
|
||||
this.props.getInitialValue().done(
|
||||
(result) => {
|
||||
if (this._unmounted) { return; }
|
||||
this.setState({
|
||||
busy: false,
|
||||
value: result,
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
if (this._unmounted) { return; }
|
||||
this.setState({
|
||||
errorString: error.toString(),
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
_onValueChanged(value, shouldSubmit) {
|
||||
if (!shouldSubmit) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
busy: true,
|
||||
errorString: null,
|
||||
});
|
||||
|
||||
this.props.onSubmit(value).done(
|
||||
() => {
|
||||
if (this._unmounted) { return; }
|
||||
this.setState({
|
||||
busy: false,
|
||||
value: value,
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
if (this._unmounted) { return; }
|
||||
this.setState({
|
||||
errorString: error.toString(),
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.busy) {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<Loader />
|
||||
);
|
||||
} else if (this.state.errorString) {
|
||||
return (
|
||||
<div className="error">{this.state.errorString}</div>
|
||||
);
|
||||
} else {
|
||||
var EditableText = sdk.getComponent('elements.EditableText');
|
||||
return (
|
||||
<EditableText initialValue={this.state.value}
|
||||
placeholder={this.props.placeholder}
|
||||
onValueChanged={this._onValueChanged}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
EditableTextContainer.propTypes = {
|
||||
/* callback to retrieve the initial value. */
|
||||
getInitialValue: React.PropTypes.func,
|
||||
|
||||
/* initial value; used if getInitialValue is not given */
|
||||
initialValue: React.PropTypes.string,
|
||||
|
||||
/* placeholder text to use when the value is empty (and not being
|
||||
* edited) */
|
||||
placeholder: React.PropTypes.string,
|
||||
|
||||
/* callback to update the value. Called with a single argument: the new
|
||||
* value. */
|
||||
onSubmit: React.PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
EditableTextContainer.defaultProps = {
|
||||
initialValue: "",
|
||||
placeholder: "",
|
||||
onSubmit: function(v) {return q(); },
|
||||
};
|
@ -34,10 +34,15 @@ module.exports = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
value: React.PropTypes.number.isRequired,
|
||||
|
||||
// if true, the <select/> should be a 'controlled' form element and updated by React
|
||||
// to reflect the current value, rather than left freeform.
|
||||
// MemberInfo uses controlled; RoomSettings uses non-controlled.
|
||||
controlled: React.PropTypes.bool.isRequired,
|
||||
//
|
||||
// ignored if disabled is truthy. false by default.
|
||||
controlled: React.PropTypes.bool,
|
||||
|
||||
// should the user be able to change the value? false by default.
|
||||
disabled: React.PropTypes.bool,
|
||||
onChange: React.PropTypes.func,
|
||||
},
|
||||
|
@ -35,8 +35,16 @@ module.exports = React.createClass({
|
||||
displayName: 'RegistrationForm',
|
||||
|
||||
propTypes: {
|
||||
// Values pre-filled in the input boxes when the component loads
|
||||
defaultEmail: 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,
|
||||
minPasswordLength: React.PropTypes.number,
|
||||
onError: React.PropTypes.func,
|
||||
@ -55,10 +63,6 @@ module.exports = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
email: this.props.defaultEmail,
|
||||
username: null,
|
||||
password: null,
|
||||
passwordConfirm: null,
|
||||
fieldValid: {}
|
||||
};
|
||||
},
|
||||
@ -103,7 +107,7 @@ module.exports = React.createClass({
|
||||
|
||||
_doSubmit: function() {
|
||||
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(),
|
||||
email: this.refs.email.value.trim()
|
||||
});
|
||||
@ -144,7 +148,7 @@ module.exports = React.createClass({
|
||||
break;
|
||||
case FIELD_USERNAME:
|
||||
// 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) {
|
||||
this.markFieldValid(
|
||||
field_id,
|
||||
@ -225,7 +229,7 @@ module.exports = React.createClass({
|
||||
emailSection = (
|
||||
<input className="mx_Login_field" type="text" ref="email"
|
||||
autoFocus={true} placeholder="Email address (optional)"
|
||||
defaultValue={this.state.email}
|
||||
defaultValue={this.props.defaultEmail}
|
||||
style={this._styleField(FIELD_EMAIL)}
|
||||
onBlur={function() {self.validateField(FIELD_EMAIL)}} />
|
||||
);
|
||||
@ -237,8 +241,8 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
var placeholderUserName = "User name";
|
||||
if (this.props.defaultUsername) {
|
||||
placeholderUserName += " (default: " + this.props.defaultUsername + ")"
|
||||
if (this.props.guestUsername) {
|
||||
placeholderUserName += " (default: " + this.props.guestUsername + ")"
|
||||
}
|
||||
|
||||
return (
|
||||
@ -247,23 +251,23 @@ module.exports = React.createClass({
|
||||
{emailSection}
|
||||
<br />
|
||||
<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)}
|
||||
onBlur={function() {self.validateField(FIELD_USERNAME)}} />
|
||||
<br />
|
||||
{ this.props.defaultUsername ?
|
||||
{ this.props.guestUsername ?
|
||||
<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"
|
||||
style={this._styleField(FIELD_PASSWORD)}
|
||||
onBlur={function() {self.validateField(FIELD_PASSWORD)}}
|
||||
placeholder="Password" defaultValue={this.state.password} />
|
||||
placeholder="Password" defaultValue={this.props.defaultPassword} />
|
||||
<br />
|
||||
<input className="mx_Login_field" type="password" ref="passwordConfirm"
|
||||
placeholder="Confirm password"
|
||||
style={this._styleField(FIELD_PASSWORD_CONFIRM)}
|
||||
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM)}}
|
||||
defaultValue={this.state.passwordConfirm} />
|
||||
defaultValue={this.props.defaultPassword} />
|
||||
<br />
|
||||
{registerButton}
|
||||
</form>
|
||||
|
@ -34,7 +34,7 @@ module.exports = React.createClass({
|
||||
}
|
||||
if (fullWidth < thumbWidth && fullHeight < thumbHeight) {
|
||||
// no scaling needs to be applied
|
||||
return fullHeight;
|
||||
return 1;
|
||||
}
|
||||
var widthMulti = thumbWidth / fullWidth;
|
||||
var heightMulti = thumbHeight / fullHeight;
|
||||
|
@ -38,6 +38,9 @@ module.exports = React.createClass({
|
||||
/* link URL for the highlights */
|
||||
highlightLink: React.PropTypes.string,
|
||||
|
||||
/* should show URL previews for this event */
|
||||
showUrlPreview: React.PropTypes.bool,
|
||||
|
||||
/* callback called when dynamic content in events are loaded */
|
||||
onWidgetLoad: React.PropTypes.func,
|
||||
},
|
||||
@ -71,6 +74,7 @@ module.exports = React.createClass({
|
||||
|
||||
return <BodyType ref="body" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />;
|
||||
},
|
||||
});
|
||||
|
@ -39,6 +39,9 @@ module.exports = React.createClass({
|
||||
/* link URL for the highlights */
|
||||
highlightLink: React.PropTypes.string,
|
||||
|
||||
/* should show URL previews for this event */
|
||||
showUrlPreview: React.PropTypes.bool,
|
||||
|
||||
/* callback for when our widget has loaded */
|
||||
onWidgetLoad: React.PropTypes.func,
|
||||
},
|
||||
@ -56,34 +59,47 @@ module.exports = React.createClass({
|
||||
|
||||
componentDidMount: function() {
|
||||
linkifyElement(this.refs.content, linkifyMatrix.options);
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
this.calculateUrlPreview();
|
||||
|
||||
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
|
||||
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this));
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
this.calculateUrlPreview();
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
//console.log("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||
|
||||
// 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() ||
|
||||
nextProps.highlights !== this.props.highlights ||
|
||||
nextProps.highlightLink !== this.props.highlightLink ||
|
||||
nextProps.showUrlPreview !== this.props.showUrlPreview ||
|
||||
nextState.links !== this.state.links ||
|
||||
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) {
|
||||
var links = [];
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
@ -163,12 +179,14 @@ module.exports = React.createClass({
|
||||
render: function() {
|
||||
var mxEvent = this.props.mxEvent;
|
||||
var content = mxEvent.getContent();
|
||||
var body = HtmlUtils.bodyToHtml(content, this.props.highlights,
|
||||
{highlightLink: this.props.highlightLink});
|
||||
var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {});
|
||||
|
||||
if (this.props.highlightLink) {
|
||||
body = <a href={ this.props.highlightLink }>{ body }</a>;
|
||||
}
|
||||
|
||||
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');
|
||||
widgets = this.state.links.map((link)=>{
|
||||
return <LinkPreviewWidget
|
||||
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||
var React = require('react');
|
||||
|
||||
var TextForEvent = require('../../../TextForEvent');
|
||||
import {emojifyText} from '../../../HtmlUtils';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'TextualEvent',
|
||||
@ -31,11 +32,11 @@ module.exports = React.createClass({
|
||||
|
||||
render: function() {
|
||||
var text = TextForEvent.textForEvent(this.props.mxEvent);
|
||||
if (text == null || text.length == 0) return null;
|
||||
if (text == null || text.length === 0) return null;
|
||||
let textHTML = emojifyText(TextForEvent.textForEvent(this.props.mxEvent));
|
||||
|
||||
return (
|
||||
<div className="mx_TextualEvent">
|
||||
{TextForEvent.textForEvent(this.props.mxEvent)}
|
||||
<div className="mx_TextualEvent" dangerouslySetInnerHTML={textHTML}>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -83,13 +83,11 @@ module.exports = React.createClass({
|
||||
alias: this.state.canonicalAlias
|
||||
}, ""
|
||||
)
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// save new aliases for m.room.aliases
|
||||
var aliasOperations = this.getAliasOperations();
|
||||
var promises = [];
|
||||
for (var i = 0; i < aliasOperations.length; i++) {
|
||||
var alias_operation = aliasOperations[i];
|
||||
console.log("alias %s %s", alias_operation.place, alias_operation.val);
|
||||
@ -301,7 +299,7 @@ module.exports = React.createClass({
|
||||
<div className="mx_RoomSettings_addAlias">
|
||||
<img src="img/plus.svg" width="14" height="14" alt="Add"
|
||||
onClick={ self.onAliasAdded.bind(self, undefined) }/>
|
||||
</div>
|
||||
</div>
|
||||
</div> : ""
|
||||
}
|
||||
</div>
|
||||
|
@ -57,7 +57,7 @@ module.exports = React.createClass({
|
||||
data.primary_color = scheme.primary_color;
|
||||
data.secondary_color = scheme.secondary_color;
|
||||
data.index = this._getColorIndex(data);
|
||||
|
||||
|
||||
if (data.index === -1) {
|
||||
// append the unrecognised colours to our palette
|
||||
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>
|
||||
);
|
||||
|
||||
}
|
||||
});
|
164
src/components/views/rooms/Autocomplete.js
Normal file
164
src/components/views/rooms/Autocomplete.js
Normal file
@ -0,0 +1,164 @@
|
||||
import React from 'react';
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
import classNames from 'classnames';
|
||||
import flatMap from 'lodash/flatMap';
|
||||
|
||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
||||
|
||||
export default class Autocomplete extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onConfirm = this.onConfirm.bind(this);
|
||||
|
||||
this.state = {
|
||||
// list of completionResults, each containing completions
|
||||
completions: [],
|
||||
|
||||
// array of completions, so we can look up current selection by offset quickly
|
||||
completionList: [],
|
||||
|
||||
// how far down the completion list we are
|
||||
selectionOffset: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props, state) {
|
||||
if (props.query === this.props.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
getCompletions(props.query, props.selection).forEach(completionResult => {
|
||||
try {
|
||||
completionResult.completions.then(completions => {
|
||||
let i = this.state.completions.findIndex(
|
||||
completion => completion.provider === completionResult.provider
|
||||
);
|
||||
|
||||
i = i === -1 ? this.state.completions.length : i;
|
||||
let newCompletions = Object.assign([], this.state.completions);
|
||||
completionResult.completions = completions;
|
||||
newCompletions[i] = completionResult;
|
||||
|
||||
this.setState({
|
||||
completions: newCompletions,
|
||||
completionList: flatMap(newCompletions, provider => provider.completions),
|
||||
});
|
||||
}, err => {
|
||||
console.error(err);
|
||||
});
|
||||
} catch (e) {
|
||||
// An error in one provider shouldn't mess up the rest.
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
countCompletions(): number {
|
||||
return this.state.completions.map(completionResult => {
|
||||
return completionResult.completions.length;
|
||||
}).reduce((l, r) => l + r);
|
||||
}
|
||||
|
||||
// called from MessageComposerInput
|
||||
onUpArrow(): boolean {
|
||||
let completionCount = this.countCompletions(),
|
||||
selectionOffset = (completionCount + this.state.selectionOffset - 1) % completionCount;
|
||||
if (!completionCount) {
|
||||
return false;
|
||||
}
|
||||
this.setSelection(selectionOffset);
|
||||
return true;
|
||||
}
|
||||
|
||||
// called from MessageComposerInput
|
||||
onDownArrow(): boolean {
|
||||
let completionCount = this.countCompletions(),
|
||||
selectionOffset = (this.state.selectionOffset + 1) % completionCount;
|
||||
if (!completionCount) {
|
||||
return false;
|
||||
}
|
||||
this.setSelection(selectionOffset);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** called from MessageComposerInput
|
||||
* @returns {boolean} whether confirmation was handled
|
||||
*/
|
||||
onConfirm(): boolean {
|
||||
if (this.countCompletions() === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let selectedCompletion = this.state.completionList[this.state.selectionOffset];
|
||||
this.props.onConfirm(selectedCompletion.range, selectedCompletion.completion);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setSelection(selectionOffset: number) {
|
||||
this.setState({selectionOffset});
|
||||
}
|
||||
|
||||
render() {
|
||||
let position = 0;
|
||||
let renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||
let completions = completionResult.completions.map((completion, i) => {
|
||||
let className = classNames('mx_Autocomplete_Completion', {
|
||||
'selected': position === this.state.selectionOffset,
|
||||
});
|
||||
let componentPosition = position;
|
||||
position++;
|
||||
|
||||
let onMouseOver = () => this.setSelection(componentPosition);
|
||||
let onClick = () => {
|
||||
this.setSelection(componentPosition);
|
||||
this.onConfirm();
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={i}
|
||||
className={className}
|
||||
onMouseOver={onMouseOver}
|
||||
onClick={onClick}>
|
||||
{completion.component}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
return completions.length > 0 ? (
|
||||
<div key={i} className="mx_Autocomplete_ProviderSection">
|
||||
<span className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</span>
|
||||
<ReactCSSTransitionGroup
|
||||
component="div"
|
||||
transitionName="autocomplete"
|
||||
transitionEnterTimeout={300}
|
||||
transitionLeaveTimeout={300}>
|
||||
{completions}
|
||||
</ReactCSSTransitionGroup>
|
||||
</div>
|
||||
) : null;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx_Autocomplete">
|
||||
<ReactCSSTransitionGroup
|
||||
component="div"
|
||||
transitionName="autocomplete"
|
||||
transitionEnterTimeout={300}
|
||||
transitionLeaveTimeout={300}>
|
||||
{renderedCompletions}
|
||||
</ReactCSSTransitionGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Autocomplete.propTypes = {
|
||||
// the query string for which to show autocomplete suggestions
|
||||
query: React.PropTypes.string.isRequired,
|
||||
|
||||
// method invoked with range and text content when completion is confirmed
|
||||
onConfirm: React.PropTypes.func.isRequired,
|
||||
};
|
@ -20,6 +20,7 @@ var React = require('react');
|
||||
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var sdk = require('../../../index');
|
||||
import {emojifyText} from '../../../HtmlUtils';
|
||||
|
||||
|
||||
var PRESENCE_CLASS = {
|
||||
@ -28,6 +29,23 @@ var PRESENCE_CLASS = {
|
||||
"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({
|
||||
displayName: 'EntityTile',
|
||||
|
||||
@ -78,10 +96,14 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
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 ";
|
||||
mainClassName += presenceClass + (this.props.className ? (" " + this.props.className) : "");
|
||||
var nameEl;
|
||||
let nameHTML = emojifyText(this.props.name);
|
||||
|
||||
if (this.state.hover && !this.props.suppressOnHover) {
|
||||
var activeAgo = this.props.presenceLastActiveAgo ?
|
||||
@ -92,7 +114,7 @@ module.exports = React.createClass({
|
||||
nameEl = (
|
||||
<div className="mx_EntityTile_details">
|
||||
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
|
||||
<div className="mx_EntityTile_name_hover">{ this.props.name }</div>
|
||||
<div className="mx_EntityTile_name_hover" dangerouslySetInnerHTML={nameHTML}></div>
|
||||
<PresenceLabel activeAgo={ activeAgo }
|
||||
currentlyActive={this.props.presenceCurrentlyActive}
|
||||
presenceState={this.props.presenceState} />
|
||||
@ -101,8 +123,7 @@ module.exports = React.createClass({
|
||||
}
|
||||
else {
|
||||
nameEl = (
|
||||
<div className="mx_EntityTile_name">
|
||||
{ this.props.name }
|
||||
<div className="mx_EntityTile_name" dangerouslySetInnerHTML={nameHTML}>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ var sdk = require('../../../index');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg')
|
||||
var TextForEvent = require('../../../TextForEvent');
|
||||
|
||||
var ContextualMenu = require('../../../ContextualMenu');
|
||||
var ContextualMenu = require('../../structures/ContextualMenu');
|
||||
var dispatcher = require("../../../dispatcher");
|
||||
|
||||
var ObjectUtils = require('../../../ObjectUtils');
|
||||
@ -101,6 +101,9 @@ module.exports = React.createClass({
|
||||
/* link URL for the highlights */
|
||||
highlightLink: React.PropTypes.string,
|
||||
|
||||
/* should show URL previews for this event */
|
||||
showUrlPreview: React.PropTypes.bool,
|
||||
|
||||
/* is this the focused event */
|
||||
isSelectedEvent: React.PropTypes.bool,
|
||||
|
||||
@ -139,7 +142,8 @@ module.exports = React.createClass({
|
||||
|
||||
componentDidMount: function() {
|
||||
this._suppressReadReceiptAnimation = false;
|
||||
MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified);
|
||||
MatrixClientPeg.get().on("deviceVerificationChanged",
|
||||
this.onDeviceVerificationChanged);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function (nextProps) {
|
||||
@ -163,11 +167,12 @@ module.exports = React.createClass({
|
||||
componentWillUnmount: function() {
|
||||
var client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("deviceVerified", this.onDeviceVerified);
|
||||
client.removeListener("deviceVerificationChanged",
|
||||
this.onDeviceVerificationChanged);
|
||||
}
|
||||
},
|
||||
|
||||
onDeviceVerified: function(userId, device) {
|
||||
onDeviceVerificationChanged: function(userId, device) {
|
||||
if (userId == this.props.mxEvent.getSender()) {
|
||||
this._verifyEvent(this.props.mxEvent);
|
||||
}
|
||||
@ -244,12 +249,15 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
onEditClicked: function(e) {
|
||||
var MessageContextMenu = sdk.getComponent('rooms.MessageContextMenu');
|
||||
var MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
||||
var buttonRect = e.target.getBoundingClientRect()
|
||||
var x = buttonRect.right;
|
||||
var y = buttonRect.top + (e.target.height / 2);
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
var x = buttonRect.right + window.pageXOffset;
|
||||
var y = (buttonRect.top + (e.target.height / 2) + window.pageYOffset) - 19;
|
||||
var self = this;
|
||||
ContextualMenu.createMenu(MessageContextMenu, {
|
||||
chevronOffset: 10,
|
||||
mxEvent: this.props.mxEvent,
|
||||
left: x,
|
||||
top: y,
|
||||
@ -357,6 +365,8 @@ module.exports = React.createClass({
|
||||
var SenderProfile = sdk.getComponent('messages.SenderProfile');
|
||||
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 msgtype = content.msgtype;
|
||||
|
||||
@ -418,6 +428,7 @@ module.exports = React.createClass({
|
||||
<div className="mx_EventTile_line">
|
||||
<EventTileType ref="tile" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -37,17 +37,14 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
var cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
|
||||
this._emailEntity = null;
|
||||
// Load the complete user list for inviting new users
|
||||
// TODO: Keep this list bleeding-edge up-to-date. Practically speaking,
|
||||
// it will do for now not being updated as random new users join different
|
||||
// rooms as this list will be reloaded every room swap.
|
||||
if (this._room) {
|
||||
this._userList = MatrixClientPeg.get().getUsers().filter((u) => {
|
||||
return !this._room.hasMembershipState(u.userId, "join");
|
||||
});
|
||||
}
|
||||
|
||||
// we have to update the list whenever membership changes
|
||||
// particularly to avoid bug https://github.com/vector-im/vector-web/issues/1813
|
||||
this._updateList();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
@ -55,6 +52,28 @@ module.exports = React.createClass({
|
||||
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) {
|
||||
this.props.onInvite(this._input);
|
||||
},
|
||||
|
@ -36,32 +36,75 @@ module.exports = React.createClass({
|
||||
);
|
||||
},
|
||||
|
||||
onBlockClick: function() {
|
||||
MatrixClientPeg.get().setDeviceBlocked(
|
||||
this.props.userId, this.props.device.id, true
|
||||
);
|
||||
},
|
||||
|
||||
onUnblockClick: function() {
|
||||
MatrixClientPeg.get().setDeviceBlocked(
|
||||
this.props.userId, this.props.device.id, false
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var indicator = null, button = null;
|
||||
if (this.props.device.verified) {
|
||||
indicator = (
|
||||
<div className="mx_MemberDeviceInfo_verified">✔</div>
|
||||
var indicator = null, blockButton = null, verifyButton = null;
|
||||
if (this.props.device.blocked) {
|
||||
blockButton = (
|
||||
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblock"
|
||||
onClick={this.onUnblockClick}>
|
||||
Unblock
|
||||
</div>
|
||||
);
|
||||
button = (
|
||||
} else {
|
||||
blockButton = (
|
||||
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_block"
|
||||
onClick={this.onBlockClick}>
|
||||
Block
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.device.verified) {
|
||||
verifyButton = (
|
||||
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
|
||||
onClick={this.onUnverifyClick}>
|
||||
Unverify
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
button = (
|
||||
verifyButton = (
|
||||
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
|
||||
onClick={this.onVerifyClick}>
|
||||
Verify
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.device.blocked) {
|
||||
indicator = (
|
||||
<div className="mx_MemberDeviceInfo_blocked">✖</div>
|
||||
);
|
||||
} else if (this.props.device.verified) {
|
||||
indicator = (
|
||||
<div className="mx_MemberDeviceInfo_verified">✔</div>
|
||||
);
|
||||
|
||||
} else {
|
||||
indicator = (
|
||||
<div className="mx_MemberDeviceInfo_unverified">?</div>
|
||||
);
|
||||
}
|
||||
|
||||
var deviceName = this.props.device.display_name || this.props.device.id;
|
||||
|
||||
return (
|
||||
<div className="mx_MemberDeviceInfo">
|
||||
<div className="mx_MemberDeviceInfo_deviceId">{this.props.device.id}</div>
|
||||
<div className="mx_MemberDeviceInfo_deviceKey">{this.props.device.key}</div>
|
||||
<div className="mx_MemberDeviceInfo_deviceId">{deviceName}</div>
|
||||
{indicator}
|
||||
{button}
|
||||
{verifyButton}
|
||||
{blockButton}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -32,6 +32,7 @@ var Modal = require("../../../Modal");
|
||||
var sdk = require('../../../index');
|
||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||
var createRoom = require('../../../createRoom');
|
||||
import {emojifyText} from '../../../HtmlUtils';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberInfo',
|
||||
@ -60,17 +61,21 @@ module.exports = React.createClass({
|
||||
updating: 0,
|
||||
devicesLoading: true,
|
||||
devices: null,
|
||||
existingOneToOneRoomId: null,
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
componentWillMount: function() {
|
||||
this._cancelDeviceList = null;
|
||||
|
||||
this.setState({
|
||||
existingOneToOneRoomId: this.getExistingOneToOneRoomId()
|
||||
});
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._updateStateForNewMember(this.props.member);
|
||||
MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified);
|
||||
MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
@ -82,14 +87,67 @@ module.exports = React.createClass({
|
||||
componentWillUnmount: function() {
|
||||
var client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("deviceVerified", this.onDeviceVerified);
|
||||
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
}
|
||||
if (this._cancelDeviceList) {
|
||||
this._cancelDeviceList();
|
||||
}
|
||||
},
|
||||
|
||||
onDeviceVerified: function(userId, device) {
|
||||
getExistingOneToOneRoomId: function() {
|
||||
const rooms = MatrixClientPeg.get().getRooms();
|
||||
const userIds = [
|
||||
this.props.member.userId,
|
||||
MatrixClientPeg.get().credentials.userId
|
||||
];
|
||||
let existingRoomId = null;
|
||||
let invitedRoomId = null;
|
||||
|
||||
// roomId can be null here because of a hack in MatrixChat.onUserClick where we
|
||||
// abuse this to view users rather than room members.
|
||||
let currentMembers;
|
||||
if (this.props.member.roomId) {
|
||||
const currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId);
|
||||
currentMembers = currentRoom.getJoinedMembers();
|
||||
}
|
||||
|
||||
// reuse the first private 1:1 we find
|
||||
existingRoomId = null;
|
||||
|
||||
for (let i = 0; i < rooms.length; i++) {
|
||||
// don't try to reuse public 1:1 rooms
|
||||
const join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", '');
|
||||
if (join_rules && join_rules.getContent().join_rule === 'public') continue;
|
||||
|
||||
const 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;
|
||||
}
|
||||
|
||||
const invited = rooms[i].getMembersWithMembership('invite');
|
||||
if (members.length === 1 &&
|
||||
invited.length === 1 &&
|
||||
userIds.indexOf(members[0].userId) !== -1 &&
|
||||
userIds.indexOf(invited[0].userId) !== -1 &&
|
||||
invitedRoomId === null)
|
||||
{
|
||||
invitedRoomId = rooms[i].roomId;
|
||||
// keep looking: we'll use this one if there's nothing better
|
||||
}
|
||||
}
|
||||
|
||||
if (existingRoomId === null) {
|
||||
existingRoomId = invitedRoomId;
|
||||
}
|
||||
|
||||
return existingRoomId;
|
||||
},
|
||||
|
||||
onDeviceVerificationChanged: function(userId, device) {
|
||||
if (userId == this.props.member.userId) {
|
||||
// no need to re-download the whole thing; just update our copy of
|
||||
// the list.
|
||||
@ -348,61 +406,29 @@ module.exports = React.createClass({
|
||||
|
||||
onChatClick: function() {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
// TODO: keep existingOneToOneRoomId updated if we see any room member changes anywhere
|
||||
|
||||
const 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)
|
||||
// If so, just view that room. If not, create a private room with them.
|
||||
var self = this;
|
||||
var rooms = MatrixClientPeg.get().getRooms();
|
||||
var userIds = [
|
||||
this.props.member.userId,
|
||||
MatrixClientPeg.get().credentials.userId
|
||||
];
|
||||
var existingRoomId;
|
||||
|
||||
var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId);
|
||||
var currentMembers = currentRoom.getJoinedMembers();
|
||||
// if we're currently in a 1:1 with this user, start a new chat
|
||||
if (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) {
|
||||
if (useExistingOneToOneRoom) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: existingRoomId
|
||||
room_id: this.state.existingOneToOneRoomId,
|
||||
});
|
||||
this.props.onFinished();
|
||||
}
|
||||
else {
|
||||
self.setState({ updating: self.state.updating + 1 });
|
||||
this.setState({ updating: this.state.updating + 1 });
|
||||
createRoom({
|
||||
createOpts: {
|
||||
invite: [this.props.member.userId],
|
||||
},
|
||||
}).finally(function() {
|
||||
self.props.onFinished();
|
||||
self.setState({ updating: self.state.updating - 1 });
|
||||
}).finally(() => {
|
||||
this.props.onFinished();
|
||||
this.setState({ updating: this.state.updating - 1 });
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
@ -535,7 +561,9 @@ module.exports = React.createClass({
|
||||
return (
|
||||
<div>
|
||||
<h3>Devices</h3>
|
||||
{devComponents}
|
||||
<div className="mx_MemberInfo_devices">
|
||||
{devComponents}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@ -545,7 +573,22 @@ module.exports = React.createClass({
|
||||
if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) {
|
||||
// FIXME: we're referring to a vector component from react-sdk
|
||||
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) {
|
||||
@ -594,6 +637,8 @@ module.exports = React.createClass({
|
||||
</div>
|
||||
}
|
||||
|
||||
let memberNameHTML = emojifyText(this.props.member.name);
|
||||
|
||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
return (
|
||||
@ -603,7 +648,7 @@ module.exports = React.createClass({
|
||||
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
|
||||
</div>
|
||||
|
||||
<h2>{ this.props.member.name }</h2>
|
||||
<h2 dangerouslySetInnerHTML={memberNameHTML}></h2>
|
||||
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
|
@ -54,7 +54,7 @@ module.exports = React.createClass({
|
||||
|
||||
this.memberDict = this.getMemberDict();
|
||||
|
||||
state.members = this.roomMembers(INITIAL_LOAD_NUM_MEMBERS);
|
||||
state.members = this.roomMembers();
|
||||
return state;
|
||||
},
|
||||
|
||||
@ -64,7 +64,10 @@ module.exports = React.createClass({
|
||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
cli.on("RoomState.events", this.onRoomStateEvent);
|
||||
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);
|
||||
},
|
||||
|
||||
@ -75,24 +78,11 @@ module.exports = React.createClass({
|
||||
cli.removeListener("RoomMember.name", this.onRoomMemberName);
|
||||
cli.removeListener("RoomState.events", this.onRoomStateEvent);
|
||||
cli.removeListener("Room", this.onRoom);
|
||||
cli.removeListener("User.presence", this.onUserPresence);
|
||||
cli.removeListener("User.lastPresenceTs", this.onUserLastPresenceTs);
|
||||
// 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) {
|
||||
// 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
|
||||
// member tile and re-render it. This is more efficient than every tile
|
||||
// evar attaching their own listener.
|
||||
@ -325,7 +315,7 @@ module.exports = React.createClass({
|
||||
return all_members;
|
||||
},
|
||||
|
||||
roomMembers: function(limit) {
|
||||
roomMembers: function() {
|
||||
var all_members = this.memberDict || {};
|
||||
var all_user_ids = Object.keys(all_members);
|
||||
var ConferenceHandler = CallHandler.getConferenceHandler();
|
||||
@ -334,7 +324,7 @@ module.exports = React.createClass({
|
||||
|
||||
var to_display = [];
|
||||
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 m = all_members[user_id];
|
||||
|
||||
@ -425,27 +415,7 @@ module.exports = React.createClass({
|
||||
|
||||
// For now, let's just order things by timestamp. It's really annoying
|
||||
// that a user disappears from sight just because they temporarily go offline
|
||||
/*
|
||||
var presenceMap = {
|
||||
online: 3,
|
||||
unavailable: 2,
|
||||
offline: 1
|
||||
};
|
||||
|
||||
var presenceOrdA = userA ? presenceMap[userA.presence] : 0;
|
||||
var presenceOrdB = userB ? presenceMap[userB.presence] : 0;
|
||||
|
||||
if (presenceOrdA != presenceOrdB) {
|
||||
return presenceOrdB - presenceOrdA;
|
||||
}
|
||||
*/
|
||||
|
||||
var lastActiveTsA = userA && userA.lastActiveTs ? userA.lastActiveTs : 0;
|
||||
var lastActiveTsB = userB && userB.lastActiveTs ? userB.lastActiveTs : 0;
|
||||
|
||||
// console.log("comparing ts: " + lastActiveTsA + " and " + lastActiveTsB);
|
||||
|
||||
return lastActiveTsB - lastActiveTsA;
|
||||
return userB.getLastActiveTs() - userA.getLastActiveTs();
|
||||
},
|
||||
|
||||
onSearchQueryChanged: function(input) {
|
||||
@ -462,9 +432,16 @@ module.exports = React.createClass({
|
||||
|
||||
var memberList = self.state.members.filter(function(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;
|
||||
}).map(function(userId) {
|
||||
var m = self.memberDict[userId];
|
||||
@ -515,7 +492,7 @@ module.exports = React.createClass({
|
||||
invitedSection = (
|
||||
<div className="mx_MemberList_invited">
|
||||
<h2>Invited</h2>
|
||||
<div autoshow={true} className="mx_MemberList_wrapper">
|
||||
<div className="mx_MemberList_wrapper">
|
||||
{invitedMemberTiles}
|
||||
</div>
|
||||
</div>
|
||||
@ -544,7 +521,6 @@ module.exports = React.createClass({
|
||||
<div className="mx_MemberList">
|
||||
{inviteMemberListSection}
|
||||
<GeminiScrollbar autoshow={true}
|
||||
relayoutOnUpdate={false}
|
||||
className="mx_MemberList_joined mx_MemberList_outerWrapper">
|
||||
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile}>
|
||||
|
@ -20,54 +20,53 @@ var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var Modal = require('../../../Modal');
|
||||
var sdk = require('../../../index');
|
||||
var dis = require('../../../dispatcher');
|
||||
import Autocomplete from './Autocomplete';
|
||||
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MessageComposer',
|
||||
export default class MessageComposer extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onCallClick = this.onCallClick.bind(this);
|
||||
this.onHangupClick = this.onHangupClick.bind(this);
|
||||
this.onUploadClick = this.onUploadClick.bind(this);
|
||||
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
||||
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
||||
this.onInputContentChanged = this.onInputContentChanged.bind(this);
|
||||
this.onUpArrow = this.onUpArrow.bind(this);
|
||||
this.onDownArrow = this.onDownArrow.bind(this);
|
||||
this._tryComplete = this._tryComplete.bind(this);
|
||||
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
|
||||
|
||||
propTypes: {
|
||||
tabComplete: React.PropTypes.any,
|
||||
this.state = {
|
||||
autocompleteQuery: '',
|
||||
selection: null,
|
||||
};
|
||||
|
||||
// a callback which is called when the height of the composer is
|
||||
// changed due to a change in content.
|
||||
onResize: React.PropTypes.func,
|
||||
}
|
||||
|
||||
// js-sdk Room object
|
||||
room: React.PropTypes.object.isRequired,
|
||||
|
||||
// string representing the current voip call state
|
||||
callState: React.PropTypes.string,
|
||||
|
||||
// callback when a file to upload is chosen
|
||||
uploadFile: React.PropTypes.func.isRequired,
|
||||
|
||||
// opacity for dynamic UI fading effects
|
||||
opacity: React.PropTypes.number,
|
||||
},
|
||||
|
||||
onUploadClick: function(ev) {
|
||||
onUploadClick(ev) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
Modal.createDialog(NeedToRegisterDialog, {
|
||||
title: "Please Register",
|
||||
description: "Guest users can't upload files. Please register to upload."
|
||||
description: "Guest users can't upload files. Please register to upload.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.refs.uploadInput.click();
|
||||
},
|
||||
}
|
||||
|
||||
onUploadFileSelected: function(ev) {
|
||||
var files = ev.target.files;
|
||||
onUploadFileSelected(ev) {
|
||||
let files = ev.target.files;
|
||||
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
let TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
var fileList = [];
|
||||
for(var i=0; i<files.length; i++) {
|
||||
let fileList = [];
|
||||
for (let i=0; i<files.length; i++) {
|
||||
fileList.push(<li>
|
||||
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name}
|
||||
</li>);
|
||||
@ -94,11 +93,11 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
this.refs.uploadInput.value = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
onHangupClick: function() {
|
||||
onHangupClick() {
|
||||
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||
//var call = CallHandler.getAnyActiveCall();
|
||||
if (!call) {
|
||||
@ -108,27 +107,55 @@ module.exports = React.createClass({
|
||||
action: 'hangup',
|
||||
// hangup the call for this room, which may not be the room in props
|
||||
// (e.g. conferences which will hangup the 1:1 room instead)
|
||||
room_id: call.roomId
|
||||
room_id: call.roomId,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
onCallClick: function(ev) {
|
||||
onCallClick(ev) {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: ev.shiftKey ? "screensharing" : "video",
|
||||
room_id: this.props.room.roomId
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
onVoiceCallClick: function(ev) {
|
||||
onVoiceCallClick(ev) {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: 'voice',
|
||||
room_id: this.props.room.roomId
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
onInputContentChanged(content: string, selection: {start: number, end: number}) {
|
||||
this.setState({
|
||||
autocompleteQuery: content,
|
||||
selection,
|
||||
});
|
||||
}
|
||||
|
||||
onUpArrow() {
|
||||
return this.refs.autocomplete.onUpArrow();
|
||||
}
|
||||
|
||||
onDownArrow() {
|
||||
return this.refs.autocomplete.onDownArrow();
|
||||
}
|
||||
|
||||
_tryComplete(): boolean {
|
||||
if (this.refs.autocomplete) {
|
||||
return this.refs.autocomplete.onConfirm();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_onAutocompleteConfirm(range, completion) {
|
||||
if (this.messageComposerInput) {
|
||||
this.messageComposerInput.onConfirmAutocompletion(range, completion);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
var uploadInputStyle = {display: 'none'};
|
||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
@ -154,12 +181,12 @@ module.exports = React.createClass({
|
||||
else {
|
||||
callButton =
|
||||
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title="Voice call">
|
||||
<TintableSvg src="img/voice.svg" width="16" height="26"/>
|
||||
</div>
|
||||
<TintableSvg src="img/icon-call.svg" width="35" height="35"/>
|
||||
</div>;
|
||||
videoCallButton =
|
||||
<div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title="Video call">
|
||||
<TintableSvg src="img/call.svg" width="30" height="22"/>
|
||||
</div>
|
||||
<TintableSvg src="img/icons-video.svg" width="35" height="35"/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
var canSendMessages = this.props.room.currentState.maySendMessage(
|
||||
@ -172,7 +199,7 @@ module.exports = React.createClass({
|
||||
var uploadButton = (
|
||||
<div key="controls_upload" className="mx_MessageComposer_upload"
|
||||
onClick={this.onUploadClick} title="Upload file">
|
||||
<TintableSvg src="img/upload.svg" width="19" height="24"/>
|
||||
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
|
||||
<input ref="uploadInput" type="file"
|
||||
style={uploadInputStyle}
|
||||
multiple
|
||||
@ -181,8 +208,16 @@ module.exports = React.createClass({
|
||||
);
|
||||
|
||||
controls.push(
|
||||
<MessageComposerInput key="controls_input" tabComplete={this.props.tabComplete}
|
||||
onResize={this.props.onResize} room={this.props.room} />,
|
||||
<MessageComposerInput
|
||||
ref={c => this.messageComposerInput = c}
|
||||
key="controls_input"
|
||||
onResize={this.props.onResize}
|
||||
room={this.props.room}
|
||||
tryComplete={this._tryComplete}
|
||||
onUpArrow={this.onUpArrow}
|
||||
onDownArrow={this.onDownArrow}
|
||||
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
|
||||
onContentChanged={this.onInputContentChanged} />,
|
||||
uploadButton,
|
||||
hangupButton,
|
||||
callButton,
|
||||
@ -198,6 +233,13 @@ module.exports = React.createClass({
|
||||
|
||||
return (
|
||||
<div className="mx_MessageComposer mx_fadable" style={{ opacity: this.props.opacity }}>
|
||||
<div className="mx_MessageComposer_autocomplete_wrapper">
|
||||
<Autocomplete
|
||||
ref="autocomplete"
|
||||
onConfirm={this._onAutocompleteConfirm}
|
||||
query={this.state.autocompleteQuery}
|
||||
selection={this.state.selection} />
|
||||
</div>
|
||||
<div className="mx_MessageComposer_wrapper">
|
||||
<div className="mx_MessageComposer_row">
|
||||
{controls}
|
||||
@ -206,5 +248,24 @@ module.exports = React.createClass({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
MessageComposer.propTypes = {
|
||||
tabComplete: React.PropTypes.any,
|
||||
|
||||
// a callback which is called when the height of the composer is
|
||||
// changed due to a change in content.
|
||||
onResize: React.PropTypes.func,
|
||||
|
||||
// js-sdk Room object
|
||||
room: React.PropTypes.object.isRequired,
|
||||
|
||||
// string representing the current voip call state
|
||||
callState: React.PropTypes.string,
|
||||
|
||||
// callback when a file to upload is chosen
|
||||
uploadFile: React.PropTypes.func.isRequired,
|
||||
|
||||
// opacity for dynamic UI fading effects
|
||||
opacity: React.PropTypes.number
|
||||
};
|
||||
|
@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
var marked = require("marked");
|
||||
import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent';
|
||||
import marked from 'marked';
|
||||
marked.setOptions({
|
||||
renderer: new marked.Renderer(),
|
||||
gfm: true,
|
||||
@ -24,7 +24,7 @@ marked.setOptions({
|
||||
pedantic: false,
|
||||
sanitize: true,
|
||||
smartLists: true,
|
||||
smartypants: false
|
||||
smartypants: false,
|
||||
});
|
||||
|
||||
import {Editor, EditorState, RichUtils, CompositeDecorator,
|
||||
@ -33,14 +33,14 @@ import {Editor, EditorState, RichUtils, CompositeDecorator,
|
||||
|
||||
import {stateToMarkdown} from 'draft-js-export-markdown';
|
||||
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var SlashCommands = require("../../../SlashCommands");
|
||||
var Modal = require("../../../Modal");
|
||||
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
|
||||
var sdk = require('../../../index');
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
||||
import SlashCommands from '../../../SlashCommands';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
|
||||
var dis = require("../../../dispatcher");
|
||||
var KeyCode = require("../../../KeyCode");
|
||||
import dis from '../../../dispatcher';
|
||||
import KeyCode from '../../../KeyCode';
|
||||
|
||||
import * as RichText from '../../../RichText';
|
||||
|
||||
@ -49,8 +49,8 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
||||
const KEY_M = 77;
|
||||
|
||||
// FIXME Breaks markdown with multiple paragraphs, since it only strips first and last <p>
|
||||
function mdownToHtml(mdown) {
|
||||
var html = marked(mdown) || "";
|
||||
function mdownToHtml(mdown: string): string {
|
||||
let html = marked(mdown) || "";
|
||||
html = html.trim();
|
||||
// strip start and end <p> tags else you get 'orrible spacing
|
||||
if (html.indexOf("<p>") === 0) {
|
||||
@ -66,23 +66,38 @@ function mdownToHtml(mdown) {
|
||||
* The textInput part of the MessageComposer
|
||||
*/
|
||||
export default class MessageComposerInput extends React.Component {
|
||||
static getKeyBinding(e: SyntheticKeyboardEvent): string {
|
||||
// C-m => Toggles between rich text and markdown modes
|
||||
if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
|
||||
return 'toggle-mode';
|
||||
}
|
||||
|
||||
return getDefaultKeyBinding(e);
|
||||
}
|
||||
|
||||
client: MatrixClient;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onAction = this.onAction.bind(this);
|
||||
this.onInputClick = this.onInputClick.bind(this);
|
||||
this.handleReturn = this.handleReturn.bind(this);
|
||||
this.handleKeyCommand = this.handleKeyCommand.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.setEditorState = this.setEditorState.bind(this);
|
||||
this.onUpArrow = this.onUpArrow.bind(this);
|
||||
this.onDownArrow = this.onDownArrow.bind(this);
|
||||
this.onTab = this.onTab.bind(this);
|
||||
this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this);
|
||||
|
||||
let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled');
|
||||
if(isRichtextEnabled == null) {
|
||||
if (isRichtextEnabled == null) {
|
||||
isRichtextEnabled = 'true';
|
||||
}
|
||||
isRichtextEnabled = isRichtextEnabled === 'true';
|
||||
|
||||
this.state = {
|
||||
isRichtextEnabled: isRichtextEnabled,
|
||||
editorState: null
|
||||
editorState: null,
|
||||
};
|
||||
|
||||
// bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled
|
||||
@ -91,15 +106,6 @@ export default class MessageComposerInput extends React.Component {
|
||||
this.client = MatrixClientPeg.get();
|
||||
}
|
||||
|
||||
static getKeyBinding(e: SyntheticKeyboardEvent): string {
|
||||
// C-m => Toggles between rich text and markdown modes
|
||||
if(e.keyCode == KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
|
||||
return 'toggle-mode';
|
||||
}
|
||||
|
||||
return getDefaultKeyBinding(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* "Does the right thing" to create an EditorState, based on:
|
||||
* - whether we've got rich text mode enabled
|
||||
@ -207,11 +213,9 @@ export default class MessageComposerInput extends React.Component {
|
||||
let contentJSON = window.sessionStorage.getItem("mx_messagecomposer_input_" + this.roomId);
|
||||
if (contentJSON) {
|
||||
let content = convertFromRaw(JSON.parse(contentJSON));
|
||||
component.setState({
|
||||
editorState: component.createEditorState(component.state.isRichtextEnabled, content)
|
||||
});
|
||||
component.setEditorState(component.createEditorState(component.state.isRichtextEnabled, content));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -233,7 +237,7 @@ export default class MessageComposerInput extends React.Component {
|
||||
}
|
||||
|
||||
onAction(payload) {
|
||||
var editor = this.refs.editor;
|
||||
let editor = this.refs.editor;
|
||||
|
||||
switch (payload.action) {
|
||||
case 'focus_composer':
|
||||
@ -251,7 +255,7 @@ export default class MessageComposerInput extends React.Component {
|
||||
payload.displayname
|
||||
);
|
||||
this.setState({
|
||||
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters')
|
||||
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'),
|
||||
});
|
||||
editor.focus();
|
||||
}
|
||||
@ -344,28 +348,31 @@ export default class MessageComposerInput extends React.Component {
|
||||
this.refs.editor.focus();
|
||||
}
|
||||
|
||||
onChange(editorState: EditorState) {
|
||||
setEditorState(editorState: EditorState) {
|
||||
editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
|
||||
this.setState({editorState});
|
||||
|
||||
if(editorState.getCurrentContent().hasText()) {
|
||||
this.onTypingActivity()
|
||||
if (editorState.getCurrentContent().hasText()) {
|
||||
this.onTypingActivity();
|
||||
} else {
|
||||
this.onFinishedTyping();
|
||||
}
|
||||
|
||||
if (this.props.onContentChanged) {
|
||||
this.props.onContentChanged(editorState.getCurrentContent().getPlainText(),
|
||||
RichText.selectionStateToTextOffsets(editorState.getSelection(),
|
||||
editorState.getCurrentContent().getBlocksAsArray()));
|
||||
}
|
||||
}
|
||||
|
||||
enableRichtext(enabled: boolean) {
|
||||
if (enabled) {
|
||||
let html = mdownToHtml(this.state.editorState.getCurrentContent().getPlainText());
|
||||
this.setState({
|
||||
editorState: this.createEditorState(enabled, RichText.HTMLtoContentState(html))
|
||||
});
|
||||
this.setEditorState(this.createEditorState(enabled, RichText.HTMLtoContentState(html)));
|
||||
} else {
|
||||
let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()),
|
||||
contentState = ContentState.createFromText(markdown);
|
||||
this.setState({
|
||||
editorState: this.createEditorState(enabled, contentState)
|
||||
});
|
||||
this.setEditorState(this.createEditorState(enabled, contentState));
|
||||
}
|
||||
|
||||
window.localStorage.setItem('mx_editor_rte_enabled', enabled);
|
||||
@ -376,7 +383,7 @@ export default class MessageComposerInput extends React.Component {
|
||||
}
|
||||
|
||||
handleKeyCommand(command: string): boolean {
|
||||
if(command === 'toggle-mode') {
|
||||
if (command === 'toggle-mode') {
|
||||
this.enableRichtext(!this.state.isRichtextEnabled);
|
||||
return true;
|
||||
}
|
||||
@ -384,7 +391,7 @@ export default class MessageComposerInput extends React.Component {
|
||||
let newState: ?EditorState = null;
|
||||
|
||||
// Draft handles rich text mode commands by default but we need to do it ourselves for Markdown.
|
||||
if(!this.state.isRichtextEnabled) {
|
||||
if (!this.state.isRichtextEnabled) {
|
||||
let contentState = this.state.editorState.getCurrentContent(),
|
||||
selection = this.state.editorState.getSelection();
|
||||
|
||||
@ -392,10 +399,10 @@ export default class MessageComposerInput extends React.Component {
|
||||
bold: text => `**${text}**`,
|
||||
italic: text => `*${text}*`,
|
||||
underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
|
||||
code: text => `\`${text}\``
|
||||
code: text => `\`${text}\``,
|
||||
}[command];
|
||||
|
||||
if(modifyFn) {
|
||||
if (modifyFn) {
|
||||
newState = EditorState.push(
|
||||
this.state.editorState,
|
||||
RichText.modifyText(contentState, selection, modifyFn),
|
||||
@ -404,23 +411,26 @@ export default class MessageComposerInput extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
if(newState == null)
|
||||
if (newState == null)
|
||||
newState = RichUtils.handleKeyCommand(this.state.editorState, command);
|
||||
|
||||
if (newState != null) {
|
||||
this.onChange(newState);
|
||||
this.setEditorState(newState);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
handleReturn(ev) {
|
||||
if(ev.shiftKey)
|
||||
if (ev.shiftKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const contentState = this.state.editorState.getCurrentContent();
|
||||
if(!contentState.hasText())
|
||||
if (!contentState.hasText()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
let contentText = contentState.getPlainText(), contentHTML;
|
||||
|
||||
@ -489,10 +499,47 @@ export default class MessageComposerInput extends React.Component {
|
||||
return true;
|
||||
}
|
||||
|
||||
onUpArrow(e) {
|
||||
if (this.props.onUpArrow && this.props.onUpArrow()) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
onDownArrow(e) {
|
||||
if (this.props.onDownArrow && this.props.onDownArrow()) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
onTab(e) {
|
||||
if (this.props.tryComplete) {
|
||||
if (this.props.tryComplete()) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onConfirmAutocompletion(range, content: string) {
|
||||
let contentState = Modifier.replaceText(
|
||||
this.state.editorState.getCurrentContent(),
|
||||
RichText.textOffsetsToSelectionState(range, this.state.editorState.getCurrentContent().getBlocksAsArray()),
|
||||
content
|
||||
);
|
||||
|
||||
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||
|
||||
editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
|
||||
|
||||
this.setEditorState(editorState);
|
||||
|
||||
// for some reason, doing this right away does not update the editor :(
|
||||
setTimeout(() => this.refs.editor.focus(), 50);
|
||||
}
|
||||
|
||||
render() {
|
||||
let className = "mx_MessageComposer_input";
|
||||
|
||||
if(this.state.isRichtextEnabled) {
|
||||
if (this.state.isRichtextEnabled) {
|
||||
className += " mx_MessageComposer_input_rte"; // placeholder indicator for RTE mode
|
||||
}
|
||||
|
||||
@ -502,11 +549,14 @@ export default class MessageComposerInput extends React.Component {
|
||||
<Editor ref="editor"
|
||||
placeholder="Type a message…"
|
||||
editorState={this.state.editorState}
|
||||
onChange={this.onChange}
|
||||
onChange={this.setEditorState}
|
||||
keyBindingFn={MessageComposerInput.getKeyBinding}
|
||||
handleKeyCommand={this.handleKeyCommand}
|
||||
handleReturn={this.handleReturn}
|
||||
stripPastedStyles={!this.state.isRichtextEnabled}
|
||||
onTab={this.onTab}
|
||||
onUpArrow={this.onUpArrow}
|
||||
onDownArrow={this.onDownArrow}
|
||||
spellCheck={true} />
|
||||
</div>
|
||||
);
|
||||
@ -521,5 +571,15 @@ MessageComposerInput.propTypes = {
|
||||
onResize: React.PropTypes.func,
|
||||
|
||||
// js-sdk Room object
|
||||
room: React.PropTypes.object.isRequired
|
||||
room: React.PropTypes.object.isRequired,
|
||||
|
||||
// called with current plaintext content (as a string) whenever it changes
|
||||
onContentChanged: React.PropTypes.func,
|
||||
|
||||
onUpArrow: React.PropTypes.func,
|
||||
|
||||
onDownArrow: React.PropTypes.func,
|
||||
|
||||
// attempts to confirm currently selected completion, returns whether actually confirmed
|
||||
tryComplete: React.PropTypes.func,
|
||||
};
|
||||
|
@ -163,13 +163,13 @@ module.exports = React.createClass({
|
||||
};
|
||||
|
||||
return (
|
||||
<Velociraptor>
|
||||
<Velociraptor
|
||||
startStyles={this.state.startStyles}
|
||||
enterTransitionOpts={this.state.enterTransitionOpts} >
|
||||
<MemberAvatar
|
||||
member={this.props.member}
|
||||
width={14} height={14} resizeMethod="crop"
|
||||
style={style}
|
||||
startStyle={this.state.startStyles}
|
||||
enterTransitionOpts={this.state.enterTransitionOpts}
|
||||
onClick={this.props.onClick}
|
||||
/>
|
||||
</Velociraptor>
|
||||
|
@ -24,6 +24,7 @@ var Modal = require("../../../Modal");
|
||||
var linkify = require('linkifyjs');
|
||||
var linkifyElement = require('linkifyjs/element');
|
||||
var linkifyMatrix = require('../../../linkify-matrix');
|
||||
import {emojifyText} from '../../../HtmlUtils';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
@ -211,13 +212,12 @@ module.exports = React.createClass({
|
||||
roomName = this.props.room.name;
|
||||
}
|
||||
|
||||
let roomNameHTML = emojifyText(roomName);
|
||||
|
||||
name =
|
||||
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
|
||||
<div className={ "mx_RoomHeader_nametext " + (settingsHint ? "mx_RoomHeader_settingsHint" : "") } title={ roomName }>{ roomName }</div>
|
||||
<div className={ "mx_RoomHeader_nametext " + (settingsHint ? "mx_RoomHeader_settingsHint" : "") } title={ roomName } dangerouslySetInnerHTML={roomNameHTML}></div>
|
||||
{ searchStatus }
|
||||
<div className="mx_RoomHeader_settingsButton" title="Settings">
|
||||
<TintableSvg src="img/settings.svg" width="12" height="12"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -263,10 +263,18 @@ module.exports = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
var settings_button;
|
||||
if (this.props.onSettingsClick) {
|
||||
settings_button =
|
||||
<div className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title="Settings">
|
||||
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
var leave_button;
|
||||
if (this.props.onLeaveClick) {
|
||||
leave_button =
|
||||
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton" onClick={this.props.onLeaveClick} title="Leave room">
|
||||
<div className="mx_RoomHeader_button" onClick={this.props.onLeaveClick} title="Leave room">
|
||||
<TintableSvg src="img/leave.svg" width="26" height="20"/>
|
||||
</div>;
|
||||
}
|
||||
@ -274,7 +282,7 @@ module.exports = React.createClass({
|
||||
var forget_button;
|
||||
if (this.props.onForgetClick) {
|
||||
forget_button =
|
||||
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton" onClick={this.props.onForgetClick} title="Forget room">
|
||||
<div className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title="Forget room">
|
||||
<TintableSvg src="img/leave.svg" width="26" height="20"/>
|
||||
</div>;
|
||||
}
|
||||
@ -288,10 +296,11 @@ module.exports = React.createClass({
|
||||
if (!this.props.editing) {
|
||||
right_row =
|
||||
<div className="mx_RoomHeader_rightRow">
|
||||
{ settings_button }
|
||||
{ forget_button }
|
||||
{ leave_button }
|
||||
<div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
|
||||
<TintableSvg src="img/search.svg" width="21" height="19"/>
|
||||
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
|
||||
</div>
|
||||
{ rightPanel_buttons }
|
||||
</div>;
|
||||
|
@ -268,9 +268,11 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
_repositionTooltip: function(e) {
|
||||
if (this.tooltip && this.tooltip.parentElement) {
|
||||
// We access the parent of the parent, as the tooltip is inside a container
|
||||
// Needs refactoring into a better multipurpose tooltip
|
||||
if (this.tooltip && this.tooltip.parentElement && this.tooltip.parentElement.parentElement) {
|
||||
var scroll = ReactDOM.findDOMNode(this);
|
||||
this.tooltip.style.top = (70 + scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px";
|
||||
this.tooltip.style.top = (3 + scroll.parentElement.offsetTop + this.tooltip.parentElement.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px";
|
||||
}
|
||||
},
|
||||
|
||||
@ -325,7 +327,6 @@ module.exports = React.createClass({
|
||||
|
||||
return (
|
||||
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
||||
relayoutOnUpdate={false}
|
||||
autoshow={true} onScroll={ self._repositionTooltips } ref="gemscroll">
|
||||
<div className="mx_RoomList">
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
|
||||
|
@ -33,16 +33,24 @@ module.exports = React.createClass({
|
||||
|
||||
// If invited by 3rd party invite, the email address the invite was sent to
|
||||
invitedEmail: React.PropTypes.string,
|
||||
canJoin: React.PropTypes.bool,
|
||||
|
||||
// A standard client/server API error object. If supplied, indicates that the
|
||||
// caller was unable to fetch details about the room for the given reason.
|
||||
error: React.PropTypes.object,
|
||||
|
||||
canPreview: React.PropTypes.bool,
|
||||
spinner: React.PropTypes.bool,
|
||||
room: React.PropTypes.object,
|
||||
|
||||
// The alias that was used to access this room, if appropriate
|
||||
// If given, this will be how the room is referred to (eg.
|
||||
// in error messages).
|
||||
roomAlias: React.PropTypes.object,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onJoinClick: function() {},
|
||||
canJoin: false,
|
||||
canPreview: true,
|
||||
};
|
||||
},
|
||||
@ -115,8 +123,24 @@ module.exports = React.createClass({
|
||||
);
|
||||
|
||||
}
|
||||
else if (this.props.canJoin) {
|
||||
var name = this.props.room ? this.props.room.name : "";
|
||||
else if (this.props.error) {
|
||||
var name = this.props.roomAlias || "This room";
|
||||
var error;
|
||||
if (this.props.error.errcode == 'M_NOT_FOUND') {
|
||||
error = name + " does not exist";
|
||||
} else {
|
||||
error = name + " is not accessible at this time";
|
||||
}
|
||||
joinBlock = (
|
||||
<div>
|
||||
<div className="mx_RoomPreviewBar_join_text">
|
||||
{ error }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
var name = this.props.room ? this.props.room.name : (this.props.room_alias || "");
|
||||
name = name ? <b>{ name }</b> : "a room";
|
||||
joinBlock = (
|
||||
<div>
|
||||
|
@ -23,6 +23,14 @@ var Modal = require('../../../Modal');
|
||||
var ObjectUtils = require("../../../ObjectUtils");
|
||||
var dis = require("../../../dispatcher");
|
||||
var ScalarAuthClient = require("../../../ScalarAuthClient");
|
||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||
|
||||
// parse a string as an integer; if the input is undefined, or cannot be parsed
|
||||
// as an integer, return a default.
|
||||
function parseIntWithDefault(val, def) {
|
||||
var res = parseInt(val);
|
||||
return isNaN(res) ? def : res;
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomSettings',
|
||||
@ -59,9 +67,18 @@ module.exports = React.createClass({
|
||||
tags_changed: false,
|
||||
tags: tags,
|
||||
areNotifsMuted: areNotifsMuted,
|
||||
<<<<<<< HEAD
|
||||
isRoomPublished: this._originalIsRoomPublished, // loaded async in componentWillMount
|
||||
scalar_token: null,
|
||||
scalar_error: null,
|
||||
=======
|
||||
// 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,
|
||||
>>>>>>> develop
|
||||
};
|
||||
},
|
||||
|
||||
@ -209,11 +226,17 @@ module.exports = React.createClass({
|
||||
}
|
||||
});
|
||||
}
|
||||
console.log("Performing %s operations", promises.length);
|
||||
|
||||
// color scheme
|
||||
promises.push(this.saveColor());
|
||||
|
||||
// url preview settings
|
||||
promises.push(this.saveUrlPreviewSettings());
|
||||
|
||||
// encryption
|
||||
promises.push(this.saveEncryption());
|
||||
|
||||
console.log("Performing %s operations: %s", promises.length, JSON.stringify(promises));
|
||||
return q.allSettled(promises);
|
||||
},
|
||||
|
||||
@ -227,6 +250,24 @@ module.exports = React.createClass({
|
||||
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 () {
|
||||
if (!this.refs.encrypt) { return q(); }
|
||||
|
||||
var encrypt = this.refs.encrypt.checked;
|
||||
if (!encrypt) { return q(); }
|
||||
|
||||
var roomId = this.props.room.roomId;
|
||||
return MatrixClientPeg.get().sendStateEvent(
|
||||
roomId, "m.room.encryption",
|
||||
{ algorithm: "m.olm.v1.curve25519-aes-sha2" }
|
||||
);
|
||||
},
|
||||
|
||||
_hasDiff: function(strA, strB) {
|
||||
// treat undefined as an empty string because other components may blindly
|
||||
// call setName("") when there has been no diff made to the name!
|
||||
@ -261,7 +302,7 @@ module.exports = React.createClass({
|
||||
power_levels_changed: true
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
_yankValueFromEvent: function(stateEventType, keyName, defaultValue) {
|
||||
// E.g.("m.room.name","name") would yank the "name" content key from "m.room.name"
|
||||
var event = this.props.room.currentState.getStateEvents(stateEventType, '');
|
||||
@ -296,7 +337,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
_onRoomAccessRadioToggle: function(ev) {
|
||||
|
||||
// join_rule
|
||||
@ -400,68 +441,72 @@ module.exports = React.createClass({
|
||||
}, "");
|
||||
},
|
||||
|
||||
_renderEncryptionSection: function() {
|
||||
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var cli = MatrixClientPeg.get();
|
||||
var roomState = this.props.room.currentState;
|
||||
var isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
|
||||
|
||||
var text = "Encryption is " + (isEncrypted ? "" : "not ") +
|
||||
"enabled in this room.";
|
||||
|
||||
var button;
|
||||
if (!isEncrypted &&
|
||||
roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
|
||||
button = (
|
||||
<label>
|
||||
<input type="checkbox" ref="encrypt" />
|
||||
Enable encryption (warning: cannot be disabled again!)
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSettings_toggles">
|
||||
<h3>Encryption</h3>
|
||||
<label>{text}</label>
|
||||
{button}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// TODO: go through greying out things you don't have permission to change
|
||||
// (or turning them into informative stuff)
|
||||
|
||||
var AliasSettings = sdk.getComponent("room_settings.AliasSettings");
|
||||
var ColorSettings = sdk.getComponent("room_settings.ColorSettings");
|
||||
var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
||||
var EditableText = sdk.getComponent('elements.EditableText');
|
||||
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
var Loader = sdk.getComponent("elements.Spinner")
|
||||
|
||||
var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
|
||||
var events_levels = (power_levels ? power_levels.getContent().events : {}) || {};
|
||||
var cli = MatrixClientPeg.get();
|
||||
var roomState = this.props.room.currentState;
|
||||
var user_id = cli.credentials.userId;
|
||||
|
||||
if (power_levels) {
|
||||
power_levels = power_levels.getContent();
|
||||
var power_level_event = roomState.getStateEvents('m.room.power_levels', '');
|
||||
var power_levels = power_level_event ? power_level_event.getContent() : {};
|
||||
var events_levels = power_levels.events || {};
|
||||
var user_levels = power_levels.users || {};
|
||||
|
||||
var ban_level = parseInt(power_levels.ban);
|
||||
var kick_level = parseInt(power_levels.kick);
|
||||
var redact_level = parseInt(power_levels.redact);
|
||||
var invite_level = parseInt(power_levels.invite || 0);
|
||||
var send_level = parseInt(power_levels.events_default || 0);
|
||||
var state_level = parseInt(power_levels.state_default || 50);
|
||||
var default_user_level = parseInt(power_levels.users_default || 0);
|
||||
var ban_level = parseIntWithDefault(power_levels.ban, 50);
|
||||
var kick_level = parseIntWithDefault(power_levels.kick, 50);
|
||||
var redact_level = parseIntWithDefault(power_levels.redact, 50);
|
||||
var invite_level = parseIntWithDefault(power_levels.invite, 50);
|
||||
var send_level = parseIntWithDefault(power_levels.events_default, 0);
|
||||
var state_level = power_level_event ? parseIntWithDefault(power_levels.state_default, 50) : 0;
|
||||
var default_user_level = parseIntWithDefault(power_levels.users_default, 0);
|
||||
|
||||
if (power_levels.ban == undefined) ban_level = 50;
|
||||
if (power_levels.kick == undefined) kick_level = 50;
|
||||
if (power_levels.redact == undefined) redact_level = 50;
|
||||
|
||||
var user_levels = power_levels.users || {};
|
||||
|
||||
var current_user_level = user_levels[user_id];
|
||||
if (current_user_level == undefined) current_user_level = default_user_level;
|
||||
|
||||
var power_level_level = events_levels["m.room.power_levels"];
|
||||
if (power_level_level == undefined) {
|
||||
power_level_level = state_level;
|
||||
}
|
||||
|
||||
var can_change_levels = current_user_level >= power_level_level;
|
||||
} else {
|
||||
var ban_level = 50;
|
||||
var kick_level = 50;
|
||||
var redact_level = 50;
|
||||
var invite_level = 0;
|
||||
var send_level = 0;
|
||||
var state_level = 0;
|
||||
var default_user_level = 0;
|
||||
|
||||
var user_levels = [];
|
||||
var events_levels = [];
|
||||
|
||||
var current_user_level = 0;
|
||||
|
||||
var power_level_level = 0;
|
||||
|
||||
var can_change_levels = false;
|
||||
var current_user_level = user_levels[user_id];
|
||||
if (current_user_level === undefined) {
|
||||
current_user_level = default_user_level;
|
||||
}
|
||||
|
||||
var state_default = (parseInt(power_levels ? power_levels.state_default : 0) || 0);
|
||||
var can_change_levels = roomState.mayClientSendStateEvent("m.room.power_levels", cli);
|
||||
|
||||
var canSetTag = !cli.isGuest();
|
||||
|
||||
@ -530,7 +575,7 @@ module.exports = React.createClass({
|
||||
|
||||
var tagsSection = null;
|
||||
if (canSetTag || self.state.tags) {
|
||||
var tagsSection =
|
||||
var tagsSection =
|
||||
<div className="mx_RoomSettings_tags">
|
||||
Tagged as: { canSetTag ?
|
||||
(tags.map(function(tag, i) {
|
||||
@ -666,10 +711,6 @@ module.exports = React.createClass({
|
||||
Members only (since they joined)
|
||||
</label>
|
||||
</div>
|
||||
<label className="mx_RoomSettings_encrypt">
|
||||
<input type="checkbox" />
|
||||
Encrypt room
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
@ -690,6 +731,8 @@ module.exports = React.createClass({
|
||||
canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')}
|
||||
aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} />
|
||||
|
||||
<UrlPreviewSettings ref="url_preview_settings" room={this.props.room} />
|
||||
|
||||
<h3>Permissions</h3>
|
||||
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
@ -737,6 +780,8 @@ module.exports = React.createClass({
|
||||
|
||||
{ bannedUsersSection }
|
||||
|
||||
{ this._renderEncryptionSection() }
|
||||
|
||||
<h3>Advanced</h3>
|
||||
<div className="mx_RoomSettings_settings">
|
||||
This room's internal ID is <code>{ this.props.room.roomId }</code>
|
||||
|
@ -21,6 +21,8 @@ var classNames = require('classnames');
|
||||
var dis = require("../../../dispatcher");
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var sdk = require('../../../index');
|
||||
var ContextualMenu = require('../../structures/ContextualMenu');
|
||||
import {emojifyText} from '../../../HtmlUtils';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomTile',
|
||||
@ -42,13 +44,48 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return( { hover : false });
|
||||
var areNotifsMuted = false;
|
||||
var cli = MatrixClientPeg.get();
|
||||
if (!cli.isGuest()) {
|
||||
var roomPushRule = cli.getRoomPushRule("global", this.props.room.roomId);
|
||||
if (roomPushRule) {
|
||||
if (0 <= roomPushRule.actions.indexOf("dont_notify")) {
|
||||
areNotifsMuted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return({
|
||||
hover : false,
|
||||
badgeHover : false,
|
||||
menu: false,
|
||||
areNotifsMuted: areNotifsMuted,
|
||||
});
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
switch (payload.action) {
|
||||
case 'notification_change':
|
||||
// Is the notification about this room?
|
||||
if (payload.roomId === this.props.room.roomId) {
|
||||
this.setState( { areNotifsMuted : payload.isMuted });
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
onClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.room.roomId
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
},
|
||||
|
||||
@ -60,6 +97,48 @@ module.exports = React.createClass({
|
||||
this.setState( { hover : false });
|
||||
},
|
||||
|
||||
badgeOnMouseEnter: function() {
|
||||
// Only allow none guests to access the context menu
|
||||
// and only change it if it needs to change
|
||||
if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) {
|
||||
this.setState( { badgeHover : true } );
|
||||
}
|
||||
},
|
||||
|
||||
badgeOnMouseLeave: function() {
|
||||
this.setState( { badgeHover : false } );
|
||||
},
|
||||
|
||||
onBadgeClicked: function(e) {
|
||||
// Only allow none guests to access the context menu
|
||||
if (!MatrixClientPeg.get().isGuest()) {
|
||||
|
||||
// If the badge is clicked, then no longer show tooltip
|
||||
if (this.props.collapsed) {
|
||||
this.setState({ hover: false });
|
||||
}
|
||||
|
||||
var Menu = sdk.getComponent('context_menus.NotificationStateContextMenu');
|
||||
var elementRect = e.target.getBoundingClientRect();
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
var x = elementRect.right + window.pageXOffset + 3;
|
||||
var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 53;
|
||||
var self = this;
|
||||
ContextualMenu.createMenu(Menu, {
|
||||
menuWidth: 188,
|
||||
menuHeight: 126,
|
||||
chevronOffset: 45,
|
||||
left: x,
|
||||
top: y,
|
||||
room: this.props.room,
|
||||
onFinished: function() {
|
||||
self.setState({ menu: false });
|
||||
}
|
||||
});
|
||||
this.setState({ menu: true });
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
var me = this.props.room.currentState.members[myUserId];
|
||||
@ -72,42 +151,64 @@ module.exports = React.createClass({
|
||||
'mx_RoomTile_selected': this.props.selected,
|
||||
'mx_RoomTile_unread': this.props.unread,
|
||||
'mx_RoomTile_unreadNotify': notificationCount > 0,
|
||||
'mx_RoomTile_read': !(this.props.highlight || notificationCount > 0),
|
||||
'mx_RoomTile_highlight': this.props.highlight,
|
||||
'mx_RoomTile_invited': (me && me.membership == 'invite'),
|
||||
'mx_RoomTile_menu': this.state.menu,
|
||||
});
|
||||
|
||||
var avatarClasses = classNames({
|
||||
'mx_RoomTile_avatar': true,
|
||||
'mx_RoomTile_mute': this.state.areNotifsMuted,
|
||||
});
|
||||
|
||||
var badgeClasses = classNames({
|
||||
'mx_RoomTile_badge': true,
|
||||
'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menu,
|
||||
'mx_RoomTile_badgeMute': this.state.areNotifsMuted,
|
||||
});
|
||||
|
||||
// XXX: We should never display raw room IDs, but sometimes the
|
||||
// room name js sdk gives is undefined (cannot repro this -- k)
|
||||
var name = this.props.room.name || this.props.room.roomId;
|
||||
|
||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
||||
var badge;
|
||||
if (this.props.highlight || notificationCount > 0) {
|
||||
badge = <div className="mx_RoomTile_badge">{ notificationCount ? notificationCount : '!' }</div>;
|
||||
var badgeContent;
|
||||
|
||||
if (this.state.badgeHover || this.state.menu) {
|
||||
badgeContent = "\u00B7\u00B7\u00B7";
|
||||
} else if (this.props.highlight || notificationCount > 0) {
|
||||
var limitedCount = (notificationCount > 99) ? '99+' : notificationCount;
|
||||
badgeContent = notificationCount ? limitedCount : '!';
|
||||
} else {
|
||||
badgeContent = '\u200B';
|
||||
}
|
||||
/*
|
||||
if (this.props.highlight) {
|
||||
badge = <div className="mx_RoomTile_badge">!</div>;
|
||||
|
||||
if (this.state.areNotifsMuted && !(this.state.badgeHover || this.state.menu)) {
|
||||
badge = <div className={ badgeClasses } onClick={this.onBadgeClicked} onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}><img className="mx_RoomTile_badgeIcon" src="img/icon-context-mute.svg" width="16" height="12" /></div>;
|
||||
} else {
|
||||
badge = <div className={ badgeClasses } onClick={this.onBadgeClicked} onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}>{ badgeContent }</div>;
|
||||
}
|
||||
else if (this.props.unread) {
|
||||
badge = <div className="mx_RoomTile_badge">1</div>;
|
||||
}
|
||||
var nameCell;
|
||||
if (badge) {
|
||||
nameCell = <div className="mx_RoomTile_nameBadge"><div className="mx_RoomTile_name">{name}</div><div className="mx_RoomTile_badgeCell">{badge}</div></div>;
|
||||
}
|
||||
else {
|
||||
nameCell = <div className="mx_RoomTile_name">{name}</div>;
|
||||
}
|
||||
*/
|
||||
|
||||
var label;
|
||||
var tooltip;
|
||||
if (!this.props.collapsed) {
|
||||
var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : '');
|
||||
var nameClasses = classNames({
|
||||
'mx_RoomTile_name': true,
|
||||
'mx_RoomTile_invite': this.props.isInvite,
|
||||
'mx_RoomTile_mute': this.state.areNotifsMuted,
|
||||
'mx_RoomTile_badgeShown': this.props.highlight || notificationCount > 0 || this.state.badgeHover || this.state.menu || this.state.areNotifsMuted,
|
||||
});
|
||||
|
||||
let nameHTML = emojifyText(name);
|
||||
if (this.props.selected) {
|
||||
name = <span>{ name }</span>;
|
||||
let nameSelected = <span dangerouslySetInnerHTML={nameHTML}></span>;
|
||||
|
||||
label = <div title={ name } onClick={this.onClick} className={ nameClasses }>{ nameSelected }</div>;
|
||||
} else {
|
||||
label = <div title={ name } onClick={this.onClick} className={ nameClasses } dangerouslySetInnerHTML={nameHTML}></div>;
|
||||
}
|
||||
label = <div className={ className }>{ name }</div>;
|
||||
}
|
||||
else if (this.state.hover) {
|
||||
var RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
@ -129,13 +230,16 @@ module.exports = React.createClass({
|
||||
var connectDropTarget = this.props.connectDropTarget;
|
||||
|
||||
return connectDragSource(connectDropTarget(
|
||||
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
<div className={classes} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||
<div className={avatarClasses}>
|
||||
<RoomAvatar onClick={this.onClick} room={this.props.room} width={24} height={24} />
|
||||
</div>
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
{ label }
|
||||
{ badge }
|
||||
</div>
|
||||
{ label }
|
||||
{ badge }
|
||||
{ incomingCallBox }
|
||||
{ tooltip }
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
@ -179,7 +179,6 @@ var SearchableEntityList = React.createClass({
|
||||
}
|
||||
list = (
|
||||
<GeminiScrollbar autoshow={true}
|
||||
relayoutOnUpdate={false}
|
||||
className="mx_SearchableEntityList_listWrapper">
|
||||
{ list }
|
||||
</GeminiScrollbar>
|
||||
|
@ -24,17 +24,17 @@ module.exports = React.createClass({
|
||||
displayName: 'TabCompleteBar',
|
||||
|
||||
propTypes: {
|
||||
entries: React.PropTypes.array.isRequired
|
||||
tabComplete: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_TabCompleteBar">
|
||||
{this.props.entries.map(function(entry, i) {
|
||||
{this.props.tabComplete.peek(6).map((entry, i) => {
|
||||
return (
|
||||
<div key={entry.getKey() || i + ""}
|
||||
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
|
||||
onClick={entry.onClick.bind(entry)} >
|
||||
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
|
||||
onClick={this.props.tabComplete.onEntryClick.bind(this.props.tabComplete, entry)} >
|
||||
{entry.getImageJsx()}
|
||||
<span className="mx_TabCompleteBar_text">
|
||||
{entry.getText()}
|
||||
|
@ -21,29 +21,10 @@ var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ChangeDisplayName',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onFinished: function() {},
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
busy: false,
|
||||
errorString: null
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
_getDisplayName: function() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
this.setState({busy: true});
|
||||
var self = this;
|
||||
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
|
||||
|
||||
return cli.getProfileInfo(cli.credentials.userId).then(function(result) {
|
||||
var displayname = result.displayname;
|
||||
if (!displayname) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
@ -53,68 +34,26 @@ module.exports = React.createClass({
|
||||
displayname = MatrixClientPeg.get().getUserIdLocalpart();
|
||||
}
|
||||
}
|
||||
|
||||
self.setState({
|
||||
displayName: displayname,
|
||||
busy: false
|
||||
});
|
||||
return displayname;
|
||||
}, function(error) {
|
||||
self.setState({
|
||||
errorString: "Failed to fetch display name",
|
||||
busy: false
|
||||
});
|
||||
throw new Error("Failed to fetch display name");
|
||||
});
|
||||
},
|
||||
|
||||
changeDisplayname: function(new_displayname) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
errorString: null,
|
||||
})
|
||||
|
||||
var self = this;
|
||||
MatrixClientPeg.get().setDisplayName(new_displayname).then(function() {
|
||||
self.setState({
|
||||
busy: false,
|
||||
displayName: new_displayname
|
||||
});
|
||||
}, function(error) {
|
||||
self.setState({
|
||||
busy: false,
|
||||
errorString: "Failed to set display name"
|
||||
});
|
||||
_changeDisplayName: function(new_displayname) {
|
||||
var cli = MatrixClientPeg.get();
|
||||
return cli.setDisplayName(new_displayname).catch(function(e) {
|
||||
throw new Error("Failed to set display name");
|
||||
});
|
||||
},
|
||||
|
||||
edit: function() {
|
||||
this.refs.displayname_edit.edit()
|
||||
},
|
||||
|
||||
onValueChanged: function(new_value, shouldSubmit) {
|
||||
if (shouldSubmit) {
|
||||
this.changeDisplayname(new_value);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.busy) {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<Loader />
|
||||
);
|
||||
} else if (this.state.errorString) {
|
||||
return (
|
||||
<div className="error">{this.state.errorString}</div>
|
||||
);
|
||||
} else {
|
||||
var EditableText = sdk.getComponent('elements.EditableText');
|
||||
return (
|
||||
<EditableText ref="displayname_edit" initialValue={this.state.displayName}
|
||||
className="mx_EditableText"
|
||||
placeholderClassName="mx_EditableText_placeholder"
|
||||
placeholder="No display name"
|
||||
onValueChanged={this.onValueChanged} />
|
||||
);
|
||||
}
|
||||
var EditableTextContainer = sdk.getComponent('elements.EditableTextContainer');
|
||||
return (
|
||||
<EditableTextContainer
|
||||
getInitialValue={this._getDisplayName}
|
||||
placeholder="No display name"
|
||||
onSubmit={this._changeDisplayName} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
138
src/components/views/settings/DevicesPanel.js
Normal file
138
src/components/views/settings/DevicesPanel.js
Normal file
@ -0,0 +1,138 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
|
||||
export default class DevicesPanel extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
devices: undefined,
|
||||
deviceLoadError: undefined,
|
||||
};
|
||||
|
||||
this._unmounted = false;
|
||||
|
||||
this._renderDevice = this._renderDevice.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._loadDevices();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
_loadDevices() {
|
||||
MatrixClientPeg.get().getDevices().done(
|
||||
(resp) => {
|
||||
if (this._unmounted) { return; }
|
||||
this.setState({devices: resp.devices || []});
|
||||
},
|
||||
(error) => {
|
||||
if (this._unmounted) { return; }
|
||||
var errtxt;
|
||||
if (err.httpStatus == 404) {
|
||||
// 404 probably means the HS doesn't yet support the API.
|
||||
errtxt = "Your home server does not support device management.";
|
||||
} else {
|
||||
console.error("Error loading devices:", error);
|
||||
errtxt = "Unable to load device list.";
|
||||
}
|
||||
this.setState({deviceLoadError: errtxt});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* compare two devices, sorting from most-recently-seen to least-recently-seen
|
||||
* (and then, for stability, by device id)
|
||||
*/
|
||||
_deviceCompare(a, b) {
|
||||
// return < 0 if a comes before b, > 0 if a comes after b.
|
||||
const lastSeenDelta =
|
||||
(b.last_seen_ts || 0) - (a.last_seen_ts || 0);
|
||||
|
||||
if (lastSeenDelta !== 0) { return lastSeenDelta; }
|
||||
|
||||
const idA = a.device_id;
|
||||
const idB = b.device_id;
|
||||
return (idA < idB) ? -1 : (idA > idB) ? 1 : 0;
|
||||
}
|
||||
|
||||
_onDeviceDeleted(device) {
|
||||
if (this._unmounted) { return; }
|
||||
|
||||
// delete the removed device from our list.
|
||||
const removed_id = device.device_id;
|
||||
this.setState((state, props) => {
|
||||
const newDevices = state.devices.filter(
|
||||
d => { return d.device_id != removed_id }
|
||||
);
|
||||
return { devices: newDevices };
|
||||
});
|
||||
}
|
||||
|
||||
_renderDevice(device) {
|
||||
var DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry');
|
||||
return (
|
||||
<DevicesPanelEntry key={device.device_id} device={device}
|
||||
onDeleted={()=>{this._onDeviceDeleted(device)}} />
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
if (this.state.deviceLoadError !== undefined) {
|
||||
const classes = classNames(this.props.className, "error");
|
||||
return (
|
||||
<div className={classes}>
|
||||
{this.state.deviceLoadError}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const devices = this.state.devices;
|
||||
if (devices === undefined) {
|
||||
// still loading
|
||||
const classes = this.props.className;
|
||||
return <Spinner className={classes}/>;
|
||||
}
|
||||
|
||||
devices.sort(this._deviceCompare);
|
||||
|
||||
const classes = classNames(this.props.className, "mx_DevicesPanel");
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_DevicesPanel_header">
|
||||
<div className="mx_DevicesPanel_deviceName">Name</div>
|
||||
<div className="mx_DevicesPanel_deviceLastSeen">Last seen</div>
|
||||
<div className="mx_DevicesPanel_deviceButtons"></div>
|
||||
</div>
|
||||
{devices.map(this._renderDevice)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
136
src/components/views/settings/DevicesPanelEntry.js
Normal file
136
src/components/views/settings/DevicesPanelEntry.js
Normal file
@ -0,0 +1,136 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import q from 'q';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import DateUtils from '../../../DateUtils';
|
||||
|
||||
export default class DevicesPanelEntry extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
deleting: false,
|
||||
deleteError: undefined,
|
||||
};
|
||||
|
||||
this._unmounted = false;
|
||||
|
||||
this._onDeleteClick = this._onDeleteClick.bind(this);
|
||||
this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
_onDisplayNameChanged(value) {
|
||||
const device = this.props.device;
|
||||
return MatrixClientPeg.get().setDeviceDetails(device.device_id, {
|
||||
display_name: value,
|
||||
}).catch((e) => {
|
||||
console.error("Error setting device display name", e);
|
||||
throw new Error("Failed to set display name");
|
||||
});
|
||||
}
|
||||
|
||||
_onDeleteClick() {
|
||||
const device = this.props.device;
|
||||
this.setState({deleting: true});
|
||||
|
||||
MatrixClientPeg.get().deleteDevice(device.device_id).done(
|
||||
() => {
|
||||
this.props.onDeleted();
|
||||
if (this._unmounted) { return; }
|
||||
this.setState({ deleting: false });
|
||||
},
|
||||
(e) => {
|
||||
console.error("Error deleting device", e);
|
||||
if (this._unmounted) { return; }
|
||||
this.setState({
|
||||
deleting: false,
|
||||
deleteError: "Failed to delete device",
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const EditableTextContainer = sdk.getComponent('elements.EditableTextContainer');
|
||||
|
||||
const device = this.props.device;
|
||||
|
||||
if (this.state.deleting) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
return (
|
||||
<div className="mx_DevicesPanel_device">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let lastSeen = "";
|
||||
if (device.last_seen_ts) {
|
||||
// todo: format the timestamp as "5 minutes ago" or whatever.
|
||||
const lastSeenDate = new Date(device.last_seen_ts);
|
||||
lastSeen = device.last_seen_ip + " @ " +
|
||||
lastSeenDate.toLocaleString();
|
||||
}
|
||||
|
||||
let deleteButton;
|
||||
if (this.state.deleteError) {
|
||||
deleteButton = <div className="error">{this.state.deleteError}</div>
|
||||
} else {
|
||||
deleteButton = (
|
||||
<div className="textButton"
|
||||
onClick={this._onDeleteClick}>
|
||||
Delete
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_DevicesPanel_device">
|
||||
<div className="mx_DevicesPanel_deviceName">
|
||||
<EditableTextContainer initialValue={device.display_name}
|
||||
onSubmit={this._onDisplayNameChanged}
|
||||
placeholder={device.device_id}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_DevicesPanel_lastSeen">
|
||||
{lastSeen}
|
||||
</div>
|
||||
<div className="mx_DevicesPanel_deviceButtons">
|
||||
{deleteButton}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DevicesPanelEntry.propTypes = {
|
||||
device: React.PropTypes.object.isRequired,
|
||||
onDeleted: React.PropTypes.func,
|
||||
};
|
||||
|
||||
DevicesPanelEntry.defaultProps = {
|
||||
onDeleted: function() {},
|
||||
};
|
@ -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() {
|
||||
modal.close();
|
||||
}).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({
|
||||
action: 'view_room',
|
||||
room_id: res.room_id
|
||||
|
@ -1,38 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 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.
|
||||
*/
|
||||
|
||||
function enableEncyption(client, roomId, members) {
|
||||
members = members.slice(0);
|
||||
members.push(client.credentials.userId);
|
||||
// TODO: Check the keys actually match what keys the user has.
|
||||
// TODO: Don't redownload keys each time.
|
||||
return client.downloadKeys(members, "forceDownload").then(function(res) {
|
||||
return client.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
members: members,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function disableEncryption(client, roomId) {
|
||||
return client.disableRoomEncryption(roomId);
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
enableEncryption: enableEncyption,
|
||||
disableEncryption: disableEncryption,
|
||||
}
|
@ -14,6 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 'debounces' a function to only execute every n milliseconds.
|
||||
* Useful when react-sdk gets many, many events but only wants
|
||||
* to update the interface once for all of them.
|
||||
*
|
||||
* Note that the function must not take arguments, since the args
|
||||
* could be different for each invocarion of the function.
|
||||
*/
|
||||
module.exports = function(f, minIntervalMs) {
|
||||
this.lastCall = 0;
|
||||
this.scheduledCall = undefined;
|
||||
|
@ -210,7 +210,7 @@ describe('TimelinePanel', function() {
|
||||
var N_EVENTS = 600;
|
||||
|
||||
// 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
|
||||
// sinon's spy with a fast noop.
|
||||
@ -220,12 +220,14 @@ describe('TimelinePanel', function() {
|
||||
for (var i = 0; i < N_EVENTS; i++) {
|
||||
timeline.addEvent(mkMessage());
|
||||
}
|
||||
console.log("added events to timeline");
|
||||
|
||||
var scrollDefer;
|
||||
var panel = ReactDOM.render(
|
||||
<TimelinePanel room={room} onScroll={()=>{scrollDefer.resolve()}} />,
|
||||
<TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}} />,
|
||||
parentDiv
|
||||
);
|
||||
console.log("TimelinePanel rendered");
|
||||
|
||||
var messagePanel = ReactTestUtils.findRenderedComponentWithType(
|
||||
panel, sdk.getComponent('structures.MessagePanel'));
|
||||
@ -236,16 +238,29 @@ describe('TimelinePanel', function() {
|
||||
// the TimelinePanel fires a scroll event
|
||||
var awaitScroll = function() {
|
||||
scrollDefer = q.defer();
|
||||
return scrollDefer.promise;
|
||||
return scrollDefer.promise.then(() => {
|
||||
console.log("got scroll event; scrollTop now " +
|
||||
scrollingDiv.scrollTop);
|
||||
});
|
||||
};
|
||||
|
||||
function setScrollTop(scrollTop) {
|
||||
const before = scrollingDiv.scrollTop;
|
||||
scrollingDiv.scrollTop = scrollTop;
|
||||
console.log("setScrollTop: before update: " + before +
|
||||
"; assigned: " + scrollTop +
|
||||
"; after update: " + scrollingDiv.scrollTop);
|
||||
}
|
||||
|
||||
function backPaginate() {
|
||||
scrollingDiv.scrollTop = 0;
|
||||
console.log("back paginating...");
|
||||
setScrollTop(0);
|
||||
return awaitScroll().then(() => {
|
||||
if(scrollingDiv.scrollTop > 0) {
|
||||
// need to go further
|
||||
return backPaginate();
|
||||
}
|
||||
console.log("paginated to start.");
|
||||
|
||||
// hopefully, we got to the start of the timeline
|
||||
expect(messagePanel.props.backPaginating).toBe(false);
|
||||
@ -267,6 +282,7 @@ describe('TimelinePanel', function() {
|
||||
|
||||
// we should now be able to scroll down, and paginate in the other
|
||||
// direction.
|
||||
setScrollTop(scrollingDiv.scrollHeight);
|
||||
scrollingDiv.scrollTop = scrollingDiv.scrollHeight;
|
||||
return awaitScroll();
|
||||
}).then(() => {
|
||||
|
@ -51,7 +51,7 @@ module.exports.stubClient = function() {
|
||||
// 'sandbox.restore()' doesn't work correctly on inherited methods,
|
||||
// so we do this for each method
|
||||
var methods = ['get', 'unset', 'replaceUsingUrls',
|
||||
'replaceUsingAccessToken'];
|
||||
'replaceUsingCreds'];
|
||||
for (var i = 0; i < methods.length; i++) {
|
||||
sandbox.stub(peg, methods[i]);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user