Merge branch 'develop' into dbkr/email_notifs

This commit is contained in:
David Baker 2016-04-21 10:11:55 +01:00
commit 3cb092051e
25 changed files with 995 additions and 232 deletions

17
.gitignore vendored
View File

@ -1,8 +1,9 @@
node_modules /cert.pem
vector/bundle.* /.DS_Store
lib /karma-reports
.DS_Store /key.pem
key.pem /lib
cert.pem /node_modules
vector/components.css /packages/
packages/ /vector/bundle.*
/vector/components.css

View File

@ -13,8 +13,11 @@ npm install
# we may be using a dev branch of react-sdk, in which case we need to build it # we may be using a dev branch of react-sdk, in which case we need to build it
(cd node_modules/matrix-react-sdk && npm run build) (cd node_modules/matrix-react-sdk && npm run build)
# run the mocha tests
npm run test
# build our artifacts; dumps them in ./vector # build our artifacts; dumps them in ./vector
npm run build npm run build:dev
# gzip up ./vector # gzip up ./vector
rm vector-*.tar.gz || true # rm previous artifacts without failing if it doesn't exist rm vector-*.tar.gz || true # rm previous artifacts without failing if it doesn't exist

136
karma.conf.js Normal file
View File

@ -0,0 +1,136 @@
// karma.conf.js - the config file for karma, which runs our tests.
var path = require('path');
var webpack = require('webpack');
/*
* We use webpack to build our tests. It's a pain to have to wait for webpack
* to build everything; however it's the easiest way to load our dependencies
* from node_modules.
*
* If you run karma in multi-run mode (with `npm run test:multi`), it will watch
* the tests for changes, and webpack will rebuild using a cache. This is much quicker
* than a clean rebuild.
*/
// the name of the test file. By default, a special file which runs all tests.
var testFile = process.env.KARMA_TEST_FILE || 'test/all-tests.js';
process.env.PHANTOMJS_BIN = 'node_modules/.bin/phantomjs';
process.env.Q_DEBUG = 1;
module.exports = function (config) {
config.set({
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha'],
// list of files / patterns to load in the browser
files: [
testFile,
{pattern: 'vector/img/*', watched: false, included: false, served: true, nocache: false},
],
// redirect img links to the karma server
proxies: {
"/img/": "/base/vector/img/",
},
// preprocess matching files before serving them to the browser
// available preprocessors:
// https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'test/**/*.js': ['webpack', 'sourcemap']
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress', 'junit'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR ||
// config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file
// changes
autoWatch: true,
// start these browsers
// available browser launchers:
// https://npmjs.org/browse/keyword/karma-launcher
browsers: [
'Chrome',
//'PhantomJS',
],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
// singleRun: false,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity,
junitReporter: {
outputDir: 'karma-reports',
},
webpack: {
module: {
loaders: [
{ test: /\.json$/, loader: "json" },
{
test: /\.js$/, loader: "babel",
include: [path.resolve('./src'),
path.resolve('./test'),
],
query: {
// we're using babel 5, for consistency with
// the release build, which doesn't use the
// presets.
// presets: ['react', 'es2015'],
},
},
],
noParse: [
// don't parse the languages within highlight.js. They
// cause stack overflows
// (https://github.com/webpack/webpack/issues/1721), and
// there is no need for webpack to parse them - they can
// just be included as-is.
/highlight\.js\/lib\/languages/,
// also disable parsing for sinon, because it
// tries to do voodoo with 'require' which upsets
// webpack (https://github.com/webpack/webpack/issues/304)
/sinon\/pkg\/sinon\.js$/,
],
},
resolve: {
alias: {
// alias any requires to the react module to the one in our path, otherwise
// we tend to get the react source included twice when using npm link.
react: path.resolve('./node_modules/react'),
// same goes for js-sdk
"matrix-js-sdk": path.resolve('./node_modules/matrix-js-sdk'),
sinon: 'sinon/pkg/sinon.js',
},
root: [
path.resolve('./src'),
path.resolve('./test'),
],
},
devtool: 'inline-source-map',
},
});
};

View File

