Add desktop notifications, overridable in the same way as other components (although it's not a react component). Also extend the flux dispatcher a little to be less dumb about dispatching while something else is already dispatching.

This commit is contained in:
David Baker 2015-07-03 11:12:54 +01:00
parent 947f389e51
commit fd20e82123
9 changed files with 288 additions and 3 deletions

View file

@ -0,0 +1,38 @@
/*
Copyright 2015 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 EnableNotificationsButtonController = require("../../../../src/controllers/atoms/EnableNotificationsButton");
module.exports = React.createClass({
displayName: 'EnableNotificationsButton',
mixins: [EnableNotificationsButtonController],
render: function() {
if (this.enabled()) {
return (
<button className="mx_EnableNotificationsButton" onClick={this.onClick}>Disable Notifications</button>
);
} else {
return (
<button className="mx_EnableNotificationsButton" onClick={this.onClick}>Enable Notifications</button>
);
}
}
});

View file

@ -21,6 +21,7 @@ var React = require('react');
var ComponentBroker = require('../../../../src/ComponentBroker'); var ComponentBroker = require('../../../../src/ComponentBroker');
var LogoutButton = ComponentBroker.get("atoms/LogoutButton"); var LogoutButton = ComponentBroker.get("atoms/LogoutButton");
var EnableNotificationsButton = ComponentBroker.get("atoms/EnableNotificationsButton");
var MatrixToolbarController = require("../../../../src/controllers/molecules/MatrixToolbar"); var MatrixToolbarController = require("../../../../src/controllers/molecules/MatrixToolbar");
@ -32,6 +33,7 @@ module.exports = React.createClass({
return ( return (
<div className="mx_MatrixToolbar"> <div className="mx_MatrixToolbar">
<LogoutButton /> <LogoutButton />
<EnableNotificationsButton />
</div> </div>
); );
} }

View file

@ -0,0 +1,102 @@
/*
Copyright 2015 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 NotifierController = require("../../../../src/controllers/organisms/Notifier");
var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
var extend = require("../../../../src/extend");
var dis = require("../../../../src/dispatcher");
var NotifierView = {
notificationMessageForEvent: function(ev) {
var senderDisplayName = ev.sender.name;
var message = null;
if (ev.event.type === "m.room.message") {
message = ev.getContent().body;
if (ev.getContent().msgtype === "m.emote") {
message = "* " + senderDisplayName + " " + message;
} else if (ev.getContent().msgtype === "m.image") {
message = senderDisplayName + " sent an image.";
}
} else if (ev.event.type == "m.room.member") {
if (ev.event.state_key !== MatrixClientPeg.get().credentials.userId && "join" === ev.getContent().membership) {
// Notify when another user joins
message = ev.target.name + " joined";
} else if (ev.event.state_key === MatrixClientPeg.get().credentials.userId && "invite" === ev.getContent().membership) {
// notify when you are invited
message = senderDisplayName + " invited you to a room";
}
}
return message;
},
displayNotification: function(ev, room) {
if (!global.Notification || global.Notification.permission != 'granted') {
return;
}
if (global.document.hasFocus()) {
return;
}
var msg = this.notificationMessageForEvent(ev);
if (!msg) return;
var title;
if (room.name == ev.sender.name) {
title = room.name;
} else {
title = ev.sender.name + " (" + room.name + ")";
}
var notification = new global.Notification(
title,
{
"body": msg,
"icon": MatrixClientPeg.get().getAvatarUrlForMember(ev.sender)
}
);
notification.onclick = function() {
dis.dispatch({
action: 'view_room',
room_id: room.roomId
});
global.focus();
};
/*var audioClip;
if (audioNotification) {
audioClip = playAudio(audioNotification);
}*/
global.setTimeout(function() {
notification.close();
}, 5 * 1000);
}
};
var NotifierClass = function() {};
extend(NotifierClass.prototype, NotifierController);
extend(NotifierClass.prototype, NotifierView);
module.exports = new NotifierClass();

View file

