6c3b8e2add
Also fixes it to notice avatar updates https://bugzilla.gnome.org/show_bug.cgi?id=603546
436 lines
13 KiB
JavaScript
436 lines
13 KiB
JavaScript
/* -*- 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 AVATAR_SIZE = 24;
|
|
|
|
const TELEPATHY = "org.freedesktop.Telepathy.";
|
|
const CONN = TELEPATHY + "Connection";
|
|
const CHANNEL = TELEPATHY + "Channel";
|
|
const CHANNELTEXT = CHANNEL + ".Type.Text";
|
|
const ACCOUNTMANAGER = TELEPATHY + 'AccountManager';
|
|
|
|
const ClientIface = {
|
|
name: TELEPATHY + "Client",
|
|
properties: [{ name: "Interfaces",
|
|
signature: "as",
|
|
access: "read" }]
|
|
};
|
|
|
|
const ClientObserverIface = {
|
|
name: TELEPATHY + "Client.Observer",
|
|
methods: [{ name: "ObserveChannels",
|
|
inSignature: "ooa(oa{sv})oaoa{sv}",
|
|
outSignature: "" }],
|
|
properties: [{ name: "ObserverChannelFilter",
|
|
signature: "aa{sv}",
|
|
access: "read" }]
|
|
};
|
|
|
|
const ConnectionIface = {
|
|
name: CONN,
|
|
methods: [
|
|
// This is deprecated, but the alternative requires building
|
|
// another interface object...
|
|
{ name: "ListChannels",
|
|
inSignature: "",
|
|
outSignature: "a(osuu)"
|
|
}
|
|
],
|
|
signals: [
|
|
{ name: 'StatusChanged', inSignature: 'u' }
|
|
]
|
|
};
|
|
|
|
function Connection(path) {
|
|
this._init(path);
|
|
};
|
|
|
|
Connection.prototype = {
|
|
_init: function(path) {
|
|
DBus.session.proxifyObject(this, nameify(path), path);
|
|
}
|
|
};
|
|
|
|
DBus.proxifyPrototype(Connection.prototype, ConnectionIface);
|
|
|
|
const ConnectionAvatarsIface = {
|
|
name: CONN + '.Interface.Avatars',
|
|
methods: [
|
|
{ name: 'GetKnownAvatarTokens',
|
|
inSignature: 'au',
|
|
outSignature: 'a{us}'
|
|
},
|
|
{ name: 'RequestAvatars',
|
|
inSignature: 'au',
|
|
outSignature: ''
|
|
}
|
|
],
|
|
signals: [
|
|
{ name: 'AvatarRetrieved',
|
|
inSignature: 'usays'
|
|
},
|
|
{ name: 'AvatarUpdated',
|
|
inSignature: 'us'
|
|
}
|
|
]
|
|
};
|
|
|
|
function ConnectionAvatars(path) {
|
|
this._init(path);
|
|
};
|
|
|
|
ConnectionAvatars.prototype = {
|
|
_init: function(path) {
|
|
DBus.session.proxifyObject(this, nameify(path), path);
|
|
|
|
this.connect('AvatarUpdated', Lang.bind(this, this._avatarUpdated));
|
|
this.connect('AvatarRetrieved', Lang.bind(this, this._avatarRetrieved));
|
|
|
|
// _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.
|
|
this._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.
|
|
this._icons = {};
|
|
},
|
|
|
|
_avatarUpdated: function(iface, handle, token) {
|
|
if (!this._avatarData[handle]) {
|
|
// This would only happen if we get an AvatarUpdated
|
|
// signal for an avatar while there's already a
|
|
// RequestAvatars() call pending, so we don't need to do
|
|
// anything.
|
|
return;
|
|
}
|
|
|
|
if (token == '') {
|
|
// Invoke the next async callback in the chain, telling
|
|
// it to use the default image.
|
|
this._avatarRetrieved(this, 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.
|
|
this.RequestAvatarsRemote([handle]);
|
|
}
|
|
},
|
|
|
|
_createIcon: function(iconData, size) {
|
|
let textureCache = Shell.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(iface, handle, token, avatarData, mimeType) {
|
|
this._avatarData[handle] = avatarData;
|
|
if (!this._icons[handle])
|
|
return;
|
|
|
|
for (let i = 0; i < this._icons[handle].length; i++) {
|
|
let iconBox = this._icons[handle][i];
|
|
let size = iconBox.child.height;
|
|
iconBox.child = this._createIcon(avatarData, size);
|
|
}
|
|
},
|
|
|
|
createAvatar: function(handle, size) {
|
|
let iconBox = new St.Bin({ style_class: 'avatar-box' });
|
|
|
|
if (!this._icons[handle])
|
|
this._icons[handle] = [];
|
|
this._icons[handle].push(iconBox);
|
|
|
|
iconBox.connect('destroy', Lang.bind(this,
|
|
function() {
|
|
let i = this._icons[handle].indexOf(iconBox);
|
|
if (i != -1)
|
|
this._icons[handle].splice(i, 1);
|
|
}));
|
|
|
|
let avatarData = this._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);
|
|
this.GetKnownAvatarTokensRemote([handle], Lang.bind(this,
|
|
function (tokens, excp) {
|
|
if (tokens && tokens[handle])
|
|
this.RequestAvatarsRemote([handle]);
|
|
else
|
|
this._avatarData[handle] = 'default';
|
|
}));
|
|
|
|
return iconBox;
|
|
}
|
|
};
|
|
|
|
DBus.proxifyPrototype(ConnectionAvatars.prototype, ConnectionAvatarsIface);
|
|
|
|
const ChannelIface = {
|
|
name: CHANNEL,
|
|
properties: [
|
|
{ name: "TargetHandle",
|
|
signature: "u",
|
|
access: "read" },
|
|
{ name: "TargetID",
|
|
signature: "s",
|
|
access: "read" }
|
|
],
|
|
signals: [
|
|
{ name: 'Closed', inSignature: '' }
|
|
]
|
|
};
|
|
|
|
function Channel(name, path) {
|
|
this._init(name, path);
|
|
};
|
|
|
|
Channel.prototype = {
|
|
_init: function(name, path) {
|
|
DBus.session.proxifyObject(this, name, path);
|
|
}
|
|
};
|
|
|
|
DBus.proxifyPrototype(Channel.prototype, ChannelIface);
|
|
|
|
const ChannelTextIface = {
|
|
name: CHANNELTEXT,
|
|
methods: [
|
|
{ name: 'ListPendingMessages',
|
|
inSignature: 'b',
|
|
outSignature: 'a(uuuuus)'
|
|
}
|
|
],
|
|
signals: [
|
|
{ name: 'Received', inSignature: 'uuuuus' }
|
|
]
|
|
};
|
|
|
|
function ChannelText(name, path) {
|
|
this._init(name, path);
|
|
};
|
|
|
|
ChannelText.prototype = {
|
|
_init: function(name, path) {
|
|
DBus.session.proxifyObject(this, name, path);
|
|
}
|
|
};
|
|
|
|
DBus.proxifyPrototype(ChannelText.prototype, ChannelTextIface);
|
|
|
|
const AccountManagerIface = {
|
|
name: ACCOUNTMANAGER,
|
|
|
|
properties: [{ name: "ValidAccounts",
|
|
signature: "ao",
|
|
access: "read" }]
|
|
};
|
|
|
|
function AccountManager() {
|
|
this._init();
|
|
}
|
|
|
|
AccountManager.prototype = {
|
|
_init: function() {
|
|
DBus.session.proxifyObject(this,
|
|
ACCOUNTMANAGER,
|
|
pathify(ACCOUNTMANAGER));
|
|
}
|
|
};
|
|
|
|
DBus.proxifyPrototype(AccountManager.prototype, AccountManagerIface);
|
|
|
|
const AccountIface = {
|
|
name: 'org.freedesktop.Telepathy.Account',
|
|
|
|
properties: [{ name: "Connection",
|
|
signature: "o",
|
|
access: "read" }]
|
|
};
|
|
|
|
function Account(name, path) {
|
|
this._init(name, path);
|
|
}
|
|
|
|
Account.prototype = {
|
|
_init: function(name, path) {
|
|
DBus.session.proxifyObject(this, name, path);
|
|
}
|
|
};
|
|
|
|
DBus.proxifyPrototype(Account.prototype, AccountIface);
|
|
|
|
let nameify = function(path) {
|
|
return path.substr(1).replace('/', '.', 'g');
|
|
};
|
|
|
|
let pathify = function(name) {
|
|
return '/' + name.replace('.', '/', 'g');
|
|
};
|
|
|
|
function Messaging() {
|
|
this._init();
|
|
};
|
|
|
|
Messaging.prototype = {
|
|
_init : function() {
|
|
let name = TELEPATHY + "Client.GnomeShell";
|
|
DBus.session.exportObject(pathify(name), this);
|
|
DBus.session.acquire_name(name, DBus.SINGLE_INSTANCE,
|
|
function(name){log("Acquired name " + name);},
|
|
function(name){log("Lost name " + name);});
|
|
|
|
this._conns = {};
|
|
this._channels = {};
|
|
|
|
// Acquire existing connections. (This wouldn't really be
|
|
// needed if gnome-shell was only being started at the start
|
|
// of a session, but it's very useful for making things
|
|
// continue to work after restarting the shell.)
|
|
let accountManager = new AccountManager();
|
|
accountManager.GetRemote('ValidAccounts', Lang.bind(this, this._gotValidAccounts));
|
|
},
|
|
|
|
_gotValidAccounts: function(accounts, excp) {
|
|
if (!accounts)
|
|
return;
|
|
|
|
for (let i = 0; i < accounts.length; i++) {
|
|
let account = new Account(ACCOUNTMANAGER, accounts[i]);
|
|
account.GetRemote('Connection', Lang.bind(this,
|
|
function (conn_path, excp) {
|
|
if (!conn_path || conn_path == '/')
|
|
return;
|
|
|
|
let conn = new Connection(conn_path);
|
|
conn.ListChannelsRemote(Lang.bind(this,
|
|
function(channels, excp) {
|
|
if (!channels) {
|
|
log('no channels on ' + conn.getPath() + ': ' + excp);
|
|
return;
|
|
}
|
|
for (let i = 0; i < channels.length; i++) {
|
|
let [path, channel_type, handle_type, handle] = channels[i];
|
|
if (channel_type != CHANNELTEXT)
|
|
continue;
|
|
if (this._channels[path])
|
|
continue;
|
|
|
|
let connName = nameify(conn.getPath());
|
|
let channel = new Channel(connName, path);
|
|
channel.GetAllRemote(Lang.bind(this,
|
|
function(props, excp) {
|
|
this._addChannel(conn, path,
|
|
props['TargetHandle'],
|
|
props['TargetID']);
|
|
}));
|
|
}
|
|
}));
|
|
}));
|
|
}
|
|
},
|
|
|
|
get Interfaces() {
|
|
return [TELEPATHY + "Client.Observer"];
|
|
},
|
|
|
|
get ObserverChannelFilter() {
|
|
return [
|
|
{ 'org.freedesktop.Telepathy.Channel.ChannelType': CHANNELTEXT }
|
|
];
|
|
},
|
|
|
|
ObserveChannels: function(account, conn_path, channels,
|
|
dispatch_operation, requests_satisfied,
|
|
observer_info) {
|
|
let conn = new Connection(conn_path);
|
|
let conn_name = nameify(conn_path);
|
|
for (let i = 0; i < channels.length; i++) {
|
|
let channelPath = channels[i][0];
|
|
let props = channels[i][1];
|
|
let targetHandle = props[CHANNEL + '.TargetHandle'];
|
|
let targetId = props[CHANNEL + '.TargetID'];
|
|
this._addChannel(conn, channelPath, targetHandle, targetId);
|
|
}
|
|
|
|
return [true];
|
|
},
|
|
|
|
_addChannel: function(conn, channelPath, targetHandle, targetId) {
|
|
this._channels[channelPath] = new Source(conn, channelPath, targetHandle, targetId);
|
|
}
|
|
};
|
|
|
|
DBus.conformExport(Messaging.prototype, ClientIface);
|
|
DBus.conformExport(Messaging.prototype, ClientObserverIface);
|
|
|
|
function Source(conn, channelPath, channel_props, targetId) {
|
|
this._init(conn, channelPath, channel_props, targetId);
|
|
}
|
|
|
|
Source.prototype = {
|
|
_init: function(conn, channelPath, targetHandle, targetId) {
|
|
let connName = nameify(conn.getPath());
|
|
this._channel = new Channel(connName, channelPath);
|
|
this._closedId = this._channel.connect('Closed', Lang.bind(this, this._channelClosed));
|
|
|
|
this._targetHandle = targetHandle;
|
|
this._targetId = targetId;
|
|
log('channel for ' + this._targetId + ' channelPath ' + channelPath);
|
|
|
|
this._avatars = new ConnectionAvatars(conn.getPath());
|
|
|
|
this._channelText = new ChannelText(connName, channelPath);
|
|
this._receivedId = this._channelText.connect('Received', Lang.bind(this, this._receivedMessage));
|
|
|
|
this._channelText.ListPendingMessagesRemote(false, Lang.bind(this, this._gotPendingMessages));
|
|
},
|
|
|
|
_createIcon: function() {
|
|
return this._avatars.createAvatar(this._targetHandle, AVATAR_SIZE);
|
|
},
|
|
|
|
_gotPendingMessages: function(msgs, excp) {
|
|
if (!msgs)
|
|
return;
|
|
|
|
for (let i = 0; i < msgs.length; i++)
|
|
this._receivedMessage.apply(this, [this._channel].concat(msgs[i]));
|
|
},
|
|
|
|
_channelClosed: function() {
|
|
log('Channel closed ' + this._targetId);
|
|
this._channel.disconnect(this._closedId);
|
|
this._channelText.disconnect(this._receivedId);
|
|
Main.messageTray.remove(this._targetId);
|
|
},
|
|
|
|
_receivedMessage: function(channel, id, timestamp, sender,
|
|
type, flags, text) {
|
|
log('Received: id ' + id + ', time ' + timestamp + ', sender ' + sender + ', type ' + type + ', flags ' + flags + ': ' + text);
|
|
let popupAvatar = this._createIcon();
|
|
Main.notificationPopup.show(popupAvatar, text);
|
|
if (!Main.messageTray.contains(this._targetId)) {
|
|
let trayAvatar = this._createIcon();
|
|
Main.messageTray.add(this._targetId, trayAvatar);
|
|
}
|
|
}
|
|
};
|