99b768ec54
Instead of keeping track of the old adjustment.upper keep track of the old adjustment.value that corresponded to the bottom scroll position. This fixes the integrated chatview not always scrolling to the bottom by removing the assumption that page_size is constant between updates, which is not the case as the view is presented in various different ways. https://bugzilla.gnome.org/show_bug.cgi?id=686571
1438 lines
54 KiB
JavaScript
1438 lines
54 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
const Gio = imports.gi.Gio;
|
|
const GLib = imports.gi.GLib;
|
|
const Lang = imports.lang;
|
|
const Mainloop = imports.mainloop;
|
|
const Shell = imports.gi.Shell;
|
|
const Signals = imports.signals;
|
|
const St = imports.gi.St;
|
|
const Tpl = imports.gi.TelepathyLogger;
|
|
const Tp = imports.gi.TelepathyGLib;
|
|
|
|
const History = imports.misc.history;
|
|
const Main = imports.ui.main;
|
|
const MessageTray = imports.ui.messageTray;
|
|
const Params = imports.misc.params;
|
|
const PopupMenu = imports.ui.popupMenu;
|
|
|
|
// See Notification.appendMessage
|
|
const SCROLLBACK_IMMEDIATE_TIME = 60; // 1 minute
|
|
const SCROLLBACK_RECENT_TIME = 15 * 60; // 15 minutes
|
|
const SCROLLBACK_RECENT_LENGTH = 20;
|
|
const SCROLLBACK_IDLE_LENGTH = 5;
|
|
|
|
// See Source._displayPendingMessages
|
|
const SCROLLBACK_HISTORY_LINES = 10;
|
|
|
|
// See Notification._onEntryChanged
|
|
const COMPOSING_STOP_TIMEOUT = 5;
|
|
|
|
const NotificationDirection = {
|
|
SENT: 'chat-sent',
|
|
RECEIVED: 'chat-received'
|
|
};
|
|
|
|
function makeMessageFromTpMessage(tpMessage, direction) {
|
|
let [text, flags] = tpMessage.to_text();
|
|
|
|
let timestamp = tpMessage.get_sent_timestamp();
|
|
if (timestamp == 0)
|
|
timestamp = tpMessage.get_received_timestamp();
|
|
|
|
return {
|
|
messageType: tpMessage.get_message_type(),
|
|
text: text,
|
|
sender: tpMessage.sender.alias,
|
|
timestamp: timestamp,
|
|
direction: direction
|
|
};
|
|
}
|
|
|
|
|
|
function makeMessageFromTplEvent(event) {
|
|
let sent = event.get_sender().get_entity_type() == Tpl.EntityType.SELF;
|
|
let direction = sent ? NotificationDirection.SENT : NotificationDirection.RECEIVED;
|
|
|
|
return {
|
|
messageType: event.get_message_type(),
|
|
text: event.get_message(),
|
|
sender: event.get_sender().get_alias(),
|
|
timestamp: event.get_timestamp(),
|
|
direction: direction
|
|
};
|
|
}
|
|
|
|
const TelepathyClient = new Lang.Class({
|
|
Name: 'TelepathyClient',
|
|
|
|
_init: function() {
|
|
// channel path -> ChatSource
|
|
this._chatSources = {};
|
|
this._chatState = Tp.ChannelChatState.ACTIVE;
|
|
|
|
// account path -> AccountNotification
|
|
this._accountNotifications = {};
|
|
|
|
// Define features we want
|
|
this._accountManager = Tp.AccountManager.dup();
|
|
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_channel_features([Tp.Channel.get_feature_quark_contacts()]);
|
|
factory.add_contact_features([Tp.ContactFeature.ALIAS,
|
|
Tp.ContactFeature.AVATAR_DATA,
|
|
Tp.ContactFeature.PRESENCE,
|
|
Tp.ContactFeature.SUBSCRIPTION_STATES]);
|
|
|
|
// Set up a SimpleObserver, which will call _observeChannels whenever a
|
|
// channel matching its filters is detected.
|
|
// The second argument, recover, means _observeChannels will be run
|
|
// for any existing channel as well.
|
|
this._tpClient = new Shell.TpClient({ name: 'GnomeShell',
|
|
account_manager: this._accountManager,
|
|
uniquify_name: true });
|
|
this._tpClient.set_observe_channels_func(
|
|
Lang.bind(this, this._observeChannels));
|
|
this._tpClient.set_approve_channels_func(
|
|
Lang.bind(this, this._approveChannels));
|
|
this._tpClient.set_handle_channels_func(
|
|
Lang.bind(this, this._handleChannels));
|
|
|
|
// Watch subscription requests and connection errors
|
|
this._subscriptionSource = null;
|
|
this._accountSource = null;
|
|
|
|
// 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(
|
|
Lang.bind(this, this._delegatedChannelsCb));
|
|
},
|
|
|
|
enable: function() {
|
|
try {
|
|
this._tpClient.register();
|
|
} catch (e) {
|
|
throw new Error('Couldn\'t register Telepathy client. Error: \n' + e);
|
|
}
|
|
|
|
this._accountManagerValidityChangedId = this._accountManager.connect('account-validity-changed',
|
|
Lang.bind(this, this._accountValidityChanged));
|
|
|
|
if (!this._accountManager.is_prepared(Tp.AccountManager.get_feature_quark_core()))
|
|
this._accountManager.prepare_async(null, Lang.bind(this, this._accountManagerPrepared));
|
|
},
|
|
|
|
disable: function() {
|
|
this._tpClient.unregister();
|
|
this._accountManager.disconnect(this._accountManagerValidityChangedId);
|
|
this._accountManagerValidityChangedId = 0;
|
|
},
|
|
|
|
_observeChannels: function(observer, account, conn, channels,
|
|
dispatchOp, requests, context) {
|
|
let len = channels.length;
|
|
for (let i = 0; i < len; i++) {
|
|
let channel = channels[i];
|
|
let [targetHandle, targetHandleType] = channel.get_handle();
|
|
|
|
if (channel.get_invalidated())
|
|
continue;
|
|
|
|
/* Only observe contact text channels */
|
|
if ((!(channel instanceof Tp.TextChannel)) ||
|
|
targetHandleType != Tp.HandleType.CONTACT)
|
|
continue;
|
|
|
|
this._createChatSource(account, conn, channel, channel.get_target_contact());
|
|
}
|
|
|
|
context.accept();
|
|
},
|
|
|
|
_createChatSource: function(account, conn, channel, contact) {
|
|
if (this._chatSources[channel.get_object_path()])
|
|
return;
|
|
|
|
let source = new ChatSource(account, conn, channel, contact, this._tpClient);
|
|
|
|
this._chatSources[channel.get_object_path()] = source;
|
|
source.connect('destroy', Lang.bind(this,
|
|
function() {
|
|
if (this._tpClient.is_handling_channel(channel)) {
|
|
// The chat box has been destroyed so it can't
|
|
// handle the channel any more.
|
|
channel.close_async(function(src, result) {
|
|
channel.close_finish(result);
|
|
});
|
|
}
|
|
|
|
delete this._chatSources[channel.get_object_path()];
|
|
}));
|
|
},
|
|
|
|
_handleChannels: function(handler, account, conn, channels,
|
|
requests, user_action_time, context) {
|
|
this._handlingChannels(account, conn, channels, true);
|
|
context.accept();
|
|
},
|
|
|
|
_handlingChannels: function(account, conn, channels, notify) {
|
|
let len = channels.length;
|
|
for (let i = 0; i < len; i++) {
|
|
let channel = channels[i];
|
|
|
|
// We can only handle text channel, so close any other channel
|
|
if (!(channel instanceof Tp.TextChannel)) {
|
|
channel.close_async(null);
|
|
continue;
|
|
}
|
|
|
|
if (channel.get_invalidated())
|
|
continue;
|
|
|
|
// 'notify' will be true when coming from an actual HandleChannels
|
|
// call, and not when from a successful Claim call. The point is
|
|
// we don't want to notify for a channel we just claimed which
|
|
// has no new messages (for example, a new channel which only has
|
|
// a delivery notification). We rely on _displayPendingMessages()
|
|
// and _messageReceived() to notify for new messages.
|
|
|
|
// But we should still notify from HandleChannels because the
|
|
// Telepathy spec states that handlers must foreground channels
|
|
// in HandleChannels calls which are already being handled.
|
|
|
|
if (notify && this._tpClient.is_handling_channel(channel)) {
|
|
// We are already handling the channel, display the source
|
|
let source = this._chatSources[channel.get_object_path()];
|
|
if (source)
|
|
source.notify();
|
|
}
|
|
}
|
|
},
|
|
|
|
_displayRoomInvitation: function(conn, channel, dispatchOp, context) {
|
|
// We can only approve the rooms if we have been invited to it
|
|
let selfContact = channel.group_get_self_contact();
|
|
if (selfContact == null) {
|
|
context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
|
|
message: 'Not invited to the room' }));
|
|
return;
|
|
}
|
|
|
|
let [invited, inviter, reason, msg] = channel.group_get_local_pending_contact_info(selfContact);
|
|
if (!invited) {
|
|
context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
|
|
message: 'Not invited to the room' }));
|
|
return;
|
|
}
|
|
|
|
// FIXME: We don't have a 'chat room' icon (bgo #653737) use
|
|
// system-users for now as Empathy does.
|
|
let source = new ApproverSource(dispatchOp, _("Invitation"),
|
|
Gio.icon_new_for_string('system-users'));
|
|
Main.messageTray.add(source);
|
|
|
|
let notif = new RoomInviteNotification(source, dispatchOp, channel, inviter);
|
|
source.notify(notif);
|
|
context.accept();
|
|
},
|
|
|
|
_approveChannels: function(approver, account, conn, channels,
|
|
dispatchOp, context) {
|
|
let channel = channels[0];
|
|
let chanType = channel.get_channel_type();
|
|
|
|
if (channel.get_invalidated()) {
|
|
context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
|
|
message: 'Channel is invalidated' }));
|
|
return;
|
|
}
|
|
|
|
if (chanType == Tp.IFACE_CHANNEL_TYPE_TEXT)
|
|
this._approveTextChannel(account, conn, channel, dispatchOp, context);
|
|
else if (chanType == Tp.IFACE_CHANNEL_TYPE_CALL)
|
|
this._approveCall(account, conn, channel, dispatchOp, context);
|
|
else if (chanType == Tp.IFACE_CHANNEL_TYPE_FILE_TRANSFER)
|
|
this._approveFileTransfer(account, conn, channel, dispatchOp, context);
|
|
else
|
|
context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
|
|
message: 'Unsupported channel type' }));
|
|
},
|
|
|
|
_approveTextChannel: function(account, conn, channel, dispatchOp, context) {
|
|
let [targetHandle, targetHandleType] = channel.get_handle();
|
|
|
|
if (targetHandleType == Tp.HandleType.CONTACT) {
|
|
// Approve private text channels right away as we are going to handle it
|
|
dispatchOp.claim_with_async(this._tpClient,
|
|
Lang.bind(this, function(dispatchOp, result) {
|
|
try {
|
|
dispatchOp.claim_with_finish(result);
|
|
this._handlingChannels(account, conn, [channel], false);
|
|
} catch (err) {
|
|
throw new Error('Failed to Claim channel: ' + err);
|
|
}}));
|
|
|
|
context.accept();
|
|
} else {
|
|
this._displayRoomInvitation(conn, channel, dispatchOp, context);
|
|
}
|
|
},
|
|
|
|
_approveCall: function(account, conn, channel, dispatchOp, context) {
|
|
let isVideo = false;
|
|
|
|
let props = channel.borrow_immutable_properties();
|
|
|
|
if (props[Tp.PROP_CHANNEL_TYPE_CALL_INITIAL_VIDEO])
|
|
isVideo = true;
|
|
|
|
// We got the TpContact
|
|
let source = new ApproverSource(dispatchOp, _("Call"), isVideo ?
|
|
Gio.icon_new_for_string('camera-web') :
|
|
Gio.icon_new_for_string('audio-input-microphone'));
|
|
Main.messageTray.add(source);
|
|
|
|
let notif = new AudioVideoNotification(source, dispatchOp, channel,
|
|
channel.get_target_contact(), isVideo);
|
|
source.notify(notif);
|
|
context.accept();
|
|
},
|
|
|
|
_approveFileTransfer: function(account, conn, channel, dispatchOp, context) {
|
|
// Use the icon of the file being transferred
|
|
let gicon = Gio.content_type_get_icon(channel.get_mime_type());
|
|
|
|
// We got the TpContact
|
|
let source = new ApproverSource(dispatchOp, _("File Transfer"), gicon);
|
|
Main.messageTray.add(source);
|
|
|
|
let notif = new FileTransferNotification(source, dispatchOp, channel,
|
|
channel.get_target_contact());
|
|
source.notify(notif);
|
|
context.accept();
|
|
},
|
|
|
|
_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;
|
|
|
|
// It would be better to connect to "status-changed" but we cannot.
|
|
// See discussion in https://bugzilla.gnome.org/show_bug.cgi?id=654159
|
|
account.connect("notify::connection-status",
|
|
Lang.bind(this, this._accountConnectionStatusNotifyCb));
|
|
|
|
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();
|
|
|
|
let notif = new SubscriptionRequestNotification(source, contact);
|
|
source.notify(notif);
|
|
},
|
|
|
|
_ensureSubscriptionSource: function() {
|
|
if (this._subscriptionSource == null) {
|
|
this._subscriptionSource = new MessageTray.Source(_("Subscription request"),
|
|
'gtk-dialog-question');
|
|
Main.messageTray.add(this._subscriptionSource);
|
|
this._subscriptionSource.connect('destroy', Lang.bind(this, function () {
|
|
this._subscriptionSource = null;
|
|
}));
|
|
}
|
|
|
|
return this._subscriptionSource;
|
|
},
|
|
|
|
_accountConnectionStatusNotifyCb: function(account) {
|
|
let connectionError = account.connection_error;
|
|
|
|
if (account.connection_status != Tp.ConnectionStatus.DISCONNECTED ||
|
|
connectionError == Tp.error_get_dbus_name(Tp.Error.CANCELLED)) {
|
|
return;
|
|
}
|
|
|
|
let notif = this._accountNotifications[account.get_object_path()];
|
|
if (notif)
|
|
return;
|
|
|
|
/* Display notification that account failed to connect */
|
|
let source = this._ensureAccountSource();
|
|
|
|
notif = new AccountNotification(source, account, connectionError);
|
|
this._accountNotifications[account.get_object_path()] = notif;
|
|
notif.connect('destroy', Lang.bind(this, function() {
|
|
delete this._accountNotifications[account.get_object_path()];
|
|
}));
|
|
source.notify(notif);
|
|
},
|
|
|
|
_ensureAccountSource: function() {
|
|
if (this._accountSource == null) {
|
|
this._accountSource = new MessageTray.Source(_("Connection error"),
|
|
'gtk-dialog-error');
|
|
Main.messageTray.add(this._accountSource);
|
|
this._accountSource.connect('destroy', Lang.bind(this, function () {
|
|
this._accountSource = null;
|
|
}));
|
|
}
|
|
|
|
return this._accountSource;
|
|
}
|
|
});
|
|
|
|
const ChatSource = new Lang.Class({
|
|
Name: 'ChatSource',
|
|
Extends: MessageTray.Source,
|
|
|
|
_init: function(account, conn, channel, contact, client) {
|
|
this._account = account;
|
|
this._contact = contact;
|
|
this._client = client;
|
|
|
|
this.parent(contact.get_alias());
|
|
|
|
this.isChat = true;
|
|
this._pendingMessages = [];
|
|
|
|
this._conn = conn;
|
|
this._channel = channel;
|
|
this._closedId = this._channel.connect('invalidated', Lang.bind(this, this._channelClosed));
|
|
|
|
this._notification = new ChatNotification(this);
|
|
this._notification.setUrgency(MessageTray.Urgency.HIGH);
|
|
this._notifyTimeoutId = 0;
|
|
|
|
// We ack messages when the user expands the new notification or views the summary
|
|
// notification, in which case the notification is also expanded.
|
|
this._notification.connect('expanded', Lang.bind(this, this._ackMessages));
|
|
|
|
this._presence = contact.get_presence_type();
|
|
|
|
this._sentId = this._channel.connect('message-sent', Lang.bind(this, this._messageSent));
|
|
this._receivedId = this._channel.connect('message-received', Lang.bind(this, this._messageReceived));
|
|
this._pendingId = this._channel.connect('pending-message-removed', Lang.bind(this, this._pendingRemoved));
|
|
|
|
this._notifyAliasId = this._contact.connect('notify::alias', Lang.bind(this, this._updateAlias));
|
|
this._notifyAvatarId = this._contact.connect('notify::avatar-file', Lang.bind(this, this._updateAvatarIcon));
|
|
this._presenceChangedId = this._contact.connect('presence-changed', Lang.bind(this, this._presenceChanged));
|
|
|
|
// Add ourselves as a source.
|
|
Main.messageTray.add(this);
|
|
this.pushNotification(this._notification);
|
|
|
|
this._getLogMessages();
|
|
},
|
|
|
|
buildRightClickMenu: function() {
|
|
let item;
|
|
|
|
let rightClickMenu = this.parent();
|
|
item = new PopupMenu.PopupMenuItem('');
|
|
item.actor.connect('notify::mapped', Lang.bind(this, function() {
|
|
item.label.set_text(this.isMuted ? _("Unmute") : _("Mute"));
|
|
}));
|
|
item.connect('activate', Lang.bind(this, function() {
|
|
this.setMuted(!this.isMuted);
|
|
this.emit('done-displaying-content');
|
|
}));
|
|
rightClickMenu.add(item.actor);
|
|
return rightClickMenu;
|
|
},
|
|
|
|
_updateAlias: function() {
|
|
let oldAlias = this.title;
|
|
let newAlias = this._contact.get_alias();
|
|
|
|
if (oldAlias == newAlias)
|
|
return;
|
|
|
|
this.setTitle(newAlias);
|
|
this._notification.appendAliasChange(oldAlias, newAlias);
|
|
},
|
|
|
|
createIcon: function(size) {
|
|
this._iconBox = new St.Bin({ style_class: 'avatar-box' });
|
|
this._iconBox._size = size;
|
|
let textureCache = St.TextureCache.get_default();
|
|
let file = this._contact.get_avatar_file();
|
|
|
|
if (file) {
|
|
let uri = file.get_uri();
|
|
this._iconBox.child = textureCache.load_uri_async(uri, this._iconBox._size, this._iconBox._size);
|
|
} else {
|
|
this._iconBox.child = new St.Icon({ icon_name: 'avatar-default',
|
|
icon_size: this._iconBox._size });
|
|
}
|
|
|
|
return this._iconBox;
|
|
},
|
|
|
|
createSecondaryIcon: function() {
|
|
let iconBox = new St.Bin();
|
|
iconBox.child = new St.Icon({ style_class: 'secondary-icon' });
|
|
let presenceType = this._contact.get_presence_type();
|
|
|
|
switch (presenceType) {
|
|
case Tp.ConnectionPresenceType.AVAILABLE:
|
|
iconBox.child.icon_name = 'user-available';
|
|
break;
|
|
case Tp.ConnectionPresenceType.BUSY:
|
|
iconBox.child.icon_name = 'user-busy';
|
|
break;
|
|
case Tp.ConnectionPresenceType.OFFLINE:
|
|
iconBox.child.icon_name = 'user-offline';
|
|
break;
|
|
case Tp.ConnectionPresenceType.HIDDEN:
|
|
iconBox.child.icon_name = 'user-invisible';
|
|
break;
|
|
case Tp.ConnectionPresenceType.AWAY:
|
|
iconBox.child.icon_name = 'user-away';
|
|
break;
|
|
case Tp.ConnectionPresenceType.EXTENDED_AWAY:
|
|
iconBox.child.icon_name = 'user-idle';
|
|
break;
|
|
default:
|
|
iconBox.child.icon_name = 'user-offline';
|
|
}
|
|
return iconBox;
|
|
},
|
|
|
|
_updateAvatarIcon: function() {
|
|
this.iconUpdated();
|
|
this._notification.update(this._notification.title, null, { customContent: true });
|
|
},
|
|
|
|
open: function(notification) {
|
|
if (this._client.is_handling_channel(this._channel)) {
|
|
// We are handling the channel, try to pass it to Empathy
|
|
this._client.delegate_channels_async([this._channel],
|
|
global.get_current_time(),
|
|
'org.freedesktop.Telepathy.Client.Empathy.Chat', null);
|
|
}
|
|
else {
|
|
// We are not the handler, just ask to present the channel
|
|
let dbus = Tp.DBusDaemon.dup();
|
|
let cd = Tp.ChannelDispatcher.new(dbus);
|
|
|
|
cd.present_channel_async(this._channel, global.get_current_time(), null);
|
|
}
|
|
},
|
|
|
|
_getLogMessages: function() {
|
|
let logManager = Tpl.LogManager.dup_singleton();
|
|
let entity = Tpl.Entity.new_from_tp_contact(this._contact, Tpl.EntityType.CONTACT);
|
|
|
|
logManager.get_filtered_events_async(this._account, entity,
|
|
Tpl.EventTypeMask.TEXT, SCROLLBACK_HISTORY_LINES,
|
|
null, Lang.bind(this, this._displayPendingMessages));
|
|
},
|
|
|
|
_displayPendingMessages: function(logManager, result) {
|
|
let [success, events] = logManager.get_filtered_events_finish(result);
|
|
|
|
let logMessages = events.map(makeMessageFromTplEvent);
|
|
|
|
let pendingTpMessages = this._channel.get_pending_messages();
|
|
let pendingMessages = [];
|
|
|
|
for (let i = 0; i < pendingTpMessages.length; i++) {
|
|
let message = pendingTpMessages[i];
|
|
|
|
if (message.get_message_type() == Tp.ChannelTextMessageType.DELIVERY_REPORT)
|
|
continue;
|
|
|
|
pendingMessages.push(makeMessageFromTpMessage(message, NotificationDirection.RECEIVED));
|
|
|
|
this._pendingMessages.push(message);
|
|
}
|
|
|
|
this.countUpdated();
|
|
|
|
let showTimestamp = false;
|
|
|
|
for (let i = 0; i < logMessages.length; i++) {
|
|
let logMessage = logMessages[i];
|
|
let isPending = false;
|
|
|
|
// Skip any log messages that are also in pendingMessages
|
|
for (let j = 0; j < pendingMessages.length; j++) {
|
|
let pending = pendingMessages[j];
|
|
if (logMessage.timestamp == pending.timestamp && logMessage.text == pending.text) {
|
|
isPending = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!isPending) {
|
|
showTimestamp = true;
|
|
this._notification.appendMessage(logMessage, true, ['chat-log-message']);
|
|
}
|
|
}
|
|
|
|
if (showTimestamp)
|
|
this._notification.appendTimestamp();
|
|
|
|
for (let i = 0; i < pendingMessages.length; i++)
|
|
this._notification.appendMessage(pendingMessages[i], true);
|
|
|
|
if (pendingMessages.length > 0)
|
|
this.notify();
|
|
},
|
|
|
|
_channelClosed: function() {
|
|
this._channel.disconnect(this._closedId);
|
|
this._channel.disconnect(this._receivedId);
|
|
this._channel.disconnect(this._pendingId);
|
|
this._channel.disconnect(this._sentId);
|
|
|
|
this._contact.disconnect(this._notifyAliasId);
|
|
this._contact.disconnect(this._notifyAvatarId);
|
|
this._contact.disconnect(this._presenceChangedId);
|
|
|
|
this.destroy();
|
|
},
|
|
|
|
/* All messages are new messages for Telepathy sources */
|
|
get count() {
|
|
return this._pendingMessages.length;
|
|
},
|
|
|
|
get unseenCount() {
|
|
return this.count;
|
|
},
|
|
|
|
get countVisible() {
|
|
return this.count > 0;
|
|
},
|
|
|
|
_messageReceived: function(channel, message) {
|
|
if (message.get_message_type() == Tp.ChannelTextMessageType.DELIVERY_REPORT)
|
|
return;
|
|
|
|
this._pendingMessages.push(message);
|
|
this.countUpdated();
|
|
|
|
message = makeMessageFromTpMessage(message, NotificationDirection.RECEIVED);
|
|
this._notification.appendMessage(message);
|
|
|
|
// Wait a bit before notifying for the received message, a handler
|
|
// could ack it in the meantime.
|
|
if (this._notifyTimeoutId != 0)
|
|
Mainloop.source_remove(this._notifyTimeoutId);
|
|
this._notifyTimeoutId = Mainloop.timeout_add(500,
|
|
Lang.bind(this, this._notifyTimeout));
|
|
},
|
|
|
|
_notifyTimeout: function() {
|
|
if (this._pendingMessages.length != 0)
|
|
this.notify();
|
|
|
|
this._notifyTimeoutId = 0;
|
|
|
|
return false;
|
|
},
|
|
|
|
// This is called for both messages we send from
|
|
// our client and other clients as well.
|
|
_messageSent: function(channel, message, flags, token) {
|
|
message = makeMessageFromTpMessage(message, NotificationDirection.SENT);
|
|
this._notification.appendMessage(message);
|
|
},
|
|
|
|
notify: function() {
|
|
this.parent(this._notification);
|
|
},
|
|
|
|
respond: function(text) {
|
|
let type;
|
|
if (text.slice(0, 4) == '/me ') {
|
|
type = Tp.ChannelTextMessageType.ACTION;
|
|
text = text.slice(4);
|
|
} else {
|
|
type = Tp.ChannelTextMessageType.NORMAL;
|
|
}
|
|
|
|
let msg = Tp.ClientMessage.new_text(type, text);
|
|
this._channel.send_message_async(msg, 0, Lang.bind(this, function (src, result) {
|
|
this._channel.send_message_finish(result);
|
|
}));
|
|
},
|
|
|
|
setChatState: function(state) {
|
|
// We don't want to send COMPOSING every time a letter is typed into
|
|
// the entry. We send the state only when it changes. Telepathy/Empathy
|
|
// might change it behind our back if the user is using both
|
|
// gnome-shell's entry and the Empathy conversation window. We could
|
|
// keep track of it with the ChatStateChanged signal but it is good
|
|
// enough right now.
|
|
if (state != this._chatState) {
|
|
this._chatState = state;
|
|
this._channel.set_chat_state_async(state, null);
|
|
}
|
|
},
|
|
|
|
_presenceChanged: function (contact, presence, status, message) {
|
|
let msg, title;
|
|
|
|
title = GLib.markup_escape_text(this.title, -1);
|
|
|
|
this._notification.update(this._notification.title, null, { customContent: true, secondaryIcon: this.createSecondaryIcon() });
|
|
|
|
if (message)
|
|
msg += ' <i>(' + GLib.markup_escape_text(message, -1) + ')</i>';
|
|
},
|
|
|
|
_pendingRemoved: function(channel, message) {
|
|
let idx = this._pendingMessages.indexOf(message);
|
|
|
|
if (idx >= 0) {
|
|
this._pendingMessages.splice(idx, 1);
|
|
this.countUpdated();
|
|
}
|
|
},
|
|
|
|
_ackMessages: function() {
|
|
// Don't clear our messages here, tp-glib will send a
|
|
// 'pending-message-removed' for each one.
|
|
this._channel.ack_all_pending_messages_async(Lang.bind(this, function(src, result) {
|
|
this._channel.ack_all_pending_messages_finish(result);}));
|
|
}
|
|
});
|
|
|
|
const ChatNotification = new Lang.Class({
|
|
Name: 'ChatNotification',
|
|
Extends: MessageTray.Notification,
|
|
|
|
_init: function(source) {
|
|
this.parent(source, source.title, null, { customContent: true, secondaryIcon: source.createSecondaryIcon() });
|
|
this.setResident(true);
|
|
|
|
this._responseEntry = new St.Entry({ style_class: 'chat-response',
|
|
can_focus: true });
|
|
this._responseEntry.clutter_text.connect('activate', Lang.bind(this, this._onEntryActivated));
|
|
this._responseEntry.clutter_text.connect('text-changed', Lang.bind(this, this._onEntryChanged));
|
|
this.setActionArea(this._responseEntry);
|
|
|
|
this._responseEntry.clutter_text.connect('key-focus-in', Lang.bind(this, function() {
|
|
this.focused = true;
|
|
}));
|
|
this._responseEntry.clutter_text.connect('key-focus-out', Lang.bind(this, function() {
|
|
this.focused = false;
|
|
this.emit('unfocused');
|
|
}));
|
|
|
|
this._createScrollArea();
|
|
this._lastGroup = null;
|
|
this._lastGroupActor = null;
|
|
|
|
// Keep track of the bottom position for the current adjustment and
|
|
// force a scroll to the bottom if things change while we were at the
|
|
// bottom
|
|
this._oldMaxScrollValue = this._scrollArea.vscroll.adjustment.value;
|
|
this._scrollArea.vscroll.adjustment.connect('changed', Lang.bind(this, function(adjustment) {
|
|
if (adjustment.value == this._oldMaxScrollValue)
|
|
this.scrollTo(St.Side.BOTTOM);
|
|
this._oldMaxScrollValue = Math.max(adjustment.lower, adjustment.upper - adjustment.page_size);
|
|
}));
|
|
|
|
this._inputHistory = new History.HistoryManager({ entry: this._responseEntry.clutter_text });
|
|
|
|
this._history = [];
|
|
this._timestampTimeoutId = 0;
|
|
this._composingTimeoutId = 0;
|
|
},
|
|
|
|
/**
|
|
* appendMessage:
|
|
* @message: An object with the properties:
|
|
* text: the body of the message,
|
|
* messageType: a #Tp.ChannelTextMessageType,
|
|
* sender: the name of the sender,
|
|
* timestamp: the time the message was sent
|
|
* direction: a #NotificationDirection
|
|
*
|
|
* @noTimestamp: Whether to add a timestamp. If %true, no timestamp
|
|
* will be added, regardless of the difference since the
|
|
* last timestamp
|
|
*/
|
|
appendMessage: function(message, noTimestamp) {
|
|
let messageBody = GLib.markup_escape_text(message.text, -1);
|
|
let styles = [message.direction];
|
|
|
|
if (message.messageType == Tp.ChannelTextMessageType.ACTION) {
|
|
let senderAlias = GLib.markup_escape_text(message.sender, -1);
|
|
messageBody = '<i>%s</i> %s'.format(senderAlias, messageBody);
|
|
styles.push('chat-action');
|
|
}
|
|
|
|
if (message.direction == NotificationDirection.RECEIVED) {
|
|
this.update(this.source.title, messageBody, { customContent: true,
|
|
bannerMarkup: true });
|
|
}
|
|
|
|
let group = (message.direction == NotificationDirection.RECEIVED ?
|
|
'received' : 'sent');
|
|
|
|
this._append({ body: messageBody,
|
|
group: group,
|
|
styles: styles,
|
|
timestamp: message.timestamp,
|
|
noTimestamp: noTimestamp });
|
|
},
|
|
|
|
_filterMessages: function() {
|
|
if (this._history.length < 1)
|
|
return;
|
|
|
|
let lastMessageTime = this._history[0].time;
|
|
let currentTime = (Date.now() / 1000);
|
|
|
|
// Keep the scrollback from growing too long. If the most
|
|
// recent message (before the one we just added) is within
|
|
// SCROLLBACK_RECENT_TIME, we will keep
|
|
// SCROLLBACK_RECENT_LENGTH previous messages. Otherwise
|
|
// we'll keep SCROLLBACK_IDLE_LENGTH messages.
|
|
|
|
let maxLength = (lastMessageTime < currentTime - SCROLLBACK_RECENT_TIME) ?
|
|
SCROLLBACK_IDLE_LENGTH : SCROLLBACK_RECENT_LENGTH;
|
|
|
|
let filteredHistory = this._history.filter(function(item) { return item.realMessage });
|
|
if (filteredHistory.length > maxLength) {
|
|
let lastMessageToKeep = filteredHistory[maxLength];
|
|
let expired = this._history.splice(this._history.indexOf(lastMessageToKeep));
|
|
for (let i = 0; i < expired.length; i++)
|
|
expired[i].actor.destroy();
|
|
}
|
|
|
|
let groups = this._contentArea.get_children();
|
|
for (let i = 0; i < groups.length; i++) {
|
|
let group = groups[i];
|
|
if (group.get_n_children() == 0)
|
|
group.destroy();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* _append:
|
|
* @props: An object with the properties:
|
|
* body: The text of the message.
|
|
* group: The group of the message, one of:
|
|
* 'received', 'sent', 'meta'.
|
|
* styles: Style class names for the message to have.
|
|
* timestamp: The timestamp of the message.
|
|
* noTimestamp: suppress timestamp signal?
|
|
* childProps: props to add the actor with.
|
|
*/
|
|
_append: function(props) {
|
|
let currentTime = (Date.now() / 1000);
|
|
props = Params.parse(props, { body: null,
|
|
group: null,
|
|
styles: [],
|
|
timestamp: currentTime,
|
|
noTimestamp: false,
|
|
childProps: null });
|
|
|
|
// Reset the old message timeout
|
|
if (this._timestampTimeoutId)
|
|
Mainloop.source_remove(this._timestampTimeoutId);
|
|
|
|
let highlighter = new MessageTray.URLHighlighter(props.body,
|
|
true, // line wrap?
|
|
true); // allow markup?
|
|
|
|
let body = highlighter.actor;
|
|
|
|
let styles = props.styles;
|
|
for (let i = 0; i < styles.length; i++)
|
|
body.add_style_class_name(styles[i]);
|
|
|
|
let group = props.group;
|
|
if (group != this._lastGroup) {
|
|
let style = 'chat-group-' + group;
|
|
this._lastGroup = group;
|
|
this._lastGroupActor = new St.BoxLayout({ style_class: style,
|
|
vertical: true });
|
|
this.addActor(this._lastGroupActor);
|
|
}
|
|
|
|
this._lastGroupActor.add(body, props.childProps);
|
|
|
|
this.updated();
|
|
|
|
let timestamp = props.timestamp;
|
|
this._history.unshift({ actor: body, time: timestamp,
|
|
realMessage: group != 'meta' });
|
|
|
|
if (!props.noTimestamp) {
|
|
if (timestamp < currentTime - SCROLLBACK_IMMEDIATE_TIME)
|
|
this.appendTimestamp();
|
|
else
|
|
// Schedule a new timestamp in SCROLLBACK_IMMEDIATE_TIME
|
|
// from the timestamp of the message.
|
|
this._timestampTimeoutId = Mainloop.timeout_add_seconds(
|
|
SCROLLBACK_IMMEDIATE_TIME - (currentTime - timestamp),
|
|
Lang.bind(this, this.appendTimestamp));
|
|
}
|
|
|
|
this._filterMessages();
|
|
},
|
|
|
|
_formatTimestamp: function(date) {
|
|
let now = new Date();
|
|
|
|
var daysAgo = (now.getTime() - date.getTime()) / (24 * 60 * 60 * 1000);
|
|
|
|
let format;
|
|
|
|
// Show a week day and time if date is in the last week
|
|
if (daysAgo < 1 || (daysAgo < 7 && now.getDay() != date.getDay())) {
|
|
/* Translators: this is a time format string followed by a date.
|
|
If applicable, replace %X with a strftime format valid for your
|
|
locale, without seconds. */
|
|
// xgettext:no-c-format
|
|
format = _("Sent at <b>%X</b> on <b>%A</b>");
|
|
|
|
} else if (date.getYear() == now.getYear()) {
|
|
/* Translators: this is a time format in the style of "Wednesday, May 25",
|
|
shown when you get a chat message in the same year. */
|
|
// xgettext:no-c-format
|
|
format = _("Sent on <b>%A</b>, <b>%B %d</b>");
|
|
} else {
|
|
/* Translators: this is a time format in the style of "Wednesday, May 25, 2012",
|
|
shown when you get a chat message in a different year. */
|
|
// xgettext:no-c-format
|
|
format = _("Sent on <b>%A</b>, <b>%B %d</b>, %Y");
|
|
}
|
|
|
|
return date.toLocaleFormat(format);
|
|
},
|
|
|
|
appendTimestamp: function() {
|
|
let lastMessageTime = this._history[0].time;
|
|
let lastMessageDate = new Date(lastMessageTime * 1000);
|
|
|
|
let timeLabel = this._append({ body: this._formatTimestamp(lastMessageDate),
|
|
group: 'meta',
|
|
styles: ['chat-meta-message'],
|
|
childProps: { expand: true, x_fill: false,
|
|
x_align: St.Align.END },
|
|
noTimestamp: true,
|
|
timestamp: lastMessageTime });
|
|
|
|
this._filterMessages();
|
|
|
|
return false;
|
|
},
|
|
|
|
appendAliasChange: function(oldAlias, newAlias) {
|
|
oldAlias = GLib.markup_escape_text(oldAlias, -1);
|
|
newAlias = GLib.markup_escape_text(newAlias, -1);
|
|
|
|
/* Translators: this is the other person changing their old IM name to their new
|
|
IM name. */
|
|
let message = '<i>' + _("%s is now known as %s").format(oldAlias, newAlias) + '</i>';
|
|
|
|
let label = this._append({ body: message,
|
|
group: 'meta',
|
|
styles: ['chat-meta-message'] });
|
|
|
|
this.update(newAlias, null, { customContent: true });
|
|
|
|
this._filterMessages();
|
|
},
|
|
|
|
_onEntryActivated: function() {
|
|
let text = this._responseEntry.get_text();
|
|
if (text == '')
|
|
return;
|
|
|
|
this._inputHistory.addItem(text);
|
|
|
|
// Telepathy sends out the Sent signal for us.
|
|
// see Source._messageSent
|
|
this._responseEntry.set_text('');
|
|
this.source.respond(text);
|
|
},
|
|
|
|
_composingStopTimeout: function() {
|
|
this._composingTimeoutId = 0;
|
|
|
|
this.source.setChatState(Tp.ChannelChatState.PAUSED);
|
|
|
|
return false;
|
|
},
|
|
|
|
_onEntryChanged: function() {
|
|
let text = this._responseEntry.get_text();
|
|
|
|
// If we're typing, we want to send COMPOSING.
|
|
// If we empty the entry, we want to send ACTIVE.
|
|
// If we've stopped typing for COMPOSING_STOP_TIMEOUT
|
|
// seconds, we want to send PAUSED.
|
|
|
|
// Remove composing timeout.
|
|
if (this._composingTimeoutId > 0) {
|
|
Mainloop.source_remove(this._composingTimeoutId);
|
|
this._composingTimeoutId = 0;
|
|
}
|
|
|
|
if (text != '') {
|
|
this.source.setChatState(Tp.ChannelChatState.COMPOSING);
|
|
|
|
this._composingTimeoutId = Mainloop.timeout_add_seconds(
|
|
COMPOSING_STOP_TIMEOUT,
|
|
Lang.bind(this, this._composingStopTimeout));
|
|
} else {
|
|
this.source.setChatState(Tp.ChannelChatState.ACTIVE);
|
|
}
|
|
}
|
|
});
|
|
|
|
const ApproverSource = new Lang.Class({
|
|
Name: 'ApproverSource',
|
|
Extends: MessageTray.Source,
|
|
|
|
_init: function(dispatchOp, text, gicon) {
|
|
this._gicon = gicon;
|
|
|
|
this.parent(text);
|
|
|
|
this._dispatchOp = dispatchOp;
|
|
|
|
// Destroy the source if the channel dispatch operation is invalidated
|
|
// as we can't approve any more.
|
|
this._invalidId = dispatchOp.connect('invalidated',
|
|
Lang.bind(this, function(domain, code, msg) {
|
|
this.destroy();
|
|
}));
|
|
},
|
|
|
|
destroy: function() {
|
|
if (this._invalidId != 0) {
|
|
this._dispatchOp.disconnect(this._invalidId);
|
|
this._invalidId = 0;
|
|
}
|
|
|
|
this.parent();
|
|
},
|
|
|
|
createIcon: function(size) {
|
|
return new St.Icon({ gicon: this._gicon,
|
|
icon_size: size });
|
|
}
|
|
});
|
|
|
|
const RoomInviteNotification = new Lang.Class({
|
|
Name: 'RoomInviteNotification',
|
|
Extends: MessageTray.Notification,
|
|
|
|
_init: function(source, dispatchOp, channel, inviter) {
|
|
this.parent(source,
|
|
/* translators: argument is a room name like
|
|
* room@jabber.org for example. */
|
|
_("Invitation to %s").format(channel.get_identifier()),
|
|
null,
|
|
{ customContent: true });
|
|
this.setResident(true);
|
|
|
|
/* translators: first argument is the name of a contact and the second
|
|
* one the name of a room. "Alice is inviting you to join room@jabber.org
|
|
* for example. */
|
|
this.addBody(_("%s is inviting you to join %s").format(inviter.get_alias(), channel.get_identifier()));
|
|
|
|
this.addButton('decline', _("Decline"));
|
|
this.addButton('accept', _("Accept"));
|
|
|
|
this.connect('action-invoked', Lang.bind(this, function(self, action) {
|
|
switch (action) {
|
|
case 'decline':
|
|
dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
|
|
'', function(src, result) {
|
|
src.leave_channels_finish(result)});
|
|
break;
|
|
case 'accept':
|
|
dispatchOp.handle_with_time_async('', global.get_current_time(),
|
|
function(src, result) {
|
|
src.handle_with_time_finish(result)});
|
|
break;
|
|
}
|
|
this.destroy();
|
|
}));
|
|
}
|
|
});
|
|
|
|
// Audio Video
|
|
const AudioVideoNotification = new Lang.Class({
|
|
Name: 'AudioVideoNotification',
|
|
Extends: MessageTray.Notification,
|
|
|
|
_init: function(source, dispatchOp, channel, contact, isVideo) {
|
|
let title = '';
|
|
|
|
if (isVideo)
|
|
/* translators: argument is a contact name like Alice for example. */
|
|
title = _("Video call from %s").format(contact.get_alias());
|
|
else
|
|
/* translators: argument is a contact name like Alice for example. */
|
|
title = _("Call from %s").format(contact.get_alias());
|
|
|
|
this.parent(source, title, null, { customContent: true });
|
|
this.setResident(true);
|
|
|
|
this.addButton('reject', _("Reject"));
|
|
/* translators: this is a button label (verb), not a noun */
|
|
this.addButton('answer', _("Answer"));
|
|
|
|
this.connect('action-invoked', Lang.bind(this, function(self, action) {
|
|
switch (action) {
|
|
case 'reject':
|
|
dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
|
|
'', function(src, result) {
|
|
src.leave_channels_finish(result)});
|
|
break;
|
|
case 'answer':
|
|
dispatchOp.handle_with_time_async('', global.get_current_time(),
|
|
function(src, result) {
|
|
src.handle_with_time_finish(result)});
|
|
break;
|
|
}
|
|
this.destroy();
|
|
}));
|
|
}
|
|
});
|
|
|
|
// File Transfer
|
|
const FileTransferNotification = new Lang.Class({
|
|
Name: 'FileTransferNotification',
|
|
Extends: MessageTray.Notification,
|
|
|
|
_init: function(source, dispatchOp, channel, contact) {
|
|
this.parent(source,
|
|
/* To translators: The first parameter is
|
|
* the contact's alias and the second one is the
|
|
* file name. The string will be something
|
|
* like: "Alice is sending you test.ogg"
|
|
*/
|
|
_("%s is sending you %s").format(contact.get_alias(),
|
|
channel.get_filename()),
|
|
null,
|
|
{ customContent: true });
|
|
this.setResident(true);
|
|
|
|
this.addButton('decline', _("Decline"));
|
|
this.addButton('accept', _("Accept"));
|
|
|
|
this.connect('action-invoked', Lang.bind(this, function(self, action) {
|
|
switch (action) {
|
|
case 'decline':
|
|
dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
|
|
'', function(src, result) {
|
|
src.leave_channels_finish(result)});
|
|
break;
|
|
case 'accept':
|
|
dispatchOp.handle_with_time_async('', global.get_current_time(),
|
|
function(src, result) {
|
|
src.handle_with_time_finish(result)});
|
|
break;
|
|
}
|
|
this.destroy();
|
|
}));
|
|
}
|
|
});
|
|
|
|
// Subscription request
|
|
const SubscriptionRequestNotification = new Lang.Class({
|
|
Name: 'SubscriptionRequestNotification',
|
|
Extends: MessageTray.Notification,
|
|
|
|
_init: function(source, contact) {
|
|
this.parent(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_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;
|
|
}
|
|
|
|
this.parent();
|
|
},
|
|
|
|
_subscriptionStatesChangedCb: function(contact, subscribe, publish, msg) {
|
|
// Destroy the notification if the subscription request has been
|
|
// answered
|
|
if (publish != Tp.SubscriptionState.ASK)
|
|
this.destroy();
|
|
}
|
|
});
|
|
|
|
// Messages from empathy/libempathy/empathy-utils.c
|
|
// create_errors_to_message_hash()
|
|
|
|
/* Translator note: these should be the same messages that are
|
|
* used in Empathy, so just copy and paste from there. */
|
|
let _connectionErrorMessages = {};
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.NETWORK_ERROR)]
|
|
= _("Network error");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.AUTHENTICATION_FAILED)]
|
|
= _("Authentication failed");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ENCRYPTION_ERROR)]
|
|
= _("Encryption error");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_NOT_PROVIDED)]
|
|
= _("Certificate not provided");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_UNTRUSTED)]
|
|
= _("Certificate untrusted");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_EXPIRED)]
|
|
= _("Certificate expired");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_NOT_ACTIVATED)]
|
|
= _("Certificate not activated");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_HOSTNAME_MISMATCH)]
|
|
= _("Certificate hostname mismatch");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_FINGERPRINT_MISMATCH)]
|
|
= _("Certificate fingerprint mismatch");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_SELF_SIGNED)]
|
|
= _("Certificate self-signed");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CANCELLED)]
|
|
= _("Status is set to offline");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ENCRYPTION_NOT_AVAILABLE)]
|
|
= _("Encryption is not available");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_INVALID)]
|
|
= _("Certificate is invalid");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_REFUSED)]
|
|
= _("Connection has been refused");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_FAILED)]
|
|
= _("Connection can't be established");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_LOST)]
|
|
= _("Connection has been lost");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ALREADY_CONNECTED)]
|
|
= _("This account is already connected to the server");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_REPLACED)]
|
|
= _("Connection has been replaced by a new connection using the same resource");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.REGISTRATION_EXISTS)]
|
|
= _("The account already exists on the server");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.SERVICE_BUSY)]
|
|
= _("Server is currently too busy to handle the connection");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_REVOKED)]
|
|
= _("Certificate has been revoked");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_INSECURE)]
|
|
= _("Certificate uses an insecure cipher algorithm or is cryptographically weak");
|
|
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_LIMIT_EXCEEDED)]
|
|
= _("The length of the server certificate, or the depth of the server certificate chain, exceed the limits imposed by the cryptography library");
|
|
_connectionErrorMessages['org.freedesktop.DBus.Error.NoReply']
|
|
= _("Internal error");
|
|
|
|
const AccountNotification = new Lang.Class({
|
|
Name: 'AccountNotification',
|
|
Extends: MessageTray.Notification,
|
|
|
|
_init: function(source, account, connectionError) {
|
|
this.parent(source,
|
|
/* translators: argument is the account name, like
|
|
* name@jabber.org for example. */
|
|
_("Connection to %s failed").format(account.get_display_name()),
|
|
null, { customContent: true });
|
|
|
|
this._label = new St.Label();
|
|
this.addActor(this._label);
|
|
this._updateMessage(connectionError);
|
|
|
|
this._account = account;
|
|
|
|
this.addButton('reconnect', _("Reconnect"));
|
|
this.addButton('edit', _("Edit account"));
|
|
|
|
this.connect('action-invoked', Lang.bind(this, function(self, action) {
|
|
switch (action) {
|
|
case 'reconnect':
|
|
// If it fails again, a new notification should pop up with the
|
|
// new error.
|
|
account.reconnect_async(null);
|
|
break;
|
|
case 'edit':
|
|
let cmd = '/usr/bin/empathy-accounts'
|
|
+ ' --select-account=%s'
|
|
.format(account.get_path_suffix());
|
|
let app_info = Gio.app_info_create_from_commandline(cmd, null, 0);
|
|
app_info.launch([], global.create_app_launch_context());
|
|
break;
|
|
}
|
|
this.destroy();
|
|
}));
|
|
|
|
this._enabledId = account.connect('notify::enabled',
|
|
Lang.bind(this, function() {
|
|
if (!account.is_enabled())
|
|
this.destroy();
|
|
}));
|
|
|
|
this._invalidatedId = account.connect('invalidated',
|
|
Lang.bind(this, this.destroy));
|
|
|
|
this._connectionStatusId = account.connect('notify::connection-status',
|
|
Lang.bind(this, function() {
|
|
let status = account.connection_status;
|
|
if (status == Tp.ConnectionStatus.CONNECTED) {
|
|
this.destroy();
|
|
} else if (status == Tp.ConnectionStatus.DISCONNECTED) {
|
|
this._updateMessage(account.connection_error);
|
|
}
|
|
}));
|
|
},
|
|
|
|
_updateMessage: function(connectionError) {
|
|
let message;
|
|
if (connectionError in _connectionErrorMessages) {
|
|
message = _connectionErrorMessages[connectionError];
|
|
} else {
|
|
message = _("Unknown reason");
|
|
}
|
|
this._label.set_text(message);
|
|
},
|
|
|
|
destroy: function() {
|
|
if (this._enabledId != 0) {
|
|
this._account.disconnect(this._enabledId);
|
|
this._enabledId = 0;
|
|
}
|
|
|
|
if (this._invalidatedId != 0) {
|
|
this._account.disconnect(this._invalidatedId);
|
|
this._invalidatedId = 0;
|
|
}
|
|
|
|
if (this._connectionStatusId != 0) {
|
|
this._account.disconnect(this._connectionStatusId);
|
|
this._connectionStatusId = 0;
|
|
}
|
|
|
|
this.parent();
|
|
}
|
|
});
|
|
const Component = TelepathyClient;
|