[gnome-shell] Use Telepathy for IM notifications
- From: Dan Winship <danw src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-shell] Use Telepathy for IM notifications
- Date: Tue, 6 Apr 2010 13:28:19 +0000 (UTC)
commit 5bce103a40ce869ffd14addfc4e84ba420f04150
Author: Dan Winship <danw gnome org>
Date: Tue Feb 2 10:21:47 2010 -0500
Use Telepathy for IM notifications
And suppress libnotify-based notifications from Empathy
https://bugzilla.gnome.org/show_bug.cgi?id=608999
js/misc/Makefile.am | 3 +-
js/misc/telepathy.js | 255 ++++++++++++++++++++++++++++++
js/ui/Makefile.am | 1 +
js/ui/main.js | 7 +-
js/ui/notificationDaemon.js | 11 ++
js/ui/telepathyClient.js | 364 +++++++++++++++++++++++++++++++++++++++++++
6 files changed, 638 insertions(+), 3 deletions(-)
---
diff --git a/js/misc/Makefile.am b/js/misc/Makefile.am
index 9118b1f..5f9278e 100644
--- a/js/misc/Makefile.am
+++ b/js/misc/Makefile.am
@@ -3,4 +3,5 @@ jsmiscdir = $(pkgdatadir)/js/misc
dist_jsmisc_DATA = \
docInfo.js \
format.js \
- params.js
+ params.js \
+ telepathy.js
diff --git a/js/misc/telepathy.js b/js/misc/telepathy.js
new file mode 100644
index 0000000..d97cfaa
--- /dev/null
+++ b/js/misc/telepathy.js
@@ -0,0 +1,255 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const DBus = imports.dbus;
+
+// D-Bus utils; should eventually move to gjs.
+// https://bugzilla.gnome.org/show_bug.cgi?id=610859
+
+function makeProxyClass(iface) {
+ let constructor = function() { this._init.apply(this, arguments); };
+
+ constructor.prototype._init = function(bus, name, path) {
+ bus.proxifyObject(this, name, path);
+ };
+
+ DBus.proxifyPrototype(constructor.prototype, iface);
+ return constructor;
+}
+
+function nameToPath(name) {
+ return '/' + name.replace('.', '/', 'g');
+};
+
+function pathToName(path) {
+ if (path[0] != '/')
+ throw new Error('not a D-Bus path: ' + path);
+ return path.substr(1).replace('/', '.', 'g');
+};
+
+// Telepathy D-Bus interface definitions. Note that most of these are
+// incomplete, and only cover the methods/properties/signals that
+// we're currently using.
+
+const TELEPATHY = 'org.freedesktop.Telepathy';
+
+const CLIENT_NAME = TELEPATHY + '.Client';
+const ClientIface = {
+ name: CLIENT_NAME,
+ properties: [
+ { name: 'Interfaces',
+ signature: 'as',
+ access: 'read' }
+ ]
+};
+
+const CLIENT_APPROVER_NAME = TELEPATHY + '.Client.Approver';
+const ClientApproverIface = {
+ name: CLIENT_APPROVER_NAME,
+ methods: [
+ { name: 'AddDispatchOperation',
+ inSignature: 'a(oa{sv})oa{sv}',
+ outSignature: '' }
+ ],
+ properties: [
+ { name: 'ApproverChannelFilter',
+ signature: 'aa{sv}',
+ access: 'read' }
+ ]
+};
+
+const CLIENT_HANDLER_NAME = TELEPATHY + '.Client.Handler';
+const ClientHandlerIface = {
+ name: CLIENT_HANDLER_NAME,
+ methods: [
+ { name: 'HandleChannels',
+ inSignature: 'ooa(oa{sv})aota{sv}',
+ outSignature: '' }
+ ],
+ properties: [
+ { name: 'HandlerChannelFilter',
+ signature: 'aa{sv}',
+ access: 'read' }
+ ]
+};
+
+const CLIENT_OBSERVER_NAME = TELEPATHY + '.Client.Observer';
+const ClientObserverIface = {
+ name: CLIENT_OBSERVER_NAME,
+ methods: [
+ { name: 'ObserveChannels',
+ inSignature: 'ooa(oa{sv})oaoa{sv}',
+ outSignature: '' }
+ ],
+ properties: [
+ { name: 'ObserverChannelFilter',
+ signature: 'aa{sv}',
+ access: 'read' }
+ ]
+};
+
+const CHANNEL_DISPATCH_OPERATION_NAME = TELEPATHY + '.ChannelDispatchOperation';
+const ChannelDispatchOperationIface = {
+ name: CHANNEL_DISPATCH_OPERATION_NAME,
+ methods: [
+ { name: 'HandleWith',
+ inSignature: 's',
+ outSignature: '' },
+ { name: 'Claim',
+ inSignature: '',
+ outSignature: '' }
+ ]
+};
+let ChannelDispatchOperation = makeProxyClass(ChannelDispatchOperationIface);
+
+const CONNECTION_NAME = TELEPATHY + '.Connection';
+const ConnectionIface = {
+ name: CONNECTION_NAME,
+ signals: [
+ { name: 'StatusChanged',
+ inSignature: 'uu' }
+ ]
+};
+let Connection = makeProxyClass(ConnectionIface);
+
+const ConnectionStatus = {
+ CONNECTED: 0,
+ CONNECTING: 1,
+ DISCONNECTED: 2
+};
+
+const CONNECTION_ALIASING_NAME = CONNECTION_NAME + '.Interface.Aliasing';
+const ConnectionAliasingIface = {
+ name: CONNECTION_ALIASING_NAME,
+ methods: [
+ { name: 'RequestAliases',
+ inSignature: 'au',
+ outSignature: 'as'
+ }
+ ],
+ signals: [
+ { name: 'AliasesChanged',
+ inSignature: 'a(us)' }
+ ]
+};
+let ConnectionAliasing = makeProxyClass(ConnectionAliasingIface);
+
+const CONNECTION_AVATARS_NAME = CONNECTION_NAME + '.Interface.Avatars';
+const ConnectionAvatarsIface = {
+ name: CONNECTION_AVATARS_NAME,
+ methods: [
+ { name: 'GetKnownAvatarTokens',
+ inSignature: 'au',
+ outSignature: 'a{us}'
+ },
+ { name: 'RequestAvatars',
+ inSignature: 'au',
+ outSignature: ''
+ }
+ ],
+ signals: [
+ { name: 'AvatarRetrieved',
+ inSignature: 'usays'
+ },
+ { name: 'AvatarUpdated',
+ inSignature: 'us'
+ }
+ ]
+};
+let ConnectionAvatars = makeProxyClass(ConnectionAvatarsIface);
+
+const CONNECTION_REQUESTS_NAME = CONNECTION_NAME + '.Interface.Requests';
+const ConnectionRequestsIface = {
+ name: CONNECTION_REQUESTS_NAME,
+ methods: [
+ { name: 'CreateChannel',
+ inSignature: 'a{sv}',
+ outSignature: 'oa{sv}'
+ },
+ { name: 'EnsureChannel',
+ inSignature: 'a{sv}',
+ outSignature: 'boa{sv}'
+ }
+ ],
+ properties: [
+ { name: 'Channels',
+ signature: 'a(oa{sv})',
+ access: 'read' }
+ ],
+ signals: [
+ { name: 'NewChannels',
+ inSignature: 'a(oa{sv})'
+ },
+ { name: 'ChannelClosed',
+ inSignature: 'o'
+ }
+ ]
+};
+let ConnectionRequests = makeProxyClass(ConnectionRequestsIface);
+
+const HandleType = {
+ NONE: 0,
+ CONTACT: 1,
+ ROOM: 2,
+ LIST: 3,
+ GROUP: 4
+};
+
+const CHANNEL_NAME = TELEPATHY + '.Channel';
+const ChannelIface = {
+ name: CHANNEL_NAME,
+ signals: [
+ { name: 'Closed',
+ inSignature: '' }
+ ]
+};
+let Channel = makeProxyClass(ChannelIface);
+
+const CHANNEL_TEXT_NAME = CHANNEL_NAME + '.Type.Text';
+const ChannelTextIface = {
+ name: CHANNEL_TEXT_NAME,
+ methods: [
+ { name: 'ListPendingMessages',
+ inSignature: 'b',
+ outSignature: 'a(uuuuus)'
+ },
+ { name: 'AcknowledgePendingMessages',
+ inSignature: 'au',
+ outSignature: ''
+ }
+ ],
+ signals: [
+ { name: 'Received',
+ inSignature: 'uuuuus' }
+ ]
+};
+let ChannelText = makeProxyClass(ChannelTextIface);
+
+const ChannelTextMessageType = {
+ NORMAL: 0,
+ ACTION: 1,
+ NOTICE: 2,
+ AUTO_REPLY: 3,
+ DELIVERY_REPORT: 4
+};
+
+const ACCOUNT_MANAGER_NAME = TELEPATHY + '.AccountManager';
+const AccountManagerIface = {
+ name: ACCOUNT_MANAGER_NAME,
+ properties: [
+ { name: 'ValidAccounts',
+ signature: 'ao',
+ access: 'read' }
+ ]
+};
+let AccountManager = makeProxyClass(AccountManagerIface);
+
+const ACCOUNT_NAME = TELEPATHY + '.Account';
+const AccountIface = {
+ name: ACCOUNT_NAME,
+ properties: [
+ { name: 'Connection',
+ signature: 'o',
+ access: 'read' }
+ ]
+};
+let Account = makeProxyClass(AccountIface);
diff --git a/js/ui/Makefile.am b/js/ui/Makefile.am
index 7cd7cc9..691cfee 100644
--- a/js/ui/Makefile.am
+++ b/js/ui/Makefile.am
@@ -25,6 +25,7 @@ dist_jsui_DATA = \
search.js \
shellDBus.js \
statusMenu.js \
+ telepathyClient.js \
tweener.js \
windowAttentionHandler.js \
windowManager.js \
diff --git a/js/ui/main.js b/js/ui/main.js
index 677e587..253e8a7 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -24,6 +24,7 @@ const LookingGlass = imports.ui.lookingGlass;
const NotificationDaemon = imports.ui.notificationDaemon;
const WindowAttentionHandler = imports.ui.windowAttentionHandler;
const ShellDBus = imports.ui.shellDBus;
+const TelepathyClient = imports.ui.telepathyClient;
const WindowManager = imports.ui.windowManager;
const DEFAULT_BACKGROUND_COLOR = new Clutter.Color();
@@ -36,9 +37,10 @@ let overview = null;
let runDialog = null;
let lookingGlass = null;
let wm = null;
-let notificationDaemon = null;
let messageTray = null;
+let notificationDaemon = null;
let windowAttentionHandler = null;
+let telepathyClient = null;
let recorder = null;
let shellDBusService = null;
let modalCount = 0;
@@ -108,9 +110,10 @@ function start() {
chrome = new Chrome.Chrome();
panel = new Panel.Panel();
wm = new WindowManager.WindowManager();
+ messageTray = new MessageTray.MessageTray();
notificationDaemon = new NotificationDaemon.NotificationDaemon();
windowAttentionHandler = new WindowAttentionHandler.WindowAttentionHandler();
- messageTray = new MessageTray.MessageTray();
+ telepathyClient = new TelepathyClient.Client();
_startDate = new Date();
diff --git a/js/ui/notificationDaemon.js b/js/ui/notificationDaemon.js
index 618d06e..2de216c 100644
--- a/js/ui/notificationDaemon.js
+++ b/js/ui/notificationDaemon.js
@@ -140,6 +140,17 @@ NotificationDaemon.prototype = {
let source = Main.messageTray.getSource(this._sourceId(appName));
let id = null;
+ // Filter out notifications from Empathy, since we
+ // handle that information from telepathyClient.js
+ if (appName == 'Empathy') {
+ id = nextNotificationId++;
+ Mainloop.idle_add(Lang.bind(this,
+ function () {
+ this._emitNotificationClosed(id, NotificationClosedReason.DISMISSED);
+ }));
+ return id;
+ }
+
// Source may be null if we have never received a notification from
// this app or if all notifications from this app have been acknowledged.
if (source == null) {
diff --git a/js/ui/telepathyClient.js b/js/ui/telepathyClient.js
new file mode 100644
index 0000000..7de1995
--- /dev/null
+++ b/js/ui/telepathyClient.js
@@ -0,0 +1,364 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const DBus = imports.dbus;
+const Lang = imports.lang;
+const Shell = imports.gi.Shell;
+const St = imports.gi.St;
+
+const Main = imports.ui.main;
+const MessageTray = imports.ui.messageTray;
+const Telepathy = imports.misc.telepathy;
+
+let avatarManager;
+
+// This is GNOME Shell's implementation of the Telepathy "Client"
+// interface. Specifically, the shell is a Telepathy "Approver", which
+// lets us control the routing of incoming messages, a "Handler",
+// which lets us receive and respond to messages, and an "Observer",
+// which lets us see messages even if they belong to another app (eg,
+// a conversation started from within Empathy).
+
+function Client() {
+ this._init();
+};
+
+Client.prototype = {
+ _init : function() {
+ let name = Telepathy.CLIENT_NAME + '.GnomeShell';
+ DBus.session.exportObject(Telepathy.nameToPath(name), this);
+ DBus.session.acquire_name(name, DBus.SINGLE_INSTANCE,
+ function (name) { /* FIXME: acquired */ },
+ function (name) { /* FIXME: lost */ });
+
+ this._channels = {};
+
+ avatarManager = new AvatarManager();
+
+ // Acquire existing connections. (Needed to make things work
+ // through a restart.)
+ let accountManager = new Telepathy.AccountManager(DBus.session,
+ Telepathy.ACCOUNT_MANAGER_NAME,
+ Telepathy.nameToPath(Telepathy.ACCOUNT_MANAGER_NAME));
+ accountManager.GetRemote('ValidAccounts', Lang.bind(this, this._gotValidAccounts));
+ },
+
+ _gotValidAccounts: function(accounts, err) {
+ if (!accounts)
+ return;
+
+ for (let i = 0; i < accounts.length; i++) {
+ let account = new Telepathy.Account(DBus.session,
+ Telepathy.ACCOUNT_MANAGER_NAME,
+ accounts[i]);
+ account.GetRemote('Connection', Lang.bind(this,
+ function (connPath, err) {
+ if (!connPath || connPath == '/')
+ return;
+
+ let connReq = new Telepathy.ConnectionRequests(DBus.session,
+ Telepathy.pathToName(connPath),
+ connPath);
+ connReq.GetRemote('Channels', Lang.bind(this,
+ function(channels, err) {
+ if (!channels)
+ return;
+ this._addChannels(connPath, channels);
+ }));
+ }));
+ }
+ },
+
+ get Interfaces() {
+ return [ Telepathy.CLIENT_APPROVER_NAME,
+ Telepathy.CLIENT_HANDLER_NAME,
+ Telepathy.CLIENT_OBSERVER_NAME ];
+ },
+
+ get ApproverChannelFilter() {
+ return [
+ { 'org.freedesktop.Telepathy.Channel.ChannelType': Telepathy.CHANNEL_TEXT_NAME }
+ ];
+ },
+
+ AddDispatchOperation: function(channels, dispatchOperationPath, properties) {
+ let sender = DBus.getCurrentMessageContext().sender;
+ let op = new Telepathy.ChannelDispatchOperation(DBus.session, sender,
+ dispatchOperationPath);
+ op.ClaimRemote();
+ },
+
+ get HandlerChannelFilter() {
+ return [
+ { 'org.freedesktop.Telepathy.Channel.ChannelType': Telepathy.CHANNEL_TEXT_NAME }
+ ];
+ },
+
+ HandleChannels: function(account, connPath, channels,
+ requestsSatisfied, userActionTime,
+ handlerInfo) {
+ this._addChannels(connPath, channels);
+ },
+
+ get ObserverChannelFilter() {
+ return [
+ { 'org.freedesktop.Telepathy.Channel.ChannelType': Telepathy.CHANNEL_TEXT_NAME }
+ ];
+ },
+
+ ObserveChannels: function(account, connPath, channels,
+ dispatchOperation, requestsSatisfied,
+ observerInfo) {
+ this._addChannels(connPath, channels);
+ },
+
+ _addChannels: function(connPath, channelDetailsList) {
+ for (let i = 0; i < channelDetailsList.length; i++) {
+ let [channelPath, props] = channelDetailsList[i];
+ if (this._channels[channelPath])
+ continue;
+
+ let channelType = props[Telepathy.CHANNEL_NAME + '.ChannelType'];
+ if (channelType != Telepathy.CHANNEL_TEXT_NAME)
+ continue;
+
+ let targetHandle = props[Telepathy.CHANNEL_NAME + '.TargetHandle'];
+ let targetHandleType = props[Telepathy.CHANNEL_NAME + '.TargetHandleType'];
+ let targetId = props[Telepathy.CHANNEL_NAME + '.TargetID'];
+
+ let source = new Source(connPath, channelPath,
+ targetHandle, targetHandleType, targetId);
+ this._channels[channelPath] = source;
+ source.connect('destroy', Lang.bind(this,
+ function() {
+ delete this._channels[channelPath];
+ }));
+ }
+ }
+};
+DBus.conformExport(Client.prototype, Telepathy.ClientIface);
+DBus.conformExport(Client.prototype, Telepathy.ClientApproverIface);
+DBus.conformExport(Client.prototype, Telepathy.ClientHandlerIface);
+DBus.conformExport(Client.prototype, Telepathy.ClientObserverIface);
+
+
+function AvatarManager() {
+ this._init();
+};
+
+AvatarManager.prototype = {
+ _init: function() {
+ this._connections = {};
+ },
+
+ _addConnection: function(conn) {
+ if (this._connections[conn.getPath()])
+ return this._connections[conn.getPath()];
+
+ let info = {};
+
+ // avatarData[handle] describes the icon for @handle:
+ // either the string 'default', meaning to use the default
+ // avatar, or an array of bytes containing, eg, PNG data.
+ info.avatarData = {};
+
+ // icons[handle] is an array of the icon actors currently
+ // being displayed for @handle. These will be updated
+ // automatically if @handle's avatar changes.
+ info.icons = {};
+
+ info.connectionAvatars = new Telepathy.ConnectionAvatars(DBus.session,
+ conn.getBusName(),
+ conn.getPath());
+ info.updatedId = info.connectionAvatars.connect(
+ 'AvatarUpdated', Lang.bind(this, this._avatarUpdated));
+ info.retrievedId = info.connectionAvatars.connect(
+ 'AvatarRetrieved', Lang.bind(this, this._avatarRetrieved));
+
+ info.statusChangedId = conn.connect('StatusChanged', Lang.bind(this,
+ function (status, reason) {
+ if (status == Telepathy.ConnectionStatus.DISCONNECTED)
+ this._removeConnection(conn);
+ }));
+
+ this._connections[conn.getPath()] = info;
+ return info;
+ },
+
+ _removeConnection: function(conn) {
+ let info = this._connections[conn.getPath()];
+ if (!info)
+ return;
+
+ conn.disconnect(info.statusChangedId);
+ info.connectionAvatars.disconnect(info.updatedId);
+ info.connectionAvatars.disconnect(info.retrievedId);
+
+ delete this._connections[conn.getPath()];
+ },
+
+ _avatarUpdated: function(conn, handle, token) {
+ let info = this._connections[conn.getPath()];
+ if (!info)
+ return;
+
+ if (!info.avatarData[handle]) {
+ // This would only happen if either (a) the initial
+ // RequestAvatars() call hasn't returned yet, or (b)
+ // Telepathy is informing us about avatars we didn't ask
+ // about. Either way, we don't have to do anything here.
+ return;
+ }
+
+ if (token == '') {
+ // Invoke the next async callback in the chain, telling
+ // it to use the default image.
+ this._avatarRetrieved(conn, handle, token, 'default', null);
+ } else {
+ // In this case, @token is some sort of UUID. Telepathy
+ // expects us to cache avatar images to disk and use the
+ // tokens to figure out when we already have the right
+ // images cached. But we don't do that, we just
+ // ignore @token and request the image unconditionally.
+ info.connectionAvatars.RequestAvatarsRemote([handle]);
+ }
+ },
+
+ _createIcon: function(iconData, size) {
+ let textureCache = St.TextureCache.get_default();
+ if (iconData == 'default')
+ return textureCache.load_icon_name('stock_person', size);
+ else
+ return textureCache.load_from_data(iconData, iconData.length, size);
+ },
+
+ _avatarRetrieved: function(conn, handle, token, avatarData, mimeType) {
+ let info = this._connections[conn.getPath()];
+ if (!info)
+ return;
+
+ info.avatarData[handle] = avatarData;
+ if (!info.icons[handle])
+ return;
+
+ for (let i = 0; i < info.icons[handle].length; i++) {
+ let iconBox = info.icons[handle][i];
+ let size = iconBox.child.height;
+ iconBox.child = this._createIcon(avatarData, size);
+ }
+ },
+
+ createAvatar: function(conn, handle, size) {
+ let iconBox = new St.Bin({ style_class: 'avatar-box' });
+
+ let info = this._connections[conn.getPath()];
+ if (!info)
+ info = this._addConnection(conn);
+
+ if (!info.icons[handle])
+ info.icons[handle] = [];
+ info.icons[handle].push(iconBox);
+
+ iconBox.connect('destroy', Lang.bind(this,
+ function() {
+ let i = info.icons[handle].indexOf(iconBox);
+ if (i != -1)
+ info.icons[handle].splice(i, 1);
+ }));
+
+ let avatarData = info.avatarData[handle];
+ if (avatarData) {
+ iconBox.child = this._createIcon(avatarData, size);
+ return iconBox;
+ }
+
+ // Fill in the default icon and then asynchronously load
+ // the real avatar.
+ iconBox.child = this._createIcon('default', size);
+ info.connectionAvatars.GetKnownAvatarTokensRemote([handle], Lang.bind(this,
+ function (tokens, err) {
+ if (tokens && tokens[handle])
+ info.connectionAvatars.RequestAvatarsRemote([handle]);
+ else
+ info.avatarData[handle] = 'default';
+ }));
+
+ return iconBox;
+ }
+};
+
+
+function Source(connPath, channelPath, targetHandle, targetHandleType, targetId) {
+ this._init(connPath, channelPath, targetHandle, targetHandleType, targetId);
+}
+
+Source.prototype = {
+ __proto__: MessageTray.Source.prototype,
+
+ _init: function(connPath, channelPath, targetHandle, targetHandleType, targetId) {
+ MessageTray.Source.prototype._init.call(this, targetId);
+
+ let connName = Telepathy.pathToName(connPath);
+ this._conn = new Telepathy.Connection(DBus.session, connName, connPath);
+ this._channel = new Telepathy.Channel(DBus.session, connName, channelPath);
+ this._closedId = this._channel.connect('Closed', Lang.bind(this, this._channelClosed));
+
+ this._targetHandle = targetHandle;
+ this._targetId = targetId;
+
+ this.name = this._targetId;
+ if (targetHandleType == Telepathy.HandleType.CONTACT) {
+ let aliasing = new Telepathy.ConnectionAliasing(DBus.session, connName, connPath);
+ aliasing.RequestAliasesRemote([this._targetHandle], Lang.bind(this,
+ function (aliases, err) {
+ if (aliases && aliases.length)
+ this.name = aliases[0];
+ }));
+ }
+
+ this._channelText = new Telepathy.ChannelText(DBus.session, connName, channelPath);
+ this._receivedId = this._channelText.connect('Received', Lang.bind(this, this._receivedMessage));
+
+ this._channelText.ListPendingMessagesRemote(false, Lang.bind(this, this._gotPendingMessages));
+ },
+
+ createIcon: function(size) {
+ return avatarManager.createAvatar(this._conn, this._targetHandle, size);
+ },
+
+ _gotPendingMessages: function(msgs, err) {
+ if (!msgs)
+ return;
+
+ for (let i = 0; i < msgs.length; i++)
+ this._receivedMessage.apply(this, [this._channel].concat(msgs[i]));
+ },
+
+ _channelClosed: function() {
+ this._channel.disconnect(this._closedId);
+ this._channelText.disconnect(this._receivedId);
+ this.destroy();
+ },
+
+ _receivedMessage: function(channel, id, timestamp, sender,
+ type, flags, text) {
+ if (!Main.messageTray.contains(this))
+ Main.messageTray.add(this);
+
+ let notification = new Notification(this._targetId, this, text);
+ this.notify(notification);
+
+ this._channelText.AcknowledgePendingMessagesRemote([id]);
+ }
+};
+
+function Notification(id, source, text) {
+ this._init(id, source, text);
+}
+
+Notification.prototype = {
+ __proto__: MessageTray.Notification.prototype,
+
+ _init: function(id, source, text) {
+ MessageTray.Notification.prototype._init.call(this, id, source, source.name, text, true);
+ }
+};
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]