diff --git a/CHANGELOG.md b/CHANGELOG.md index c9faad2b..b0991182 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +Changes in [0.6.0](https://github.com/vector-im/vector-web/releases/tag/v0.6.0) (2016-04-19) +============================================================================================ +[Full Changelog](https://github.com/vector-im/vector-web/compare/v0.5.0...v0.6.0) + + * Matthew/design tweaks + [\#1402](https://github.com/vector-im/vector-web/pull/1402) + * Improve handling of notification rules we can't parse + [\#1399](https://github.com/vector-im/vector-web/pull/1399) + * Do less mangling of jenkins builds + [\#1391](https://github.com/vector-im/vector-web/pull/1391) + * Start Notifications component refactor + [\#1386](https://github.com/vector-im/vector-web/pull/1386) + * make the UI fadable to help with decluttering + [\#1376](https://github.com/vector-im/vector-web/pull/1376) + * Get and display a user's pushers in settings + [\#1374](https://github.com/vector-im/vector-web/pull/1374) + * URL previewing support + [\#1343](https://github.com/vector-im/vector-web/pull/1343) + * 😄 Emoji autocomplete and unicode emoji to image conversion using emojione. + [\#1332](https://github.com/vector-im/vector-web/pull/1332) + * Show full-size avatar on MemberInfo avatar click + [\#1340](https://github.com/vector-im/vector-web/pull/1340) + * Numerous other changes via [matrix-react-sdk 0.5.1](https://github.com/matrix-org/matrix-react-sdk/blob/v0.5.1/CHANGELOG.md) + Changes in [0.5.0](https://github.com/vector-im/vector-web/releases/tag/v0.5.0) (2016-03-30) ============================================================================================ [Full Changelog](https://github.com/vector-im/vector-web/compare/v0.4.1...v0.5.0) @@ -71,7 +95,7 @@ Changes in vector v0.1.2 (2015-10-28) * Better hover-over on member list * Support CAS auth * Many other bug fixes - + Changes in vector v0.1.1 (2015-08-10) ====================================== diff --git a/package.json b/package.json index 6c85f552..43b17325 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vector-web", - "version": "0.5.0", + "version": "0.6.0", "description": "Vector webapp", "author": "matrix.org", "repository": { @@ -16,7 +16,7 @@ "build:css": "catw \"src/skins/vector/css/**/*.css\" -o vector/components.css --no-watch", "build:compile": "babel --source-maps -d lib src", "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:bundle:dev": "NODE_ENV=production 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:dev": "npm run build:css && npm run build:compile && npm run build:bundle:dev", "package": "scripts/package.sh", @@ -37,7 +37,7 @@ "extract-text-webpack-plugin": "^0.9.1", "filesize": "^3.1.2", "flux": "~2.0.3", - "gemini-scrollbar": "matrix-org/gemini-scrollbar#7dc736d", + "gemini-scrollbar": "matrix-org/gemini-scrollbar#87ebaa7", "gfm.css": "^1.1.1", "highlight.js": "^9.0.0", "linkifyjs": "^2.0.0-beta.4", @@ -45,11 +45,11 @@ "matrix-react-sdk": "matrix-org/matrix-react-sdk#develop", "modernizr": "^3.1.0", "q": "^1.4.1", - "react": "^0.14.8", - "react-dnd": "^2.0.2", - "react-dnd-html5-backend": "^2.0.0", - "react-dom": "^0.14.2", - "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#869a86b", + "react": "^15.0.1", + "react-dnd": "^2.1.4", + "react-dnd-html5-backend": "^2.1.2", + "react-dom": "^15.0.1", + "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e", "sanitize-html": "^1.11.1" }, "devDependencies": { @@ -72,7 +72,8 @@ "mocha": "^2.4.5", "parallelshell": "^1.2.0", "phantomjs-prebuilt": "^2.1.7", - "react-addons-test-utils": "^0.14.8", + "react-addons-test-utils": "^15.0.1", + "react-addons-perf": "^15.0", "rimraf": "^2.4.3", "source-map-loader": "^0.1.5", "webpack": "^1.12.14" diff --git a/src/component-index.js b/src/component-index.js index b25b5ef9..b3baf22a 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -19,6 +19,9 @@ limitations under the License. * You can edit it you like, but your changes will be overwritten, * so you'd just be trying to swim upstream like a salmon. * You are not a salmon. + * + * To update it, run: + * ./reskindex.js -h header */ module.exports.components = require('matrix-react-sdk/lib/component-index').components; @@ -29,6 +32,7 @@ module.exports.components['structures.LeftPanel'] = require('./components/struct module.exports.components['structures.RightPanel'] = require('./components/structures/RightPanel'); module.exports.components['structures.RoomDirectory'] = require('./components/structures/RoomDirectory'); module.exports.components['structures.RoomSubList'] = require('./components/structures/RoomSubList'); +module.exports.components['structures.SearchBox'] = require('./components/structures/SearchBox'); module.exports.components['structures.ViewSource'] = require('./components/structures/ViewSource'); module.exports.components['views.elements.ImageView'] = require('./components/views/elements/ImageView'); module.exports.components['views.elements.Spinner'] = require('./components/views/elements/Spinner'); diff --git a/src/components/structures/BottomLeftMenu.js b/src/components/structures/BottomLeftMenu.js index a4d89fcf..ae49a347 100644 --- a/src/components/structures/BottomLeftMenu.js +++ b/src/components/structures/BottomLeftMenu.js @@ -47,12 +47,19 @@ module.exports = React.createClass({ render: function() { var BottomLeftMenuTile = sdk.getComponent('rooms.BottomLeftMenuTile'); + var TintableSvg = sdk.getComponent('elements.TintableSvg'); return (
- - - +
+ +
+
+ +
+
+ +
); diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 5c27abc5..58347a06 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -31,6 +31,7 @@ var LeftPanel = React.createClass({ getInitialState: function() { return { showCallElement: null, + searchFilter: '', }; }, @@ -84,9 +85,14 @@ var LeftPanel = React.createClass({ } }, + onSearch: function(term) { + this.setState({ searchFilter: term }); + }, + render: function() { var RoomList = sdk.getComponent('rooms.RoomList'); var BottomLeftMenu = sdk.getComponent('structures.BottomLeftMenu'); + var SearchBox = sdk.getComponent('structures.SearchBox'); var collapseButton; var classes = "mx_LeftPanel mx_fadable"; @@ -110,11 +116,13 @@ var LeftPanel = React.createClass({ return ( diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index eab75fe3..431bdba5 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -155,7 +155,10 @@ module.exports = React.createClass({ panel = } } + } + if (!panel) { + panel =
; } var classes = "mx_RightPanel mx_fadable"; @@ -169,6 +172,8 @@ module.exports = React.createClass({ { buttonGroup } { panel } +
+
); } diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index f3614092..d7808230 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -43,6 +43,14 @@ module.exports = React.createClass({ } }, + componentWillMount: function() { + // dis.dispatch({ + // action: 'ui_opacity', + // sideOpacity: 0.3, + // middleOpacity: 0.3, + // }); + }, + componentDidMount: function() { var self = this; MatrixClientPeg.get().publicRooms(function (err, data) { @@ -65,6 +73,14 @@ module.exports = React.createClass({ }); }, + componentWillUnmount: function() { + // dis.dispatch({ + // action: 'ui_opacity', + // sideOpacity: 1.0, + // middleOpacity: 1.0, + // }); + }, + showRoom: function(roomId) { // extract the metadata from the publicRooms structure to pass // as out-of-band data to view_room, because we get information @@ -113,8 +129,8 @@ module.exports = React.createClass({ var rooms = this.state.publicRooms.filter(function(a) { // FIXME: if incrementally typing, keep narrowing down the search set // incrementally rather than starting over each time. - return (((a.name && a.name.toLowerCase().search(filter.toLowerCase()) >= 0) || - (a.aliases && a.aliases[0].toLowerCase().search(filter.toLowerCase()) >= 0)) && + return (((a.name && a.name.toLowerCase().search(filter.toLowerCase()) >= 0) || + (a.aliases && a.aliases[0].toLowerCase().search(filter.toLowerCase()) >= 0)) && a.num_joined_members > 0); }).sort(function(a,b) { return a.num_joined_members - b.num_joined_members; @@ -197,7 +213,8 @@ module.exports = React.createClass({
- + { this.getRows(this.state.roomAlias) } @@ -209,4 +226,3 @@ module.exports = React.createClass({ ); } }); - diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 0b96ed18..497acdec 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -65,16 +65,12 @@ var RoomSubList = React.createClass({ selectedRoom: React.PropTypes.string.isRequired, startAsHidden: React.PropTypes.bool, showSpinner: React.PropTypes.bool, // true to show a spinner if 0 elements when expanded - - // TODO: Fix the name of this. This is too easily confused with the - // "hidden" state which is the expanded (or not) view of the list of rooms. - // What this prop *really* does is control whether the room name is displayed - // so it should be named as such. - collapsed: React.PropTypes.bool.isRequired, + collapsed: React.PropTypes.bool.isRequired, // is LeftPanel collapsed? onHeaderClick: React.PropTypes.func, alwaysShowHeader: React.PropTypes.bool, incomingCall: React.PropTypes.object, - onShowMoreRooms: React.PropTypes.func + onShowMoreRooms: React.PropTypes.func, + searchFilter: React.PropTypes.string, }, getInitialState: function() { @@ -93,13 +89,20 @@ var RoomSubList = React.createClass({ }, componentWillMount: function() { - this.sortList(this.props.list, this.props.order); + this.sortList(this.applySearchFilter(this.props.list, this.props.searchFilter), this.props.order); }, componentWillReceiveProps: function(newProps) { // order the room list appropriately before we re-render //if (debug) console.log("received new props, list = " + newProps.list); - this.sortList(newProps.list, newProps.order); + this.sortList(this.applySearchFilter(newProps.list, newProps.searchFilter), newProps.order); + }, + + applySearchFilter: function(list, filter) { + if (filter === "") return list; + return list.filter((room) => { + return room.name && room.name.toLowerCase().indexOf(filter.toLowerCase()) >= 0 + }); }, onClick: function(ev) { @@ -278,7 +281,7 @@ var RoomSubList = React.createClass({ return (

{ this.props.collapsed ? '' : this.props.label } -

diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js new file mode 100644 index 00000000..a49f845e --- /dev/null +++ b/src/components/structures/SearchBox.js @@ -0,0 +1,109 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +var React = require('react'); +var sdk = require('matrix-react-sdk') +var dis = require('matrix-react-sdk/lib/dispatcher'); +var rate_limited_func = require('matrix-react-sdk/lib/ratelimitedfunc'); + +module.exports = React.createClass({ + displayName: 'SearchBox', + + propTypes: { + collapsed: React.PropTypes.bool, + onSearch: React.PropTypes.func, + }, + + getInitialState: function() { + return { + searchTerm: "", + }; + }, + + onChange: function() { + if (!this.refs.search) return; + this.setState({ searchTerm: this.refs.search.value }); + this.onSearch(); + }, + + onSearch: new rate_limited_func( + function() { + this.props.onSearch(this.refs.search.value); + }, + 100 + ), + + onToggleCollapse: function(show) { + if (show) { + dis.dispatch({ + action: 'show_left_panel', + }); + } + else { + dis.dispatch({ + action: 'hide_left_panel', + }); + } + }, + + render: function() { + var TintableSvg = sdk.getComponent('elements.TintableSvg'); + + var toggleCollapse; + if (this.props.collapsed) { + toggleCollapse = +
+ +
+ } + else { + toggleCollapse = +
+ +
+ } + + var searchControls; + if (!this.props.collapsed) { + searchControls = [ + , + + ]; + } + + var self = this; + return ( +
+ { searchControls } + { toggleCollapse } +
+ ); + } +}); diff --git a/src/components/views/rooms/RoomTooltip.js b/src/components/views/rooms/RoomTooltip.js index fbe2fa56..2f5de837 100644 --- a/src/components/views/rooms/RoomTooltip.js +++ b/src/components/views/rooms/RoomTooltip.js @@ -34,7 +34,7 @@ module.exports = React.createClass({ }); } else { - tooltip.style.top = tooltip.parentElement.getBoundingClientRect().top + "px"; + tooltip.style.top = (70 + tooltip.parentElement.getBoundingClientRect().top) + "px"; tooltip.style.display = "block"; } }, diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 46c47994..2a66fff4 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -27,9 +27,13 @@ var notifications = require('../../../notifications'); // TODO: this "view" component still has far to much application logic in it, // which should be factored out to other files. +// TODO: this component also does a lot of direct poking into this.state, which +// is VERY NAUGHTY. + var NotificationUtils = notifications.NotificationUtils; var VectorPushRulesDefinitions = notifications.VectorPushRulesDefinitions; var PushRuleVectorState = notifications.PushRuleVectorState; +var ContentRules = notifications.ContentRules; /** * Rules that Vector used to set in order to override the actions of default rules. @@ -104,6 +108,7 @@ module.exports = React.createClass({ }, onNotifStateButtonClicked: function(event) { + // FIXME: use .bind() rather than className metadata here surely var vectorRuleId = event.target.className.split("-")[0]; var newPushRuleVectorState = event.target.className.split("-")[1]; @@ -411,7 +416,10 @@ module.exports = React.createClass({ _refreshFromServer: function() { var self = this; - var pushRulesPromise = MatrixClientPeg.get().getPushRules().then(self._portRulesToNewAPI).done(function(rulesets) { + var pushRulesPromise = MatrixClientPeg.get().getPushRules().then(self._portRulesToNewAPI).then(function(rulesets) { + //console.log("resolving pushRulesPromise"); + + /// XXX seriously? wtf is this? MatrixClientPeg.get().pushRules = rulesets; // Get homeserver default rules and triage them by categories @@ -434,8 +442,6 @@ module.exports = React.createClass({ // HS default rules var defaultRules = {master: [], vector: {}, others: []}; - // Content/keyword rules - var contentRules = {on: [], on_but_disabled:[], loud: [], loud_but_disabled: [], other: []}; for (var kind in rulesets.global) { for (var i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) { @@ -454,84 +460,25 @@ module.exports = React.createClass({ defaultRules['others'].push(r); } } - else if (kind === 'content') { - switch (PushRuleVectorState.contentRuleVectorStateKind(r)) { - case PushRuleVectorState.ON: - if (r.enabled) { - contentRules.on.push(r); - } - else { - contentRules.on_but_disabled.push(r); - } - break; - case PushRuleVectorState.LOUD: - if (r.enabled) { - contentRules.loud.push(r); - } - else { - contentRules.loud_but_disabled.push(r); - } - break; - default: - contentRules.other.push(r); - break; - } - } } } - // Decide which content rules to display in Vector UI. - // Vector displays a single global rule for a list of keywords - // whereas Matrix has a push rule per keyword. - // Vector can set the unique rule in ON, LOUD or OFF state. - // Matrix has enabled/disabled plus a combination of (highlight, sound) tweaks. - - // The code below determines which set of user's content push rules can be - // displayed by the vector UI. - // Push rules that does not fit, ie defined by another Matrix client, ends - // in self.state.externalContentRules. - // There is priority in the determination of which set will be the displayed one. - // The set with rules that have LOUD tweaks is the first choice. Then, the ones - // with ON tweaks (no tweaks). - if (contentRules.loud.length) { - self.state.vectorContentRules = { - vectorState: PushRuleVectorState.LOUD, - rules: contentRules.loud - } - self.state.externalContentRules = [].concat(contentRules.loud_but_disabled, contentRules.on, contentRules.on_but_disabled, contentRules.other); - } - else if (contentRules.loud_but_disabled.length) { - self.state.vectorContentRules = { - vectorState: PushRuleVectorState.OFF, - rules: contentRules.loud_but_disabled - } - self.state.externalContentRules = [].concat(contentRules.on, contentRules.on_but_disabled, contentRules.other); - } - else if (contentRules.on.length) { - self.state.vectorContentRules = { - vectorState: PushRuleVectorState.ON, - rules: contentRules.on - } - self.state.externalContentRules = [].concat(contentRules.on_but_disabled, contentRules.other); - } - else if (contentRules.on_but_disabled.length) { - self.state.vectorContentRules = { - vectorState: PushRuleVectorState.OFF, - rules: contentRules.on_but_disabled - } - self.state.externalContentRules = contentRules.other; - } - else { - self.state.externalContentRules = contentRules.other; - } - // Get the master rule if any defined by the hs if (defaultRules.master.length > 0) { self.state.masterPushRule = defaultRules.master[0]; } + // parse the keyword rules into our state + var contentRules = ContentRules.parseContentRules(rulesets); + self.state.vectorContentRules = { + vectorState: contentRules.vectorState, + rules: contentRules.rules, + }; + self.state.externalContentRules = contentRules.externalRules; + // Build the rules displayed in the Vector UI matrix table self.state.vectorPushRules = []; + self.state.externalPushRules = []; var vectorRuleIds = [ '.m.rule.contains_display_name', @@ -545,7 +492,6 @@ module.exports = React.createClass({ ]; for (var i in vectorRuleIds) { var vectorRuleId = vectorRuleIds[i]; - var ruleDefinition = VectorPushRulesDefinitions[vectorRuleId]; if (vectorRuleId === '_keywords') { // keywords needs a special handling @@ -558,42 +504,12 @@ module.exports = React.createClass({ }); } else { + var ruleDefinition = VectorPushRulesDefinitions[vectorRuleId]; var rule = defaultRules.vector[vectorRuleId]; - // Translate the rule actions and its enabled value into vector state - var vectorState; - if (rule) { - for (var stateKey in PushRuleVectorState) { - var state = PushRuleVectorState[stateKey]; - var vectorStateToActions = ruleDefinition.vectorStateToActions[state]; + var vectorState = ruleDefinition.ruleToVectorState(rule); - if (!vectorStateToActions) { - // No defined actions means that this vector state expects a disabled default hs rule - if (rule.enabled === false) { - vectorState = state; - break; - } - } - else { - // The actions must match to the ones expected by vector state - if (JSON.stringify(rule.actions) === JSON.stringify(vectorStateToActions)) { - // And the rule must be enabled. - if (rule.enabled === true) { - vectorState = state; - break; - } - } - } - } - - if (!vectorState) { - console.error("Cannot translate rule actions into Vector rule state. Rule: " + rule); - vectorState = PushRuleVectorState.OFF; - } - } - else { - vectorState = PushRuleVectorState.OFF; - } + //console.log("Refreshing vectorPushRules for " + vectorRuleId +", "+ ruleDefinition.description +", " + rule +", " + vectorState); self.state.vectorPushRules.push({ "vectorRuleId": vectorRuleId, @@ -601,6 +517,12 @@ module.exports = React.createClass({ "rule": rule, "vectorState": vectorState, }); + + // if there was a rule which we couldn't parse, add it to the external list + if (rule && !vectorState) { + rule.description = ruleDefinition.description; + self.state.externalPushRules.push(rule); + } } } @@ -610,7 +532,6 @@ module.exports = React.createClass({ '.m.rule.fallback': "Notify me for anything else" }; - self.state.externalPushRules = []; for (var i in defaultRules.others) { var rule = defaultRules.others[i]; var ruleDescription = otherRulesDescriptions[rule.rule_id]; @@ -624,10 +545,11 @@ module.exports = React.createClass({ }); var pushersPromise = MatrixClientPeg.get().getPushers().then(function(resp) { + //console.log("resolving pushersPromise"); self.setState({pushers: resp.pushers}); }); - q.all([pushRulesPromise, pushersPromise]).done(function() { + q.all([pushRulesPromise, pushersPromise]).then(function() { self.setState({ phase: self.phases.DISPLAY }); @@ -635,7 +557,16 @@ module.exports = React.createClass({ self.setState({ phase: self.phases.ERROR }); - }); + }).finally(() => { + // actually explicitly update our state having been deep-manipulating it + self.setState({ + masterPushRule: self.state.masterPushRule, + vectorContentRules: self.state.vectorContentRules, + vectorPushRules: self.state.vectorPushRules, + externalContentRules: self.state.externalContentRules, + externalPushRules: self.state.externalPushRules, + }); + }).done(); }, _updatePushRuleActions: function(rule, actions, enabled) { @@ -655,7 +586,7 @@ module.exports = React.createClass({ renderNotifRulesTableRow: function(title, className, pushRuleVectorState) { return ( -
+ @@ -688,6 +619,7 @@ module.exports = React.createClass({ var rows = []; for (var i in this.state.vectorPushRules) { var rule = this.state.vectorPushRules[i]; + //console.log("rendering: " + rule.description + ", " + rule.vectorRuleId + ", " + rule.vectorState); rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState)); } return rows; @@ -731,13 +663,10 @@ module.exports = React.createClass({ render: function() { var self = this; + var spinner; if (this.state.phase === this.phases.LOADING) { var Loader = sdk.getComponent("elements.Spinner"); - return ( -
- -
- ); + spinner = ; } if (this.state.masterPushRule) { @@ -820,19 +749,21 @@ module.exports = React.createClass({ // and this wouldn't be hard to add. var rows = []; for (var i = 0; i < this.state.pushers.length; ++i) { - var p = this.state.pushers[i]; - - rows.push( - - + rows.push( + + ); } - devicesSection = (
{title}
{p.app_display_name}{p.device_display_name}
{this.state.pushers[i].app_display_name}{this.state.pushers[i].device_display_name}
- - - - - {rows} + devicesSection = (
ApplicationDevice
+ + + + + + + + {rows} +
ApplicationDevice
); } @@ -857,6 +788,8 @@ module.exports = React.createClass({
+ { spinner } +
Off On - Loud + Highlight
& sound diff --git a/src/notifications/ContentRules.js b/src/notifications/ContentRules.js new file mode 100644 index 00000000..25a7bac9 --- /dev/null +++ b/src/notifications/ContentRules.js @@ -0,0 +1,125 @@ +/* +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 PushRuleVectorState = require('./PushRuleVectorState'); + +module.exports = { + /** + * Extract the keyword rules from a list of rules, and parse them + * into a form which is useful for Vector's UI. + * + * Returns an object containing: + * rules: the primary list of keyword rules + * vectorState: a PushRuleVectorState indicating whether those rules are + * OFF/ON/LOUD + * externalRules: a list of other keyword rules, with states other than + * vectorState + */ + parseContentRules: function(rulesets) { + // first categorise the keyword rules in terms of their actions + var contentRules = this._categoriseContentRules(rulesets); + + // Decide which content rules to display in Vector UI. + // Vector displays a single global rule for a list of keywords + // whereas Matrix has a push rule per keyword. + // Vector can set the unique rule in ON, LOUD or OFF state. + // Matrix has enabled/disabled plus a combination of (highlight, sound) tweaks. + + // The code below determines which set of user's content push rules can be + // displayed by the vector UI. + // Push rules that does not fit, ie defined by another Matrix client, ends + // in externalRules. + // There is priority in the determination of which set will be the displayed one. + // The set with rules that have LOUD tweaks is the first choice. Then, the ones + // with ON tweaks (no tweaks). + + if (contentRules.loud.length) { + return { + vectorState: PushRuleVectorState.LOUD, + rules: contentRules.loud, + externalRules: [].concat(contentRules.loud_but_disabled, contentRules.on, contentRules.on_but_disabled, contentRules.other), + }; + } + else if (contentRules.loud_but_disabled.length) { + return { + vectorState: PushRuleVectorState.OFF, + rules: contentRules.loud_but_disabled, + externalRules: [].concat(contentRules.on, contentRules.on_but_disabled, contentRules.other), + }; + } + else if (contentRules.on.length) { + return { + vectorState: PushRuleVectorState.ON, + rules: contentRules.on, + externalRules: [].concat(contentRules.on_but_disabled, contentRules.other), + }; + } + else if (contentRules.on_but_disabled.length) { + return { + vectorState: PushRuleVectorState.OFF, + rules: contentRules.on_but_disabled, + externalRules: contentRules.other, + } + } else { + return { + vectorState: PushRuleVectorState.ON, + rules: [], + externalRules: contentRules.other, + } + } + }, + + _categoriseContentRules: function(rulesets) { + var contentRules = {on: [], on_but_disabled:[], loud: [], loud_but_disabled: [], other: []}; + for (var kind in rulesets.global) { + for (var i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) { + var r = rulesets.global[kind][i]; + + // check it's not a default rule + if (r.rule_id[0] === '.' || kind !== 'content') { + continue; + } + + r.kind = kind; // is this needed? not sure + + switch (PushRuleVectorState.contentRuleVectorStateKind(r)) { + case PushRuleVectorState.ON: + if (r.enabled) { + contentRules.on.push(r); + } + else { + contentRules.on_but_disabled.push(r); + } + break; + case PushRuleVectorState.LOUD: + if (r.enabled) { + contentRules.loud.push(r); + } + else { + contentRules.loud_but_disabled.push(r); + } + break; + default: + contentRules.other.push(r); + break; + } + } + } + return contentRules; + }, +}; diff --git a/src/notifications/PushRuleVectorState.js b/src/notifications/PushRuleVectorState.js index 5c6934aa..c838aa20 100644 --- a/src/notifications/PushRuleVectorState.js +++ b/src/notifications/PushRuleVectorState.js @@ -16,12 +16,10 @@ limitations under the License. 'use strict'; -/** - * Enum for state of a push rule as defined by the Vector UI. - * @readonly - * @enum {string} - */ -module.exports = { +var StandardActions = require('./StandardActions'); +var NotificationUtils = require('./NotificationUtils'); + +var states = { /** The push rule is disabled */ OFF: "off", @@ -31,6 +29,16 @@ module.exports = { /** The user will receive push notification for this rule with sound and highlight if this is legitimate */ LOUD: "loud", +}; + + +module.exports = { + /** + * Enum for state of a push rule as defined by the Vector UI. + * @readonly + * @enum {string} + */ + states: states, /** * Convert a PushRuleVectorState to a list of actions @@ -39,10 +47,10 @@ module.exports = { */ actionsFor: function(pushRuleVectorState) { if (pushRuleVectorState === this.ON) { - return ACTION_NOTIFY; + return StandardActions.ACTION_NOTIFY; } else if (pushRuleVectorState === this.LOUD) { - return ACTION_HIGHLIGHT_DEFAULT_SOUND; + return StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND; } }, @@ -51,20 +59,24 @@ module.exports = { * * 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. + * state. Returns null if it does not match these categories. */ contentRuleVectorStateKind: function(rule) { - var stateKind; + var decoded = NotificationUtils.decodeActions(rule.actions); + + if (!decoded) { + return null; + } // 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++; - } + if (decoded.sound) { + tweaks++; } + if (decoded.highlight) { + tweaks++; + } + var stateKind = null; switch (tweaks) { case 0: stateKind = this.ON; @@ -76,3 +88,7 @@ module.exports = { return stateKind; }, }; + +for (var k in states) { + module.exports[k] = states[k]; +}; diff --git a/src/notifications/StandardActions.js b/src/notifications/StandardActions.js new file mode 100644 index 00000000..22a8f1db --- /dev/null +++ b/src/notifications/StandardActions.js @@ -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. +*/ + +'use strict'; + +var NotificationUtils = require('./NotificationUtils'); + +var encodeActions = NotificationUtils.encodeActions; + +module.exports = { + ACTION_NOTIFY: encodeActions({notify: true}), + ACTION_NOTIFY_DEFAULT_SOUND: encodeActions({notify: true, sound: "default"}), + ACTION_NOTIFY_RING_SOUND: encodeActions({notify: true, sound: "ring"}), + ACTION_HIGHLIGHT_DEFAULT_SOUND: encodeActions({notify: true, sound: "default", highlight: true}), + ACTION_DONT_NOTIFY: encodeActions({notify: false}), + ACTION_DISABLED: null, +}; diff --git a/src/notifications/VectorPushRulesDefinitions.js b/src/notifications/VectorPushRulesDefinitions.js index 8e2a0a65..dfbc06c0 100644 --- a/src/notifications/VectorPushRulesDefinitions.js +++ b/src/notifications/VectorPushRulesDefinitions.js @@ -16,17 +16,47 @@ limitations under the License. 'use strict'; -var NotificationUtils = require('./NotificationUtils'); +var StandardActions = require('./StandardActions'); +var PushRuleVectorState = require('./PushRuleVectorState'); -var encodeActions = NotificationUtils.encodeActions; -var decodeActions = NotificationUtils.decodeActions; +class VectorPushRuleDefinition { + constructor(opts) { + this.kind = opts.kind; + this.description = opts.description; + this.vectorStateToActions = opts.vectorStateToActions; + } -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; + // Translate the rule actions and its enabled value into vector state + ruleToVectorState(rule) { + var enabled = false; + var actions = null; + if (rule) { + enabled = rule.enabled; + actions = rule.actions; + } + + for (var stateKey in PushRuleVectorState.states) { + var state = PushRuleVectorState.states[stateKey]; + var vectorStateToActions = this.vectorStateToActions[state]; + + if (!vectorStateToActions) { + // No defined actions means that this vector state expects a disabled (or absent) rule + if (!enabled) { + return state; + } + } else { + // The actions must match to the ones expected by vector state + if (enabled && JSON.stringify(rule.actions) === JSON.stringify(vectorStateToActions)) { + return state; + } + } + } + + console.error("Cannot translate rule actions into Vector rule state. Rule: " + + JSON.stringify(rule)); + return undefined; + } +}; /** * The descriptions of rules managed by the Vector UI. @@ -34,71 +64,71 @@ const ACTION_DISABLED = null; module.exports = { // Messages containing user's display name // (skip contains_user_name which is too geeky) - ".m.rule.contains_display_name": { + ".m.rule.contains_display_name": new VectorPushRuleDefinition({ 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 + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND, + off: StandardActions.ACTION_DISABLED } - }, + }), // Messages just sent to the user in a 1:1 room - ".m.rule.room_one_to_one": { + ".m.rule.room_one_to_one": new VectorPushRuleDefinition({ kind: "underride", description: "Messages in one-to-one chats", vectorStateToActions: { - on: ACTION_NOTIFY, - loud: ACTION_NOTIFY_DEFAULT_SOUND, - off: ACTION_DONT_NOTIFY + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + off: StandardActions.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": { + ".m.rule.message": new VectorPushRuleDefinition({ kind: "underride", description: "Messages in group chats", vectorStateToActions: { - on: ACTION_NOTIFY, - loud: ACTION_NOTIFY_DEFAULT_SOUND, - off: ACTION_DONT_NOTIFY + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + off: StandardActions.ACTION_DONT_NOTIFY } - }, + }), // Invitation for the user - ".m.rule.invite_for_me": { + ".m.rule.invite_for_me": new VectorPushRuleDefinition({ kind: "underride", description: "When I'm invited to a room", vectorStateToActions: { - on: ACTION_NOTIFY, - loud: ACTION_NOTIFY_DEFAULT_SOUND, - off: ACTION_DISABLED + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + off: StandardActions.ACTION_DISABLED } - }, + }), // Incoming call - ".m.rule.call": { + ".m.rule.call": new VectorPushRuleDefinition({ kind: "underride", description: "Call invitation", vectorStateToActions: { - on: ACTION_NOTIFY, - loud: ACTION_NOTIFY_RING_SOUND, - off: ACTION_DISABLED + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_NOTIFY_RING_SOUND, + off: StandardActions.ACTION_DISABLED } - }, + }), // Notifications from bots - ".m.rule.suppress_notices": { + ".m.rule.suppress_notices": new VectorPushRuleDefinition({ 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, + on: StandardActions.ACTION_DISABLED, + loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + off: StandardActions.ACTION_DONT_NOTIFY, } - } + }), }; diff --git a/src/notifications/index.js b/src/notifications/index.js index 9672b67c..8ed77e9d 100644 --- a/src/notifications/index.js +++ b/src/notifications/index.js @@ -20,4 +20,5 @@ module.exports = { NotificationUtils: require('./NotificationUtils'), PushRuleVectorState: require('./PushRuleVectorState'), VectorPushRulesDefinitions: require('./VectorPushRulesDefinitions'), + ContentRules: require('./ContentRules'), }; diff --git a/src/skins/vector/css/matrix-react-sdk/structures/RoomStatusBar.css b/src/skins/vector/css/matrix-react-sdk/structures/RoomStatusBar.css index 0f6955ce..4d91755c 100644 --- a/src/skins/vector/css/matrix-react-sdk/structures/RoomStatusBar.css +++ b/src/skins/vector/css/matrix-react-sdk/structures/RoomStatusBar.css @@ -1,7 +1,23 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + .mx_RoomStatusBar { - margin-top: 5px; + margin-top: 15px; margin-left: 65px; - min-height: 24px; + min-height: 34px; } /* position the indicator in the same place horizontally as .mx_EventTile_avatar. */ @@ -17,8 +33,9 @@ .mx_RoomStatusBar_placeholderIndicator span { color: #4a4a4a; opacity: 0.5; -/* position: relative; + top: -4px; +/* animation-duration: 1s; animation-name: bounce; animation-direction: alternate; @@ -99,7 +116,7 @@ .mx_RoomStatusBar_tabCompleteWrapper { display: flex; display: -webkit-flex; - height: 24px; + height: 26px; } .mx_RoomStatusBar_tabCompleteWrapper .mx_TabCompleteBar { diff --git a/src/skins/vector/css/matrix-react-sdk/structures/RoomView.css b/src/skins/vector/css/matrix-react-sdk/structures/RoomView.css index 4c014461..c8772b49 100644 --- a/src/skins/vector/css/matrix-react-sdk/structures/RoomView.css +++ b/src/skins/vector/css/matrix-react-sdk/structures/RoomView.css @@ -36,8 +36,8 @@ limitations under the License. -webkit-order: 1; order: 1; - -webkit-flex: 0 0 83px; - flex: 0 0 83px; + -webkit-flex: 0 0 70px; + flex: 0 0 70px; } .mx_RoomView_fileDropTarget { @@ -64,7 +64,7 @@ limitations under the License. border: 2px #e1dddd solid; border-bottom: none; position: absolute; - top: 83px; + top: 70px; bottom: 0px; z-index: 3000; } @@ -89,7 +89,7 @@ limitations under the License. margin: auto; overflow: auto; - border-bottom: 1px solid #ccc; + border-bottom: 1px solid #e5e5e5; -webkit-flex: 0 0 auto; flex: 0 0 auto; @@ -158,7 +158,7 @@ limitations under the License. margin-bottom: 8px; margin-left: 63px; padding-bottom: 6px; - border-bottom: 1px solid #eee; + border-bottom: 1px solid #e5e5e5; } .mx_RoomView_invitePrompt { @@ -207,11 +207,12 @@ hr.mx_RoomView_myReadMarker { .mx_RoomView_statusAreaBox { max-width: 960px; margin: auto; - min-height: 36px; + min-height: 60px; } .mx_RoomView_statusAreaBox_line { - border-top: 1px solid #eee; + margin-left: 65px; + border-top: 1px solid #e5e5e5; height: 1px; } diff --git a/src/skins/vector/css/matrix-react-sdk/structures/SearchBox.css b/src/skins/vector/css/matrix-react-sdk/structures/SearchBox.css new file mode 100644 index 00000000..93e6d7c8 --- /dev/null +++ b/src/skins/vector/css/matrix-react-sdk/structures/SearchBox.css @@ -0,0 +1,63 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_SearchBox { + height: 24px; + margin-left: 16px; + margin-right: 20px; + padding-top: 24px; + padding-bottom: 22px; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + + display: flex; + display: -webkit-flex; +} + +.mx_SearchBox_searchButton { + margin-right: 10px; +} + +.mx_SearchBox_search { + flex: 1 1 auto; + -webkit-flex: 1 1 auto; + width: 0px; + font-family: 'Open Sans', Arial, Helvetica, Sans-Serif; + font-size: 12px; + margin-top: -2px; + height: 24px; + border: 0px ! important; + /* border-bottom: 1px solid rgba(0, 0, 0, 0.1) ! important; */ + background-color: transparent; + border: 0px; +} + +.mx_SearchBox_minimise, +.mx_SearchBox_maximise { + margin-top: 3px; + cursor: pointer; +} + +.mx_SearchBox_minimise { + margin-left: 10px; +} + +.mx_SearchBox_maximise { + margin-left: 9px; +} + +.mx_SearchBox object { + pointer-events: none; +} \ No newline at end of file diff --git a/src/skins/vector/css/matrix-react-sdk/structures/UploadBar.css b/src/skins/vector/css/matrix-react-sdk/structures/UploadBar.css index 5a22e91c..b489e132 100644 --- a/src/skins/vector/css/matrix-react-sdk/structures/UploadBar.css +++ b/src/skins/vector/css/matrix-react-sdk/structures/UploadBar.css @@ -1,16 +1,33 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + .mx_UploadBar { position: relative; } .mx_UploadBar_uploadProgressOuter { - height: 4px; + height: 5px; margin-left: 63px; margin-top: -1px; + padding-bottom: 5px; } .mx_UploadBar_uploadProgressInner { background-color: #76cfa6; - height: 4px; + height: 5px; } .mx_UploadBar_uploadFilename { @@ -22,7 +39,7 @@ .mx_UploadBar_uploadIcon { float: left; - margin-top: 1px; + margin-top: 5px; margin-left: 14px; } diff --git a/src/skins/vector/css/matrix-react-sdk/structures/UserSettings.css b/src/skins/vector/css/matrix-react-sdk/structures/UserSettings.css index 71a9b299..2ab1f5eb 100644 --- a/src/skins/vector/css/matrix-react-sdk/structures/UserSettings.css +++ b/src/skins/vector/css/matrix-react-sdk/structures/UserSettings.css @@ -36,8 +36,8 @@ limitations under the License. -webkit-order: 1; order: 1; - -webkit-flex: 0 0 83px; - flex: 0 0 83px; + -webkit-flex: 0 0 70px; + flex: 0 0 70px; } .mx_UserSettings_body { @@ -50,9 +50,25 @@ limitations under the License. -webkit-flex: 1 1 0; flex: 1 1 0; + margin-top: -20px; overflow-y: auto; } +.mx_UserSettings h3 { + clear: both; + margin-left: 63px; + text-transform: uppercase; + color: #3d3b39; + font-weight: 600; + font-size: 13px; + margin-top: 26px; + margin-bottom: 10px; +} + +.mx_UserSettings_section h3 { + margin-left: 0px; +} + .mx_UserSettings_spinner { display: inline-block; vertical-align: middle; @@ -78,22 +94,6 @@ limitations under the License. cursor: pointer; } -.mx_UserSettings h2 { - clear: both; - margin-top: 32px; - margin-bottom: 8px; - margin-left: 63px; - padding-bottom: 6px; - border-bottom: 1px solid #eee; -} - -.mx_UserSettings h3 { - font-weight: bold; - font-size: 15px; - margin-top: 4px; - margin-bottom: 4px; -} - .mx_UserSettings_section { margin-left: 63px; margin-top: 28px; @@ -106,6 +106,10 @@ limitations under the License. display: table; } +.mx_UserSettings_notifTable .mx_Spinner { + position: absolute; +} + .mx_UserSettings_profileTable { display: table; diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/MemberInfo.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/MemberInfo.css index e6a340df..30de9324 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/MemberInfo.css +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/MemberInfo.css @@ -15,6 +15,7 @@ limitations under the License. */ .mx_MemberInfo { + margin-top: 20px; height: 100%; } diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/MemberList.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/MemberList.css index 283addcb..88f7fafe 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/MemberList.css +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/MemberList.css @@ -17,6 +17,8 @@ limitations under the License. .mx_MemberList { height: 100%; + margin-top: 12px; + -webkit-flex: 1; flex: 1; @@ -77,17 +79,6 @@ limitations under the License. } */ -.mx_MemberList_bottom { - order: 4; - flex: 0 0 72px; - -webkit-flex: 0 0 72px; -} - -.mx_MemberList_bottomRule { - border-top: 2px solid #e1dddd; - margin-right: 15px; -} - .mx_MemberList_invited h2 { text-transform: uppercase; color: #3d3b39; diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/MessageComposer.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/MessageComposer.css index 9ed5f1cb..daf15001 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/MessageComposer.css +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/MessageComposer.css @@ -18,7 +18,7 @@ limitations under the License. max-width: 960px; vertical-align: middle; margin: auto; - border-top: 2px solid #e1dddd; + border-top: 1px solid #e5e5e5; } .mx_MessageComposer_row { @@ -45,7 +45,7 @@ limitations under the License. display: table-cell; width: 100%; vertical-align: middle; - height: 70px; + height: 60px; text-align: center; font-style: italic; color: #888; @@ -55,7 +55,7 @@ limitations under the License. display: table-cell; width: 100%; vertical-align: middle; - height: 70px; + height: 60px; } .mx_MessageComposer_input textarea { diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomHeader.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomHeader.css index eeb45b4d..c0e919b8 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomHeader.css +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomHeader.css @@ -23,8 +23,7 @@ limitations under the License. .mx_RoomHeader_wrapper { max-width: 960px; margin: auto; - height: 83px; - border-bottom: 1px solid #eeeeee; + height: 70px; -webkit-align-items: center; align-items: center; @@ -36,10 +35,6 @@ limitations under the License. display: flex; } -.mx_RoomHeader_editing .mx_RoomHeader_wrapper { - border-bottom: 1px solid transparent; -} - .mx_RoomHeader_leftRow { margin-left: -2px; @@ -123,7 +118,7 @@ limitations under the License. } .mx_RoomHeader_simpleHeader { - line-height: 83px; + line-height: 70px; color: #454545; font-size: 22px; font-weight: bold; @@ -133,11 +128,8 @@ limitations under the License. width: 100%; } -.mx_RoomHeader_simpleHeaderCancel { +.mx_RoomHeader_simpleHeader .mx_RoomHeader_cancelButton { float: right; - margin-top: 8px; - padding: 24px; - cursor: pointer; } .mx_RoomHeader_name { @@ -215,8 +207,9 @@ limitations under the License. vertical-align: bottom; float: left; max-height: 42px; - color: #454545; + color: #A2A2A2; font-weight: 300; + font-size: 13px; margin-left: 19px; margin-right: 16px; overflow: hidden; diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomList.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomList.css index 1a4ec869..a2241853 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomList.css +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomList.css @@ -15,7 +15,7 @@ limitations under the License. */ .mx_RoomList { - padding-top: 24px; + padding-top: 8px; padding-bottom: 12px; min-height: 400px; } diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomTile.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomTile.css index d62c5d17..27b2d229 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomTile.css +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomTile.css @@ -23,10 +23,10 @@ limitations under the License. .mx_RoomTile_avatar { display: table-cell; - padding-right: 8px; + padding-right: 11px; padding-top: 6px; padding-bottom: 6px; - padding-left: 18px; + padding-left: 20px; width: 24px; height: 24px; position: relative; @@ -38,8 +38,8 @@ limitations under the License. width: 100%; vertical-align: middle; overflow: hidden; - text-overflow: ellipsis; - padding-right: 16px; + word-break: break-word; + padding-right: 19px; color: rgba(69, 69, 69, 0.8); } @@ -98,11 +98,13 @@ limitations under the License. .mx_RoomTile_badge { background-color: #ff0064; - width: 4px; + width: 8px; + height: 8px; position: absolute; - left: 0px; - top: 5px; - bottom: 5px; + left: 7px; + top: 50%; + margin-top: -4px; + border-radius: 4px; } .mx_RoomTile_unreadNotify .mx_RoomTile_badge { @@ -119,18 +121,35 @@ limitations under the License. } .mx_RoomTile_selected .mx_RoomTile_name { - color: #76cfa6 ! important; + padding-right: 23px; } .mx_RoomTile_highlight .mx_RoomTile_name { color: #ff0064 ! important; } +.mx_RoomTile.mx_RoomTile_selected .mx_RoomTile_avatar { + padding-right: 7px; +} + +.mx_RoomTile.mx_RoomTile_selected .mx_RoomTile_name span { + display: inline-block; + position: relative; + width: 100%; + padding: 4px; + margin-top: -4px; + margin-bottom: -4px; + border-radius: 2px; + background-color: rgba(118,207,166,0.2); +} + +/* .mx_RoomTile.mx_RoomTile_selected .mx_RoomTile_name { background: url('img/selected.png'); background-repeat: no-repeat; background-position: right center; } +*/ .mx_RoomTile_arrow { position: absolute; diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/TabCompleteBar.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/TabCompleteBar.css index e0d0965e..f7f4a0bd 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/TabCompleteBar.css +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/TabCompleteBar.css @@ -21,6 +21,7 @@ limitations under the License. .mx_TabCompleteBar_item { display: inline-block; margin-right: 15px; + margin-bottom: 2px; cursor: pointer; } @@ -37,6 +38,7 @@ limitations under the License. .mx_TabCompleteBar_command .mx_TabCompleteBar_text { opacity: 1.0; + vertical-align: initial; color: #fff; } @@ -47,5 +49,6 @@ limitations under the License. .mx_TabCompleteBar_text { color: #4a4a4a; + vertical-align: middle; opacity: 0.5; } diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/TopUnreadMessagesBar.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/TopUnreadMessagesBar.css index ef639e2e..77184d42 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/TopUnreadMessagesBar.css +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/TopUnreadMessagesBar.css @@ -19,7 +19,7 @@ limitations under the License. max-width: 960px; padding-top: 5px; padding-bottom: 5px; - border-bottom: 1px solid #eee; + border-bottom: 1px solid #e5e5e5; } .mx_TopUnreadMessagesBar_scrollUp { diff --git a/src/skins/vector/css/vector-web/structures/LeftPanel.css b/src/skins/vector/css/vector-web/structures/LeftPanel.css index 4ee44426..c78dfd33 100644 --- a/src/skins/vector/css/vector-web/structures/LeftPanel.css +++ b/src/skins/vector/css/vector-web/structures/LeftPanel.css @@ -58,23 +58,40 @@ limitations under the License. -webkit-order: 3; order: 3; - -webkit-flex: 0 0 140px; - flex: 0 0 140px; - - background-color: rgba(118,207,166,0.2); + border-top: 1px solid rgba(0, 0, 0, 0.1); + margin-left: 20px; + margin-right: 20px; + -webkit-flex: 0 0 60px; + flex: 0 0 60px; } -.mx_LeftPanel .mx_BottomLeftMenu .mx_RoomTile { - color: #454545; +.mx_LeftPanel .mx_BottomLeftMenu_options { + margin-top: 18px; } -.mx_LeftPanel .mx_BottomLeftMenu .mx_BottomLeftMenu_options { - margin-top: 15px; - width: 100%; +.mx_BottomLeftMenu_options object { + pointer-events: none; } -.mx_LeftPanel .mx_BottomLeftMenu img { - border-radius: 0px; - background-color: transparent; - vertical-align: middle; -} \ No newline at end of file +.mx_LeftPanel .mx_BottomLeftMenu_createRoom, +.mx_LeftPanel .mx_BottomLeftMenu_directory, +.mx_LeftPanel .mx_BottomLeftMenu_settings { + display: inline-block; + cursor: pointer; +} + +.collapsed .mx_BottomLeftMenu_createRoom, +.collapsed .mx_BottomLeftMenu_directory, +.collapsed .mx_BottomLeftMenu_settings { + margin-left: 0px ! important; + padding-top: 3px ! important; + padding-bottom: 3px ! important; +} + +.mx_LeftPanel .mx_BottomLeftMenu_directory { + margin-left: 10px; +} + +.mx_LeftPanel .mx_BottomLeftMenu_settings { + float: right; +} diff --git a/src/skins/vector/css/vector-web/structures/RightPanel.css b/src/skins/vector/css/vector-web/structures/RightPanel.css index 7cad2649..7257d8b4 100644 --- a/src/skins/vector/css/vector-web/structures/RightPanel.css +++ b/src/skins/vector/css/vector-web/structures/RightPanel.css @@ -33,14 +33,17 @@ limitations under the License. -webkit-order: 1; order: 1; - -webkit-flex: 0 0 83px; - flex: 0 0 83px; + border-bottom: 1px solid #e5e5e5; + margin-right: 20px; + + -webkit-flex: 0 0 70px; + flex: 0 0 70px; } /** Fixme - factor this out with the main header **/ .mx_RightPanel_headerButtonGroup { - margin-top: 32px; + margin-top: 25px; float: left; background-color: #fff; margin-left: -4px; @@ -83,10 +86,27 @@ limitations under the License. } .mx_RightPanel .mx_MemberList, -.mx_RightPanel .mx_MemberInfo { +.mx_RightPanel .mx_MemberInfo, +.mx_RightPanel_blank { -webkit-box-ordinal-group: 2; -moz-box-ordinal-group: 2; -ms-flex-order: 2; -webkit-order: 2; order: 2; + flex: 1; + -webkit-flex: 1; +} + +.mx_RightPanel_footer { + -webkit-box-ordinal-group: 3; + -moz-box-ordinal-group: 3; + -ms-flex-order: 3; + -webkit-order: 3; + order: 3; + + border-top: 1px solid #e5e5e5; + margin-right: 20px; + + -webkit-flex: 0 0 60px; + flex: 0 0 60px; } diff --git a/src/skins/vector/css/vector-web/structures/RoomDirectory.css b/src/skins/vector/css/vector-web/structures/RoomDirectory.css index 2f75724d..c745706b 100644 --- a/src/skins/vector/css/vector-web/structures/RoomDirectory.css +++ b/src/skins/vector/css/vector-web/structures/RoomDirectory.css @@ -22,6 +22,8 @@ limitations under the License. margin-bottom: 12px; color: #4a4a4a; + border-top: 1px solid #c5c5c5; + display: -webkit-box; display: -moz-box; display: -ms-flexbox; diff --git a/src/skins/vector/css/vector-web/structures/RoomSubList.css b/src/skins/vector/css/vector-web/structures/RoomSubList.css index d385397b..8f9db8fe 100644 --- a/src/skins/vector/css/vector-web/structures/RoomSubList.css +++ b/src/skins/vector/css/vector-web/structures/RoomSubList.css @@ -29,13 +29,14 @@ limitations under the License. padding-right: 12px; margin-top: 8px; margin-bottom: 4px; + cursor: pointer; } .mx_RoomSubList_chevron { - padding-left: 5px; + padding-left: 4px; pointer-events: none; } .collapsed .mx_RoomSubList_chevron { - padding-left: 13px; + padding-left: 12px; } diff --git a/src/skins/vector/img/icons-create-room.svg b/src/skins/vector/img/icons-create-room.svg new file mode 100644 index 00000000..6ed94b4b --- /dev/null +++ b/src/skins/vector/img/icons-create-room.svg @@ -0,0 +1,20 @@ + + + + icons_create_room + Created with sketchtool. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/skins/vector/img/icons-directory.svg b/src/skins/vector/img/icons-directory.svg new file mode 100644 index 00000000..00869b9b --- /dev/null +++ b/src/skins/vector/img/icons-directory.svg @@ -0,0 +1,21 @@ + + + + icons_directory + Created with sketchtool. + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/skins/vector/img/icons-settings.svg b/src/skins/vector/img/icons-settings.svg new file mode 100644 index 00000000..60969530 --- /dev/null +++ b/src/skins/vector/img/icons-settings.svg @@ -0,0 +1,17 @@ + + + + icons_settings + Created with sketchtool. + + + + + + + + + + + + \ No newline at end of file diff --git a/src/skins/vector/img/list-close.svg b/src/skins/vector/img/list-close.svg index eb60864e..cd88b2a8 100644 --- a/src/skins/vector/img/list-close.svg +++ b/src/skins/vector/img/list-close.svg @@ -1,10 +1,15 @@ - - - - Slice 1 - Created with Sketch. - - - - - \ No newline at end of file + + + + +Slice 1 +Created with Sketch. + + + + diff --git a/src/skins/vector/img/list-open.svg b/src/skins/vector/img/list-open.svg index a682ec90..e180be88 100644 --- a/src/skins/vector/img/list-open.svg +++ b/src/skins/vector/img/list-open.svg @@ -1,10 +1,15 @@ - - - - Slice 1 - Created with Sketch. - - - - - \ No newline at end of file + + + + +Slice 1 +Created with Sketch. + + + + diff --git a/src/skins/vector/img/maximise.svg b/src/skins/vector/img/maximise.svg new file mode 100644 index 00000000..79c6c0ab --- /dev/null +++ b/src/skins/vector/img/maximise.svg @@ -0,0 +1,19 @@ + + + +minimise +Created with sketchtool. + + + + + + + + + + + + diff --git a/src/skins/vector/img/minimise.svg b/src/skins/vector/img/minimise.svg new file mode 100644 index 00000000..491756b1 --- /dev/null +++ b/src/skins/vector/img/minimise.svg @@ -0,0 +1,18 @@ + + + + minimise + Created with sketchtool. + + + + + + + + + + + + + diff --git a/src/skins/vector/img/right_search.svg b/src/skins/vector/img/right_search.svg new file mode 100644 index 00000000..b430a6be --- /dev/null +++ b/src/skins/vector/img/right_search.svg @@ -0,0 +1,17 @@ + + + + right_search + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/src/vector/index.js b/src/vector/index.js index 6dd975d2..e3178c96 100644 --- a/src/vector/index.js +++ b/src/vector/index.js @@ -27,8 +27,15 @@ require('gemini-scrollbar/gemini-scrollbar.css'); require('gfm.css/gfm.css'); require('highlight.js/styles/github.css'); + + // add React and ReactPerf to the global namespace, to make them easier to + // access via the console +global.React = require("react"); +if (process.env.NODE_ENV !== 'production') { + global.ReactPerf = require("react-addons-perf"); +} + var RunModernizrTests = require("./modernizr"); // this side-effects a global -var React = require("react"); var ReactDOM = require("react-dom"); var sdk = require("matrix-react-sdk"); sdk.loadSkin(require('../component-index')); diff --git a/test/unit-tests/notifications/ContentRules-test.js b/test/unit-tests/notifications/ContentRules-test.js new file mode 100644 index 00000000..e7928147 --- /dev/null +++ b/test/unit-tests/notifications/ContentRules-test.js @@ -0,0 +1,117 @@ +/* +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 ContentRules = notifications.ContentRules; +var PushRuleVectorState = notifications.PushRuleVectorState; + +var expect = require('expect'); +var test_utils = require('../../test-utils'); + +var NORMAL_RULE = { + actions: [ + "notify", + { set_tweak: "highlight", value: false }, + ], + enabled: true, + pattern: "vdh2", + rule_id: "vdh2", +}; + +var LOUD_RULE = { + actions: [ + "notify", + { set_tweak: "highlight" }, + { set_tweak: "sound", value: "default" }, + ], + enabled: true, + pattern: "vdh2", + rule_id: "vdh2", +}; + +var USERNAME_RULE = { + actions: [ + "notify", + { set_tweak: "sound", value: "default" }, + { set_tweak: "highlight" }, + ], + default: true, + enabled: true, + pattern: "richvdh", + rule_id: ".m.rule.contains_user_name", +}; + + + +describe("ContentRules", function() { + beforeEach(function() { + test_utils.beforeEach(this); + }); + + describe("parseContentRules", function() { + it("should handle there being no keyword rules", function() { + var rules = { 'global': { 'content': [ + USERNAME_RULE, + ]}}; + var parsed = ContentRules.parseContentRules(rules); + expect(parsed.rules).toEqual([]); + expect(parsed.vectorState).toEqual(PushRuleVectorState.ON); + expect(parsed.externalRules).toEqual([]); + }); + + it("should parse regular keyword notifications", function() { + var rules = { 'global': { 'content': [ + NORMAL_RULE, + USERNAME_RULE, + ]}}; + + var parsed = ContentRules.parseContentRules(rules); + expect(parsed.rules.length).toEqual(1); + expect(parsed.rules[0]).toEqual(NORMAL_RULE); + expect(parsed.vectorState).toEqual(PushRuleVectorState.ON); + expect(parsed.externalRules).toEqual([]); + }); + + it("should parse loud keyword notifications", function() { + var rules = { 'global': { 'content': [ + LOUD_RULE, + USERNAME_RULE, + ]}}; + + var parsed = ContentRules.parseContentRules(rules); + expect(parsed.rules.length).toEqual(1); + expect(parsed.rules[0]).toEqual(LOUD_RULE); + expect(parsed.vectorState).toEqual(PushRuleVectorState.LOUD); + expect(parsed.externalRules).toEqual([]); + }); + + it("should parse mixed keyword notifications", function() { + var rules = { 'global': { 'content': [ + LOUD_RULE, + NORMAL_RULE, + USERNAME_RULE, + ]}}; + + var parsed = ContentRules.parseContentRules(rules); + expect(parsed.rules.length).toEqual(1); + expect(parsed.rules[0]).toEqual(LOUD_RULE); + expect(parsed.vectorState).toEqual(PushRuleVectorState.LOUD); + expect(parsed.externalRules.length).toEqual(1); + expect(parsed.externalRules[0]).toEqual(NORMAL_RULE); + }); + }); +}); diff --git a/test/unit-tests/notifications/PushRuleVectorState-test.js b/test/unit-tests/notifications/PushRuleVectorState-test.js index 48084f08..6b0f81c6 100644 --- a/test/unit-tests/notifications/PushRuleVectorState-test.js +++ b/test/unit-tests/notifications/PushRuleVectorState-test.js @@ -23,8 +23,40 @@ var expect = require('expect'); describe("PushRuleVectorState", function() { describe("contentRuleVectorStateKind", function() { it("should understand normal notifications", function () { - expect(prvs.contentRuleVectorStateKind(["notify"])). + var rule = { + actions: [ + "notify", + ], + }; + + expect(prvs.contentRuleVectorStateKind(rule)). toEqual(prvs.ON); }); + + it("should handle loud notifications", function () { + var rule = { + actions: [ + "notify", + { set_tweak: "highlight", value: true }, + { set_tweak: "sound", value: "default" }, + ] + }; + + expect(prvs.contentRuleVectorStateKind(rule)). + toEqual(prvs.LOUD); + }); + + it("should understand missing highlight.value", function () { + var rule = { + actions: [ + "notify", + { set_tweak: "highlight" }, + { set_tweak: "sound", value: "default" }, + ] + }; + + expect(prvs.contentRuleVectorStateKind(rule)). + toEqual(prvs.LOUD); + }); }); }); diff --git a/webpack.config.js b/webpack.config.js index dfe127d6..297881f3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -41,6 +41,7 @@ module.exports = { // 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'), + "react-addons-perf": path.resolve('./node_modules/react-addons-perf'), // same goes for js-sdk "matrix-js-sdk": path.resolve('./node_modules/matrix-js-sdk'),