@ -16,7 +16,9 @@
"build:css": "catw \"src/skins/vector/css/**/*.css\" -o vector/components.css --no-watch", "build:css": "catw \"src/skins/vector/css/**/*.css\" -o vector/components.css --no-watch",
"build:compile": "babel --source-maps -d lib src", "build:compile": "babel --source-maps -d lib src",
"build:bundle": "NODE_ENV=production webpack -p lib/vector/index.js vector/bundle.js", "build:bundle": "NODE_ENV=production webpack -p lib/vector/index.js vector/bundle.js",
"build:bundle:dev": "webpack --optimize-occurence-order lib/vector/index.js vector/bundle.js",
"build": "npm run build:css && npm run build:compile && npm run build:bundle", "build": "npm run build:css && npm run build:compile && npm run build:bundle",
"build:dev": "npm run build:css && npm run build:compile && npm run build:bundle:dev",
"package": "scripts/package.sh", "package": "scripts/package.sh",
"start:js": "webpack -w src/vector/index.js vector/bundle.js", "start:js": "webpack -w src/vector/index.js vector/bundle.js",
"start:js:prod": "NODE_ENV=production webpack -w src/vector/index.js vector/bundle.js", "start:js:prod": "NODE_ENV=production webpack -w src/vector/index.js vector/bundle.js",
@ -25,7 +27,9 @@
"start": "parallelshell \"npm run start:js\" \"npm run start:skins:css\" \"http-server -c 1 vector\"", "start": "parallelshell \"npm run start:js\" \"npm run start:skins:css\" \"http-server -c 1 vector\"",
"start:prod": "parallelshell \"npm run start:js:prod\" \"npm run start:skins:css\" \"http-server -c 1 vector\"", "start:prod": "parallelshell \"npm run start:js:prod\" \"npm run start:skins:css\" \"http-server -c 1 vector\"",
"clean": "rimraf lib vector/bundle.css vector/bundle.js vector/bundle.js.map vector/webpack.css*", "clean": "rimraf lib vector/bundle.css vector/bundle.js vector/bundle.js.map vector/webpack.css*",
"prepublish": "npm run build:css && npm run build:compile" "prepublish": "npm run build:css && npm run build:compile",
"test": "karma start --single-run=true --autoWatch=false --browsers PhantomJS --colors=false",
"test:multi": "karma start"
}, },
"dependencies": { "dependencies": {
"babel-polyfill": "^6.5.0", "babel-polyfill": "^6.5.0",
@ -41,7 +45,7 @@
"matrix-react-sdk": "matrix-org/matrix-react-sdk#develop", "matrix-react-sdk": "matrix-org/matrix-react-sdk#develop",
"modernizr": "^3.1.0", "modernizr": "^3.1.0",
"q": "^1.4.1", "q": "^1.4.1",
"react": "^0.14.2", "react": "^0.14.8",
"react-dnd": "^2.0.2", "react-dnd": "^2.0.2",
"react-dnd-html5-backend": "^2.0.0", "react-dnd-html5-backend": "^2.0.0",
"react-dom": "^0.14.2", "react-dom": "^0.14.2",
@ -54,11 +58,23 @@
"babel-loader": "^5.3.2", "babel-loader": "^5.3.2",
"catw": "^1.0.1", "catw": "^1.0.1",
"css-raw-loader": "^0.1.1", "css-raw-loader": "^0.1.1",
"expect": "^1.16.0",
"http-server": "^0.8.4", "http-server": "^0.8.4",
"json-loader": "^0.5.3", "json-loader": "^0.5.3",
"karma": "^0.13.22",
"karma-chrome-launcher": "^0.2.3",
"karma-cli": "^0.1.2",
"karma-junit-reporter": "^0.4.1",
"karma-mocha": "^0.2.2",
"karma-phantomjs-launcher": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^1.7.0",
"mocha": "^2.4.5",
"parallelshell": "^1.2.0", "parallelshell": "^1.2.0",
"phantomjs-prebuilt": "^2.1.7",
"react-addons-test-utils": "^0.14.8",
"rimraf": "^2.4.3", "rimraf": "^2.4.3",
"source-map-loader": "^0.1.5", "source-map-loader": "^0.1.5",
"webpack": "^1.12.13" "webpack": "^1.12.14"
} }
} }

View File

