diff --git a/skins/base/views/atoms/EnableNotificationsButton.js b/skins/base/views/atoms/EnableNotificationsButton.js
new file mode 100644
index 00000000..7caebb76
--- /dev/null
+++ b/skins/base/views/atoms/EnableNotificationsButton.js
@@ -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 (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+ }
+});
diff --git a/skins/base/views/molecules/MatrixToolbar.js b/skins/base/views/molecules/MatrixToolbar.js
index fe2c7614..e4444ee9 100644
--- a/skins/base/views/molecules/MatrixToolbar.js
+++ b/skins/base/views/molecules/MatrixToolbar.js
@@ -21,6 +21,7 @@ var React = require('react');
var ComponentBroker = require('../../../../src/ComponentBroker');
var LogoutButton = ComponentBroker.get("atoms/LogoutButton");
+var EnableNotificationsButton = ComponentBroker.get("atoms/EnableNotificationsButton");
var MatrixToolbarController = require("../../../../src/controllers/molecules/MatrixToolbar");
@@ -32,6 +33,7 @@ module.exports = React.createClass({
return (
+
);
}
diff --git a/skins/base/views/organisms/Notifier.js b/skins/base/views/organisms/Notifier.js
new file mode 100644
index 00000000..110c21e2
--- /dev/null
+++ b/skins/base/views/organisms/Notifier.js
@@ -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();
+
diff --git a/src/ComponentBroker.js b/src/ComponentBroker.js
index 7d711e9b..07261061 100644
--- a/src/ComponentBroker.js
+++ b/src/ComponentBroker.js
@@ -42,6 +42,7 @@ module.exports = {
// Must be in this file (because the require is file-specific) and
// must be at the end because the components include this file.
require('../skins/base/views/atoms/LogoutButton');
+require('../skins/base/views/atoms/EnableNotificationsButton');
require('../skins/base/views/atoms/MessageTimestamp');
require('../skins/base/views/molecules/MatrixToolbar');
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/RoomView');
require('../skins/base/views/templates/Login');
+require('../skins/base/views/organisms/Notifier');
diff --git a/src/controllers/atoms/EnableNotificationsButton.js b/src/controllers/atoms/EnableNotificationsButton.js
new file mode 100644
index 00000000..02d99f65
--- /dev/null
+++ b/src/controllers/atoms/EnableNotificationsButton.js
@@ -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();
+ }
+ },
+};
diff --git a/src/controllers/organisms/Notifier.js b/src/controllers/organisms/Notifier.js
new file mode 100644
index 00000000..9c16d7ea
--- /dev/null
+++ b/src/controllers/organisms/Notifier.js
@@ -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);
+ }
+ }
+};
+
diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js
index 6f2d4ba3..04735c0b 100644
--- a/src/controllers/pages/MatrixChat.js
+++ b/src/controllers/pages/MatrixChat.js
@@ -23,10 +23,14 @@ var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher");
+var ComponentBroker = require('../../ComponentBroker');
+
+var Notifier = ComponentBroker.get('organisms/Notifier');
+
module.exports = {
getInitialState: function() {
return {
- logged_in: !!(MatrixClientPeg.get() && mxCliPeg.get().credentials),
+ logged_in: !!(MatrixClientPeg.get() && MatrixClientPeg.get().credentials),
ready: false
};
},
@@ -61,14 +65,15 @@ module.exports = {
logged_in: false,
ready: false
});
+ Notifier.stop();
MatrixClientPeg.get().removeAllListeners();
MatrixClientPeg.replace(null);
break;
case 'view_room':
+ this.focusComposer = true;
this.setState({
currentRoom: payload.room_id
});
- this.focusComposer = true;
break;
case 'view_prev_room':
roomIndexDelta = -1;
@@ -105,6 +110,7 @@ module.exports = {
that.setState({ready: true, currentRoom: firstRoom});
dis.dispatch({action: 'focus_composer'});
});
+ Notifier.start();
cli.startClient();
},
diff --git a/src/dispatcher.js b/src/dispatcher.js
index ede63912..3edb9c69 100644
--- a/src/dispatcher.js
+++ b/src/dispatcher.js
@@ -17,4 +17,21 @@ limitations under the License.
'use strict';
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();
diff --git a/src/extend.js b/src/extend.js
new file mode 100644
index 00000000..f5658b58
--- /dev/null
+++ b/src/extend.js
@@ -0,0 +1,8 @@
+module.exports = function(dest, src) {
+ for (var i in src) {
+ if (src.hasOwnProperty(i)) {
+ dest[i] = src[i];
+ }
+ }
+ return dest;
+}