Merge branch 'develop' into new-guest-access

Conflicts:
	src/components/structures/MatrixChat.js
This commit is contained in:
Luke Barnard 2017-05-22 16:19:10 +01:00
commit 05aaa599cc
43 changed files with 839 additions and 1047 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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