mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-18 06:35:35 +08:00
Merge branch 'develop' into new-guest-access
Conflicts: src/components/structures/MatrixChat.js
This commit is contained in:
commit
05aaa599cc
151
CHANGELOG.md
151
CHANGELOG.md
@ -1,3 +1,154 @@
|
|||||||
|
Changes in [0.8.9](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.9) (2017-05-22)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.9-rc.1...v0.8.9)
|
||||||
|
|
||||||
|
* No changes
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [0.8.9-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.9-rc.1) (2017-05-19)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8...v0.8.9-rc.1)
|
||||||
|
|
||||||
|
* Prevent an exception getting scroll node
|
||||||
|
[\#902](https://github.com/matrix-org/matrix-react-sdk/pull/902)
|
||||||
|
* Fix a few remaining snags with country dd
|
||||||
|
[\#901](https://github.com/matrix-org/matrix-react-sdk/pull/901)
|
||||||
|
* Add left_aligned class to CountryDropdown
|
||||||
|
[\#900](https://github.com/matrix-org/matrix-react-sdk/pull/900)
|
||||||
|
* Swap to new flag files (which are stored as GB.png)
|
||||||
|
[\#899](https://github.com/matrix-org/matrix-react-sdk/pull/899)
|
||||||
|
* Improve phone number country dropdown for registration and login (Act. 2,
|
||||||
|
Return of the Prefix)
|
||||||
|
[\#897](https://github.com/matrix-org/matrix-react-sdk/pull/897)
|
||||||
|
* Support for pasting files into normal composer
|
||||||
|
[\#892](https://github.com/matrix-org/matrix-react-sdk/pull/892)
|
||||||
|
* tell guests they can't use filepanel until they register
|
||||||
|
[\#887](https://github.com/matrix-org/matrix-react-sdk/pull/887)
|
||||||
|
* Prevent reskindex -w from running when file names have not changed
|
||||||
|
[\#888](https://github.com/matrix-org/matrix-react-sdk/pull/888)
|
||||||
|
* I broke UserSettings for webpack-dev-server
|
||||||
|
[\#884](https://github.com/matrix-org/matrix-react-sdk/pull/884)
|
||||||
|
* various fixes to RoomHeader
|
||||||
|
[\#880](https://github.com/matrix-org/matrix-react-sdk/pull/880)
|
||||||
|
* remove /me whether or not it has a space after it
|
||||||
|
[\#885](https://github.com/matrix-org/matrix-react-sdk/pull/885)
|
||||||
|
* show error if we can't set a filter because no room
|
||||||
|
[\#883](https://github.com/matrix-org/matrix-react-sdk/pull/883)
|
||||||
|
* Fix RM not updating if RR event unpaginated
|
||||||
|
[\#874](https://github.com/matrix-org/matrix-react-sdk/pull/874)
|
||||||
|
* change roomsettings wording
|
||||||
|
[\#878](https://github.com/matrix-org/matrix-react-sdk/pull/878)
|
||||||
|
* make reskindex windows friendly
|
||||||
|
[\#875](https://github.com/matrix-org/matrix-react-sdk/pull/875)
|
||||||
|
* Fixes 2 issues with Dialog closing
|
||||||
|
[\#867](https://github.com/matrix-org/matrix-react-sdk/pull/867)
|
||||||
|
* Automatic Reskindex
|
||||||
|
[\#871](https://github.com/matrix-org/matrix-react-sdk/pull/871)
|
||||||
|
* Put room name in 'leave room' confirmation dialog
|
||||||
|
[\#873](https://github.com/matrix-org/matrix-react-sdk/pull/873)
|
||||||
|
* Fix this/self fail in LeftPanel
|
||||||
|
[\#872](https://github.com/matrix-org/matrix-react-sdk/pull/872)
|
||||||
|
* Don't show null URL previews
|
||||||
|
[\#870](https://github.com/matrix-org/matrix-react-sdk/pull/870)
|
||||||
|
* Fix keys for AddressSelector
|
||||||
|
[\#869](https://github.com/matrix-org/matrix-react-sdk/pull/869)
|
||||||
|
* Make left panel better for new users (mk II)
|
||||||
|
[\#859](https://github.com/matrix-org/matrix-react-sdk/pull/859)
|
||||||
|
* Explicitly save composer content onUnload
|
||||||
|
[\#866](https://github.com/matrix-org/matrix-react-sdk/pull/866)
|
||||||
|
* Warn on unload
|
||||||
|
[\#851](https://github.com/matrix-org/matrix-react-sdk/pull/851)
|
||||||
|
* Log deviceid at login
|
||||||
|
[\#862](https://github.com/matrix-org/matrix-react-sdk/pull/862)
|
||||||
|
* Guests can't send RR so no point trying
|
||||||
|
[\#860](https://github.com/matrix-org/matrix-react-sdk/pull/860)
|
||||||
|
* Remove babelcheck
|
||||||
|
[\#861](https://github.com/matrix-org/matrix-react-sdk/pull/861)
|
||||||
|
* T3chguy/settings versions improvements
|
||||||
|
[\#857](https://github.com/matrix-org/matrix-react-sdk/pull/857)
|
||||||
|
* Change max-len 90->120
|
||||||
|
[\#852](https://github.com/matrix-org/matrix-react-sdk/pull/852)
|
||||||
|
* Remove DM-guessing code
|
||||||
|
[\#829](https://github.com/matrix-org/matrix-react-sdk/pull/829)
|
||||||
|
* Fix jumping to an unread event when in MELS
|
||||||
|
[\#855](https://github.com/matrix-org/matrix-react-sdk/pull/855)
|
||||||
|
* Validate phone number on login
|
||||||
|
[\#856](https://github.com/matrix-org/matrix-react-sdk/pull/856)
|
||||||
|
* Failed to enable HTML5 Notifications Error Dialogs
|
||||||
|
[\#827](https://github.com/matrix-org/matrix-react-sdk/pull/827)
|
||||||
|
* Pin filesize ver to fix break upstream
|
||||||
|
[\#854](https://github.com/matrix-org/matrix-react-sdk/pull/854)
|
||||||
|
* Improve RoomDirectory Look & Feel
|
||||||
|
[\#848](https://github.com/matrix-org/matrix-react-sdk/pull/848)
|
||||||
|
* Only show jumpToReadMarker bar when RM !== RR
|
||||||
|
[\#845](https://github.com/matrix-org/matrix-react-sdk/pull/845)
|
||||||
|
* Allow MELS to have its own RM
|
||||||
|
[\#846](https://github.com/matrix-org/matrix-react-sdk/pull/846)
|
||||||
|
* Use document.onkeydown instead of onkeypress
|
||||||
|
[\#844](https://github.com/matrix-org/matrix-react-sdk/pull/844)
|
||||||
|
* (Room)?Avatar: Request 96x96 avatars on high DPI screens
|
||||||
|
[\#808](https://github.com/matrix-org/matrix-react-sdk/pull/808)
|
||||||
|
* Add mx_EventTile_emote class
|
||||||
|
[\#842](https://github.com/matrix-org/matrix-react-sdk/pull/842)
|
||||||
|
* Fix dialog reappearing after hitting Enter
|
||||||
|
[\#841](https://github.com/matrix-org/matrix-react-sdk/pull/841)
|
||||||
|
* Fix spinner that shows until the first sync
|
||||||
|
[\#840](https://github.com/matrix-org/matrix-react-sdk/pull/840)
|
||||||
|
* Show spinner until first sync has completed
|
||||||
|
[\#839](https://github.com/matrix-org/matrix-react-sdk/pull/839)
|
||||||
|
* Style fixes for LoggedInView
|
||||||
|
[\#838](https://github.com/matrix-org/matrix-react-sdk/pull/838)
|
||||||
|
* Fix specifying custom server for registration
|
||||||
|
[\#834](https://github.com/matrix-org/matrix-react-sdk/pull/834)
|
||||||
|
* Improve country dropdown UX and expose +prefix
|
||||||
|
[\#833](https://github.com/matrix-org/matrix-react-sdk/pull/833)
|
||||||
|
* Fix user settings store
|
||||||
|
[\#836](https://github.com/matrix-org/matrix-react-sdk/pull/836)
|
||||||
|
* show the room name in the UDE Dialog
|
||||||
|
[\#832](https://github.com/matrix-org/matrix-react-sdk/pull/832)
|
||||||
|
* summarise profile changes in MELS
|
||||||
|
[\#826](https://github.com/matrix-org/matrix-react-sdk/pull/826)
|
||||||
|
* Transform h1 and h2 tags to h3 tags
|
||||||
|
[\#820](https://github.com/matrix-org/matrix-react-sdk/pull/820)
|
||||||
|
* limit our keyboard shortcut modifiers correctly
|
||||||
|
[\#825](https://github.com/matrix-org/matrix-react-sdk/pull/825)
|
||||||
|
* Specify cross platform regexes and add olm to noParse
|
||||||
|
[\#823](https://github.com/matrix-org/matrix-react-sdk/pull/823)
|
||||||
|
* Remember element that was in focus before rendering dialog
|
||||||
|
[\#822](https://github.com/matrix-org/matrix-react-sdk/pull/822)
|
||||||
|
* move user settings outward and use built in read receipts disabling
|
||||||
|
[\#824](https://github.com/matrix-org/matrix-react-sdk/pull/824)
|
||||||
|
* File Download Consistency
|
||||||
|
[\#802](https://github.com/matrix-org/matrix-react-sdk/pull/802)
|
||||||
|
* Show Access Token under Advanced in Settings
|
||||||
|
[\#806](https://github.com/matrix-org/matrix-react-sdk/pull/806)
|
||||||
|
* Link tags/commit hashes in the UserSettings version section
|
||||||
|
[\#810](https://github.com/matrix-org/matrix-react-sdk/pull/810)
|
||||||
|
* On return to RoomView from auxPanel, send focus back to Composer
|
||||||
|
[\#813](https://github.com/matrix-org/matrix-react-sdk/pull/813)
|
||||||
|
* Change presence status labels to 'for' instead of 'ago'
|
||||||
|
[\#817](https://github.com/matrix-org/matrix-react-sdk/pull/817)
|
||||||
|
* Disable Scalar Integrations if urls passed to it are falsey
|
||||||
|
[\#816](https://github.com/matrix-org/matrix-react-sdk/pull/816)
|
||||||
|
* Add option to hide other people's read receipts.
|
||||||
|
[\#818](https://github.com/matrix-org/matrix-react-sdk/pull/818)
|
||||||
|
* Add option to not send typing notifications
|
||||||
|
[\#819](https://github.com/matrix-org/matrix-react-sdk/pull/819)
|
||||||
|
* Sync RM across instances of Riot
|
||||||
|
[\#805](https://github.com/matrix-org/matrix-react-sdk/pull/805)
|
||||||
|
* First iteration on improving login UI
|
||||||
|
[\#811](https://github.com/matrix-org/matrix-react-sdk/pull/811)
|
||||||
|
* focus on composer after jumping to bottom
|
||||||
|
[\#809](https://github.com/matrix-org/matrix-react-sdk/pull/809)
|
||||||
|
* Improve RoomList performance via side-stepping React
|
||||||
|
[\#807](https://github.com/matrix-org/matrix-react-sdk/pull/807)
|
||||||
|
* Don't show link preview when link is inside of a quote
|
||||||
|
[\#762](https://github.com/matrix-org/matrix-react-sdk/pull/762)
|
||||||
|
* Escape closes UserSettings
|
||||||
|
[\#765](https://github.com/matrix-org/matrix-react-sdk/pull/765)
|
||||||
|
* Implement user power-level changes in timeline
|
||||||
|
[\#794](https://github.com/matrix-org/matrix-react-sdk/pull/794)
|
||||||
|
|
||||||
Changes in [0.8.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8) (2017-04-25)
|
Changes in [0.8.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8) (2017-04-25)
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.2...v0.8.8)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.2...v0.8.8)
|
||||||
|
@ -69,25 +69,41 @@ General Style
|
|||||||
console.log("I am a fish"); // Bad
|
console.log("I am a fish"); // Bad
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
- No new line before else, catch, finally, etc:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (x) {
|
||||||
|
console.log("I am a fish");
|
||||||
|
} else {
|
||||||
|
console.log("I am a chimp"); // Good
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x) {
|
||||||
|
console.log("I am a fish");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("I am a chimp"); // Bad
|
||||||
|
}
|
||||||
|
```
|
||||||
- Declare one variable per var statement (consistent with Node). Unless they
|
- 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,
|
are simple and closely related. If you put the next declaration on a new line,
|
||||||
treat yourself to another `var`:
|
treat yourself to another `var`:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var key = "foo",
|
const key = "foo",
|
||||||
comparator = function(x, y) {
|
comparator = function(x, y) {
|
||||||
return x - y;
|
return x - y;
|
||||||
}; // Bad
|
}; // Bad
|
||||||
|
|
||||||
var key = "foo";
|
const key = "foo";
|
||||||
var comparator = function(x, y) {
|
const comparator = function(x, y) {
|
||||||
return x - y;
|
return x - y;
|
||||||
}; // Good
|
}; // Good
|
||||||
|
|
||||||
var x = 0, y = 0; // Fine
|
let x = 0, y = 0; // Fine
|
||||||
|
|
||||||
var x = 0;
|
let x = 0;
|
||||||
var y = 0; // Also fine
|
let y = 0; // Also fine
|
||||||
```
|
```
|
||||||
- A single line `if` is fine, all others have braces. This prevents errors when adding to the code.:
|
- A single line `if` is fine, all others have braces. This prevents errors when adding to the code.:
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.8.8",
|
"version": "0.8.9",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
@ -63,14 +63,13 @@
|
|||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"linkifyjs": "^2.1.3",
|
"linkifyjs": "^2.1.3",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "0.7.8",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"prop-types": "^15.5.8",
|
|
||||||
"q": "^1.4.1",
|
"q": "^1.4.1",
|
||||||
"react": "^15.4.0",
|
"react": "^15.4.0",
|
||||||
"react-addons-css-transition-group": "15.3.2",
|
"react-addons-css-transition-group": "15.3.2",
|
||||||
"react-dom": "^15.4.0",
|
"react-dom": "^15.4.0",
|
||||||
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#39d858c",
|
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
||||||
"sanitize-html": "^1.11.1",
|
"sanitize-html": "^1.11.1",
|
||||||
"text-encoding-utf-8": "^1.0.1",
|
"text-encoding-utf-8": "^1.0.1",
|
||||||
"velocity-vector": "vector-im/velocity#059e3b2",
|
"velocity-vector": "vector-im/velocity#059e3b2",
|
||||||
|
@ -6,14 +6,22 @@ var args = require('optimist').argv;
|
|||||||
var chokidar = require('chokidar');
|
var chokidar = require('chokidar');
|
||||||
|
|
||||||
var componentIndex = path.join('src', 'component-index.js');
|
var componentIndex = path.join('src', 'component-index.js');
|
||||||
|
var componentIndexTmp = componentIndex+".tmp";
|
||||||
var componentsDir = path.join('src', 'components');
|
var componentsDir = path.join('src', 'components');
|
||||||
var componentGlob = '**/*.js';
|
var componentGlob = '**/*.js';
|
||||||
|
var prevFiles = [];
|
||||||
|
|
||||||
function reskindex() {
|
function reskindex() {
|
||||||
|
var files = glob.sync(componentGlob, {cwd: componentsDir}).sort();
|
||||||
|
if (!filesHaveChanged(files, prevFiles)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prevFiles = files;
|
||||||
|
|
||||||
var header = args.h || args.header;
|
var header = args.h || args.header;
|
||||||
var packageJson = JSON.parse(fs.readFileSync('./package.json'));
|
var packageJson = JSON.parse(fs.readFileSync('./package.json'));
|
||||||
|
|
||||||
var strm = fs.createWriteStream(componentIndex);
|
var strm = fs.createWriteStream(componentIndexTmp);
|
||||||
|
|
||||||
if (header) {
|
if (header) {
|
||||||
strm.write(fs.readFileSync(header));
|
strm.write(fs.readFileSync(header));
|
||||||
@ -28,16 +36,18 @@ function reskindex() {
|
|||||||
strm.write(" */\n\n");
|
strm.write(" */\n\n");
|
||||||
|
|
||||||
if (packageJson['matrix-react-parent']) {
|
if (packageJson['matrix-react-parent']) {
|
||||||
|
const parentIndex = packageJson['matrix-react-parent'] +
|
||||||
|
'/lib/component-index';
|
||||||
strm.write(
|
strm.write(
|
||||||
"module.exports.components = require('"+
|
`let components = require('${parentIndex}').components;
|
||||||
packageJson['matrix-react-parent']+
|
if (!components) {
|
||||||
"/lib/component-index').components;\n\n"
|
throw new Error("'${parentIndex}' didn't export components");
|
||||||
);
|
}
|
||||||
|
`);
|
||||||
} else {
|
} else {
|
||||||
strm.write("module.exports.components = {};\n");
|
strm.write("let components = {};\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = glob.sync(componentGlob, {cwd: componentsDir}).sort();
|
|
||||||
for (var i = 0; i < files.length; ++i) {
|
for (var i = 0; i < files.length; ++i) {
|
||||||
var file = files[i].replace('.js', '');
|
var file = files[i].replace('.js', '');
|
||||||
|
|
||||||
@ -45,13 +55,34 @@ function reskindex() {
|
|||||||
var importName = moduleName.replace(/\./g, "$");
|
var importName = moduleName.replace(/\./g, "$");
|
||||||
|
|
||||||
strm.write("import " + importName + " from './components/" + file + "';\n");
|
strm.write("import " + importName + " from './components/" + file + "';\n");
|
||||||
strm.write(importName + " && (module.exports.components['"+moduleName+"'] = " + importName + ");");
|
strm.write(importName + " && (components['"+moduleName+"'] = " + importName + ");");
|
||||||
strm.write('\n');
|
strm.write('\n');
|
||||||
strm.uncork();
|
strm.uncork();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
strm.write("export {components};\n");
|
||||||
strm.end();
|
strm.end();
|
||||||
console.log('Reskindex: completed');
|
fs.rename(componentIndexTmp, componentIndex, function(err) {
|
||||||
|
if(err) {
|
||||||
|
console.error("Error moving new index into place: " + err);
|
||||||
|
} else {
|
||||||
|
console.log('Reskindex: completed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expects both arrays of file names to be sorted
|
||||||
|
function filesHaveChanged(files, prevFiles) {
|
||||||
|
if (files.length !== prevFiles.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Check for name changes
|
||||||
|
for (var i = 0; i < files.length; i++) {
|
||||||
|
if (prevFiles[i] !== files[i]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -w indicates watch mode where any FS events will trigger reskindex
|
// -w indicates watch mode where any FS events will trigger reskindex
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2017 Vector Creations 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// singleton which dispatches invocations of a given type & argument
|
|
||||||
// rather than just a type (as per EventEmitter and Flux's dispatcher etc)
|
|
||||||
//
|
|
||||||
// This means you can have a single point which listens for an EventEmitter event
|
|
||||||
// and then dispatches out to one of thousands of RoomTiles (for instance) rather than
|
|
||||||
// having each RoomTile register for the EventEmitter event and having to
|
|
||||||
// iterate over all of them.
|
|
||||||
class ConstantTimeDispatcher {
|
|
||||||
constructor() {
|
|
||||||
// type -> arg -> [ listener(arg, params) ]
|
|
||||||
this.listeners = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
register(type, arg, listener) {
|
|
||||||
if (!this.listeners[type]) this.listeners[type] = {};
|
|
||||||
if (!this.listeners[type][arg]) this.listeners[type][arg] = [];
|
|
||||||
this.listeners[type][arg].push(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
unregister(type, arg, listener) {
|
|
||||||
if (this.listeners[type] && this.listeners[type][arg]) {
|
|
||||||
var i = this.listeners[type][arg].indexOf(listener);
|
|
||||||
if (i > -1) {
|
|
||||||
this.listeners[type][arg].splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.warn("Unregistering unrecognised listener (type=" + type + ", arg=" + arg + ")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(type, arg, params) {
|
|
||||||
if (!this.listeners[type] || !this.listeners[type][arg]) {
|
|
||||||
//console.warn("No registered listeners for dispatch (type=" + type + ", arg=" + arg + ")");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.listeners[type][arg].forEach(listener=>{
|
|
||||||
listener.call(arg, params);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!global.constantTimeDispatcher) {
|
|
||||||
global.constantTimeDispatcher = new ConstantTimeDispatcher();
|
|
||||||
}
|
|
||||||
module.exports = global.constantTimeDispatcher;
|
|
@ -19,13 +19,14 @@ limitations under the License.
|
|||||||
var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||||
|
|
||||||
|
function pad(n) {
|
||||||
|
return (n < 10 ? '0' : '') + n;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
formatDate: function(date) {
|
formatDate: function(date) {
|
||||||
// date.toLocaleTimeString is completely system dependent.
|
// date.toLocaleTimeString is completely system dependent.
|
||||||
// just go 24h for now
|
// just go 24h for now
|
||||||
function pad(n) {
|
|
||||||
return (n < 10 ? '0' : '') + n;
|
|
||||||
}
|
|
||||||
|
|
||||||
var now = new Date();
|
var now = new Date();
|
||||||
if (date.toDateString() === now.toDateString()) {
|
if (date.toDateString() === now.toDateString()) {
|
||||||
@ -34,19 +35,20 @@ module.exports = {
|
|||||||
else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
||||||
return days[date.getDay()] + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
return days[date.getDay()] + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||||
}
|
}
|
||||||
else /* if (now.getFullYear() === date.getFullYear()) */ {
|
else if (now.getFullYear() === date.getFullYear()) {
|
||||||
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
else {
|
else {
|
||||||
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
return this.formatFullDate(date);
|
||||||
}
|
}
|
||||||
*/
|
},
|
||||||
|
|
||||||
|
formatFullDate: function(date) {
|
||||||
|
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||||
},
|
},
|
||||||
|
|
||||||
formatTime: function(date) {
|
formatTime: function(date) {
|
||||||
//return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||||
return ('00' + date.getHours()).slice(-2) + ':' + ('00' + date.getMinutes()).slice(-2);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -148,17 +148,18 @@ var sanitizeHtmlParams = {
|
|||||||
attribs.href = m[1];
|
attribs.href = m[1];
|
||||||
delete attribs.target;
|
delete attribs.target;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
|
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
|
||||||
if (m) {
|
if (m) {
|
||||||
var entity = m[1];
|
var entity = m[1];
|
||||||
if (entity[0] === '@') {
|
if (entity[0] === '@') {
|
||||||
attribs.href = '#/user/' + entity;
|
attribs.href = '#/user/' + entity;
|
||||||
|
}
|
||||||
|
else if (entity[0] === '#' || entity[0] === '!') {
|
||||||
|
attribs.href = '#/room/' + entity;
|
||||||
|
}
|
||||||
|
delete attribs.target;
|
||||||
}
|
}
|
||||||
else if (entity[0] === '#' || entity[0] === '!') {
|
|
||||||
attribs.href = '#/room/' + entity;
|
|
||||||
}
|
|
||||||
delete attribs.target;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
||||||
|
@ -32,5 +32,4 @@ module.exports = {
|
|||||||
DELETE: 46,
|
DELETE: 46,
|
||||||
KEY_D: 68,
|
KEY_D: 68,
|
||||||
KEY_E: 69,
|
KEY_E: 69,
|
||||||
KEY_K: 75,
|
|
||||||
};
|
};
|
||||||
|
@ -25,7 +25,9 @@ module.exports = {
|
|||||||
eventTriggersUnreadCount: function(ev) {
|
eventTriggersUnreadCount: function(ev) {
|
||||||
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
||||||
return false;
|
return false;
|
||||||
} else if (ev.getType() == "m.room.member") {
|
} else if (ev.getType() == 'm.room.member') {
|
||||||
|
return false;
|
||||||
|
} else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') {
|
||||||
return false;
|
return false;
|
||||||
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
|
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
|
||||||
return false;
|
return false;
|
||||||
|
@ -59,6 +59,8 @@ var FilePanel = React.createClass({
|
|||||||
var client = MatrixClientPeg.get();
|
var client = MatrixClientPeg.get();
|
||||||
var room = client.getRoom(roomId);
|
var room = client.getRoom(roomId);
|
||||||
|
|
||||||
|
this.noRoom = !room;
|
||||||
|
|
||||||
if (room) {
|
if (room) {
|
||||||
var filter = new Matrix.Filter(client.credentials.userId);
|
var filter = new Matrix.Filter(client.credentials.userId);
|
||||||
filter.setDefinition(
|
filter.setDefinition(
|
||||||
@ -82,13 +84,22 @@ var FilePanel = React.createClass({
|
|||||||
console.error("Failed to get or create file panel filter", error);
|
console.error("Failed to get or create file panel filter", error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
console.error("Failed to add filtered timelineSet for FilePanel as no room!");
|
console.error("Failed to add filtered timelineSet for FilePanel as no room!");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
|
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
||||||
|
<div className="mx_RoomView_empty">You must <a href="#/register">register</a> to use this functionality</div>
|
||||||
|
</div>;
|
||||||
|
} else if (this.noRoom) {
|
||||||
|
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
||||||
|
<div className="mx_RoomView_empty">You must join the room to see its files</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||||
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
@ -127,18 +127,6 @@ export default React.createClass({
|
|||||||
var handled = false;
|
var handled = false;
|
||||||
|
|
||||||
switch (ev.keyCode) {
|
switch (ev.keyCode) {
|
||||||
case KeyCode.ESCAPE:
|
|
||||||
|
|
||||||
// Implemented this way so possible handling for other pages is neater
|
|
||||||
switch (this.props.page_type) {
|
|
||||||
case PageTypes.UserSettings:
|
|
||||||
this.props.onUserSettingsClose();
|
|
||||||
handled = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyCode.UP:
|
case KeyCode.UP:
|
||||||
case KeyCode.DOWN:
|
case KeyCode.DOWN:
|
||||||
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||||
|
@ -17,27 +17,24 @@ limitations under the License.
|
|||||||
|
|
||||||
import q from 'q';
|
import q from 'q';
|
||||||
|
|
||||||
var React = require('react');
|
import React from 'react';
|
||||||
var Matrix = require("matrix-js-sdk");
|
import Matrix from "matrix-js-sdk";
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
import MatrixClientPeg from "../../MatrixClientPeg";
|
||||||
var PlatformPeg = require("../../PlatformPeg");
|
import PlatformPeg from "../../PlatformPeg";
|
||||||
var SdkConfig = require("../../SdkConfig");
|
import SdkConfig from "../../SdkConfig";
|
||||||
var ContextualMenu = require("./ContextualMenu");
|
import * as RoomListSorter from "../../RoomListSorter";
|
||||||
var RoomListSorter = require("../../RoomListSorter");
|
import dis from "../../dispatcher";
|
||||||
var UserActivity = require("../../UserActivity");
|
|
||||||
var Presence = require("../../Presence");
|
|
||||||
var dis = require("../../dispatcher");
|
|
||||||
|
|
||||||
var Modal = require("../../Modal");
|
import Modal from "../../Modal";
|
||||||
var Tinter = require("../../Tinter");
|
import Tinter from "../../Tinter";
|
||||||
var sdk = require('../../index');
|
import sdk from '../../index';
|
||||||
var Rooms = require('../../Rooms');
|
import * as Rooms from '../../Rooms';
|
||||||
var linkifyMatrix = require("../../linkify-matrix");
|
import linkifyMatrix from "../../linkify-matrix";
|
||||||
var Lifecycle = require('../../Lifecycle');
|
import * as Lifecycle from '../../Lifecycle';
|
||||||
var PageTypes = require('../../PageTypes');
|
import PageTypes from '../../PageTypes';
|
||||||
|
|
||||||
var createRoom = require("../../createRoom");
|
import createRoom from "../../createRoom";
|
||||||
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
|
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
@ -89,7 +86,7 @@ module.exports = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
var s = {
|
const s = {
|
||||||
loading: true,
|
loading: true,
|
||||||
screen: undefined,
|
screen: undefined,
|
||||||
screenAfterLogin: this.props.initialScreenAfterLogin,
|
screenAfterLogin: this.props.initialScreenAfterLogin,
|
||||||
@ -156,11 +153,9 @@ module.exports = React.createClass({
|
|||||||
return this.state.register_hs_url;
|
return this.state.register_hs_url;
|
||||||
} else if (MatrixClientPeg.get()) {
|
} else if (MatrixClientPeg.get()) {
|
||||||
return MatrixClientPeg.get().getHomeserverUrl();
|
return MatrixClientPeg.get().getHomeserverUrl();
|
||||||
}
|
} else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) {
|
||||||
else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) {
|
|
||||||
return window.localStorage.getItem("mx_hs_url");
|
return window.localStorage.getItem("mx_hs_url");
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return this.getDefaultHsUrl();
|
return this.getDefaultHsUrl();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -178,11 +173,9 @@ module.exports = React.createClass({
|
|||||||
return this.state.register_is_url;
|
return this.state.register_is_url;
|
||||||
} else if (MatrixClientPeg.get()) {
|
} else if (MatrixClientPeg.get()) {
|
||||||
return MatrixClientPeg.get().getIdentityServerUrl();
|
return MatrixClientPeg.get().getIdentityServerUrl();
|
||||||
}
|
} else if (window.localStorage && window.localStorage.getItem("mx_is_url")) {
|
||||||
else if (window.localStorage && window.localStorage.getItem("mx_is_url")) {
|
|
||||||
return window.localStorage.getItem("mx_is_url");
|
return window.localStorage.getItem("mx_is_url");
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return this.getDefaultIsUrl();
|
return this.getDefaultIsUrl();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -324,28 +317,14 @@ module.exports = React.createClass({
|
|||||||
onAction: function(payload) {
|
onAction: function(payload) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
var roomIndexDelta = 1;
|
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
||||||
|
|
||||||
var self = this;
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'logout':
|
case 'logout':
|
||||||
Lifecycle.logout();
|
Lifecycle.logout();
|
||||||
break;
|
break;
|
||||||
case 'start_registration':
|
case 'start_registration':
|
||||||
const params = payload.params || {};
|
this._startRegistration(payload.params || {});
|
||||||
this.setStateForNewScreen({
|
|
||||||
screen: 'register',
|
|
||||||
// these params may be undefined, but if they are,
|
|
||||||
// unset them from our state: we don't want to
|
|
||||||
// resume a previous registration session if the
|
|
||||||
// user just clicked 'register'
|
|
||||||
register_client_secret: params.client_secret,
|
|
||||||
register_session_id: params.session_id,
|
|
||||||
register_hs_url: params.hs_url,
|
|
||||||
register_is_url: params.is_url,
|
|
||||||
register_id_sid: params.sid,
|
|
||||||
});
|
|
||||||
this.notifyNewScreen('register');
|
|
||||||
break;
|
break;
|
||||||
case 'start_login':
|
case 'start_login':
|
||||||
if (MatrixClientPeg.get() &&
|
if (MatrixClientPeg.get() &&
|
||||||
@ -362,7 +341,7 @@ module.exports = React.createClass({
|
|||||||
break;
|
break;
|
||||||
case 'start_post_registration':
|
case 'start_post_registration':
|
||||||
this.setState({ // don't clobber loggedIn status
|
this.setState({ // don't clobber loggedIn status
|
||||||
screen: 'post_registration'
|
screen: 'post_registration',
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'start_upgrade_registration':
|
case 'start_upgrade_registration':
|
||||||
@ -392,34 +371,7 @@ module.exports = React.createClass({
|
|||||||
this.notifyNewScreen('forgot_password');
|
this.notifyNewScreen('forgot_password');
|
||||||
break;
|
break;
|
||||||
case 'leave_room':
|
case 'leave_room':
|
||||||
const roomToLeave = MatrixClientPeg.get().getRoom(payload.room_id);
|
this._leaveRoom(payload.room_id);
|
||||||
Modal.createDialog(QuestionDialog, {
|
|
||||||
title: "Leave room",
|
|
||||||
description: <span>Are you sure you want to leave the room <i>{roomToLeave.name}</i>?</span>,
|
|
||||||
onFinished: (should_leave) => {
|
|
||||||
if (should_leave) {
|
|
||||||
const d = MatrixClientPeg.get().leave(payload.room_id);
|
|
||||||
|
|
||||||
// FIXME: controller shouldn't be loading a view :(
|
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
|
||||||
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
|
||||||
|
|
||||||
d.then(() => {
|
|
||||||
modal.close();
|
|
||||||
if (this.currentRoomId === payload.room_id) {
|
|
||||||
dis.dispatch({action: 'view_next_room'});
|
|
||||||
}
|
|
||||||
}, (err) => {
|
|
||||||
modal.close();
|
|
||||||
console.error("Failed to leave room " + payload.room_id + " " + err);
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: "Failed to leave room",
|
|
||||||
description: (err && err.message ? err.message : "Server may be unavailable, overloaded, or you hit a bug."),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case 'reject_invite':
|
case 'reject_invite':
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createDialog(QuestionDialog, {
|
||||||
@ -440,11 +392,11 @@ module.exports = React.createClass({
|
|||||||
modal.close();
|
modal.close();
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Failed to reject invitation",
|
title: "Failed to reject invitation",
|
||||||
description: err.toString()
|
description: err.toString(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'view_user':
|
case 'view_user':
|
||||||
@ -469,30 +421,13 @@ module.exports = React.createClass({
|
|||||||
this._viewRoom(payload);
|
this._viewRoom(payload);
|
||||||
break;
|
break;
|
||||||
case 'view_prev_room':
|
case 'view_prev_room':
|
||||||
roomIndexDelta = -1;
|
this._viewNextRoom(-1);
|
||||||
|
break;
|
||||||
case 'view_next_room':
|
case 'view_next_room':
|
||||||
var allRooms = RoomListSorter.mostRecentActivityFirst(
|
this._viewNextRoom(1);
|
||||||
MatrixClientPeg.get().getRooms()
|
|
||||||
);
|
|
||||||
var roomIndex = -1;
|
|
||||||
for (var i = 0; i < allRooms.length; ++i) {
|
|
||||||
if (allRooms[i].roomId == this.state.currentRoomId) {
|
|
||||||
roomIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
|
|
||||||
if (roomIndex < 0) roomIndex = allRooms.length - 1;
|
|
||||||
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
|
|
||||||
break;
|
break;
|
||||||
case 'view_indexed_room':
|
case 'view_indexed_room':
|
||||||
var allRooms = RoomListSorter.mostRecentActivityFirst(
|
this._viewIndexedRoom(payload.roomIndex);
|
||||||
MatrixClientPeg.get().getRooms()
|
|
||||||
);
|
|
||||||
var roomIndex = payload.roomIndex;
|
|
||||||
if (allRooms[roomIndex]) {
|
|
||||||
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'view_user_settings':
|
case 'view_user_settings':
|
||||||
this._setPage(PageTypes.UserSettings);
|
this._setPage(PageTypes.UserSettings);
|
||||||
@ -589,7 +524,7 @@ module.exports = React.createClass({
|
|||||||
case 'new_version':
|
case 'new_version':
|
||||||
this.onVersion(
|
this.onVersion(
|
||||||
payload.currentVersion, payload.newVersion,
|
payload.currentVersion, payload.newVersion,
|
||||||
payload.releaseNotes
|
payload.releaseNotes,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -601,6 +536,47 @@ module.exports = React.createClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_startRegistration: function(params) {
|
||||||
|
this.setStateForNewScreen({
|
||||||
|
screen: 'register',
|
||||||
|
// these params may be undefined, but if they are,
|
||||||
|
// unset them from our state: we don't want to
|
||||||
|
// resume a previous registration session if the
|
||||||
|
// user just clicked 'register'
|
||||||
|
register_client_secret: params.client_secret,
|
||||||
|
register_session_id: params.session_id,
|
||||||
|
register_hs_url: params.hs_url,
|
||||||
|
register_is_url: params.is_url,
|
||||||
|
register_id_sid: params.sid,
|
||||||
|
});
|
||||||
|
this.notifyNewScreen('register');
|
||||||
|
},
|
||||||
|
|
||||||
|
_viewNextRoom: function(roomIndexDelta) {
|
||||||
|
const allRooms = RoomListSorter.mostRecentActivityFirst(
|
||||||
|
MatrixClientPeg.get().getRooms(),
|
||||||
|
);
|
||||||
|
let roomIndex = -1;
|
||||||
|
for (let i = 0; i < allRooms.length; ++i) {
|
||||||
|
if (allRooms[i].roomId == this.state.currentRoomId) {
|
||||||
|
roomIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
|
||||||
|
if (roomIndex < 0) roomIndex = allRooms.length - 1;
|
||||||
|
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
|
||||||
|
},
|
||||||
|
|
||||||
|
_viewIndexedRoom: function(roomIndex) {
|
||||||
|
const allRooms = RoomListSorter.mostRecentActivityFirst(
|
||||||
|
MatrixClientPeg.get().getRooms(),
|
||||||
|
);
|
||||||
|
if (allRooms[roomIndex]) {
|
||||||
|
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// switch view to the given room
|
// switch view to the given room
|
||||||
//
|
//
|
||||||
// @param {Object} room_info Object containing data about the room to be joined
|
// @param {Object} room_info Object containing data about the room to be joined
|
||||||
@ -620,7 +596,7 @@ module.exports = React.createClass({
|
|||||||
_viewRoom: function(room_info) {
|
_viewRoom: function(room_info) {
|
||||||
this.focusComposer = true;
|
this.focusComposer = true;
|
||||||
|
|
||||||
var newState = {
|
const newState = {
|
||||||
initialEventId: room_info.event_id,
|
initialEventId: room_info.event_id,
|
||||||
highlightedEventId: room_info.event_id,
|
highlightedEventId: room_info.event_id,
|
||||||
initialEventPixelOffset: undefined,
|
initialEventPixelOffset: undefined,
|
||||||
@ -640,7 +616,7 @@ module.exports = React.createClass({
|
|||||||
//
|
//
|
||||||
// TODO: do this in RoomView rather than here
|
// TODO: do this in RoomView rather than here
|
||||||
if (!room_info.event_id && this.refs.loggedInView) {
|
if (!room_info.event_id && this.refs.loggedInView) {
|
||||||
var scrollState = this.refs.loggedInView.getScrollStateForRoom(room_info.room_id);
|
const scrollState = this.refs.loggedInView.getScrollStateForRoom(room_info.room_id);
|
||||||
if (scrollState) {
|
if (scrollState) {
|
||||||
newState.initialEventId = scrollState.focussedEvent;
|
newState.initialEventId = scrollState.focussedEvent;
|
||||||
newState.initialEventPixelOffset = scrollState.pixelOffset;
|
newState.initialEventPixelOffset = scrollState.pixelOffset;
|
||||||
@ -710,7 +686,7 @@ module.exports = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
_invite: function(roomId) {
|
_invite: function(roomId) {
|
||||||
var ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
|
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
|
||||||
Modal.createDialog(ChatInviteDialog, {
|
Modal.createDialog(ChatInviteDialog, {
|
||||||
title: "Invite new room members",
|
title: "Invite new room members",
|
||||||
button: "Send Invites",
|
button: "Send Invites",
|
||||||
@ -719,6 +695,41 @@ module.exports = React.createClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_leaveRoom: function(roomId) {
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
|
||||||
|
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||||
|
Modal.createDialog(QuestionDialog, {
|
||||||
|
title: "Leave room",
|
||||||
|
description: <span>Are you sure you want to leave the room <i>{roomToLeave.name}</i>?</span>,
|
||||||
|
onFinished: (shouldLeave) => {
|
||||||
|
if (shouldLeave) {
|
||||||
|
const d = MatrixClientPeg.get().leave(roomId);
|
||||||
|
|
||||||
|
// FIXME: controller shouldn't be loading a view :(
|
||||||
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||||
|
|
||||||
|
d.then(() => {
|
||||||
|
modal.close();
|
||||||
|
if (this.currentRoomId === roomId) {
|
||||||
|
dis.dispatch({action: 'view_next_room'});
|
||||||
|
}
|
||||||
|
}, (err) => {
|
||||||
|
modal.close();
|
||||||
|
console.error("Failed to leave room " + roomId + " " + err);
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Failed to leave room",
|
||||||
|
description: (err && err.message ? err.message :
|
||||||
|
"Server may be unavailable, overloaded, or you hit a bug."),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the sessionloader has finished
|
* Called when the sessionloader has finished
|
||||||
*/
|
*/
|
||||||
@ -737,6 +748,8 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Called whenever someone changes the theme
|
* Called whenever someone changes the theme
|
||||||
|
*
|
||||||
|
* @param {string} theme new theme
|
||||||
*/
|
*/
|
||||||
_onSetTheme: function(theme) {
|
_onSetTheme: function(theme) {
|
||||||
if (!theme) {
|
if (!theme) {
|
||||||
@ -745,12 +758,12 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
// look for the stylesheet elements.
|
// look for the stylesheet elements.
|
||||||
// styleElements is a map from style name to HTMLLinkElement.
|
// styleElements is a map from style name to HTMLLinkElement.
|
||||||
var styleElements = Object.create(null);
|
const styleElements = Object.create(null);
|
||||||
var i, a;
|
let a;
|
||||||
for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
|
for (let i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
|
||||||
var href = a.getAttribute("href");
|
const href = a.getAttribute("href");
|
||||||
// shouldn't we be using the 'title' tag rather than the href?
|
// shouldn't we be using the 'title' tag rather than the href?
|
||||||
var match = href.match(/^bundles\/.*\/theme-(.*)\.css$/);
|
const match = href.match(/^bundles\/.*\/theme-(.*)\.css$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
styleElements[match[1]] = a;
|
styleElements[match[1]] = a;
|
||||||
}
|
}
|
||||||
@ -773,14 +786,15 @@ module.exports = React.createClass({
|
|||||||
// abuse the tinter to change all the SVG's #fff to #2d2d2d
|
// abuse the tinter to change all the SVG's #fff to #2d2d2d
|
||||||
// XXX: obviously this shouldn't be hardcoded here.
|
// XXX: obviously this shouldn't be hardcoded here.
|
||||||
Tinter.tintSvgWhite('#2d2d2d');
|
Tinter.tintSvgWhite('#2d2d2d');
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
Tinter.tintSvgWhite('#ffffff');
|
Tinter.tintSvgWhite('#ffffff');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a new logged in session has started
|
* Called when a new logged in session has started
|
||||||
|
*
|
||||||
|
* @param {string} teamToken
|
||||||
*/
|
*/
|
||||||
_onLoggedIn: function(teamToken) {
|
_onLoggedIn: function(teamToken) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -811,7 +825,7 @@ module.exports = React.createClass({
|
|||||||
if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) {
|
if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) {
|
||||||
this.showScreen(
|
this.showScreen(
|
||||||
this.state.screenAfterLogin.screen,
|
this.state.screenAfterLogin.screen,
|
||||||
this.state.screenAfterLogin.params
|
this.state.screenAfterLogin.params,
|
||||||
);
|
);
|
||||||
this.notifyNewScreen(this.state.screenAfterLogin.screen);
|
this.notifyNewScreen(this.state.screenAfterLogin.screen);
|
||||||
this.setState({screenAfterLogin: null});
|
this.setState({screenAfterLogin: null});
|
||||||
@ -852,8 +866,8 @@ module.exports = React.createClass({
|
|||||||
* (useful for setting listeners)
|
* (useful for setting listeners)
|
||||||
*/
|
*/
|
||||||
_onWillStartClient() {
|
_onWillStartClient() {
|
||||||
var self = this;
|
const self = this;
|
||||||
var cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
// Allow the JS SDK to reap timeline events. This reduces the amount of
|
// Allow the JS SDK to reap timeline events. This reduces the amount of
|
||||||
// memory consumed as the JS SDK stores multiple distinct copies of room
|
// memory consumed as the JS SDK stores multiple distinct copies of room
|
||||||
@ -894,17 +908,17 @@ module.exports = React.createClass({
|
|||||||
cli.on('Call.incoming', function(call) {
|
cli.on('Call.incoming', function(call) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'incoming_call',
|
action: 'incoming_call',
|
||||||
call: call
|
call: call,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
cli.on('Session.logged_out', function(call) {
|
cli.on('Session.logged_out', function(call) {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Signed Out",
|
title: "Signed Out",
|
||||||
description: "For security, this session has been signed out. Please sign in again."
|
description: "For security, this session has been signed out. Please sign in again.",
|
||||||
});
|
});
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'logout'
|
action: 'logout',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
cli.on("accountData", function(ev) {
|
cli.on("accountData", function(ev) {
|
||||||
@ -927,17 +941,17 @@ module.exports = React.createClass({
|
|||||||
if (screen == 'register') {
|
if (screen == 'register') {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'start_registration',
|
action: 'start_registration',
|
||||||
params: params
|
params: params,
|
||||||
});
|
});
|
||||||
} else if (screen == 'login') {
|
} else if (screen == 'login') {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'start_login',
|
action: 'start_login',
|
||||||
params: params
|
params: params,
|
||||||
});
|
});
|
||||||
} else if (screen == 'forgot_password') {
|
} else if (screen == 'forgot_password') {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'start_password_recovery',
|
action: 'start_password_recovery',
|
||||||
params: params
|
params: params,
|
||||||
});
|
});
|
||||||
} else if (screen == 'new') {
|
} else if (screen == 'new') {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
@ -960,26 +974,26 @@ module.exports = React.createClass({
|
|||||||
action: 'start_post_registration',
|
action: 'start_post_registration',
|
||||||
});
|
});
|
||||||
} else if (screen.indexOf('room/') == 0) {
|
} else if (screen.indexOf('room/') == 0) {
|
||||||
var segments = screen.substring(5).split('/');
|
const segments = screen.substring(5).split('/');
|
||||||
var roomString = segments[0];
|
const roomString = segments[0];
|
||||||
var eventId = segments[1]; // undefined if no event id given
|
const eventId = segments[1]; // undefined if no event id given
|
||||||
|
|
||||||
// FIXME: sort_out caseConsistency
|
// FIXME: sort_out caseConsistency
|
||||||
var third_party_invite = {
|
const thirdPartyInvite = {
|
||||||
inviteSignUrl: params.signurl,
|
inviteSignUrl: params.signurl,
|
||||||
invitedEmail: params.email,
|
invitedEmail: params.email,
|
||||||
};
|
};
|
||||||
var oob_data = {
|
const oobData = {
|
||||||
name: params.room_name,
|
name: params.room_name,
|
||||||
avatarUrl: params.room_avatar_url,
|
avatarUrl: params.room_avatar_url,
|
||||||
inviterName: params.inviter_name,
|
inviterName: params.inviter_name,
|
||||||
};
|
};
|
||||||
|
|
||||||
var payload = {
|
const payload = {
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
event_id: eventId,
|
event_id: eventId,
|
||||||
third_party_invite: third_party_invite,
|
third_party_invite: thirdPartyInvite,
|
||||||
oob_data: oob_data,
|
oob_data: oobData,
|
||||||
};
|
};
|
||||||
if (roomString[0] == '#') {
|
if (roomString[0] == '#') {
|
||||||
payload.room_alias = roomString;
|
payload.room_alias = roomString;
|
||||||
@ -993,19 +1007,18 @@ module.exports = React.createClass({
|
|||||||
dis.dispatch(payload);
|
dis.dispatch(payload);
|
||||||
}
|
}
|
||||||
} else if (screen.indexOf('user/') == 0) {
|
} else if (screen.indexOf('user/') == 0) {
|
||||||
var userId = screen.substring(5);
|
const userId = screen.substring(5);
|
||||||
this.setState({ viewUserId: userId });
|
this.setState({ viewUserId: userId });
|
||||||
this._setPage(PageTypes.UserView);
|
this._setPage(PageTypes.UserView);
|
||||||
this.notifyNewScreen('user/' + userId);
|
this.notifyNewScreen('user/' + userId);
|
||||||
var member = new Matrix.RoomMember(null, userId);
|
const member = new Matrix.RoomMember(null, userId);
|
||||||
if (member) {
|
if (member) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_user',
|
action: 'view_user',
|
||||||
member: member,
|
member: member,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
console.info("Ignoring showScreen for '%s'", screen);
|
console.info("Ignoring showScreen for '%s'", screen);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1024,7 +1037,7 @@ module.exports = React.createClass({
|
|||||||
onUserClick: function(event, userId) {
|
onUserClick: function(event, userId) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
var member = new Matrix.RoomMember(null, userId);
|
const member = new Matrix.RoomMember(null, userId);
|
||||||
if (!member) { return; }
|
if (!member) { return; }
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_user',
|
action: 'view_user',
|
||||||
@ -1034,17 +1047,17 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
onLogoutClick: function(event) {
|
onLogoutClick: function(event) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'logout'
|
action: 'logout',
|
||||||
});
|
});
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleResize: function(e) {
|
handleResize: function(e) {
|
||||||
var hideLhsThreshold = 1000;
|
const hideLhsThreshold = 1000;
|
||||||
var showLhsThreshold = 1000;
|
const showLhsThreshold = 1000;
|
||||||
var hideRhsThreshold = 820;
|
const hideRhsThreshold = 820;
|
||||||
var showRhsThreshold = 820;
|
const showRhsThreshold = 820;
|
||||||
|
|
||||||
if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
|
if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
|
||||||
dis.dispatch({ action: 'hide_left_panel' });
|
dis.dispatch({ action: 'hide_left_panel' });
|
||||||
@ -1062,10 +1075,10 @@ module.exports = React.createClass({
|
|||||||
this.setState({width: window.innerWidth});
|
this.setState({width: window.innerWidth});
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomCreated: function(room_id) {
|
onRoomCreated: function(roomId) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "view_room",
|
action: "view_room",
|
||||||
room_id: room_id,
|
room_id: roomId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1099,7 +1112,7 @@ module.exports = React.createClass({
|
|||||||
onFinishPostRegistration: function() {
|
onFinishPostRegistration: function() {
|
||||||
// Don't confuse this with "PageType" which is the middle window to show
|
// Don't confuse this with "PageType" which is the middle window to show
|
||||||
this.setState({
|
this.setState({
|
||||||
screen: undefined
|
screen: undefined,
|
||||||
});
|
});
|
||||||
this.showScreen("settings");
|
this.showScreen("settings");
|
||||||
},
|
},
|
||||||
@ -1114,10 +1127,10 @@ module.exports = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateStatusIndicator: function(state, prevState) {
|
updateStatusIndicator: function(state, prevState) {
|
||||||
var notifCount = 0;
|
let notifCount = 0;
|
||||||
|
|
||||||
var rooms = MatrixClientPeg.get().getRooms();
|
const rooms = MatrixClientPeg.get().getRooms();
|
||||||
for (var i = 0; i < rooms.length; ++i) {
|
for (let i = 0; i < rooms.length; ++i) {
|
||||||
if (rooms[i].hasMembershipState(MatrixClientPeg.get().credentials.userId, 'invite')) {
|
if (rooms[i].hasMembershipState(MatrixClientPeg.get().credentials.userId, 'invite')) {
|
||||||
notifCount++;
|
notifCount++;
|
||||||
} else if (rooms[i].getUnreadNotificationCount()) {
|
} else if (rooms[i].getUnreadNotificationCount()) {
|
||||||
@ -1144,19 +1157,18 @@ module.exports = React.createClass({
|
|||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: this.state.currentRoomId,
|
room_id: this.state.currentRoomId,
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room_directory',
|
action: 'view_room_directory',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomIdResolved: function(room_id) {
|
onRoomIdResolved: function(roomId) {
|
||||||
// It's the RoomView's resposibility to look up room aliases, but we need the
|
// It's the RoomView's resposibility to look up room aliases, but we need the
|
||||||
// ID to pass into things like the Member List, so the Room View tells us when
|
// ID to pass into things like the Member List, so the Room View tells us when
|
||||||
// its done that resolution so we can display things that take a room ID.
|
// its done that resolution so we can display things that take a room ID.
|
||||||
this.setState({currentRoomId: room_id});
|
this.setState({currentRoomId: roomId});
|
||||||
},
|
},
|
||||||
|
|
||||||
_makeRegistrationUrl: function(params) {
|
_makeRegistrationUrl: function(params) {
|
||||||
@ -1179,14 +1191,20 @@ module.exports = React.createClass({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// needs to be before normal PageTypes as you are logged in technically
|
// needs to be before normal PageTypes as you are logged in technically
|
||||||
else if (this.state.screen == 'post_registration') {
|
if (this.state.screen == 'post_registration') {
|
||||||
const PostRegistration = sdk.getComponent('structures.login.PostRegistration');
|
const PostRegistration = sdk.getComponent('structures.login.PostRegistration');
|
||||||
return (
|
return (
|
||||||
<PostRegistration
|
<PostRegistration
|
||||||
onComplete={this.onFinishPostRegistration} />
|
onComplete={this.onFinishPostRegistration} />
|
||||||
);
|
);
|
||||||
} else if (this.state.loggedIn && this.state.ready) {
|
}
|
||||||
|
|
||||||
|
// `ready` and `loggedIn` may be set before `page_type` (because the
|
||||||
|
// latter is set via the dispatcher). If we don't yet have a `page_type`,
|
||||||
|
// keep showing the spinner for now.
|
||||||
|
if (this.state.loggedIn && this.state.ready && this.state.page_type) {
|
||||||
/* for now, we stuff the entirety of our props and state into the LoggedInView.
|
/* for now, we stuff the entirety of our props and state into the LoggedInView.
|
||||||
* we should go through and figure out what we actually need to pass down, as well
|
* we should go through and figure out what we actually need to pass down, as well
|
||||||
* as using something like redux to avoid having a billion bits of state kicking around.
|
* as using something like redux to avoid having a billion bits of state kicking around.
|
||||||
@ -1269,5 +1287,5 @@ module.exports = React.createClass({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
@ -1760,6 +1760,7 @@ module.exports = React.createClass({
|
|||||||
oobData={this.props.oobData}
|
oobData={this.props.oobData}
|
||||||
editing={this.state.editingRoomSettings}
|
editing={this.state.editingRoomSettings}
|
||||||
saving={this.state.uploadingRoomSettings}
|
saving={this.state.uploadingRoomSettings}
|
||||||
|
inRoom={myMember && myMember.membership === 'join'}
|
||||||
collapsedRhs={ this.props.collapsedRhs }
|
collapsedRhs={ this.props.collapsedRhs }
|
||||||
onSearchClick={this.onSearchClick}
|
onSearchClick={this.onSearchClick}
|
||||||
onSettingsClick={this.onSettingsClick}
|
onSettingsClick={this.onSettingsClick}
|
||||||
|
@ -177,8 +177,8 @@ var TimelinePanel = React.createClass({
|
|||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
debuglog("TimelinePanel: mounting");
|
debuglog("TimelinePanel: mounting");
|
||||||
|
|
||||||
this.last_rr_sent_event_id = undefined;
|
this.lastRRSentEventId = undefined;
|
||||||
this.last_rm_sent_event_id = undefined;
|
this.lastRMSentEventId = undefined;
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||||
@ -504,12 +504,13 @@ var TimelinePanel = React.createClass({
|
|||||||
// very possible have logged out within that timeframe, so check
|
// very possible have logged out within that timeframe, so check
|
||||||
// we still have a client.
|
// we still have a client.
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
// if no client or client is guest don't send RR
|
// if no client or client is guest don't send RR or RM
|
||||||
if (!cli || cli.isGuest()) return;
|
if (!cli || cli.isGuest()) return;
|
||||||
|
|
||||||
var currentReadUpToEventId = this._getCurrentReadReceipt(true);
|
let shouldSendRR = true;
|
||||||
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
|
|
||||||
|
|
||||||
|
const currentRREventId = this._getCurrentReadReceipt(true);
|
||||||
|
const currentRREventIndex = this._indexForEventId(currentRREventId);
|
||||||
// We want to avoid sending out read receipts when we are looking at
|
// We want to avoid sending out read receipts when we are looking at
|
||||||
// events in the past which are before the latest RR.
|
// events in the past which are before the latest RR.
|
||||||
//
|
//
|
||||||
@ -523,43 +524,60 @@ var TimelinePanel = React.createClass({
|
|||||||
// RRs) - but that is a bit of a niche case. It will sort itself out when
|
// RRs) - but that is a bit of a niche case. It will sort itself out when
|
||||||
// the user eventually hits the live timeline.
|
// the user eventually hits the live timeline.
|
||||||
//
|
//
|
||||||
if (currentReadUpToEventId && currentReadUpToEventIndex === null &&
|
if (currentRREventId && currentRREventIndex === null &&
|
||||||
this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||||
return;
|
shouldSendRR = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastReadEventIndex = this._getLastDisplayedEventIndex({
|
const lastReadEventIndex = this._getLastDisplayedEventIndex({
|
||||||
ignoreOwn: true
|
ignoreOwn: true,
|
||||||
});
|
});
|
||||||
if (lastReadEventIndex === null) return;
|
if (lastReadEventIndex === null) {
|
||||||
|
shouldSendRR = false;
|
||||||
|
}
|
||||||
|
let lastReadEvent = this.state.events[lastReadEventIndex];
|
||||||
|
shouldSendRR = shouldSendRR &&
|
||||||
|
// Only send a RR if the last read event is ahead in the timeline relative to
|
||||||
|
// the current RR event.
|
||||||
|
lastReadEventIndex > currentRREventIndex &&
|
||||||
|
// Only send a RR if the last RR set != the one we would send
|
||||||
|
this.lastRRSentEventId != lastReadEvent.getId();
|
||||||
|
|
||||||
var lastReadEvent = this.state.events[lastReadEventIndex];
|
// Only send a RM if the last RM sent != the one we would send
|
||||||
|
const shouldSendRM =
|
||||||
|
this.lastRMSentEventId != this.state.readMarkerEventId;
|
||||||
|
|
||||||
// we also remember the last read receipt we sent to avoid spamming the
|
// we also remember the last read receipt we sent to avoid spamming the
|
||||||
// same one at the server repeatedly
|
// same one at the server repeatedly
|
||||||
if ((lastReadEventIndex > currentReadUpToEventIndex &&
|
if (shouldSendRR || shouldSendRM) {
|
||||||
this.last_rr_sent_event_id != lastReadEvent.getId()) ||
|
if (shouldSendRR) {
|
||||||
this.last_rm_sent_event_id != this.state.readMarkerEventId) {
|
this.lastRRSentEventId = lastReadEvent.getId();
|
||||||
|
} else {
|
||||||
this.last_rr_sent_event_id = lastReadEvent.getId();
|
lastReadEvent = null;
|
||||||
this.last_rm_sent_event_id = this.state.readMarkerEventId;
|
}
|
||||||
|
this.lastRMSentEventId = this.state.readMarkerEventId;
|
||||||
|
|
||||||
|
debuglog('TimelinePanel: Sending Read Markers for ',
|
||||||
|
this.props.timelineSet.room.roomId,
|
||||||
|
'rm', this.state.readMarkerEventId,
|
||||||
|
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
|
||||||
|
);
|
||||||
MatrixClientPeg.get().setRoomReadMarkers(
|
MatrixClientPeg.get().setRoomReadMarkers(
|
||||||
this.props.timelineSet.room.roomId,
|
this.props.timelineSet.room.roomId,
|
||||||
this.state.readMarkerEventId,
|
this.state.readMarkerEventId,
|
||||||
lastReadEvent
|
lastReadEvent, // Could be null, in which case no RR is sent
|
||||||
).catch((e) => {
|
).catch((e) => {
|
||||||
// /read_markers API is not implemented on this HS, fallback to just RR
|
// /read_markers API is not implemented on this HS, fallback to just RR
|
||||||
if (e.errcode === 'M_UNRECOGNIZED') {
|
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
|
||||||
return MatrixClientPeg.get().sendReadReceipt(
|
return MatrixClientPeg.get().sendReadReceipt(
|
||||||
lastReadEvent
|
lastReadEvent,
|
||||||
).catch(() => {
|
).catch(() => {
|
||||||
this.last_rr_sent_event_id = undefined;
|
this.lastRRSentEventId = undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// it failed, so allow retries next time the user is active
|
// it failed, so allow retries next time the user is active
|
||||||
this.last_rr_sent_event_id = undefined;
|
this.lastRRSentEventId = undefined;
|
||||||
this.last_rm_sent_event_id = undefined;
|
this.lastRMSentEventId = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
// do a quick-reset of our unreadNotificationCount to avoid having
|
// do a quick-reset of our unreadNotificationCount to avoid having
|
||||||
@ -572,7 +590,6 @@ var TimelinePanel = React.createClass({
|
|||||||
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
|
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'on_room_read',
|
action: 'on_room_read',
|
||||||
room: this.props.timelineSet.room,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ const Email = require('../../email');
|
|||||||
const AddThreepid = require('../../AddThreepid');
|
const AddThreepid = require('../../AddThreepid');
|
||||||
const SdkConfig = require('../../SdkConfig');
|
const SdkConfig = require('../../SdkConfig');
|
||||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
|
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||||
|
|
||||||
// if this looks like a release, use the 'version' from package.json; else use
|
// if this looks like a release, use the 'version' from package.json; else use
|
||||||
// the git sha. Prepend version with v, to look like riot-web version
|
// the git sha. Prepend version with v, to look like riot-web version
|
||||||
@ -36,7 +37,7 @@ const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJ
|
|||||||
|
|
||||||
// Simple method to help prettify GH Release Tags and Commit Hashes.
|
// Simple method to help prettify GH Release Tags and Commit Hashes.
|
||||||
const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i;
|
const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i;
|
||||||
const gHVersionLabel = function(repo, token) {
|
const gHVersionLabel = function(repo, token='') {
|
||||||
const match = token.match(semVerRegex);
|
const match = token.match(semVerRegex);
|
||||||
let url;
|
let url;
|
||||||
if (match && match[1]) { // basic semVer string possibly with commit hash
|
if (match && match[1]) { // basic semVer string possibly with commit hash
|
||||||
@ -151,10 +152,10 @@ module.exports = React.createClass({
|
|||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
avatarUrl: null,
|
avatarUrl: null,
|
||||||
threePids: [],
|
threepids: [],
|
||||||
phase: "UserSettings.LOADING", // LOADING, DISPLAY
|
phase: "UserSettings.LOADING", // LOADING, DISPLAY
|
||||||
email_add_pending: false,
|
email_add_pending: false,
|
||||||
vectorVersion: null,
|
vectorVersion: undefined,
|
||||||
rejectingInvites: false,
|
rejectingInvites: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -600,7 +601,12 @@ module.exports = React.createClass({
|
|||||||
_renderCryptoInfo: function() {
|
_renderCryptoInfo: function() {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const deviceId = client.deviceId;
|
const deviceId = client.deviceId;
|
||||||
const identityKey = client.getDeviceEd25519Key() || "<not supported>";
|
let identityKey = client.getDeviceEd25519Key();
|
||||||
|
if (!identityKey) {
|
||||||
|
identityKey = "<not supported>";
|
||||||
|
} else {
|
||||||
|
identityKey = FormattingUtils.formatCryptoKey(identityKey);
|
||||||
|
}
|
||||||
|
|
||||||
let importExportButtons = null;
|
let importExportButtons = null;
|
||||||
|
|
||||||
@ -848,6 +854,7 @@ module.exports = React.createClass({
|
|||||||
addEmailSection = (
|
addEmailSection = (
|
||||||
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
|
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
|
||||||
<div className="mx_UserSettings_profileLabelCell">
|
<div className="mx_UserSettings_profileLabelCell">
|
||||||
|
<label>Email</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_profileInputCell">
|
<div className="mx_UserSettings_profileInputCell">
|
||||||
<EditableText
|
<EditableText
|
||||||
@ -997,7 +1004,7 @@ module.exports = React.createClass({
|
|||||||
? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
|
? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
|
||||||
: REACT_SDK_VERSION
|
: REACT_SDK_VERSION
|
||||||
}<br/>
|
}<br/>
|
||||||
riot-web version: {(this.state.vectorVersion !== null)
|
riot-web version: {(this.state.vectorVersion !== undefined)
|
||||||
? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
|
? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
|
||||||
: 'unknown'
|
: 'unknown'
|
||||||
}<br/>
|
}<br/>
|
||||||
|
@ -47,35 +47,18 @@ export default React.createClass({
|
|||||||
children: React.PropTypes.node,
|
children: React.PropTypes.node,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
_onKeyDown: function(e) {
|
||||||
this.priorActiveElement = document.activeElement;
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
if (this.priorActiveElement !== null) {
|
|
||||||
this.priorActiveElement.focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Don't let key{down,press} events escape the modal. Consume them all.
|
|
||||||
_eatKeyEvent: function(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
},
|
|
||||||
|
|
||||||
// Must be when the key is released (and not pressed) otherwise componentWillUnmount
|
|
||||||
// will focus another element which will receive future key events
|
|
||||||
_onKeyUp: function(e) {
|
|
||||||
if (e.keyCode === KeyCode.ESCAPE) {
|
if (e.keyCode === KeyCode.ESCAPE) {
|
||||||
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
} else if (e.keyCode === KeyCode.ENTER) {
|
} else if (e.keyCode === KeyCode.ENTER) {
|
||||||
if (this.props.onEnterPressed) {
|
if (this.props.onEnterPressed) {
|
||||||
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onEnterPressed(e);
|
this.props.onEnterPressed(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Consume all keyup events while Modal is open
|
|
||||||
e.stopPropagation();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_onCancelClick: function(e) {
|
_onCancelClick: function(e) {
|
||||||
@ -84,13 +67,9 @@ export default React.createClass({
|
|||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onKeyUp={this._onKeyUp}
|
<div onKeyDown={this._onKeyDown} className={this.props.className}>
|
||||||
onKeyDown={this._eatKeyEvent}
|
|
||||||
onKeyPress={this._eatKeyEvent}
|
|
||||||
className={this.props.className}
|
|
||||||
>
|
|
||||||
<AccessibleButton onClick={this._onCancelClick}
|
<AccessibleButton onClick={this._onCancelClick}
|
||||||
className="mx_Dialog_cancelButton"
|
className="mx_Dialog_cancelButton"
|
||||||
>
|
>
|
||||||
|
76
src/components/views/dialogs/DeviceVerifyDialog.js
Normal file
76
src/components/views/dialogs/DeviceVerifyDialog.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations 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 MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||||
|
|
||||||
|
export default function DeviceVerifyDialog(props) {
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
|
||||||
|
const key = FormattingUtils.formatCryptoKey(props.device.getFingerprint());
|
||||||
|
const body = (
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
To verify that this device can be trusted, please contact its
|
||||||
|
owner using some other means (e.g. in person or a phone call)
|
||||||
|
and ask them whether the key they see in their User Settings
|
||||||
|
for this device matches the key below:
|
||||||
|
</p>
|
||||||
|
<div className="mx_UserSettings_cryptoSection">
|
||||||
|
<ul>
|
||||||
|
<li><label>Device name:</label> <span>{ props.device.getDisplayName() }</span></li>
|
||||||
|
<li><label>Device ID:</label> <span><code>{ props.device.deviceId}</code></span></li>
|
||||||
|
<li><label>Device key:</label> <span><code><b>{ key }</b></code></span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
If it matches, press the verify button below.
|
||||||
|
If it doesnt, then someone else is intercepting this device
|
||||||
|
and you probably want to press the blacklist button instead.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In future this verification process will be more sophisticated.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function onFinished(confirm) {
|
||||||
|
if (confirm) {
|
||||||
|
MatrixClientPeg.get().setDeviceVerified(
|
||||||
|
props.userId, props.device.deviceId, true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
props.onFinished(confirm);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QuestionDialog
|
||||||
|
title="Verify device"
|
||||||
|
description={body}
|
||||||
|
button="I verify that the keys match"
|
||||||
|
onFinished={onFinished}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceVerifyDialog.propTypes = {
|
||||||
|
userId: React.PropTypes.string.isRequired,
|
||||||
|
device: React.PropTypes.object.isRequired,
|
||||||
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
|
};
|
@ -47,12 +47,6 @@ export default React.createClass({
|
|||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
if (this.props.focus) {
|
|
||||||
this.refs.button.focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const cancelButton = this.props.hasCancelButton ? (
|
const cancelButton = this.props.hasCancelButton ? (
|
||||||
@ -69,7 +63,7 @@ export default React.createClass({
|
|||||||
{this.props.description}
|
{this.props.description}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button ref="button" className="mx_Dialog_primary" onClick={this.onOk}>
|
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
|
||||||
{this.props.button}
|
{this.props.button}
|
||||||
</button>
|
</button>
|
||||||
{this.props.extraButtons}
|
{this.props.extraButtons}
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2017 Vector Creations 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 PropTypes from 'prop-types';
|
|
||||||
import AccessibleButton from './AccessibleButton';
|
|
||||||
import dis from '../../../dispatcher';
|
|
||||||
import sdk from '../../../index';
|
|
||||||
|
|
||||||
export default React.createClass({
|
|
||||||
displayName: 'RoleButton',
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
size: PropTypes.string,
|
|
||||||
tooltip: PropTypes.bool,
|
|
||||||
action: PropTypes.string.isRequired,
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
iconPath: PropTypes.string.isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
size: "25",
|
|
||||||
tooltip: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
showTooltip: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
_onClick: function(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
dis.dispatch({action: this.props.action});
|
|
||||||
},
|
|
||||||
|
|
||||||
_onMouseEnter: function() {
|
|
||||||
if (this.props.tooltip) this.setState({showTooltip: true});
|
|
||||||
},
|
|
||||||
|
|
||||||
_onMouseLeave: function() {
|
|
||||||
this.setState({showTooltip: false});
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
|
||||||
|
|
||||||
let tooltip;
|
|
||||||
if (this.state.showTooltip) {
|
|
||||||
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
|
||||||
tooltip = <RoomTooltip className="mx_RoleButton_tooltip" label={this.props.label} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AccessibleButton className="mx_RoleButton"
|
|
||||||
onClick={this._onClick}
|
|
||||||
onMouseEnter={this._onMouseEnter}
|
|
||||||
onMouseLeave={this._onMouseLeave}
|
|
||||||
>
|
|
||||||
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
|
|
||||||
{tooltip}
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2017 Vector Creations 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 PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const CreateRoomButton = function(props) {
|
|
||||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
|
||||||
return (
|
|
||||||
<ActionButton action="view_create_room"
|
|
||||||
label="Create new room"
|
|
||||||
iconPath="img/icons-create-room.svg"
|
|
||||||
size={props.size}
|
|
||||||
tooltip={props.tooltip}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
CreateRoomButton.propTypes = {
|
|
||||||
size: PropTypes.string,
|
|
||||||
tooltip: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CreateRoomButton;
|
|
@ -50,42 +50,10 @@ export default React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onVerifyClick: function() {
|
onVerifyClick: function() {
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createDialog(DeviceVerifyDialog, {
|
||||||
title: "Verify device",
|
userId: this.props.userId,
|
||||||
description: (
|
device: this.state.device,
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
To verify that this device can be trusted, please contact its
|
|
||||||
owner using some other means (e.g. in person or a phone call)
|
|
||||||
and ask them whether the key they see in their User Settings
|
|
||||||
for this device matches the key below:
|
|
||||||
</p>
|
|
||||||
<div className="mx_UserSettings_cryptoSection">
|
|
||||||
<ul>
|
|
||||||
<li><label>Device name:</label> <span>{ this.state.device.getDisplayName() }</span></li>
|
|
||||||
<li><label>Device ID:</label> <span><code>{ this.state.device.deviceId}</code></span></li>
|
|
||||||
<li><label>Device key:</label> <span><code><b>{ this.state.device.getFingerprint() }</b></code></span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
If it matches, press the verify button below.
|
|
||||||
If it doesnt, then someone else is intercepting this device
|
|
||||||
and you probably want to press the blacklist button instead.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
In future this verification process will be more sophisticated.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
button: "I verify that the keys match",
|
|
||||||
onFinished: confirm=>{
|
|
||||||
if (confirm) {
|
|
||||||
MatrixClientPeg.get().setDeviceVerified(
|
|
||||||
this.props.userId, this.state.device.deviceId, true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -152,10 +152,12 @@ export default class Dropdown extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onInputClick(ev) {
|
_onInputClick(ev) {
|
||||||
this.setState({
|
if (!this.state.expanded) {
|
||||||
expanded: !this.state.expanded,
|
this.setState({
|
||||||
});
|
expanded: true,
|
||||||
ev.preventDefault();
|
});
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMenuOptionClick(dropdownKey) {
|
_onMenuOptionClick(dropdownKey) {
|
||||||
@ -252,7 +254,7 @@ export default class Dropdown extends React.Component {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
if (options.length === 0) {
|
if (options.length === 0) {
|
||||||
return [<div className="mx_Dropdown_option">
|
return [<div key="0" className="mx_Dropdown_option">
|
||||||
No results
|
No results
|
||||||
</div>];
|
</div>];
|
||||||
}
|
}
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2017 Vector Creations 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 PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const HomeButton = function(props) {
|
|
||||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
|
||||||
return (
|
|
||||||
<ActionButton action="view_home_page"
|
|
||||||
label="Welcome page"
|
|
||||||
iconPath="img/icons-home.svg"
|
|
||||||
size={props.size}
|
|
||||||
tooltip={props.tooltip}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
HomeButton.propTypes = {
|
|
||||||
size: PropTypes.string,
|
|
||||||
tooltip: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HomeButton;
|
|
@ -269,7 +269,7 @@ module.exports = React.createClass({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<span className="mx_MemberEventListSummary_avatars">
|
<span className="mx_MemberEventListSummary_avatars" onClick={ this._toggleSummary }>
|
||||||
{avatars}
|
{avatars}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2017 Vector Creations 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 PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const RoomDirectoryButton = function(props) {
|
|
||||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
|
||||||
return (
|
|
||||||
<ActionButton action="view_room_directory"
|
|
||||||
label="Room directory"
|
|
||||||
iconPath="img/icons-directory.svg"
|
|
||||||
size={props.size}
|
|
||||||
tooltip={props.tooltip}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
RoomDirectoryButton.propTypes = {
|
|
||||||
size: PropTypes.string,
|
|
||||||
tooltip: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RoomDirectoryButton;
|
|
@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2017 Vector Creations 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 PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const SettingsButton = function(props) {
|
|
||||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
|
||||||
return (
|
|
||||||
<ActionButton action="view_user_settings"
|
|
||||||
label="Settings"
|
|
||||||
iconPath="img/icons-settings.svg"
|
|
||||||
size={props.size}
|
|
||||||
tooltip={props.tooltip}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
SettingsButton.propTypes = {
|
|
||||||
size: PropTypes.string,
|
|
||||||
tooltip: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SettingsButton;
|
|
@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2017 Vector Creations 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 PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const StartChatButton = function(props) {
|
|
||||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
|
||||||
return (
|
|
||||||
<ActionButton action="view_create_chat"
|
|
||||||
label="Start chat"
|
|
||||||
iconPath="img/icons-people.svg"
|
|
||||||
size={props.size}
|
|
||||||
tooltip={props.tooltip}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
StartChatButton.propTypes = {
|
|
||||||
size: PropTypes.string,
|
|
||||||
tooltip: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StartChatButton;
|
|
@ -19,7 +19,6 @@ import React from 'react';
|
|||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
|
||||||
import { COUNTRIES } from '../../../phonenumber';
|
import { COUNTRIES } from '../../../phonenumber';
|
||||||
import { charactersToImageNode } from '../../../HtmlUtils';
|
|
||||||
|
|
||||||
const COUNTRIES_BY_ISO2 = new Object(null);
|
const COUNTRIES_BY_ISO2 = new Object(null);
|
||||||
for (const c of COUNTRIES) {
|
for (const c of COUNTRIES) {
|
||||||
@ -27,9 +26,14 @@ for (const c of COUNTRIES) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function countryMatchesSearchQuery(query, country) {
|
function countryMatchesSearchQuery(query, country) {
|
||||||
|
// Remove '+' if present (when searching for a prefix)
|
||||||
|
if (query[0] === '+') {
|
||||||
|
query = query.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
|
if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
|
||||||
if (country.iso2 == query.toUpperCase()) return true;
|
if (country.iso2 == query.toUpperCase()) return true;
|
||||||
if (country.prefix == query) return true;
|
if (country.prefix.indexOf(query) !== -1) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,10 +42,11 @@ export default class CountryDropdown extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
this._onSearchChange = this._onSearchChange.bind(this);
|
this._onSearchChange = this._onSearchChange.bind(this);
|
||||||
this._onOptionChange = this._onOptionChange.bind(this);
|
this._onOptionChange = this._onOptionChange.bind(this);
|
||||||
|
this._getShortOption = this._getShortOption.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
@ -64,13 +69,21 @@ export default class CountryDropdown extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_flagImgForIso2(iso2) {
|
_flagImgForIso2(iso2) {
|
||||||
// Unicode Regional Indicator Symbol letter 'A'
|
return <img src={`flags/${iso2}.png`}/>;
|
||||||
const RIS_A = 0x1F1E6;
|
}
|
||||||
const ASCII_A = 65;
|
|
||||||
return charactersToImageNode(iso2, true,
|
_getShortOption(iso2) {
|
||||||
RIS_A + (iso2.charCodeAt(0) - ASCII_A),
|
if (!this.props.isSmall) {
|
||||||
RIS_A + (iso2.charCodeAt(1) - ASCII_A),
|
return undefined;
|
||||||
);
|
}
|
||||||
|
let countryPrefix;
|
||||||
|
if (this.props.showPrefix) {
|
||||||
|
countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix;
|
||||||
|
}
|
||||||
|
return <span>
|
||||||
|
{ this._flagImgForIso2(iso2) }
|
||||||
|
{ countryPrefix }
|
||||||
|
</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -99,7 +112,7 @@ export default class CountryDropdown extends React.Component {
|
|||||||
const options = displayedCountries.map((country) => {
|
const options = displayedCountries.map((country) => {
|
||||||
return <div key={country.iso2}>
|
return <div key={country.iso2}>
|
||||||
{this._flagImgForIso2(country.iso2)}
|
{this._flagImgForIso2(country.iso2)}
|
||||||
{country.name}
|
{country.name} <span>(+{country.prefix})</span>
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -107,21 +120,21 @@ export default class CountryDropdown extends React.Component {
|
|||||||
// values between mounting and the initial value propgating
|
// values between mounting and the initial value propgating
|
||||||
const value = this.props.value || COUNTRIES[0].iso2;
|
const value = this.props.value || COUNTRIES[0].iso2;
|
||||||
|
|
||||||
const getShortOption = this.props.isSmall ? this._flagImgForIso2 : undefined;
|
return <Dropdown className={this.props.className + " left_aligned"}
|
||||||
|
|
||||||
return <Dropdown className={this.props.className}
|
|
||||||
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
|
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
|
||||||
menuWidth={298} getShortOption={getShortOption}
|
menuWidth={298} getShortOption={this._getShortOption}
|
||||||
value={value} searchEnabled={true}
|
value={value} searchEnabled={true}
|
||||||
>
|
>
|
||||||
{options}
|
{options}
|
||||||
</Dropdown>
|
</Dropdown>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CountryDropdown.propTypes = {
|
CountryDropdown.propTypes = {
|
||||||
className: React.PropTypes.string,
|
className: React.PropTypes.string,
|
||||||
isSmall: React.PropTypes.bool,
|
isSmall: React.PropTypes.bool,
|
||||||
|
// if isSmall, show +44 in the selected value
|
||||||
|
showPrefix: React.PropTypes.bool,
|
||||||
onOptionChange: React.PropTypes.func.isRequired,
|
onOptionChange: React.PropTypes.func.isRequired,
|
||||||
value: React.PropTypes.string,
|
value: React.PropTypes.string,
|
||||||
};
|
};
|
||||||
|
@ -149,28 +149,26 @@ class PasswordLogin extends React.Component {
|
|||||||
</div>;
|
</div>;
|
||||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||||
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||||
const prefix = this.state.phonePrefix;
|
|
||||||
return <div className="mx_Login_phoneSection">
|
return <div className="mx_Login_phoneSection">
|
||||||
<CountryDropdown
|
<CountryDropdown
|
||||||
className="mx_Login_phoneCountry"
|
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
||||||
ref="phone_country"
|
ref="phone_country"
|
||||||
onOptionChange={this.onPhoneCountryChanged}
|
onOptionChange={this.onPhoneCountryChanged}
|
||||||
value={this.state.phoneCountry}
|
value={this.state.phoneCountry}
|
||||||
|
isSmall={true}
|
||||||
|
showPrefix={true}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="mx_Login_phoneNumberField mx_Login_field mx_Login_field_has_prefix"
|
||||||
|
ref="phoneNumber"
|
||||||
|
key="phone_input"
|
||||||
|
type="text"
|
||||||
|
name="phoneNumber"
|
||||||
|
onChange={this.onPhoneNumberChanged}
|
||||||
|
placeholder="Mobile phone number"
|
||||||
|
value={this.state.phoneNumber}
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div className="mx_Login_field_group">
|
|
||||||
<div className="mx_Login_field_prefix">+{prefix}</div>
|
|
||||||
<input
|
|
||||||
className="mx_Login_phoneNumberField mx_Login_field mx_Login_field_has_prefix"
|
|
||||||
ref="phoneNumber"
|
|
||||||
key="phone_input"
|
|
||||||
type="text"
|
|
||||||
name="phoneNumber"
|
|
||||||
onChange={this.onPhoneNumberChanged}
|
|
||||||
placeholder="Mobile phone number"
|
|
||||||
value={this.state.phoneNumber}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -314,24 +314,23 @@ module.exports = React.createClass({
|
|||||||
const phoneSection = (
|
const phoneSection = (
|
||||||
<div className="mx_Login_phoneSection">
|
<div className="mx_Login_phoneSection">
|
||||||
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
|
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
|
||||||
className="mx_Login_phoneCountry"
|
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
||||||
value={this.state.phoneCountry}
|
value={this.state.phoneCountry}
|
||||||
|
isSmall={true}
|
||||||
|
showPrefix={true}
|
||||||
|
/>
|
||||||
|
<input type="text" ref="phoneNumber"
|
||||||
|
placeholder="Mobile phone number (optional)"
|
||||||
|
defaultValue={this.props.defaultPhoneNumber}
|
||||||
|
className={this._classForField(
|
||||||
|
FIELD_PHONE_NUMBER,
|
||||||
|
'mx_Login_phoneNumberField',
|
||||||
|
'mx_Login_field',
|
||||||
|
'mx_Login_field_has_prefix'
|
||||||
|
)}
|
||||||
|
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
|
||||||
|
value={self.state.phoneNumber}
|
||||||
/>
|
/>
|
||||||
<div className="mx_Login_field_group">
|
|
||||||
<div className="mx_Login_field_prefix">+{this.state.phonePrefix}</div>
|
|
||||||
<input type="text" ref="phoneNumber"
|
|
||||||
placeholder="Mobile phone number (optional)"
|
|
||||||
defaultValue={this.props.defaultPhoneNumber}
|
|
||||||
className={this._classForField(
|
|
||||||
FIELD_PHONE_NUMBER,
|
|
||||||
'mx_Login_phoneNumberField',
|
|
||||||
'mx_Login_field',
|
|
||||||
'mx_Login_field_has_prefix'
|
|
||||||
)}
|
|
||||||
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
|
|
||||||
value={self.state.phoneNumber}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -295,16 +295,6 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
const receiptOffset = 15;
|
const receiptOffset = 15;
|
||||||
let left = 0;
|
let left = 0;
|
||||||
|
|
||||||
// It's possible that the receipt was sent several days AFTER the event.
|
|
||||||
// If it is, we want to display the complete date along with the HH:MM:SS,
|
|
||||||
// rather than just HH:MM:SS.
|
|
||||||
let dayAfterEvent = new Date(this.props.mxEvent.getTs());
|
|
||||||
dayAfterEvent.setDate(dayAfterEvent.getDate() + 1);
|
|
||||||
dayAfterEvent.setHours(0);
|
|
||||||
dayAfterEvent.setMinutes(0);
|
|
||||||
dayAfterEvent.setSeconds(0);
|
|
||||||
let dayAfterEventTime = dayAfterEvent.getTime();
|
|
||||||
|
|
||||||
var receipts = this.props.readReceipts || [];
|
var receipts = this.props.readReceipts || [];
|
||||||
for (var i = 0; i < receipts.length; ++i) {
|
for (var i = 0; i < receipts.length; ++i) {
|
||||||
var receipt = receipts[i];
|
var receipt = receipts[i];
|
||||||
@ -340,7 +330,6 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
suppressAnimation={this._suppressReadReceiptAnimation}
|
suppressAnimation={this._suppressReadReceiptAnimation}
|
||||||
onClick={this.toggleAllReadAvatars}
|
onClick={this.toggleAllReadAvatars}
|
||||||
timestamp={receipt.ts}
|
timestamp={receipt.ts}
|
||||||
showFullTimestamp={receipt.ts >= dayAfterEventTime}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -492,22 +481,22 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
var e2e;
|
var e2e;
|
||||||
// cosmetic padlocks:
|
// cosmetic padlocks:
|
||||||
if ((e2eEnabled && this.props.eventSendStatus) || this.props.mxEvent.getType() === 'm.room.encryption') {
|
if ((e2eEnabled && this.props.eventSendStatus) || this.props.mxEvent.getType() === 'm.room.encryption') {
|
||||||
e2e = <img style={{ cursor: 'initial', marginLeft: '-1px' }} className="mx_EventTile_e2eIcon" src="img/e2e-verified.svg" width="10" height="12" />;
|
e2e = <img style={{ cursor: 'initial', marginLeft: '-1px' }} className="mx_EventTile_e2eIcon" alt="Encrypted by verified device" src="img/e2e-verified.svg" width="10" height="12" />;
|
||||||
}
|
}
|
||||||
// real padlocks
|
// real padlocks
|
||||||
else if (this.props.mxEvent.isEncrypted() || (e2eEnabled && this.props.eventSendStatus)) {
|
else if (this.props.mxEvent.isEncrypted() || (e2eEnabled && this.props.eventSendStatus)) {
|
||||||
if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') {
|
if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') {
|
||||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} />;
|
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Undecryptable" src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} />;
|
||||||
}
|
}
|
||||||
else if (this.state.verified == true || (e2eEnabled && this.props.eventSendStatus)) {
|
else if (this.state.verified == true || (e2eEnabled && this.props.eventSendStatus)) {
|
||||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-verified.svg" width="10" height="12"/>;
|
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Encrypted by verified device" src="img/e2e-verified.svg" width="10" height="12"/>;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }}/>;
|
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Encrypted by unverified device" src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }}/>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (e2eEnabled) {
|
else if (e2eEnabled) {
|
||||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12"/>;
|
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt="Unencrypted message" src="img/e2e-unencrypted.svg" width="12" height="12"/>;
|
||||||
}
|
}
|
||||||
const timestamp = this.props.mxEvent.getTs() ?
|
const timestamp = this.props.mxEvent.getTs() ?
|
||||||
<MessageTimestamp ts={this.props.mxEvent.getTs()} /> : null;
|
<MessageTimestamp ts={this.props.mxEvent.getTs()} /> : null;
|
||||||
|
@ -713,8 +713,16 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
|
|
||||||
const memberName = this.props.member.name;
|
const memberName = this.props.member.name;
|
||||||
|
|
||||||
|
if (this.props.member.user) {
|
||||||
|
var presenceState = this.props.member.user.presence;
|
||||||
|
var presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
|
||||||
|
var presenceLastTs = this.props.member.user.lastPresenceTs;
|
||||||
|
var presenceCurrentlyActive = this.props.member.user.currentlyActive;
|
||||||
|
}
|
||||||
|
|
||||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||||
|
var PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||||
return (
|
return (
|
||||||
<div className="mx_MemberInfo">
|
<div className="mx_MemberInfo">
|
||||||
@ -732,6 +740,11 @@ module.exports = WithMatrixClient(React.createClass({
|
|||||||
<div className="mx_MemberInfo_profileField">
|
<div className="mx_MemberInfo_profileField">
|
||||||
Level: <b><PowerSelector controlled={true} value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b>
|
Level: <b><PowerSelector controlled={true} value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mx_MemberInfo_profileField">
|
||||||
|
<PresenceLabel activeAgo={ presenceLastActiveAgo }
|
||||||
|
currentlyActive={ presenceCurrentlyActive }
|
||||||
|
presenceState={ presenceState } />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ adminTools }
|
{ adminTools }
|
||||||
|
@ -33,6 +33,7 @@ export default class MessageComposer extends React.Component {
|
|||||||
this.onHangupClick = this.onHangupClick.bind(this);
|
this.onHangupClick = this.onHangupClick.bind(this);
|
||||||
this.onUploadClick = this.onUploadClick.bind(this);
|
this.onUploadClick = this.onUploadClick.bind(this);
|
||||||
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
||||||
|
this.uploadFiles = this.uploadFiles.bind(this);
|
||||||
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
||||||
this.onInputContentChanged = this.onInputContentChanged.bind(this);
|
this.onInputContentChanged = this.onInputContentChanged.bind(this);
|
||||||
this.onUpArrow = this.onUpArrow.bind(this);
|
this.onUpArrow = this.onUpArrow.bind(this);
|
||||||
@ -97,10 +98,11 @@ export default class MessageComposer extends React.Component {
|
|||||||
this.refs.uploadInput.click();
|
this.refs.uploadInput.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
onUploadFileSelected(files, isPasted) {
|
onUploadFileSelected(files) {
|
||||||
if (!isPasted)
|
this.uploadFiles(files.target.files);
|
||||||
files = files.target.files;
|
}
|
||||||
|
|
||||||
|
uploadFiles(files) {
|
||||||
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
let TintableSvg = sdk.getComponent("elements.TintableSvg");
|
let TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
@ -306,7 +308,7 @@ export default class MessageComposer extends React.Component {
|
|||||||
tryComplete={this._tryComplete}
|
tryComplete={this._tryComplete}
|
||||||
onUpArrow={this.onUpArrow}
|
onUpArrow={this.onUpArrow}
|
||||||
onDownArrow={this.onDownArrow}
|
onDownArrow={this.onDownArrow}
|
||||||
onUploadFileSelected={this.onUploadFileSelected}
|
onFilesPasted={this.uploadFiles}
|
||||||
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
|
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
|
||||||
onContentChanged={this.onInputContentChanged}
|
onContentChanged={this.onInputContentChanged}
|
||||||
onInputStateChanged={this.onInputStateChanged} />,
|
onInputStateChanged={this.onInputStateChanged} />,
|
||||||
|
@ -84,7 +84,6 @@ export default class MessageComposerInput extends React.Component {
|
|||||||
this.onAction = this.onAction.bind(this);
|
this.onAction = this.onAction.bind(this);
|
||||||
this.handleReturn = this.handleReturn.bind(this);
|
this.handleReturn = this.handleReturn.bind(this);
|
||||||
this.handleKeyCommand = this.handleKeyCommand.bind(this);
|
this.handleKeyCommand = this.handleKeyCommand.bind(this);
|
||||||
this.handlePastedFiles = this.handlePastedFiles.bind(this);
|
|
||||||
this.onEditorContentChanged = this.onEditorContentChanged.bind(this);
|
this.onEditorContentChanged = this.onEditorContentChanged.bind(this);
|
||||||
this.setEditorState = this.setEditorState.bind(this);
|
this.setEditorState = this.setEditorState.bind(this);
|
||||||
this.onUpArrow = this.onUpArrow.bind(this);
|
this.onUpArrow = this.onUpArrow.bind(this);
|
||||||
@ -477,10 +476,6 @@ export default class MessageComposerInput extends React.Component {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePastedFiles(files) {
|
|
||||||
this.props.onUploadFileSelected(files, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleReturn(ev) {
|
handleReturn(ev) {
|
||||||
if (ev.shiftKey) {
|
if (ev.shiftKey) {
|
||||||
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
||||||
@ -542,9 +537,9 @@ export default class MessageComposerInput extends React.Component {
|
|||||||
let sendTextFn = this.client.sendTextMessage;
|
let sendTextFn = this.client.sendTextMessage;
|
||||||
|
|
||||||
if (contentText.startsWith('/me')) {
|
if (contentText.startsWith('/me')) {
|
||||||
contentText = contentText.replace('/me ', '');
|
contentText = contentText.substring(4);
|
||||||
// bit of a hack, but the alternative would be quite complicated
|
// bit of a hack, but the alternative would be quite complicated
|
||||||
if (contentHTML) contentHTML = contentHTML.replace('/me ', '');
|
if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, '');
|
||||||
sendHtmlFn = this.client.sendHtmlEmote;
|
sendHtmlFn = this.client.sendHtmlEmote;
|
||||||
sendTextFn = this.client.sendEmoteMessage;
|
sendTextFn = this.client.sendEmoteMessage;
|
||||||
}
|
}
|
||||||
@ -734,7 +729,7 @@ export default class MessageComposerInput extends React.Component {
|
|||||||
keyBindingFn={MessageComposerInput.getKeyBinding}
|
keyBindingFn={MessageComposerInput.getKeyBinding}
|
||||||
handleKeyCommand={this.handleKeyCommand}
|
handleKeyCommand={this.handleKeyCommand}
|
||||||
handleReturn={this.handleReturn}
|
handleReturn={this.handleReturn}
|
||||||
handlePastedFiles={this.handlePastedFiles}
|
handlePastedFiles={this.props.onFilesPasted}
|
||||||
stripPastedStyles={!this.state.isRichtextEnabled}
|
stripPastedStyles={!this.state.isRichtextEnabled}
|
||||||
onTab={this.onTab}
|
onTab={this.onTab}
|
||||||
onUpArrow={this.onUpArrow}
|
onUpArrow={this.onUpArrow}
|
||||||
@ -764,7 +759,7 @@ MessageComposerInput.propTypes = {
|
|||||||
|
|
||||||
onDownArrow: React.PropTypes.func,
|
onDownArrow: React.PropTypes.func,
|
||||||
|
|
||||||
onUploadFileSelected: React.PropTypes.func,
|
onFilesPasted: React.PropTypes.func,
|
||||||
|
|
||||||
// attempts to confirm currently selected completion, returns whether actually confirmed
|
// attempts to confirm currently selected completion, returns whether actually confirmed
|
||||||
tryComplete: React.PropTypes.func,
|
tryComplete: React.PropTypes.func,
|
||||||
|
@ -69,6 +69,9 @@ export default React.createClass({
|
|||||||
|
|
||||||
// The text to use a placeholder in the input box
|
// The text to use a placeholder in the input box
|
||||||
placeholder: React.PropTypes.string.isRequired,
|
placeholder: React.PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
// callback to handle files pasted into the composer
|
||||||
|
onFilesPasted: React.PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
@ -439,10 +442,27 @@ export default React.createClass({
|
|||||||
this.refs.textarea.focus();
|
this.refs.textarea.focus();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onPaste: function(ev) {
|
||||||
|
const items = ev.clipboardData.items;
|
||||||
|
const files = [];
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.kind === 'file') {
|
||||||
|
files.push(item.getAsFile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (files.length && this.props.onFilesPasted) {
|
||||||
|
this.props.onFilesPasted(files);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
|
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
|
||||||
<textarea autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder={this.props.placeholder} />
|
<textarea autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder={this.props.placeholder}
|
||||||
|
onPaste={this._onPaste}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,8 @@ var sdk = require('../../../index');
|
|||||||
var Velociraptor = require('../../../Velociraptor');
|
var Velociraptor = require('../../../Velociraptor');
|
||||||
require('../../../VelocityBounce');
|
require('../../../VelocityBounce');
|
||||||
|
|
||||||
|
import DateUtils from '../../../DateUtils';
|
||||||
|
|
||||||
var bounce = false;
|
var bounce = false;
|
||||||
try {
|
try {
|
||||||
if (global.localStorage) {
|
if (global.localStorage) {
|
||||||
@ -63,9 +65,6 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
// Timestamp when the receipt was read
|
// Timestamp when the receipt was read
|
||||||
timestamp: React.PropTypes.number,
|
timestamp: React.PropTypes.number,
|
||||||
|
|
||||||
// True to show the full date/time rather than just the time
|
|
||||||
showFullTimestamp: React.PropTypes.bool,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
@ -170,16 +169,8 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
let title;
|
let title;
|
||||||
if (this.props.timestamp) {
|
if (this.props.timestamp) {
|
||||||
const prefix = "Seen by " + this.props.member.userId + " at ";
|
title = "Seen by " + this.props.member.userId + " at " +
|
||||||
let ts = new Date(this.props.timestamp);
|
DateUtils.formatDate(new Date(this.props.timestamp));
|
||||||
if (this.props.showFullTimestamp) {
|
|
||||||
// "15/12/2016, 7:05:45 PM (@alice:matrix.org)"
|
|
||||||
title = prefix + ts.toLocaleString();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// "7:05:45 PM (@alice:matrix.org)"
|
|
||||||
title = prefix + ts.toLocaleTimeString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
|
var classNames = require('classnames');
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
var Modal = require("../../../Modal");
|
var Modal = require("../../../Modal");
|
||||||
@ -39,6 +40,7 @@ module.exports = React.createClass({
|
|||||||
oobData: React.PropTypes.object,
|
oobData: React.PropTypes.object,
|
||||||
editing: React.PropTypes.bool,
|
editing: React.PropTypes.bool,
|
||||||
saving: React.PropTypes.bool,
|
saving: React.PropTypes.bool,
|
||||||
|
inRoom: React.PropTypes.bool,
|
||||||
collapsedRhs: React.PropTypes.bool,
|
collapsedRhs: React.PropTypes.bool,
|
||||||
onSettingsClick: React.PropTypes.func,
|
onSettingsClick: React.PropTypes.func,
|
||||||
onSaveClick: React.PropTypes.func,
|
onSaveClick: React.PropTypes.func,
|
||||||
@ -49,7 +51,7 @@ module.exports = React.createClass({
|
|||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
editing: false,
|
editing: false,
|
||||||
onSettingsClick: function() {},
|
inRoom: false,
|
||||||
onSaveClick: function() {},
|
onSaveClick: function() {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -225,10 +227,10 @@ module.exports = React.createClass({
|
|||||||
roomName = this.props.room.name;
|
roomName = this.props.room.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emojiTextClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
|
||||||
name =
|
name =
|
||||||
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
|
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
|
||||||
<EmojiText element="div" className={ "mx_RoomHeader_nametext " + (settingsHint ? "mx_RoomHeader_settingsHint" : "") } title={ roomName }>{roomName}</EmojiText>
|
<EmojiText element="div" className={emojiTextClasses} title={roomName}>{ roomName }</EmojiText>
|
||||||
{ searchStatus }
|
{ searchStatus }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
@ -299,6 +301,14 @@ module.exports = React.createClass({
|
|||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let search_button;
|
||||||
|
if (this.props.onSearchClick && this.props.inRoom) {
|
||||||
|
search_button =
|
||||||
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
|
||||||
|
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
|
||||||
|
</AccessibleButton>;
|
||||||
|
}
|
||||||
|
|
||||||
var rightPanel_buttons;
|
var rightPanel_buttons;
|
||||||
if (this.props.collapsedRhs) {
|
if (this.props.collapsedRhs) {
|
||||||
rightPanel_buttons =
|
rightPanel_buttons =
|
||||||
@ -313,9 +323,7 @@ module.exports = React.createClass({
|
|||||||
<div className="mx_RoomHeader_rightRow">
|
<div className="mx_RoomHeader_rightRow">
|
||||||
{ settings_button }
|
{ settings_button }
|
||||||
{ forget_button }
|
{ forget_button }
|
||||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
|
{ search_button }
|
||||||
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
|
|
||||||
</AccessibleButton>
|
|
||||||
{ rightPanel_buttons }
|
{ rightPanel_buttons }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -22,23 +21,15 @@ var GeminiScrollbar = require('react-gemini-scrollbar');
|
|||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
var CallHandler = require('../../../CallHandler');
|
var CallHandler = require('../../../CallHandler');
|
||||||
var RoomListSorter = require("../../../RoomListSorter");
|
var RoomListSorter = require("../../../RoomListSorter");
|
||||||
|
var Unread = require('../../../Unread');
|
||||||
var dis = require("../../../dispatcher");
|
var dis = require("../../../dispatcher");
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
var rate_limited_func = require('../../../ratelimitedfunc');
|
var rate_limited_func = require('../../../ratelimitedfunc');
|
||||||
var Rooms = require('../../../Rooms');
|
var Rooms = require('../../../Rooms');
|
||||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
var Receipt = require('../../../utils/Receipt');
|
var Receipt = require('../../../utils/Receipt');
|
||||||
var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
|
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
|
||||||
|
|
||||||
const HIDE_CONFERENCE_CHANS = true;
|
var HIDE_CONFERENCE_CHANS = true;
|
||||||
|
|
||||||
const VERBS = {
|
|
||||||
'm.favourite': 'favourite',
|
|
||||||
'im.vector.fake.direct': 'tag direct chat',
|
|
||||||
'im.vector.fake.recent': 'restore',
|
|
||||||
'm.lowpriority': 'demote',
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomList',
|
displayName: 'RoomList',
|
||||||
@ -46,29 +37,21 @@ module.exports = React.createClass({
|
|||||||
propTypes: {
|
propTypes: {
|
||||||
ConferenceHandler: React.PropTypes.any,
|
ConferenceHandler: React.PropTypes.any,
|
||||||
collapsed: React.PropTypes.bool.isRequired,
|
collapsed: React.PropTypes.bool.isRequired,
|
||||||
selectedRoom: React.PropTypes.string,
|
currentRoom: React.PropTypes.string,
|
||||||
searchFilter: React.PropTypes.string,
|
searchFilter: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldComponentUpdate: function(nextProps, nextState) {
|
|
||||||
if (nextProps.collapsed !== this.props.collapsed) return true;
|
|
||||||
if (nextProps.searchFilter !== this.props.searchFilter) return true;
|
|
||||||
if (nextState.lists !== this.state.lists ||
|
|
||||||
nextState.isLoadingLeftRooms !== this.state.isLoadingLeftRooms ||
|
|
||||||
nextState.incomingCall !== this.state.incomingCall) return true;
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
isLoadingLeftRooms: false,
|
isLoadingLeftRooms: false,
|
||||||
totalRoomCount: null,
|
|
||||||
lists: {},
|
lists: {},
|
||||||
incomingCall: null,
|
incomingCall: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
this.mounted = false;
|
||||||
|
|
||||||
var cli = MatrixClientPeg.get();
|
var cli = MatrixClientPeg.get();
|
||||||
cli.on("Room", this.onRoom);
|
cli.on("Room", this.onRoom);
|
||||||
cli.on("deleteRoom", this.onDeleteRoom);
|
cli.on("deleteRoom", this.onDeleteRoom);
|
||||||
@ -76,45 +59,23 @@ module.exports = React.createClass({
|
|||||||
cli.on("Room.name", this.onRoomName);
|
cli.on("Room.name", this.onRoomName);
|
||||||
cli.on("Room.tags", this.onRoomTags);
|
cli.on("Room.tags", this.onRoomTags);
|
||||||
cli.on("Room.receipt", this.onRoomReceipt);
|
cli.on("Room.receipt", this.onRoomReceipt);
|
||||||
cli.on("RoomState.members", this.onRoomStateMember);
|
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||||
cli.on("accountData", this.onAccountData);
|
cli.on("accountData", this.onAccountData);
|
||||||
|
|
||||||
// lookup for which lists a given roomId is currently in.
|
var s = this.getRoomLists();
|
||||||
this.listsForRoomId = {};
|
this.setState(s);
|
||||||
|
|
||||||
this.refreshRoomList();
|
|
||||||
|
|
||||||
// order of the sublists
|
|
||||||
//this.listOrder = [];
|
|
||||||
|
|
||||||
// loop count to stop a stack overflow if the user keeps waggling the
|
|
||||||
// mouse for >30s in a row, or if running under mocha
|
|
||||||
this._delayedRefreshRoomListLoopCount = 0
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
// Initialise the stickyHeaders when the component is created
|
// Initialise the stickyHeaders when the component is created
|
||||||
this._updateStickyHeaders(true);
|
this._updateStickyHeaders(true);
|
||||||
|
|
||||||
|
this.mounted = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentDidUpdate: function() {
|
||||||
// short-circuit react when the room changes
|
|
||||||
// to avoid rerendering all the sublists everywhere
|
|
||||||
if (nextProps.selectedRoom !== this.props.selectedRoom) {
|
|
||||||
if (this.props.selectedRoom) {
|
|
||||||
constantTimeDispatcher.dispatch(
|
|
||||||
"RoomTile.select", this.props.selectedRoom, {}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
constantTimeDispatcher.dispatch(
|
|
||||||
"RoomTile.select", nextProps.selectedRoom, { selected: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidUpdate: function(prevProps, prevState) {
|
|
||||||
// Reinitialise the stickyHeaders when the component is updated
|
// Reinitialise the stickyHeaders when the component is updated
|
||||||
this._updateStickyHeaders(true);
|
this._updateStickyHeaders(true);
|
||||||
this._repositionIncomingCallBox(undefined, false);
|
this._repositionIncomingCallBox(undefined, false);
|
||||||
@ -140,29 +101,17 @@ module.exports = React.createClass({
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'on_room_read':
|
case 'on_room_read':
|
||||||
// poke the right RoomTile to refresh, using the constantTimeDispatcher
|
// Force an update because the notif count state is too deep to cause
|
||||||
// to avoid each and every RoomTile registering to the 'on_room_read' event
|
// an update. This forces the local echo of reading notifs to be
|
||||||
// XXX: if we like the constantTimeDispatcher we might want to dispatch
|
// reflected by the RoomTiles.
|
||||||
// directly from TimelinePanel rather than needlessly bouncing via here.
|
this.forceUpdate();
|
||||||
constantTimeDispatcher.dispatch(
|
|
||||||
"RoomTile.refresh", payload.room.roomId, {}
|
|
||||||
);
|
|
||||||
|
|
||||||
// also have to poke the right list(s)
|
|
||||||
var lists = this.listsForRoomId[payload.room.roomId];
|
|
||||||
if (lists) {
|
|
||||||
lists.forEach(list=>{
|
|
||||||
constantTimeDispatcher.dispatch(
|
|
||||||
"RoomSubList.refreshHeader", list, { room: payload.room }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
this.mounted = false;
|
||||||
|
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||||
@ -171,7 +120,7 @@ module.exports = React.createClass({
|
|||||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||||
MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags);
|
MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags);
|
||||||
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
|
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
|
||||||
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
||||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||||
}
|
}
|
||||||
@ -180,14 +129,10 @@ module.exports = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onRoom: function(room) {
|
onRoom: function(room) {
|
||||||
// XXX: this happens rarely; ideally we should only update the correct
|
|
||||||
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
|
|
||||||
this._delayedRefreshRoomList();
|
this._delayedRefreshRoomList();
|
||||||
},
|
},
|
||||||
|
|
||||||
onDeleteRoom: function(roomId) {
|
onDeleteRoom: function(roomId) {
|
||||||
// XXX: this happens rarely; ideally we should only update the correct
|
|
||||||
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
|
|
||||||
this._delayedRefreshRoomList();
|
this._delayedRefreshRoomList();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -210,10 +155,6 @@ module.exports = React.createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_onMouseOver: function(ev) {
|
|
||||||
this._lastMouseOverTs = Date.now();
|
|
||||||
},
|
|
||||||
|
|
||||||
onSubListHeaderClick: function(isHidden, scrollToPosition) {
|
onSubListHeaderClick: function(isHidden, scrollToPosition) {
|
||||||
// The scroll area has expanded or contracted, so re-calculate sticky headers positions
|
// The scroll area has expanded or contracted, so re-calculate sticky headers positions
|
||||||
this._updateStickyHeaders(true, scrollToPosition);
|
this._updateStickyHeaders(true, scrollToPosition);
|
||||||
@ -223,98 +164,41 @@ module.exports = React.createClass({
|
|||||||
if (toStartOfTimeline) return;
|
if (toStartOfTimeline) return;
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
|
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
|
||||||
|
this._delayedRefreshRoomList();
|
||||||
// rather than regenerate our full roomlists, which is very heavy, we poke the
|
|
||||||
// correct sublists to just re-sort themselves. This isn't enormously reacty,
|
|
||||||
// but is much faster than the default react reconciler, or having to do voodoo
|
|
||||||
// with shouldComponentUpdate and a pleaseRefresh property or similar.
|
|
||||||
var lists = this.listsForRoomId[room.roomId];
|
|
||||||
if (lists) {
|
|
||||||
lists.forEach(list=>{
|
|
||||||
constantTimeDispatcher.dispatch("RoomSubList.sort", list, { room: room });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// we have to explicitly hit the roomtile which just changed
|
|
||||||
constantTimeDispatcher.dispatch(
|
|
||||||
"RoomTile.refresh", room.roomId, {}
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomReceipt: function(receiptEvent, room) {
|
onRoomReceipt: function(receiptEvent, room) {
|
||||||
// because if we read a notification, it will affect notification count
|
// because if we read a notification, it will affect notification count
|
||||||
// only bother updating if there's a receipt from us
|
// only bother updating if there's a receipt from us
|
||||||
if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) {
|
if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) {
|
||||||
var lists = this.listsForRoomId[room.roomId];
|
this._delayedRefreshRoomList();
|
||||||
if (lists) {
|
|
||||||
lists.forEach(list=>{
|
|
||||||
constantTimeDispatcher.dispatch(
|
|
||||||
"RoomSubList.refreshHeader", list, { room: room }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// we have to explicitly hit the roomtile which just changed
|
|
||||||
constantTimeDispatcher.dispatch(
|
|
||||||
"RoomTile.refresh", room.roomId, {}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomName: function(room) {
|
onRoomName: function(room) {
|
||||||
constantTimeDispatcher.dispatch(
|
|
||||||
"RoomTile.refresh", room.roomId, {}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
onRoomTags: function(event, room) {
|
|
||||||
// XXX: this happens rarely; ideally we should only update the correct
|
|
||||||
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
|
|
||||||
this._delayedRefreshRoomList();
|
this._delayedRefreshRoomList();
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomStateMember: function(ev, state, member) {
|
onRoomTags: function(event, room) {
|
||||||
if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId &&
|
this._delayedRefreshRoomList();
|
||||||
ev.getPrevContent() && ev.getPrevContent().membership === "invite")
|
},
|
||||||
{
|
|
||||||
this._delayedRefreshRoomList();
|
onRoomStateEvents: function(ev, state) {
|
||||||
}
|
this._delayedRefreshRoomList();
|
||||||
else {
|
|
||||||
constantTimeDispatcher.dispatch(
|
|
||||||
"RoomTile.refresh", member.roomId, {}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomMemberName: function(ev, member) {
|
onRoomMemberName: function(ev, member) {
|
||||||
constantTimeDispatcher.dispatch(
|
this._delayedRefreshRoomList();
|
||||||
"RoomTile.refresh", member.roomId, {}
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onAccountData: function(ev) {
|
onAccountData: function(ev) {
|
||||||
if (ev.getType() == 'm.direct') {
|
if (ev.getType() == 'm.direct') {
|
||||||
// XXX: this happens rarely; ideally we should only update the correct
|
|
||||||
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
|
|
||||||
this._delayedRefreshRoomList();
|
|
||||||
}
|
|
||||||
else if (ev.getType() == 'm.push_rules') {
|
|
||||||
this._delayedRefreshRoomList();
|
this._delayedRefreshRoomList();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_delayedRefreshRoomList: new rate_limited_func(function() {
|
_delayedRefreshRoomList: new rate_limited_func(function() {
|
||||||
// if the mouse has been moving over the RoomList in the last 500ms
|
this.refreshRoomList();
|
||||||
// then delay the refresh further to avoid bouncing around under the
|
|
||||||
// cursor
|
|
||||||
if (Date.now() - this._lastMouseOverTs > 500 || this._delayedRefreshRoomListLoopCount > 60) {
|
|
||||||
this.refreshRoomList();
|
|
||||||
this._delayedRefreshRoomListLoopCount = 0;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this._delayedRefreshRoomListLoopCount++;
|
|
||||||
this._delayedRefreshRoomList();
|
|
||||||
}
|
|
||||||
}, 500),
|
}, 500),
|
||||||
|
|
||||||
refreshRoomList: function() {
|
refreshRoomList: function() {
|
||||||
@ -322,36 +206,27 @@ module.exports = React.createClass({
|
|||||||
// (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
|
// (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
|
||||||
// );
|
// );
|
||||||
|
|
||||||
// TODO: ideally we'd calculate this once at start, and then maintain
|
// TODO: rather than bluntly regenerating and re-sorting everything
|
||||||
// any changes to it incrementally, updating the appropriate sublists
|
// every time we see any kind of room change from the JS SDK
|
||||||
// as needed.
|
// we could do incremental updates on our copy of the state
|
||||||
// Alternatively we'd do something magical with Immutable.js or similar.
|
// based on the room which has actually changed. This would stop
|
||||||
const lists = this.getRoomLists();
|
// us re-rendering all the sublists every time anything changes anywhere
|
||||||
let totalRooms = 0;
|
// in the state of the client.
|
||||||
for (const l of Object.values(lists)) {
|
this.setState(this.getRoomLists());
|
||||||
totalRooms += l.length;
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
lists: this.getRoomLists(),
|
|
||||||
totalRoomCount: totalRooms,
|
|
||||||
});
|
|
||||||
|
|
||||||
// this._lastRefreshRoomListTs = Date.now();
|
// this._lastRefreshRoomListTs = Date.now();
|
||||||
},
|
},
|
||||||
|
|
||||||
getRoomLists: function() {
|
getRoomLists: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
const lists = {};
|
var s = { lists: {} };
|
||||||
|
|
||||||
lists["im.vector.fake.invite"] = [];
|
s.lists["im.vector.fake.invite"] = [];
|
||||||
lists["m.favourite"] = [];
|
s.lists["m.favourite"] = [];
|
||||||
lists["im.vector.fake.recent"] = [];
|
s.lists["im.vector.fake.recent"] = [];
|
||||||
lists["im.vector.fake.direct"] = [];
|
s.lists["im.vector.fake.direct"] = [];
|
||||||
lists["m.lowpriority"] = [];
|
s.lists["m.lowpriority"] = [];
|
||||||
lists["im.vector.fake.archived"] = [];
|
s.lists["im.vector.fake.archived"] = [];
|
||||||
|
|
||||||
this.listsForRoomId = {};
|
|
||||||
var otherTagNames = {};
|
|
||||||
|
|
||||||
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
||||||
|
|
||||||
@ -364,13 +239,8 @@ module.exports = React.createClass({
|
|||||||
// ", target = " + me.events.member.getStateKey() +
|
// ", target = " + me.events.member.getStateKey() +
|
||||||
// ", prevMembership = " + me.events.member.getPrevContent().membership);
|
// ", prevMembership = " + me.events.member.getPrevContent().membership);
|
||||||
|
|
||||||
if (!self.listsForRoomId[room.roomId]) {
|
|
||||||
self.listsForRoomId[room.roomId] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (me.membership == "invite") {
|
if (me.membership == "invite") {
|
||||||
self.listsForRoomId[room.roomId].push("im.vector.fake.invite");
|
s.lists["im.vector.fake.invite"].push(room);
|
||||||
lists["im.vector.fake.invite"].push(room);
|
|
||||||
}
|
}
|
||||||
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
|
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
|
||||||
// skip past this room & don't put it in any lists
|
// skip past this room & don't put it in any lists
|
||||||
@ -380,62 +250,82 @@ module.exports = React.createClass({
|
|||||||
{
|
{
|
||||||
// Used to split rooms via tags
|
// Used to split rooms via tags
|
||||||
var tagNames = Object.keys(room.tags);
|
var tagNames = Object.keys(room.tags);
|
||||||
|
|
||||||
if (tagNames.length) {
|
if (tagNames.length) {
|
||||||
for (var i = 0; i < tagNames.length; i++) {
|
for (var i = 0; i < tagNames.length; i++) {
|
||||||
var tagName = tagNames[i];
|
var tagName = tagNames[i];
|
||||||
lists[tagName] = lists[tagName] || [];
|
s.lists[tagName] = s.lists[tagName] || [];
|
||||||
lists[tagName].push(room);
|
s.lists[tagNames[i]].push(room);
|
||||||
self.listsForRoomId[room.roomId].push(tagName);
|
|
||||||
otherTagNames[tagName] = 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
|
else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
|
||||||
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
|
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
|
||||||
self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
|
s.lists["im.vector.fake.direct"].push(room);
|
||||||
lists["im.vector.fake.direct"].push(room);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
|
s.lists["im.vector.fake.recent"].push(room);
|
||||||
lists["im.vector.fake.recent"].push(room);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (me.membership === "leave") {
|
else if (me.membership === "leave") {
|
||||||
self.listsForRoomId[room.roomId].push("im.vector.fake.archived");
|
s.lists["im.vector.fake.archived"].push(room);
|
||||||
lists["im.vector.fake.archived"].push(room);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.error("unrecognised membership: " + me.membership + " - this should never happen");
|
console.error("unrecognised membership: " + me.membership + " - this should never happen");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (s.lists["im.vector.fake.direct"].length == 0 &&
|
||||||
|
MatrixClientPeg.get().getAccountData('m.direct') === undefined &&
|
||||||
|
!MatrixClientPeg.get().isGuest())
|
||||||
|
{
|
||||||
|
// scan through the 'recents' list for any rooms which look like DM rooms
|
||||||
|
// and make them DM rooms
|
||||||
|
const oldRecents = s.lists["im.vector.fake.recent"];
|
||||||
|
s.lists["im.vector.fake.recent"] = [];
|
||||||
|
|
||||||
|
for (const room of oldRecents) {
|
||||||
|
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
|
|
||||||
|
if (me && Rooms.looksLikeDirectMessageRoom(room, me)) {
|
||||||
|
s.lists["im.vector.fake.direct"].push(room);
|
||||||
|
} else {
|
||||||
|
s.lists["im.vector.fake.recent"].push(room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// save these new guessed DM rooms into the account data
|
||||||
|
const newMDirectEvent = {};
|
||||||
|
for (const room of s.lists["im.vector.fake.direct"]) {
|
||||||
|
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
|
const otherPerson = Rooms.getOnlyOtherMember(room, me);
|
||||||
|
if (!otherPerson) continue;
|
||||||
|
|
||||||
|
const roomList = newMDirectEvent[otherPerson.userId] || [];
|
||||||
|
roomList.push(room.roomId);
|
||||||
|
newMDirectEvent[otherPerson.userId] = roomList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this fails, fine, we'll just do the same thing next time we get the room lists
|
||||||
|
MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done();
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log("calculated new roomLists; im.vector.fake.recent = " + s.lists["im.vector.fake.recent"]);
|
||||||
|
|
||||||
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
|
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
|
||||||
|
|
||||||
// we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down
|
return s;
|
||||||
/*
|
|
||||||
this.listOrder = [
|
|
||||||
"im.vector.fake.invite",
|
|
||||||
"m.favourite",
|
|
||||||
"im.vector.fake.recent",
|
|
||||||
"im.vector.fake.direct",
|
|
||||||
Object.keys(otherTagNames).filter(tagName=>{
|
|
||||||
return (!tagName.match(/^m\.(favourite|lowpriority)$/));
|
|
||||||
}).sort(),
|
|
||||||
"m.lowpriority",
|
|
||||||
"im.vector.fake.archived"
|
|
||||||
];
|
|
||||||
*/
|
|
||||||
|
|
||||||
return lists;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_getScrollNode: function() {
|
_getScrollNode: function() {
|
||||||
|
if (!this.mounted) return null;
|
||||||
var panel = ReactDOM.findDOMNode(this);
|
var panel = ReactDOM.findDOMNode(this);
|
||||||
if (!panel) return null;
|
if (!panel) return null;
|
||||||
|
|
||||||
// empirically, if we have gm-prevented for some reason, the scroll node
|
if (panel.classList.contains('gm-prevented')) {
|
||||||
// is still the 3rd child (i.e. the view child). This looks to be due
|
return panel;
|
||||||
// to vdh's improved resize updater logic...?
|
} else {
|
||||||
return panel.children[2]; // XXX: Fragile!
|
return panel.children[2]; // XXX: Fragile!
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_whenScrolling: function(e) {
|
_whenScrolling: function(e) {
|
||||||
@ -483,7 +373,7 @@ module.exports = React.createClass({
|
|||||||
// Use the offset of the top of the scroll area from the window
|
// Use the offset of the top of the scroll area from the window
|
||||||
// as this is used to calculate the CSS fixed top position for the stickies
|
// as this is used to calculate the CSS fixed top position for the stickies
|
||||||
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
|
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
|
||||||
// Use the offset of the top of the component from the window
|
// Use the offset of the top of the componet from the window
|
||||||
// as this is used to calculate the CSS fixed top position for the stickies
|
// as this is used to calculate the CSS fixed top position for the stickies
|
||||||
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
|
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
|
||||||
|
|
||||||
@ -577,74 +467,21 @@ module.exports = React.createClass({
|
|||||||
this.refs.gemscroll.forceUpdate();
|
this.refs.gemscroll.forceUpdate();
|
||||||
},
|
},
|
||||||
|
|
||||||
_getEmptyContent: function(section) {
|
|
||||||
const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
|
|
||||||
|
|
||||||
if (this.props.collapsed) {
|
|
||||||
return <RoomDropTarget label="" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StartChatButton = sdk.getComponent('elements.StartChatButton');
|
|
||||||
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
|
|
||||||
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
|
|
||||||
if (this.state.totalRoomCount === 0) {
|
|
||||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
|
||||||
switch (section) {
|
|
||||||
case 'im.vector.fake.direct':
|
|
||||||
return <div className="mx_RoomList_emptySubListTip">
|
|
||||||
Press
|
|
||||||
<StartChatButton size="16" />
|
|
||||||
to start a chat with someone
|
|
||||||
</div>;
|
|
||||||
case 'im.vector.fake.recent':
|
|
||||||
return <div className="mx_RoomList_emptySubListTip">
|
|
||||||
You're not in any rooms yet! Press
|
|
||||||
<CreateRoomButton size="16" />
|
|
||||||
to make a room or
|
|
||||||
<RoomDirectoryButton size="16" />
|
|
||||||
to browse the directory
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelText = 'Drop here to ' + (VERBS[section] || 'tag ' + section);
|
|
||||||
|
|
||||||
return <RoomDropTarget label={labelText} />;
|
|
||||||
},
|
|
||||||
|
|
||||||
_getHeaderItems: function(section) {
|
|
||||||
const StartChatButton = sdk.getComponent('elements.StartChatButton');
|
|
||||||
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
|
|
||||||
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
|
|
||||||
switch (section) {
|
|
||||||
case 'im.vector.fake.direct':
|
|
||||||
return <span className="mx_RoomList_headerButtons">
|
|
||||||
<StartChatButton size="16" />
|
|
||||||
</span>;
|
|
||||||
case 'im.vector.fake.recent':
|
|
||||||
return <span className="mx_RoomList_headerButtons">
|
|
||||||
<RoomDirectoryButton size="16" />
|
|
||||||
<CreateRoomButton size="16" />
|
|
||||||
</span>;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var RoomSubList = sdk.getComponent('structures.RoomSubList');
|
var RoomSubList = sdk.getComponent('structures.RoomSubList');
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
||||||
autoshow={true} onScroll={ self._whenScrolling } onResize={ self._whenScrolling } ref="gemscroll">
|
autoshow={true} onScroll={ self._whenScrolling } ref="gemscroll">
|
||||||
<div className="mx_RoomList" onMouseOver={ this._onMouseOver }>
|
<div className="mx_RoomList">
|
||||||
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
|
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
|
||||||
label="Invites"
|
label="Invites"
|
||||||
tagName="im.vector.fake.invite"
|
|
||||||
editable={ false }
|
editable={ false }
|
||||||
order="recent"
|
order="recent"
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
incomingCall={ self.state.incomingCall }
|
incomingCall={ self.state.incomingCall }
|
||||||
collapsed={ self.props.collapsed }
|
collapsed={ self.props.collapsed }
|
||||||
selectedRoom={ self.props.selectedRoom }
|
|
||||||
searchFilter={ self.props.searchFilter }
|
searchFilter={ self.props.searchFilter }
|
||||||
onHeaderClick={ self.onSubListHeaderClick }
|
onHeaderClick={ self.onSubListHeaderClick }
|
||||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||||
@ -652,12 +489,12 @@ module.exports = React.createClass({
|
|||||||
<RoomSubList list={ self.state.lists['m.favourite'] }
|
<RoomSubList list={ self.state.lists['m.favourite'] }
|
||||||
label="Favourites"
|
label="Favourites"
|
||||||
tagName="m.favourite"
|
tagName="m.favourite"
|
||||||
emptyContent={self._getEmptyContent('m.favourite')}
|
verb="favourite"
|
||||||
editable={ true }
|
editable={ true }
|
||||||
order="manual"
|
order="manual"
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
incomingCall={ self.state.incomingCall }
|
incomingCall={ self.state.incomingCall }
|
||||||
collapsed={ self.props.collapsed }
|
collapsed={ self.props.collapsed }
|
||||||
selectedRoom={ self.props.selectedRoom }
|
|
||||||
searchFilter={ self.props.searchFilter }
|
searchFilter={ self.props.searchFilter }
|
||||||
onHeaderClick={ self.onSubListHeaderClick }
|
onHeaderClick={ self.onSubListHeaderClick }
|
||||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||||
@ -665,13 +502,12 @@ module.exports = React.createClass({
|
|||||||
<RoomSubList list={ self.state.lists['im.vector.fake.direct'] }
|
<RoomSubList list={ self.state.lists['im.vector.fake.direct'] }
|
||||||
label="People"
|
label="People"
|
||||||
tagName="im.vector.fake.direct"
|
tagName="im.vector.fake.direct"
|
||||||
emptyContent={self._getEmptyContent('im.vector.fake.direct')}
|
verb="tag direct chat"
|
||||||
headerItems={self._getHeaderItems('im.vector.fake.direct')}
|
|
||||||
editable={ true }
|
editable={ true }
|
||||||
order="recent"
|
order="recent"
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
incomingCall={ self.state.incomingCall }
|
incomingCall={ self.state.incomingCall }
|
||||||
collapsed={ self.props.collapsed }
|
collapsed={ self.props.collapsed }
|
||||||
selectedRoom={ self.props.selectedRoom }
|
|
||||||
alwaysShowHeader={ true }
|
alwaysShowHeader={ true }
|
||||||
searchFilter={ self.props.searchFilter }
|
searchFilter={ self.props.searchFilter }
|
||||||
onHeaderClick={ self.onSubListHeaderClick }
|
onHeaderClick={ self.onSubListHeaderClick }
|
||||||
@ -679,30 +515,28 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
|
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
|
||||||
label="Rooms"
|
label="Rooms"
|
||||||
tagName="im.vector.fake.recent"
|
|
||||||
editable={ true }
|
editable={ true }
|
||||||
emptyContent={self._getEmptyContent('im.vector.fake.recent')}
|
verb="restore"
|
||||||
headerItems={self._getHeaderItems('im.vector.fake.recent')}
|
|
||||||
order="recent"
|
order="recent"
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
incomingCall={ self.state.incomingCall }
|
incomingCall={ self.state.incomingCall }
|
||||||
collapsed={ self.props.collapsed }
|
collapsed={ self.props.collapsed }
|
||||||
selectedRoom={ self.props.selectedRoom }
|
|
||||||
searchFilter={ self.props.searchFilter }
|
searchFilter={ self.props.searchFilter }
|
||||||
onHeaderClick={ self.onSubListHeaderClick }
|
onHeaderClick={ self.onSubListHeaderClick }
|
||||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||||
|
|
||||||
{ Object.keys(self.state.lists).sort().map(function(tagName) {
|
{ Object.keys(self.state.lists).map(function(tagName) {
|
||||||
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
||||||
return <RoomSubList list={ self.state.lists[tagName] }
|
return <RoomSubList list={ self.state.lists[tagName] }
|
||||||
key={ tagName }
|
key={ tagName }
|
||||||
label={ tagName }
|
label={ tagName }
|
||||||
tagName={ tagName }
|
tagName={ tagName }
|
||||||
emptyContent={self._getEmptyContent(tagName)}
|
verb={ "tag as " + tagName }
|
||||||
editable={ true }
|
editable={ true }
|
||||||
order="manual"
|
order="manual"
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
incomingCall={ self.state.incomingCall }
|
incomingCall={ self.state.incomingCall }
|
||||||
collapsed={ self.props.collapsed }
|
collapsed={ self.props.collapsed }
|
||||||
selectedRoom={ self.props.selectedRoom }
|
|
||||||
searchFilter={ self.props.searchFilter }
|
searchFilter={ self.props.searchFilter }
|
||||||
onHeaderClick={ self.onSubListHeaderClick }
|
onHeaderClick={ self.onSubListHeaderClick }
|
||||||
onShowMoreRooms={ self.onShowMoreRooms } />;
|
onShowMoreRooms={ self.onShowMoreRooms } />;
|
||||||
@ -713,23 +547,22 @@ module.exports = React.createClass({
|
|||||||
<RoomSubList list={ self.state.lists['m.lowpriority'] }
|
<RoomSubList list={ self.state.lists['m.lowpriority'] }
|
||||||
label="Low priority"
|
label="Low priority"
|
||||||
tagName="m.lowpriority"
|
tagName="m.lowpriority"
|
||||||
emptyContent={self._getEmptyContent('m.lowpriority')}
|
verb="demote"
|
||||||
editable={ true }
|
editable={ true }
|
||||||
order="recent"
|
order="recent"
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
incomingCall={ self.state.incomingCall }
|
incomingCall={ self.state.incomingCall }
|
||||||
collapsed={ self.props.collapsed }
|
collapsed={ self.props.collapsed }
|
||||||
selectedRoom={ self.props.selectedRoom }
|
|
||||||
searchFilter={ self.props.searchFilter }
|
searchFilter={ self.props.searchFilter }
|
||||||
onHeaderClick={ self.onSubListHeaderClick }
|
onHeaderClick={ self.onSubListHeaderClick }
|
||||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||||
|
|
||||||
<RoomSubList list={ self.state.lists['im.vector.fake.archived'] }
|
<RoomSubList list={ self.state.lists['im.vector.fake.archived'] }
|
||||||
label="Historical"
|
label="Historical"
|
||||||
tagName="im.vector.fake.archived"
|
|
||||||
editable={ false }
|
editable={ false }
|
||||||
order="recent"
|
order="recent"
|
||||||
collapsed={ self.props.collapsed }
|
|
||||||
selectedRoom={ self.props.selectedRoom }
|
selectedRoom={ self.props.selectedRoom }
|
||||||
|
collapsed={ self.props.collapsed }
|
||||||
alwaysShowHeader={ true }
|
alwaysShowHeader={ true }
|
||||||
startAsHidden={ true }
|
startAsHidden={ true }
|
||||||
showSpinner={ self.state.isLoadingLeftRooms }
|
showSpinner={ self.state.isLoadingLeftRooms }
|
||||||
|
@ -47,7 +47,7 @@ module.exports = React.createClass({
|
|||||||
// The alias that was used to access this room, if appropriate
|
// The alias that was used to access this room, if appropriate
|
||||||
// If given, this will be how the room is referred to (eg.
|
// If given, this will be how the room is referred to (eg.
|
||||||
// in error messages).
|
// in error messages).
|
||||||
roomAlias: React.PropTypes.object,
|
roomAlias: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -27,8 +27,6 @@ var RoomNotifs = require('../../../RoomNotifs');
|
|||||||
var FormattingUtils = require('../../../utils/FormattingUtils');
|
var FormattingUtils = require('../../../utils/FormattingUtils');
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||||
var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
|
|
||||||
var Unread = require('../../../Unread');
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomTile',
|
displayName: 'RoomTile',
|
||||||
@ -38,10 +36,12 @@ module.exports = React.createClass({
|
|||||||
connectDropTarget: React.PropTypes.func,
|
connectDropTarget: React.PropTypes.func,
|
||||||
onClick: React.PropTypes.func,
|
onClick: React.PropTypes.func,
|
||||||
isDragging: React.PropTypes.bool,
|
isDragging: React.PropTypes.bool,
|
||||||
selectedRoom: React.PropTypes.string,
|
|
||||||
|
|
||||||
room: React.PropTypes.object.isRequired,
|
room: React.PropTypes.object.isRequired,
|
||||||
collapsed: React.PropTypes.bool.isRequired,
|
collapsed: React.PropTypes.bool.isRequired,
|
||||||
|
selected: React.PropTypes.bool.isRequired,
|
||||||
|
unread: React.PropTypes.bool.isRequired,
|
||||||
|
highlight: React.PropTypes.bool.isRequired,
|
||||||
isInvite: React.PropTypes.bool.isRequired,
|
isInvite: React.PropTypes.bool.isRequired,
|
||||||
incomingCall: React.PropTypes.object,
|
incomingCall: React.PropTypes.object,
|
||||||
},
|
},
|
||||||
@ -54,11 +54,10 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return({
|
return({
|
||||||
hover: false,
|
hover : false,
|
||||||
badgeHover: false,
|
badgeHover : false,
|
||||||
menuDisplayed: false,
|
menuDisplayed: false,
|
||||||
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
||||||
selected: this.props.room ? (this.props.selectedRoom === this.props.room.roomId) : false,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -80,32 +79,23 @@ module.exports = React.createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onAccountData: function(accountDataEvent) {
|
||||||
|
if (accountDataEvent.getType() == 'm.push_rules') {
|
||||||
|
this.setState({
|
||||||
|
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
|
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||||
constantTimeDispatcher.register("RoomTile.select", this.props.room.roomId, this.onSelect);
|
|
||||||
this.onRefresh();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
|
var cli = MatrixClientPeg.get();
|
||||||
constantTimeDispatcher.unregister("RoomTile.select", this.props.room.roomId, this.onSelect);
|
if (cli) {
|
||||||
},
|
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||||
|
}
|
||||||
componentWillReceiveProps: function(nextProps) {
|
|
||||||
this.onRefresh();
|
|
||||||
},
|
|
||||||
|
|
||||||
onRefresh: function(params) {
|
|
||||||
this.setState({
|
|
||||||
unread: Unread.doesRoomHaveUnreadMessages(this.props.room),
|
|
||||||
highlight: this.props.room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onSelect: function(params) {
|
|
||||||
this.setState({
|
|
||||||
selected: params.selected,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onClick: function(ev) {
|
onClick: function(ev) {
|
||||||
@ -179,13 +169,13 @@ module.exports = React.createClass({
|
|||||||
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
|
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
|
||||||
|
|
||||||
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
|
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
|
||||||
const mentionBadges = this.state.highlight && this._shouldShowMentionBadge();
|
const mentionBadges = this.props.highlight && this._shouldShowMentionBadge();
|
||||||
const badges = notifBadges || mentionBadges;
|
const badges = notifBadges || mentionBadges;
|
||||||
|
|
||||||
var classes = classNames({
|
var classes = classNames({
|
||||||
'mx_RoomTile': true,
|
'mx_RoomTile': true,
|
||||||
'mx_RoomTile_selected': this.state.selected,
|
'mx_RoomTile_selected': this.props.selected,
|
||||||
'mx_RoomTile_unread': this.state.unread,
|
'mx_RoomTile_unread': this.props.unread,
|
||||||
'mx_RoomTile_unreadNotify': notifBadges,
|
'mx_RoomTile_unreadNotify': notifBadges,
|
||||||
'mx_RoomTile_highlight': mentionBadges,
|
'mx_RoomTile_highlight': mentionBadges,
|
||||||
'mx_RoomTile_invited': (me && me.membership == 'invite'),
|
'mx_RoomTile_invited': (me && me.membership == 'invite'),
|
||||||
@ -231,7 +221,7 @@ module.exports = React.createClass({
|
|||||||
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
|
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.state.selected) {
|
if (this.props.selected) {
|
||||||
let nameSelected = <EmojiText>{name}</EmojiText>;
|
let nameSelected = <EmojiText>{name}</EmojiText>;
|
||||||
|
|
||||||
label = <div title={ name } className={ nameClasses }>{ nameSelected }</div>;
|
label = <div title={ name } className={ nameClasses }>{ nameSelected }</div>;
|
||||||
@ -265,8 +255,7 @@ module.exports = React.createClass({
|
|||||||
|
|
||||||
let ret = (
|
let ret = (
|
||||||
<div> { /* Only native elements can be wrapped in a DnD object. */}
|
<div> { /* Only native elements can be wrapped in a DnD object. */}
|
||||||
<AccessibleButton className={classes} tabIndex="0" onClick={this.onClick}
|
<AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||||
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
|
||||||
<div className={avatarClasses}>
|
<div className={avatarClasses}>
|
||||||
<div className="mx_RoomTile_avatar_container">
|
<div className="mx_RoomTile_avatar_container">
|
||||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||||
|
@ -38,7 +38,7 @@ module.exports = React.createClass({
|
|||||||
title="Scroll to unread messages"/>
|
title="Scroll to unread messages"/>
|
||||||
Jump to first unread message.
|
Jump to first unread message.
|
||||||
</div>
|
</div>
|
||||||
<img className="mx_TopUnreadMessagesBar_close"
|
<img className="mx_TopUnreadMessagesBar_close mx_filterFlipColor"
|
||||||
src="img/cancel.svg" width="18" height="18"
|
src="img/cancel.svg" width="18" height="18"
|
||||||
alt="Close" title="Close"
|
alt="Close" title="Close"
|
||||||
onClick={this.props.onCloseClick} />
|
onClick={this.props.onCloseClick} />
|
||||||
|
@ -147,6 +147,7 @@ export default WithMatrixClient(React.createClass({
|
|||||||
return (
|
return (
|
||||||
<form className="mx_UserSettings_profileTableRow" onSubmit={this._onAddMsisdnSubmit}>
|
<form className="mx_UserSettings_profileTableRow" onSubmit={this._onAddMsisdnSubmit}>
|
||||||
<div className="mx_UserSettings_profileLabelCell">
|
<div className="mx_UserSettings_profileLabelCell">
|
||||||
|
<label>Phone</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_profileInputCell">
|
<div className="mx_UserSettings_profileInputCell">
|
||||||
<div className="mx_UserSettings_phoneSection">
|
<div className="mx_UserSettings_phoneSection">
|
||||||
|
@ -26,3 +26,14 @@ export function formatCount(count) {
|
|||||||
if (count < 100000000) return (count / 1000000).toFixed(0) + "M";
|
if (count < 100000000) return (count / 1000000).toFixed(0) + "M";
|
||||||
return (count / 1000000000).toFixed(1) + "B"; // 10B is enough for anyone, right? :S
|
return (count / 1000000000).toFixed(1) + "B"; // 10B is enough for anyone, right? :S
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* format a key into groups of 4 characters, for easier visual inspection
|
||||||
|
*
|
||||||
|
* @param {string} key key to format
|
||||||
|
*
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export function formatCryptoKey(key) {
|
||||||
|
return key.match(/.{1,4}/g).join(" ");
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user