@ -89,7 +89,7 @@ var LeftPanel = React.createClass({
var BottomLeftMenu = sdk.getComponent('structures.BottomLeftMenu'); var BottomLeftMenu = sdk.getComponent('structures.BottomLeftMenu');
var collapseButton; var collapseButton;
var classes = "mx_LeftPanel"; var classes = "mx_LeftPanel mx_fadable";
if (this.props.collapsed) { if (this.props.collapsed) {
classes += " collapsed"; classes += " collapsed";
} }
@ -109,7 +109,7 @@ var LeftPanel = React.createClass({
} }
return ( return (
<aside className={classes}> <aside className={classes} style={{ opacity: this.props.opacity }}>
{ collapseButton } { collapseButton }
{ callPreview } { callPreview }
<RoomList <RoomList

View File

@ -158,13 +158,13 @@ module.exports = React.createClass({
} }
var classes = "mx_RightPanel"; var classes = "mx_RightPanel mx_fadable";
if (this.props.collapsed) { if (this.props.collapsed) {
classes += " collapsed"; classes += " collapsed";
} }
return ( return (
<aside className={classes}> <aside className={classes} style={{ opacity: this.props.opacity }}>
<div className="mx_RightPanel_header"> <div className="mx_RightPanel_header">
{ buttonGroup } { buttonGroup }
</div> </div>

View File

@ -79,6 +79,17 @@ module.exports = React.createClass({
} }
var oob_data = {}; var oob_data = {};
if (room) { if (room) {
if (MatrixClientPeg.get().isGuest()) {
if (!room.world_readable && !room.guest_can_join) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Failed to join the room",
description: "This room is inaccessible to guests. You may be able to join if you register."
});
return;
}
}
oob_data = { oob_data = {
avatarUrl: room.avatar_url, avatarUrl: room.avatar_url,
// XXX: This logic is duplicated from the JS SDK which // XXX: This logic is duplicated from the JS SDK which

View File

@ -22,174 +22,14 @@ var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var UserSettingsStore = require('matrix-react-sdk/lib/UserSettingsStore'); var UserSettingsStore = require('matrix-react-sdk/lib/UserSettingsStore');
var Modal = require('matrix-react-sdk/lib/Modal'); var Modal = require('matrix-react-sdk/lib/Modal');
/** var notifications = require('../../../notifications');
* Enum for state of a push rule as defined by the Vector UI.
* @readonly
* @enum {string}
*/
var PushRuleVectorState = {
/** The push rule is disabled */
OFF: "off",
/** The user will receive push notification for this rule */
ON: "on",
/** The user will receive push notification for this rule with sound and
highlight if this is legitimate */
LOUD: "loud",
};
// Encodes a dictionary of { // TODO: this "view" component still has far to much application logic in it,
// "notify": true/false, // which should be factored out to other files.
// "sound": string or undefined,
// "highlight: true/false,
// }
// to a list of push actions.
function encodeActions(action) {
var notify = action.notify;
var sound = action.sound;
var highlight = action.highlight;
if (notify) {
var actions = ["notify"];
if (sound) {
actions.push({"set_tweak": "sound", "value": sound});
}
if (highlight) {
actions.push({"set_tweak": "highlight"});
} else {
actions.push({"set_tweak": "highlight", "value": false});
}
return actions;
} else {
return ["dont_notify"];
}
}
// Decode a list of actions to a dictionary of { var NotificationUtils = notifications.NotificationUtils;
// "notify": true/false, var VectorPushRulesDefinitions = notifications.VectorPushRulesDefinitions;
// "sound": string or undefined, var PushRuleVectorState = notifications.PushRuleVectorState;
// "highlight: true/false,
// }
// If the actions couldn't be decoded then returns null.
function decodeActions(actions) {
var notify = false;
var sound = null;
var highlight = false;
for (var i = 0; i < actions.length; ++i) {
var action = actions[i];
if (action === "notify") {
notify = true;
} else if (action === "dont_notify") {
notify = false;
} else if (typeof action === 'object') {
if (action.set_tweak === "sound") {
sound = action.value
} else if (action.set_tweak === "highlight") {
highlight = action.value;
} else {
// We don't understand this kind of tweak, so give up.
return null;
}
} else {
// We don't understand this kind of action, so give up.
return null;
}
}
if (highlight === undefined) {
// If a highlight tweak is missing a value then it defaults to true.
highlight = true;
}
var result = {notify: notify, highlight: highlight};
if (sound !== null) {
result.sound = sound;
}
return result;
}
var ACTION_NOTIFY = encodeActions({notify: true});
var ACTION_NOTIFY_DEFAULT_SOUND = encodeActions({notify: true, sound: "default"});
var ACTION_NOTIFY_RING_SOUND = encodeActions({notify: true, sound: "ring"});
var ACTION_HIGHLIGHT_DEFAULT_SOUND = encodeActions({notify: true, sound: "default", highlight: true});
var ACTION_DONT_NOTIFY = encodeActions({notify: false});
var ACTION_DISABLED = null;
/**
* The descriptions of rules managed by the Vector UI.
*/
var VectorPushRulesDefinitions = {
// Messages containing user's display name
// (skip contains_user_name which is too geeky)
".m.rule.contains_display_name": {
kind: "underride",
description: "Messages containing my name",
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
on: ACTION_NOTIFY,
loud: ACTION_HIGHLIGHT_DEFAULT_SOUND,
off: ACTION_DISABLED
}
},
// Messages just sent to the user in a 1:1 room
".m.rule.room_one_to_one": {
kind: "underride",
description: "Messages in one-to-one chats",
vectorStateToActions: {
on: ACTION_NOTIFY,
loud: ACTION_NOTIFY_DEFAULT_SOUND,
off: ACTION_DONT_NOTIFY
}
},
// Messages just sent to a group chat room
// 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined
// By opposition, all other room messages are from group chat rooms.
".m.rule.message": {
kind: "underride",
description: "Messages in group chats",
vectorStateToActions: {
on: ACTION_NOTIFY,
loud: ACTION_NOTIFY_DEFAULT_SOUND,
off: ACTION_DONT_NOTIFY
}
},
// Invitation for the user
".m.rule.invite_for_me": {
kind: "underride",
description: "When I'm invited to a room",
vectorStateToActions: {
on: ACTION_NOTIFY,
loud: ACTION_NOTIFY_DEFAULT_SOUND,
off: ACTION_DISABLED
}
},
// Incoming call
".m.rule.call": {
kind: "underride",
description: "Call invitation",
vectorStateToActions: {
on: ACTION_NOTIFY,
loud: ACTION_NOTIFY_RING_SOUND,
off: ACTION_DISABLED
}
},
// Notifications from bots
".m.rule.suppress_notices": {
kind: "override",
description: "Messages sent by bot",
vectorStateToActions: {
// .m.rule.suppress_notices is a "negative" rule, we have to invert its enabled value for vector UI
on: ACTION_DISABLED,
loud: ACTION_NOTIFY_DEFAULT_SOUND,
off: ACTION_DONT_NOTIFY,
}
}
};
/** /**
* Rules that Vector used to set in order to override the actions of default rules. * Rules that Vector used to set in order to override the actions of default rules.
@ -206,9 +46,9 @@ var LEGACY_RULES = {
}; };
function portLegacyActions(actions) { function portLegacyActions(actions) {
var decoded = decodeActions(actions); var decoded = NotificationUtils.decodeActions(actions);
if (decoded !== null) { if (decoded !== null) {
return encodeActions(decoded); return NotificationUtils.encodeActions(decoded);
} else { } else {
// We don't recognise one of the actions here, so we don't try to // We don't recognise one of the actions here, so we don't try to
// canonicalise them. // canonicalise them.
@ -216,7 +56,6 @@ function portLegacyActions(actions) {
} }
} }
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'Notififications', displayName: 'Notififications',
@ -335,40 +174,6 @@ module.exports = React.createClass({
} }
}, },
_actionsFor: function(pushRuleVectorState) {
if (pushRuleVectorState === PushRuleVectorState.ON) {
return ACTION_NOTIFY;
}
else if (pushRuleVectorState === PushRuleVectorState.LOUD) {
return ACTION_HIGHLIGHT_DEFAULT_SOUND;
}
},
// Determine whether a content rule is in the PushRuleVectorState.ON category or in PushRuleVectorState.LOUD
// regardless of its enabled state. Returns undefined if it does not match these categories.
_contentRuleVectorStateKind: function(rule) {
var stateKind;
// Count tweaks to determine if it is a ON or LOUD rule
var tweaks = 0;
for (var j in rule.actions) {
var action = rule.actions[j];
if (action.set_tweak === 'sound' ||
(action.set_tweak === 'highlight' && action.value)) {
tweaks++;
}
}
switch (tweaks) {
case 0:
stateKind = PushRuleVectorState.ON;
break;
case 2:
stateKind = PushRuleVectorState.LOUD;
break;
}
return stateKind;
},
_setPushRuleVectorState: function(rule, newPushRuleVectorState) { _setPushRuleVectorState: function(rule, newPushRuleVectorState) {
if (rule && rule.vectorState !== newPushRuleVectorState) { if (rule && rule.vectorState !== newPushRuleVectorState) {
@ -384,7 +189,7 @@ module.exports = React.createClass({
if (rule.rule) { if (rule.rule) {
var actions = ruleDefinition.vectorStateToActions[newPushRuleVectorState]; var actions = ruleDefinition.vectorStateToActions[newPushRuleVectorState];
if (actions === ACTION_DISABLED) { if (!actions) {
// The new state corresponds to disabling the rule. // The new state corresponds to disabling the rule.
deferreds.push(cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false)); deferreds.push(cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false));
} }
@ -430,7 +235,7 @@ module.exports = React.createClass({
switch (newPushRuleVectorState) { switch (newPushRuleVectorState) {
case PushRuleVectorState.ON: case PushRuleVectorState.ON:
if (rule.actions.length !== 1) { if (rule.actions.length !== 1) {
actions = this._actionsFor(PushRuleVectorState.ON); actions = PushRuleVectorState.actionsFor(PushRuleVectorState.ON);
} }
if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) { if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) {
@ -440,7 +245,7 @@ module.exports = React.createClass({
case PushRuleVectorState.LOUD: case PushRuleVectorState.LOUD:
if (rule.actions.length !== 3) { if (rule.actions.length !== 3) {
actions = this._actionsFor(PushRuleVectorState.LOUD); actions = PushRuleVectorState.actionsFor(PushRuleVectorState.LOUD);
} }
if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) { if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) {
@ -526,7 +331,7 @@ module.exports = React.createClass({
// when creating the new rule. // when creating the new rule.
// Thus, this new rule will join the 'vectorContentRules' set. // Thus, this new rule will join the 'vectorContentRules' set.
if (self.state.vectorContentRules.rules.length) { if (self.state.vectorContentRules.rules.length) {
pushRuleVectorStateKind = self._contentRuleVectorStateKind(self.state.vectorContentRules.rules[0]); pushRuleVectorStateKind = PushRuleVectorState.contentRuleVectorStateKind(self.state.vectorContentRules.rules[0]);
} }
else { else {
// ON is default // ON is default
@ -541,13 +346,13 @@ module.exports = React.createClass({
if (self.state.vectorContentRules.vectorState !== PushRuleVectorState.OFF) { if (self.state.vectorContentRules.vectorState !== PushRuleVectorState.OFF) {
deferreds.push(cli.addPushRule deferreds.push(cli.addPushRule
('global', 'content', keyword, { ('global', 'content', keyword, {
actions: self._actionsFor(pushRuleVectorStateKind), actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
pattern: keyword pattern: keyword
})); }));
} }
else { else {
deferreds.push(self._addDisabledPushRule('global', 'content', keyword, { deferreds.push(self._addDisabledPushRule('global', 'content', keyword, {
actions: self._actionsFor(pushRuleVectorStateKind), actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
pattern: keyword pattern: keyword
})); }));
} }
@ -650,7 +455,7 @@ module.exports = React.createClass({
} }
} }
else if (kind === 'content') { else if (kind === 'content') {
switch (self._contentRuleVectorStateKind(r)) { switch (PushRuleVectorState.contentRuleVectorStateKind(r)) {
case PushRuleVectorState.ON: case PushRuleVectorState.ON:
if (r.enabled) { if (r.enabled) {
contentRules.on.push(r); contentRules.on.push(r);
@ -762,7 +567,7 @@ module.exports = React.createClass({
var state = PushRuleVectorState[stateKey]; var state = PushRuleVectorState[stateKey];
var vectorStateToActions = ruleDefinition.vectorStateToActions[state]; var vectorStateToActions = ruleDefinition.vectorStateToActions[state];
if (vectorStateToActions === ACTION_DISABLED) { if (!vectorStateToActions) {
// No defined actions means that this vector state expects a disabled default hs rule // No defined actions means that this vector state expects a disabled default hs rule
if (rule.enabled === false) { if (rule.enabled === false) {
vectorState = state; vectorState = state;
@ -1107,12 +912,11 @@ module.exports = React.createClass({
</table> </table>
</div> </div>
<h3>Devices</h3>
{ devicesSection }
{ advancedSettings } { advancedSettings }
<h3>Devices</h3>
{ devicesSection }
</div> </div>
</div> </div>

View File

@ -0,0 +1,89 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
module.exports = {
// Encodes a dictionary of {
// "notify": true/false,
// "sound": string or undefined,
// "highlight: true/false,
// }
// to a list of push actions.
encodeActions: function(action) {
var notify = action.notify;
var sound = action.sound;
var highlight = action.highlight;
if (notify) {
var actions = ["notify"];
if (sound) {
actions.push({"set_tweak": "sound", "value": sound});
}
if (highlight) {
actions.push({"set_tweak": "highlight"});
} else {
actions.push({"set_tweak": "highlight", "value": false});
}
return actions;
} else {
return ["dont_notify"];
}
},
// Decode a list of actions to a dictionary of {
// "notify": true/false,
// "sound": string or undefined,
// "highlight: true/false,
// }
// If the actions couldn't be decoded then returns null.
decodeActions: function(actions) {
var notify = false;
var sound = null;
var highlight = false;
for (var i = 0; i < actions.length; ++i) {
var action = actions[i];
if (action === "notify") {
notify = true;
} else if (action === "dont_notify") {
notify = false;
} else if (typeof action === 'object') {
if (action.set_tweak === "sound") {
sound = action.value
} else if (action.set_tweak === "highlight") {
highlight = action.value;
} else {
// We don't understand this kind of tweak, so give up.
return null;
}
} else {
// We don't understand this kind of action, so give up.
return null;
}
}
if (highlight === undefined) {
// If a highlight tweak is missing a value then it defaults to true.
highlight = true;
}
var result = {notify: notify, highlight: highlight};
if (sound !== null) {
result.sound = sound;
}
return result;
},
};

View File

@ -0,0 +1,78 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
/**
* Enum for state of a push rule as defined by the Vector UI.
* @readonly
* @enum {string}
*/
module.exports = {
/** The push rule is disabled */
OFF: "off",
/** The user will receive push notification for this rule */
ON: "on",
/** The user will receive push notification for this rule with sound and
highlight if this is legitimate */
LOUD: "loud",
/**
* Convert a PushRuleVectorState to a list of actions
*
* @return [object] list of push-rule actions
*/
actionsFor: function(pushRuleVectorState) {
if (pushRuleVectorState === this.ON) {
return ACTION_NOTIFY;
}
else if (pushRuleVectorState === this.LOUD) {
return ACTION_HIGHLIGHT_DEFAULT_SOUND;
}
},
/**
* Convert a pushrule's actions to a PushRuleVectorState.
*
* Determines whether a content rule is in the PushRuleVectorState.ON
* category or in PushRuleVectorState.LOUD, regardless of its enabled
* state. Returns undefined if it does not match these categories.
*/
contentRuleVectorStateKind: function(rule) {
var stateKind;
// Count tweaks to determine if it is a ON or LOUD rule
var tweaks = 0;
for (var j in rule.actions) {
var action = rule.actions[j];
if (action.set_tweak === 'sound' ||
(action.set_tweak === 'highlight' && action.value)) {
tweaks++;
}
}
switch (tweaks) {
case 0:
stateKind = this.ON;
break;
case 2:
stateKind = this.LOUD;
break;
}
return stateKind;
},
};

View File

@ -0,0 +1,104 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var NotificationUtils = require('./NotificationUtils');
var encodeActions = NotificationUtils.encodeActions;
var decodeActions = NotificationUtils.decodeActions;
const ACTION_NOTIFY = encodeActions({notify: true});
const ACTION_NOTIFY_DEFAULT_SOUND = encodeActions({notify: true, sound: "default"});
const ACTION_NOTIFY_RING_SOUND = encodeActions({notify: true, sound: "ring"});
const ACTION_HIGHLIGHT_DEFAULT_SOUND = encodeActions({notify: true, sound: "default", highlight: true});
const ACTION_DONT_NOTIFY = encodeActions({notify: false});
const ACTION_DISABLED = null;
/**
* The descriptions of rules managed by the Vector UI.
*/
module.exports = {
// Messages containing user's display name
// (skip contains_user_name which is too geeky)
".m.rule.contains_display_name": {
kind: "underride",
description: "Messages containing my name",
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
on: ACTION_NOTIFY,
loud: ACTION_HIGHLIGHT_DEFAULT_SOUND,
off: ACTION_DISABLED
}
},
// Messages just sent to the user in a 1:1 room
".m.rule.room_one_to_one": {
kind: "underride",
description: "Messages in one-to-one chats",
vectorStateToActions: {
on: ACTION_NOTIFY,
loud: ACTION_NOTIFY_DEFAULT_SOUND,
off: ACTION_DONT_NOTIFY
}
},
// Messages just sent to a group chat room
// 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined
// By opposition, all other room messages are from group chat rooms.
".m.rule.message": {
kind: "underride",
description: "Messages in group chats",
vectorStateToActions: {
on: ACTION_NOTIFY,
loud: ACTION_NOTIFY_DEFAULT_SOUND,
off: ACTION_DONT_NOTIFY
}
},
// Invitation for the user
".m.rule.invite_for_me": {
kind: "underride",
description: "When I'm invited to a room",
vectorStateToActions: {
on: ACTION_NOTIFY,
loud: ACTION_NOTIFY_DEFAULT_SOUND,
off: ACTION_DISABLED
}
},
// Incoming call
".m.rule.call": {
kind: "underride",
description: "Call invitation",
vectorStateToActions: {
on: ACTION_NOTIFY,
loud: ACTION_NOTIFY_RING_SOUND,
off: ACTION_DISABLED
}
},
// Notifications from bots
".m.rule.suppress_notices": {
kind: "override",
description: "Messages sent by bot",
vectorStateToActions: {
// .m.rule.suppress_notices is a "negative" rule, we have to invert its enabled value for vector UI
on: ACTION_DISABLED,
loud: ACTION_NOTIFY_DEFAULT_SOUND,
off: ACTION_DONT_NOTIFY,
}
}
};

View File

@ -0,0 +1,23 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
module.exports = {
NotificationUtils: require('./NotificationUtils'),
PushRuleVectorState: require('./PushRuleVectorState'),
VectorPushRulesDefinitions: require('./VectorPushRulesDefinitions'),
};

View File

@ -58,6 +58,15 @@ input[type=text]:focus, textarea:focus {
box-shadow: none; box-shadow: none;
} }
/* applied to side-panels and messagepanel when in RoomSettings */
.mx_fadable {
opacity: 1;
-webkit-transition: opacity 0.2s ease-in-out;
-moz-transition: opacity 0.2s ease-in-out;
-ms-transition: opacity 0.2s ease-in-out;
-o-transition: opacity 0.2s ease-in-out;
}
/* XXX: critical hack to GeminiScrollbar to allow them to work in FF 42 and Chrome 48. /* XXX: critical hack to GeminiScrollbar to allow them to work in FF 42 and Chrome 48.
Stop the scrollbar view from pushing out the container's overall sizing, which causes Stop the scrollbar view from pushing out the container's overall sizing, which causes
flexbox to adapt to the new size and cause the view to keep growing. flexbox to adapt to the new size and cause the view to keep growing.

View File

@ -89,7 +89,7 @@ limitations under the License.
margin: auto; margin: auto;
overflow: auto; overflow: auto;
border-bottom: 1px solid #eee; border-bottom: 1px solid #ccc;
-webkit-flex: 0 0 auto; -webkit-flex: 0 0 auto;
flex: 0 0 auto; flex: 0 0 auto;

View File

@ -166,6 +166,7 @@ limitations under the License.
display: block; display: block;
visibility: hidden; visibility: hidden;
text-align: right; text-align: right;
white-space: nowrap;
} }
.mx_EventTile_last .mx_MessageTimestamp { .mx_EventTile_last .mx_MessageTimestamp {

View File

@ -29,6 +29,7 @@ limitations under the License.
flex: 0 0 100px; flex: 0 0 100px;
margin-left: 15px; margin-left: 15px;
text-align: center; text-align: center;
cursor: pointer;
} }
.mx_LinkPreviewWidget_caption { .mx_LinkPreviewWidget_caption {

View File

@ -53,6 +53,19 @@ limitations under the License.
flex: 1; flex: 1;
} }
.mx_RoomHeader_spinner {
height: 36px;
-webkit-box-ordinal-group: 2;
-moz-box-ordinal-group: 2;
-ms-flex-order: 2;
-webkit-order: 2;
order: 2;
padding-left: 12px;
padding-right: 12px;
}
.mx_RoomHeader_textButton { .mx_RoomHeader_textButton {
height: 36px; height: 36px;
background-color: #76cfa6; background-color: #76cfa6;

View File

@ -104,6 +104,7 @@ function parseQs(location) {
// Here, we do some crude URL analysis to allow // Here, we do some crude URL analysis to allow
// deep-linking. // deep-linking.
function routeUrl(location) { function routeUrl(location) {
console.log("Routing URL "+window.location);
var params = parseQs(location); var params = parseQs(location);
var loginToken = params.loginToken; var loginToken = params.loginToken;
if (loginToken) { if (loginToken) {
@ -134,6 +135,7 @@ var lastLoadedScreen = null;
// This will be called whenever the SDK changes screens, // This will be called whenever the SDK changes screens,
// so a web page can update the URL bar appropriately. // so a web page can update the URL bar appropriately.
var onNewScreen = function(screen) { var onNewScreen = function(screen) {
console.log("newscreen "+screen);
if (!loaded) { if (!loaded) {
lastLoadedScreen = screen; lastLoadedScreen = screen;
} else { } else {
@ -158,6 +160,7 @@ var makeRegistrationUrl = function() {
window.addEventListener('hashchange', onHashChange); window.addEventListener('hashchange', onHashChange);
window.onload = function() { window.onload = function() {
console.log("window.onload");
if (!validBrowser) { if (!validBrowser) {
return; return;
} }
@ -172,6 +175,7 @@ window.onload = function() {
} }
function loadApp() { function loadApp() {
console.log("Vector starting at "+window.location);
if (validBrowser) { if (validBrowser) {
var MatrixChat = sdk.getComponent('structures.MatrixChat'); var MatrixChat = sdk.getComponent('structures.MatrixChat');
var fragParts = parseQsFromFragment(window.location); var fragParts = parseQsFromFragment(window.location);

13
test/all-tests.js Normal file
View File

@ -0,0 +1,13 @@
// all-tests.js
//
// Our master test file: uses the webpack require API to find our test files
// and run them
// ideally these unit tests could be run under nodejs rather than in a browser
// via karma, but having two separate test frameworks in the same project
// seems confusing
var unit_tests = require.context('./unit-tests', true, /\.js$/);
unit_tests.keys().forEach(unit_tests);
var app_tests = require.context('./app-tests', true, /\.jsx?$/);
app_tests.keys().forEach(app_tests);

164
test/app-tests/joining.js Normal file
View File

@ -0,0 +1,164 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* joining.js: tests for the various paths when joining a room */
require('skin-sdk');
var jssdk = require('matrix-js-sdk');
var sdk = require('matrix-react-sdk');
var peg = require('matrix-react-sdk/lib/MatrixClientPeg');
var dis = require('matrix-react-sdk/lib/dispatcher');
var MatrixChat = sdk.getComponent('structures.MatrixChat');
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
var RoomPreviewBar = sdk.getComponent('rooms.RoomPreviewBar');
var RoomView = sdk.getComponent('structures.RoomView');
var React = require('react');
var ReactDOM = require('react-dom');
var ReactTestUtils = require('react-addons-test-utils');
var expect = require('expect');
var q = require('q');
var test_utils = require('../test-utils');
var MockHttpBackend = require('../mock-request');
var HS_URL='http://localhost';
var IS_URL='http://localhost';
var USER_ID='@me:localhost';
var ACCESS_TOKEN='access_token';
describe('joining a room', function () {
describe('over federation', function () {
var parentDiv;
var httpBackend;
var matrixChat;
beforeEach(function() {
test_utils.beforeEach(this);
httpBackend = new MockHttpBackend();
jssdk.request(httpBackend.requestFn);
parentDiv = document.createElement('div');
// uncomment this to actually add the div to the UI, to help with
// debugging (but slow things down)
// document.body.appendChild(parentDiv);
});
afterEach(function() {
if (parentDiv) {
ReactDOM.unmountComponentAtNode(parentDiv);
parentDiv.remove();
parentDiv = null;
}
});
it('should not get stuck at a spinner', function(done) {
var ROOM_ALIAS = '#alias:localhost';
var ROOM_ID = '!id:localhost';
httpBackend.when('PUT', '/presence/'+encodeURIComponent(USER_ID)+'/status')
.respond(200, {});
httpBackend.when('GET', '/pushrules').respond(200, {});
httpBackend.when('POST', '/filter').respond(200, { filter_id: 'fid' });
httpBackend.when('GET', '/sync').respond(200, {});
httpBackend.when('GET', '/publicRooms').respond(200, {chunk: []});
// start with a logged-in client
peg.replaceUsingAccessToken(HS_URL, IS_URL, USER_ID, ACCESS_TOKEN);
var mc = <MatrixChat config={{}}/>;
matrixChat = ReactDOM.render(mc, parentDiv);
// switch to the Directory
dis.dispatch({
action: 'view_room_directory',
});
var roomView;
httpBackend.flush().then(() => {
var roomDir = ReactTestUtils.findRenderedComponentWithType(
matrixChat, RoomDirectory);
// enter an alias in the input, and simulate enter
var input = ReactTestUtils.findRenderedDOMComponentWithTag(
roomDir, 'input');
input.value = ROOM_ALIAS;
ReactTestUtils.Simulate.keyUp(input, {key: 'Enter'});
// that should create a roomview which will start a peek; wait
// for the peek.
httpBackend.when('GET', '/rooms/'+encodeURIComponent(ROOM_ALIAS)+"/initialSync")
.respond(401, {errcode: 'M_GUEST_ACCESS_FORBIDDEN'});
return httpBackend.flush();
}).then(() => {
httpBackend.verifyNoOutstandingExpectation();
// we should now have a roomview, with a preview bar
roomView = ReactTestUtils.findRenderedComponentWithType(
matrixChat, RoomView);
var previewBar = ReactTestUtils.findRenderedComponentWithType(
roomView, RoomPreviewBar);
var joinLink = ReactTestUtils.findRenderedDOMComponentWithTag(
previewBar, 'a');
ReactTestUtils.Simulate.click(joinLink);
// that will fire off a request to check our displayname, followed by a
// join request
httpBackend.when('GET', '/profile/'+encodeURIComponent(USER_ID))
.respond(200, {displayname: 'boris'});
httpBackend.when('POST', '/join/'+encodeURIComponent(ROOM_ALIAS))
.respond(200, {room_id: ROOM_ID});
return httpBackend.flush();
}).then(() => {
httpBackend.verifyNoOutstandingExpectation();
// the roomview should now be loading
expect(roomView.state.room).toBe(null);
expect(roomView.state.joining).toBe(true);
// there should be a spinner
ReactTestUtils.findRenderedDOMComponentWithClass(
roomView, "mx_Spinner");
// now send the room down the /sync pipe
httpBackend.when('GET', '/sync').
respond(200, {
rooms: {
join: {
[ROOM_ID]: {
state: {},
timeline: {
events: [],
limited: true,
},
},
},
},
});
return httpBackend.flush();
}).then(() => {
// now the room should have loaded
expect(roomView.state.room).toExist();
expect(roomView.state.joining).toBe(false);
}).done(done, done);
});
});
});

228
test/mock-request.js Normal file
View File

@ -0,0 +1,228 @@
"use strict";
var q = require("q");
var expect = require('expect');
/**
* Construct a mock HTTP backend, heavily inspired by Angular.js.
* @constructor
*/
function HttpBackend() {
this.requests = [];
this.expectedRequests = [];
var self = this;
// the request function dependency that the SDK needs.
this.requestFn = function(opts, callback) {
var realReq = new Request(opts.method, opts.uri, opts.body, opts.qs);
realReq.callback = callback;
console.log("HTTP backend received request: %s %s", opts.method, opts.uri);
self.requests.push(realReq);
var abort = function() {
var idx = self.requests.indexOf(realReq);
if (idx >= 0) {
console.log("Aborting HTTP request: %s %s", opts.method, opts.uri);
self.requests.splice(idx, 1);
}
}
return {
abort: abort
};
};
}
HttpBackend.prototype = {
/**
* Respond to all of the requests (flush the queue).
* @param {string} path The path to flush (optional) default: all.
* @param {integer} numToFlush The number of things to flush (optional), default: all.
* @return {Promise} resolved when there is nothing left to flush.
*/
flush: function(path, numToFlush) {
var defer = q.defer();
var self = this;
var flushed = 0;
var triedWaiting = false;
console.log(
"HTTP backend flushing... (path=%s numToFlush=%s)", path, numToFlush
);
var tryFlush = function() {
// if there's more real requests and more expected requests, flush 'em.
console.log(
" trying to flush queue => reqs=%s expected=%s [%s]",
self.requests.length, self.expectedRequests.length, path
);
if (self._takeFromQueue(path)) {
// try again on the next tick.
console.log(" flushed. Trying for more. [%s]", path);
flushed += 1;
if (numToFlush && flushed === numToFlush) {
console.log(" [%s] Flushed assigned amount: %s", path, numToFlush);
defer.resolve();
}
else {
setTimeout(tryFlush, 0);
}
}
else if (flushed === 0 && !triedWaiting) {
// we may not have made the request yet, wait a generous amount of
// time before giving up.
setTimeout(tryFlush, 5);
triedWaiting = true;
}
else {
console.log(" no more flushes. [%s]", path);
defer.resolve();
}
};
setTimeout(tryFlush, 0);
return defer.promise;
},
/**
* Attempts to resolve requests/expected requests.
* @param {string} path The path to flush (optional) default: all.
* @return {boolean} true if something was resolved.
*/
_takeFromQueue: function(path) {
var req = null;
var i, j;
var matchingReq, expectedReq, testResponse = null;
for (i = 0; i < this.requests.length; i++) {
req = this.requests[i];
for (j = 0; j < this.expectedRequests.length; j++) {
expectedReq = this.expectedRequests[j];
if (path && path !== expectedReq.path) { continue; }
if (expectedReq.method === req.method &&
req.path.indexOf(expectedReq.path) !== -1) {
if (!expectedReq.data || (JSON.stringify(expectedReq.data) ===
JSON.stringify(req.data))) {
matchingReq = expectedReq;
this.expectedRequests.splice(j, 1);
break;
}
}
}
if (matchingReq) {
// remove from request queue
this.requests.splice(i, 1);
i--;
for (j = 0; j < matchingReq.checks.length; j++) {
matchingReq.checks[j](req);
}
testResponse = matchingReq.response;
console.log(" responding to %s", matchingReq.path);
var body = testResponse.body;
if (Object.prototype.toString.call(body) == "[object Function]") {
body = body(req.path, req.data);
}
req.callback(
testResponse.err, testResponse.response, body
);
matchingReq = null;
}
}
if (testResponse) { // flushed something
return true;
}
return false;
},
/**
* Makes sure that the SDK hasn't sent any more requests to the backend.
*/
verifyNoOutstandingRequests: function() {
var firstOutstandingReq = this.requests[0] || {};
expect(this.requests.length).toEqual(0,
"Expected no more HTTP requests but received request to " +
firstOutstandingReq.path
);
},
/**
* Makes sure that the test doesn't have any unresolved requests.
*/
verifyNoOutstandingExpectation: function() {
var firstOutstandingExpectation = this.expectedRequests[0] || {};
expect(this.expectedRequests.length).toEqual(
0,
"Expected to see HTTP request for "
+ firstOutstandingExpectation.method
+ " " + firstOutstandingExpectation.path
);
},
/**
* Create an expected request.
* @param {string} method The HTTP method
* @param {string} path The path (which can be partial)
* @param {Object} data The expected data.
* @return {Request} An expected request.
*/
when: function(method, path, data) {
var pendingReq = new Request(method, path, data);
this.expectedRequests.push(pendingReq);
return pendingReq;
}
};
function Request(method, path, data, queryParams) {
this.method = method;
this.path = path;
this.data = data;
this.queryParams = queryParams;
this.callback = null;
this.response = null;
this.checks = [];
}
Request.prototype = {
/**
* Execute a check when this request has been satisfied.
* @param {Function} fn The function to execute.
* @return {Request} for chaining calls.
*/
check: function(fn) {
this.checks.push(fn);
return this;
},
/**
* Respond with the given data when this request is satisfied.
* @param {Number} code The HTTP status code.
* @param {Object|Function} data The HTTP JSON body. If this is a function,
* it will be invoked when the JSON body is required (which should be returned).
*/
respond: function(code, data) {
this.response = {
response: {
statusCode: code,
headers: {}
},
body: data,
err: null
};
},
/**
* Fail with an Error when this request is satisfied.
* @param {Number} code The HTTP status code.
* @param {Error} err The error to throw (e.g. Network Error)
*/
fail: function(code, err) {
this.response = {
response: {
statusCode: code,
headers: {}
},
body: null,
err: err
};
},
};
/**
* The HttpBackend class.
*/
module.exports = HttpBackend;

8
test/skin-sdk.js Normal file
View File

@ -0,0 +1,8 @@
/*
* skin-sdk.js
*
* Skins the react-sdk with the vector components
*/
var sdk = require('matrix-react-sdk');
sdk.loadSkin(require('component-index'));

24
test/test-utils.js Normal file
View File

@ -0,0 +1,24 @@
"use strict";
var q = require('q');
/**
* Perform common actions before each test case, e.g. printing the test case
* name to stdout.
* @param {Mocha.Context} context The test context
*/
module.exports.beforeEach = function(context) {
var desc = context.currentTest.fullTitle();
console.log();
console.log(desc);
console.log(new Array(1 + desc.length).join("="));
};
/**
* returns true if the current environment supports webrtc
*/
module.exports.browserSupportsWebRTC = function() {
var n = global.window.navigator;
return n.getUserMedia || n.webkitGetUserMedia ||
n.mozGetUserMedia;
};

View File

@ -0,0 +1,30 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var notifications = require('notifications');
var prvs = notifications.PushRuleVectorState;
var expect = require('expect');
describe("PushRuleVectorState", function() {
describe("contentRuleVectorStateKind", function() {
it("should understand normal notifications", function () {
expect(prvs.contentRuleVectorStateKind(["notify"])).
toEqual(prvs.ON);
});
});
});

View File

@ -42,6 +42,9 @@ module.exports = {
// we tend to get the react source included twice when using npm link. // we tend to get the react source included twice when using npm link.
react: path.resolve('./node_modules/react'), react: path.resolve('./node_modules/react'),
// same goes for js-sdk
"matrix-js-sdk": path.resolve('./node_modules/matrix-js-sdk'),
// matrix-js-sdk will use olm if it is available, // matrix-js-sdk will use olm if it is available,
// but does not explicitly depend on it. Pull it // but does not explicitly depend on it. Pull it
// in from node_modules if it's there. // in from node_modules if it's there.