Merge remote-tracking branch 'origin/develop' into dbkr/email_notifs

This commit is contained in:
David Baker 2016-04-21 10:17:15 +01:00
commit a72d0c5b7f
45 changed files with 1060 additions and 320 deletions

View file

@ -47,12 +47,19 @@ module.exports = React.createClass({
render: function() {
var BottomLeftMenuTile = sdk.getComponent('rooms.BottomLeftMenuTile');
var TintableSvg = sdk.getComponent('elements.TintableSvg');
return (
<div className="mx_BottomLeftMenu">
<div className="mx_BottomLeftMenu_options">
<BottomLeftMenuTile collapsed={ this.props.collapsed } img="img/create-big.svg" label="Start chat" onClick={ this.onCreateRoomClick }/>
<BottomLeftMenuTile collapsed={ this.props.collapsed } img="img/directory-big.svg" label="Directory" onClick={ this.onRoomDirectoryClick }/>
<BottomLeftMenuTile collapsed={ this.props.collapsed } img="img/settings-big.svg" label="Settings" onClick={ this.onSettingsClick }/>
<div className="mx_BottomLeftMenu_createRoom" title="Start chat" onClick={ this.onCreateRoomClick }>
<TintableSvg src="img/icons-create-room.svg" width="24" height="24"/>
</div>
<div className="mx_BottomLeftMenu_directory" title="Room directory" onClick={ this.onRoomDirectoryClick }>
<TintableSvg src="img/icons-directory.svg" width="24" height="24"/>
</div>
<div className="mx_BottomLeftMenu_settings" title="Settings" onClick={ this.onSettingsClick }>
<TintableSvg src="img/icons-settings.svg" width="24" height="24"/>
</div>
</div>
</div>
);

View file

@ -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 (
<aside className={classes} style={{ opacity: this.props.opacity }}>
<SearchBox collapsed={ this.props.collapsed } onSearch={ this.onSearch } />
{ collapseButton }
{ callPreview }
<RoomList
selectedRoom={this.props.selectedRoom}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ConferenceHandler={VectorConferenceHandler} />
<BottomLeftMenu collapsed={this.props.collapsed}/>
</aside>

View file

@ -155,7 +155,10 @@ module.exports = React.createClass({
panel = <MemberInfo roomId={this.props.roomId} member={this.state.member} key={this.props.roomId} />
}
}
}
if (!panel) {
panel = <div className="mx_RightPanel_blank"></div>;
}
var classes = "mx_RightPanel mx_fadable";
@ -169,6 +172,8 @@ module.exports = React.createClass({
{ buttonGroup }
</div>
{ panel }
<div className="mx_RightPanel_footer">
</div>
</aside>
);
}

View file

@ -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({
<SimpleRoomHeader title="Directory" />
<div className="mx_RoomDirectory_list">
<input ref="roomAlias" placeholder="Join a room (e.g. #foo:domain.com)" className="mx_RoomDirectory_input" size="64" onKeyUp={ this.onKeyUp }/>
<GeminiScrollbar className="mx_RoomDirectory_tableWrapper">
<GeminiScrollbar className="mx_RoomDirectory_tableWrapper"
relayoutOnUpdate={false} >
<table ref="directory_table" className="mx_RoomDirectory_table">
<tbody>
{ this.getRows(this.state.roomAlias) }
@ -209,4 +226,3 @@ module.exports = React.createClass({
);
}
});

View file

@ -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 (
<h2 onClick={ this.onClick } className="mx_RoomSubList_label">
{ this.props.collapsed ? '' : this.props.label }
<TintableSvg className="mx_RoomSubList_chevron"
<img className="mx_RoomSubList_chevron"
src={ this.state.hidden ? "img/list-close.svg" : "img/list-open.svg" }
width="10" height="10" />
</h2>

View file

@ -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 =
<div className="mx_SearchBox_maximise" onClick={ this.onToggleCollapse.bind(this, true) }>
<TintableSvg src="img/maximise.svg" width="10" height="16" alt="&lt;"/>
</div>
}
else {
toggleCollapse =
<div className="mx_SearchBox_minimise" onClick={ this.onToggleCollapse.bind(this, false) }>
<TintableSvg src="img/minimise.svg" width="10" height="16" alt="&lt;"/>
</div>
}
var searchControls;
if (!this.props.collapsed) {
searchControls = [
<TintableSvg
key="button"
className="mx_SearchBox_searchButton"
src="img/right_search.svg" width="24" height="24"
/>,
<input
key="searchfield"
type="text"
ref="search"
className="mx_SearchBox_search"
value={ this.state.searchTerm }
onChange={ this.onChange }
placeholder="Search room names"
/>
];
}
var self = this;
return (
<div className="mx_SearchBox">
{ searchControls }
{ toggleCollapse }
</div>
);
}
});

View file

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

View file

@ -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 (
<tr key = {className}>
<tr key={ className }>
<th>
{title}
</th>
@ -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 (
<div className="mx_UserSettings_notifTable">
<Loader />
</div>
);
spinner = <Loader />;
}
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(<tr key={p.app_id+p.pushkey}>
<td>{p.app_display_name}</td>
<td>{p.device_display_name}</td>
rows.push(<tr key={ i }>
<td>{this.state.pushers[i].app_display_name}</td>
<td>{this.state.pushers[i].device_display_name}</td>
</tr>);
}
devicesSection = (<table className="mx_UserSettings_devicesTable"><thead>
<tr>
<th>Application</th>
<th>Device</th>
</tr></thead>
<tbody>{rows}</tbody>
devicesSection = (<table className="mx_UserSettings_devicesTable">
<thead>
<tr>
<th>Application</th>
<th>Device</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>);
}
@ -857,6 +788,8 @@ module.exports = React.createClass({
<div className="mx_UserSettings_notifTable">
{ spinner }
<div className="mx_UserNotifSettings_tableRow">
<div className="mx_UserNotifSettings_inputCell">
<input id="enableDesktopNotifications"
@ -901,7 +834,7 @@ module.exports = React.createClass({
<th width="55%"></th>
<th width="15%">Off</th>
<th width="15%">On</th>
<th width="15%">Loud</th>
<th width="15%">Highlight<br/>&amp; sound</th>
</tr>
</thead>
<tbody>