[gnome-shell] Use Telepathy for IM notifications



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]