@ -42,6 +42,7 @@ module.exports = {
// Must be in this file (because the require is file-specific) and // Must be in this file (because the require is file-specific) and
// must be at the end because the components include this file. // must be at the end because the components include this file.
require('../skins/base/views/atoms/LogoutButton'); require('../skins/base/views/atoms/LogoutButton');
require('../skins/base/views/atoms/EnableNotificationsButton');
require('../skins/base/views/atoms/MessageTimestamp'); require('../skins/base/views/atoms/MessageTimestamp');
require('../skins/base/views/molecules/MatrixToolbar'); require('../skins/base/views/molecules/MatrixToolbar');
require('../skins/base/views/molecules/RoomTile'); require('../skins/base/views/molecules/RoomTile');
@ -60,3 +61,4 @@ require('../skins/base/views/molecules/MemberTile');
require('../skins/base/views/organisms/RoomList'); require('../skins/base/views/organisms/RoomList');
require('../skins/base/views/organisms/RoomView'); require('../skins/base/views/organisms/RoomView');
require('../skins/base/views/templates/Login'); require('../skins/base/views/templates/Login');
require('../skins/base/views/organisms/Notifier');

View file

@ -0,0 +1,64 @@
/*
Copyright 2015 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 = {
notificationsAvailable: function() {
return !!global.Notification;
},
havePermission: function() {
return global.Notification.permission == 'granted';
},
enabled: function() {
if (!this.havePermission()) return false;
if (!global.localStorage) return true;
var enabled = global.localStorage.getItem('notifications_enabled');
if (enabled === null) return true;
return enabled === 'true';
},
disable: function() {
if (!global.localStorage) return;
global.localStorage.setItem('notifications_enabled', 'false');
this.forceUpdate();
},
enable: function() {
if (!this.havePermission()) {
global.Notification.requestPermission();
}
if (!global.localStorage) return;
global.localStorage.setItem('notifications_enabled', 'true');
this.forceUpdate();
},
onClick: function() {
if (!this.notificationsAvailable()) {
return;
}
if (!this.enabled()) {
this.enable();
} else {
this.disable();
}
},
};

View file

@ -0,0 +1,46 @@
/*
Copyright 2015 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 MatrixClientPeg = require("../../MatrixClientPeg");
module.exports = {
start: function() {
this.boundOnRoomTimeline = this.onRoomTimeline.bind(this);
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
},
stop: function() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
}
},
onRoomTimeline: function(ev, room, toStartOfTimeline) {
if (toStartOfTimeline) return;
if (ev.sender.userId == MatrixClientPeg.get().credentials.userId) return;
var enabled = global.localStorage.getItem('notifications_enabled');
if (enabled === 'false') return;
var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
if (actions && actions.notify) {
this.displayNotification(ev, room);
}
}
};

View file

@ -23,10 +23,14 @@ var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher"); var dis = require("../../dispatcher");
var ComponentBroker = require('../../ComponentBroker');
var Notifier = ComponentBroker.get('organisms/Notifier');
module.exports = { module.exports = {
getInitialState: function() { getInitialState: function() {
return { return {
logged_in: !!(MatrixClientPeg.get() && mxCliPeg.get().credentials), logged_in: !!(MatrixClientPeg.get() && MatrixClientPeg.get().credentials),
ready: false ready: false
}; };
}, },
@ -61,14 +65,15 @@ module.exports = {
logged_in: false, logged_in: false,
ready: false ready: false
}); });
Notifier.stop();
MatrixClientPeg.get().removeAllListeners(); MatrixClientPeg.get().removeAllListeners();
MatrixClientPeg.replace(null); MatrixClientPeg.replace(null);
break; break;
case 'view_room': case 'view_room':
this.focusComposer = true;
this.setState({ this.setState({
currentRoom: payload.room_id currentRoom: payload.room_id
}); });
this.focusComposer = true;
break; break;
case 'view_prev_room': case 'view_prev_room':
roomIndexDelta = -1; roomIndexDelta = -1;
@ -105,6 +110,7 @@ module.exports = {
that.setState({ready: true, currentRoom: firstRoom}); that.setState({ready: true, currentRoom: firstRoom});
dis.dispatch({action: 'focus_composer'}); dis.dispatch({action: 'focus_composer'});
}); });
Notifier.start();
cli.startClient(); cli.startClient();
}, },

View file

@ -17,4 +17,21 @@ limitations under the License.
'use strict'; 'use strict';
var flux = require("flux"); var flux = require("flux");
module.exports = new flux.Dispatcher(); var extend = require("./extend");
var MatrixDispatcher = function() {
flux.Dispatcher.call(this);
};
extend(MatrixDispatcher.prototype, flux.Dispatcher.prototype);
MatrixDispatcher.prototype.dispatch = function(payload) {
if (this.dispatching) {
setTimeout(flux.Dispatcher.prototype.dispatch.bind(this, payload), 0);
} else {
this.dispatching = true;
flux.Dispatcher.prototype.dispatch.call(this, payload);
this.dispatching = false;
}
}
module.exports = new MatrixDispatcher();

8
src/extend.js Normal file
View file

@ -0,0 +1,8 @@
module.exports = function(dest, src) {
for (var i in src) {
if (src.hasOwnProperty(i)) {
dest[i] = src[i];
}
}
return dest;
}