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

View file

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

View file

@ -79,6 +79,17 @@ module.exports = React.createClass({
}
var oob_data = {};
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 = {
avatarUrl: room.avatar_url,
// 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 Modal = require('matrix-react-sdk/lib/Modal');
/**
* 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",
};
var notifications = require('../../../notifications');
// Encodes a dictionary of {
// "notify": true/false,
// "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"];
}
}
// TODO: this "view" component still has far to much application logic in it,
// which should be factored out to other files.
// 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.
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,
}
}
};
var NotificationUtils = notifications.NotificationUtils;
var VectorPushRulesDefinitions = notifications.VectorPushRulesDefinitions;
var PushRuleVectorState = notifications.PushRuleVectorState;
/**
* 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) {
var decoded = decodeActions(actions);
var decoded = NotificationUtils.decodeActions(actions);
if (decoded !== null) {
return encodeActions(decoded);
return NotificationUtils.encodeActions(decoded);
} else {
// We don't recognise one of the actions here, so we don't try to
// canonicalise them.
@ -216,7 +56,6 @@ function portLegacyActions(actions) {
}
}
module.exports = React.createClass({
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) {
if (rule && rule.vectorState !== newPushRuleVectorState) {
@ -384,7 +189,7 @@ module.exports = React.createClass({
if (rule.rule) {
var actions = ruleDefinition.vectorStateToActions[newPushRuleVectorState];
if (actions === ACTION_DISABLED) {
if (!actions) {
// The new state corresponds to disabling the rule.
deferreds.push(cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false));
}
@ -430,7 +235,7 @@ module.exports = React.createClass({
switch (newPushRuleVectorState) {
case PushRuleVectorState.ON:
if (rule.actions.length !== 1) {
actions = this._actionsFor(PushRuleVectorState.ON);
actions = PushRuleVectorState.actionsFor(PushRuleVectorState.ON);
}
if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) {
@ -440,7 +245,7 @@ module.exports = React.createClass({
case PushRuleVectorState.LOUD:
if (rule.actions.length !== 3) {
actions = this._actionsFor(PushRuleVectorState.LOUD);
actions = PushRuleVectorState.actionsFor(PushRuleVectorState.LOUD);
}
if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) {
@ -526,7 +331,7 @@ module.exports = React.createClass({
// when creating the new rule.
// Thus, this new rule will join the 'vectorContentRules' set.
if (self.state.vectorContentRules.rules.length) {
pushRuleVectorStateKind = self._contentRuleVectorStateKind(self.state.vectorContentRules.rules[0]);
pushRuleVectorStateKind = PushRuleVectorState.contentRuleVectorStateKind(self.state.vectorContentRules.rules[0]);
}
else {
// ON is default
@ -541,13 +346,13 @@ module.exports = React.createClass({
if (self.state.vectorContentRules.vectorState !== PushRuleVectorState.OFF) {
deferreds.push(cli.addPushRule
('global', 'content', keyword, {
actions: self._actionsFor(pushRuleVectorStateKind),
actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
pattern: keyword
}));
}
else {
deferreds.push(self._addDisabledPushRule('global', 'content', keyword, {
actions: self._actionsFor(pushRuleVectorStateKind),
actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
pattern: keyword
}));
}
@ -650,7 +455,7 @@ module.exports = React.createClass({
}
}
else if (kind === 'content') {
switch (self._contentRuleVectorStateKind(r)) {
switch (PushRuleVectorState.contentRuleVectorStateKind(r)) {
case PushRuleVectorState.ON:
if (r.enabled) {
contentRules.on.push(r);
@ -762,7 +567,7 @@ module.exports = React.createClass({
var state = PushRuleVectorState[stateKey];
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
if (rule.enabled === false) {
vectorState = state;
@ -1107,12 +912,11 @@ module.exports = React.createClass({
</table>
</div>
<h3>Devices</h3>
{ devicesSection }
{ advancedSettings }
<h3>Devices</h3>
{ devicesSection }
</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;
}
/* 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.
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.

View file

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

View file

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

View file

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

View file

@ -53,6 +53,19 @@ limitations under the License.
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 {
height: 36px;
background-color: #76cfa6;

View file

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