diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index f6f725b6e..4fe68a8cb 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -1257,6 +1257,10 @@ StTooltip StLabel { padding-right: 4px; } +.subscription-message { + font-style: italic; +} + #notification StEntry { padding: 4px; border-radius: 4px; diff --git a/js/ui/notificationDaemon.js b/js/ui/notificationDaemon.js index 915808fcb..361c585d7 100644 --- a/js/ui/notificationDaemon.js +++ b/js/ui/notificationDaemon.js @@ -196,6 +196,7 @@ NotificationDaemon.prototype = { hints['category'] == 'x-empathy.im.room-invitation' || hints['category'] == 'x-empathy.call.incoming' || hints['category'] == 'x-empathy.call.incoming"' || + hints['category'] == 'x-empathy.im.subscription-request' || hints['category'] == 'presence.online' || hints['category'] == 'presence.offline')) { // Ignore replacesId since we already sent back a diff --git a/js/ui/telepathyClient.js b/js/ui/telepathyClient.js index b90d070a0..04a4d8165 100644 --- a/js/ui/telepathyClient.js +++ b/js/ui/telepathyClient.js @@ -87,8 +87,8 @@ Client.prototype = { // channel matching its filters is detected. // The second argument, recover, means _observeChannels will be run // for any existing channel as well. - let dbus = Tp.DBusDaemon.dup(); - this._tpClient = new Shell.TpClient({ 'dbus_daemon': dbus, + this._accountManager = Tp.AccountManager.dup(); + this._tpClient = new Shell.TpClient({ 'account-manager': this._accountManager, 'name': 'GnomeShell', 'uniquify-name': true }) this._tpClient.set_observe_channels_func( @@ -98,6 +98,11 @@ Client.prototype = { this._tpClient.set_handle_channels_func( Lang.bind(this, this._handleChannels)); + // Workaround for gjs not supporting GPtrArray in signals. + // See BGO bug #653941 for context. + this._tpClient.set_contact_list_changed_func( + Lang.bind(this, this._contactListChanged)); + // Allow other clients (such as Empathy) to pre-empt our channels if // needed this._tpClient.set_delegated_channels_callback( @@ -108,6 +113,21 @@ Client.prototype = { } catch (e) { throw new Error('Couldn\'t register Telepathy client. Error: \n' + e); } + + + // Watch subscription requests and connection errors + this._subscriptionSource = null; + let factory = this._accountManager.get_factory(); + factory.add_account_features([Tp.Account.get_feature_quark_connection()]); + factory.add_connection_features([Tp.Connection.get_feature_quark_contact_list()]); + factory.add_contact_features([Tp.ContactFeature.SUBSCRIPTION_STATES, + Tp.ContactFeature.ALIAS, + Tp.ContactFeature.AVATAR_DATA]); + + this._accountManager.connect('account-validity-changed', + Lang.bind(this, this._accountValidityChanged)); + + this._accountManager.prepare_async(null, Lang.bind(this, this._accountManagerPrepared)); }, _observeChannels: function(observer, account, conn, channels, @@ -337,6 +357,79 @@ Client.prototype = { _delegatedChannelsCb: function(client, channels) { // Nothing to do as we don't make a distinction between observed and // handled channels. + }, + + _accountManagerPrepared: function(am, result) { + am.prepare_finish(result); + + let accounts = am.get_valid_accounts(); + for (let i = 0; i < accounts.length; i++) { + this._accountValidityChanged(am, accounts[i], true); + } + }, + + _accountValidityChanged: function(am, account, valid) { + if (!valid) + return; + + account.connect('notify::connection', + Lang.bind(this, this._connectionChanged)); + this._connectionChanged(account); + }, + + _connectionChanged: function(account) { + let conn = account.get_connection(); + if (conn == null) + return; + + this._tpClient.grab_contact_list_changed(conn); + if (conn.get_contact_list_state() == Tp.ContactListState.SUCCESS) { + this._contactListChanged(conn, conn.dup_contact_list(), []); + } + }, + + _contactListChanged: function(conn, added, removed) { + for (let i = 0; i < added.length; i++) { + let contact = added[i]; + + contact.connect('subscription-states-changed', + Lang.bind(this, this._subscriptionStateChanged)); + this._subscriptionStateChanged(contact); + } + }, + + _subscriptionStateChanged: function(contact) { + if (contact.get_publish_state() != Tp.SubscriptionState.ASK) + return; + + /* Implicitly accept publish requests if contact is already subscribed */ + if (contact.get_subscribe_state() == Tp.SubscriptionState.YES || + contact.get_subscribe_state() == Tp.SubscriptionState.ASK) { + + contact.authorize_publication_async(function(src, result) { + src.authorize_publication_finish(result)}); + + return; + } + + /* Display notification to ask user to accept/reject request */ + let source = this._ensureSubscriptionSource(); + Main.messageTray.add(source); + + let notif = new SubscriptionRequestNotification(source, contact); + source.notify(notif); + }, + + _ensureSubscriptionSource: function() { + if (this._subscriptionSource == null) { + this._subscriptionSource = new MultiNotificationSource( + _("Subscription request"), 'gtk-dialog-question'); + this._subscriptionSource.connect('destroy', Lang.bind(this, function () { + this._subscriptionSource = null; + })); + } + + return this._subscriptionSource; } }; @@ -1118,3 +1211,137 @@ FileTransferNotification.prototype = { })); } }; + +// A notification source that can embed multiple notifications +function MultiNotificationSource(title, icon) { + this._init(title, icon); +} + +MultiNotificationSource.prototype = { + __proto__: MessageTray.Source.prototype, + + _init: function(title, icon) { + MessageTray.Source.prototype._init.call(this, title); + + this._icon = icon; + this._setSummaryIcon(this.createNotificationIcon()); + this._nbNotifications = 0; + }, + + notify: function(notification) { + MessageTray.Source.prototype.notify.call(this, notification); + + this._nbNotifications += 1; + + // Display the source while there is at least one notification + notification.connect('destroy', Lang.bind(this, function () { + this._nbNotifications -= 1; + + if (this._nbNotifications == 0) + this.destroy(); + })); + }, + + createNotificationIcon: function() { + return new St.Icon({ gicon: Shell.util_icon_from_string(this._icon), + icon_type: St.IconType.FULLCOLOR, + icon_size: this.ICON_SIZE }); + } +}; + +// Subscription request +function SubscriptionRequestNotification(source, contact) { + this._init(source, contact); +} + +SubscriptionRequestNotification.prototype = { + __proto__: MessageTray.Notification.prototype, + + _init: function(source, contact) { + MessageTray.Notification.prototype._init.call(this, source, + /* To translators: The parameter is the contact's alias */ + _("%s would like permission to see when you are online").format(contact.get_alias()), + null, { customContent: true }); + + this._contact = contact; + this._connection = contact.get_connection(); + + let layout = new St.BoxLayout({ vertical: false }); + + // Display avatar + let iconBox = new St.Bin({ style_class: 'avatar-box' }); + iconBox._size = 48; + + let textureCache = St.TextureCache.get_default(); + let file = contact.get_avatar_file(); + + if (file) { + let uri = file.get_uri(); + iconBox.child = textureCache.load_uri_async(uri, iconBox._size, iconBox._size); + } + else { + iconBox.child = new St.Icon({ icon_name: 'avatar-default', + icon_type: St.IconType.FULLCOLOR, + icon_size: iconBox._size }); + } + + layout.add(iconBox); + + // subscription request message + let label = new St.Label({ style_class: 'subscription-message', + text: contact.get_publish_request() }); + + layout.add(label); + + this.addActor(layout); + + this.addButton('decline', _("Decline")); + this.addButton('accept', _("Accept")); + + this.connect('action-invoked', Lang.bind(this, function(self, action) { + switch (action) { + case 'decline': + contact.remove_async(function(src, result) { + src.remove_finish(result)}); + break; + case 'accept': + // Authorize the contact and request to see his status as well + contact.authorize_publication_async(function(src, result) { + src.authorize_publication_finish(result)}); + + contact.request_subscription_async('', function(src, result) { + src.request_subscription_finish(result)}); + break; + } + + // rely on _subscriptionStatesChangedCb to destroy the + // notification + })); + + this._changedId = contact.connect('subscription-states-changed', + Lang.bind(this, this._subscriptionStatesChangedCb)); + this._invalidatedId = this._connection.connect('invalidated', + Lang.bind(this, this.destroy)); + }, + + destroy: function() { + if (this._changedId != 0) { + this._contact.disconnect(this._changedId); + this._changedId = 0; + } + + if (this._invalidatedId != 0) { + this._connection.disconnect(this._invalidatedId); + this._invalidatedId = 0; + } + + MessageTray.Notification.prototype.destroy.call(this); + }, + + _subscriptionStatesChangedCb: function(contact, subscribe, publish, msg) { + // Destroy the notification if the subscription request has been + // answered + if (publish != Tp.SubscriptionState.ASK) + this.destroy(); + } +}; diff --git a/src/shell-tp-client.c b/src/shell-tp-client.c index 48233e2ac..ee647f0da 100644 --- a/src/shell-tp-client.c +++ b/src/shell-tp-client.c @@ -19,6 +19,10 @@ struct _ShellTpClientPrivate ShellTpClientHandleChannelsImpl handle_channels_impl; gpointer user_data_handle_channels; GDestroyNotify destroy_handle_channels; + + ShellTpClientContactListChangedImpl contact_list_changed_impl; + gpointer user_data_contact_list_changed; + GDestroyNotify destroy_contact_list_changed; }; /** @@ -77,6 +81,16 @@ struct _ShellTpClientPrivate * Signature of the implementation of the HandleChannels method. */ +/** + * ShellTpClientContactListChangedImpl: + * @connection: a #TpConnection having %TP_CONNECTION_FEATURE_CORE prepared + * if possible + * @added: (element-type TelepathyGLib.Contact): a #GPtrArray of added #TpContact + * @removed: (element-type TelepathyGLib.Contact): a #GPtrArray of removed #TpContact + * + * Signature of the implementation of the ContactListChanged method. + */ + static void shell_tp_client_init (ShellTpClient *self) { @@ -220,6 +234,13 @@ shell_tp_client_dispose (GObject *object) self->priv->user_data_handle_channels = NULL; } + if (self->priv->destroy_contact_list_changed != NULL) + { + self->priv->destroy_contact_list_changed (self->priv->user_data_contact_list_changed); + self->priv->destroy_contact_list_changed = NULL; + self->priv->user_data_contact_list_changed = NULL; + } + if (dispose != NULL) dispose (object); } @@ -278,6 +299,43 @@ shell_tp_client_set_handle_channels_func (ShellTpClient *self, self->priv->destroy_handle_channels = destroy; } +void +shell_tp_client_set_contact_list_changed_func (ShellTpClient *self, + ShellTpClientContactListChangedImpl contact_list_changed_impl, + gpointer user_data, + GDestroyNotify destroy) +{ + g_assert (self->priv->contact_list_changed_impl == NULL); + + self->priv->contact_list_changed_impl = contact_list_changed_impl; + self->priv->user_data_handle_channels = user_data; + self->priv->destroy_handle_channels = destroy; +} + +static void +on_contact_list_changed (TpConnection *conn, + GPtrArray *added, + GPtrArray *removed, + gpointer user_data) +{ + ShellTpClient *self = (ShellTpClient *) user_data; + + g_assert (self->priv->contact_list_changed_impl != NULL); + + self->priv->contact_list_changed_impl (conn, + added, removed, + self->priv->user_data_contact_list_changed); +} + +void +shell_tp_client_grab_contact_list_changed (ShellTpClient *self, + TpConnection *conn) +{ + g_signal_connect (conn, "contact-list-changed", + G_CALLBACK (on_contact_list_changed), + self); +} + /* Telepathy utility functions */ /** diff --git a/src/shell-tp-client.h b/src/shell-tp-client.h index b32304587..6e013886c 100644 --- a/src/shell-tp-client.h +++ b/src/shell-tp-client.h @@ -85,6 +85,20 @@ void shell_tp_client_set_handle_channels_func (ShellTpClient *self, gpointer user_data, GDestroyNotify destroy); +typedef void (*ShellTpClientContactListChangedImpl) ( + TpConnection *connection, + GPtrArray *added, + GPtrArray *removed, + gpointer user_data); + +void shell_tp_client_set_contact_list_changed_func (ShellTpClient *self, + ShellTpClientContactListChangedImpl contact_list_changed_impl, + gpointer user_data, + GDestroyNotify destroy); + +void shell_tp_client_grab_contact_list_changed (ShellTpClient *self, + TpConnection *conn); + /* Telepathy utility functions */ typedef void (*ShellGetTpContactCb) (TpConnection *connection, GList *contacts,