1
1
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
3
const DBus = imports.dbus;
4
3
const Gio = imports.gi.Gio;
5
4
const GLib = imports.gi.GLib;
6
5
const Lang = imports.lang;
34
33
RECEIVED: 'chat-received'
37
let contactFeatures = [Tp.ContactFeature.ALIAS,
38
Tp.ContactFeature.AVATAR_DATA,
39
Tp.ContactFeature.PRESENCE];
41
// This is GNOME Shell's implementation of the Telepathy 'Client'
42
// interface. Specifically, the shell is a Telepathy 'Observer', which
43
// lets us see messages even if they belong to another app (eg,
46
36
function makeMessageFromTpMessage(tpMessage, direction) {
47
37
let [text, flags] = tpMessage.to_text();
86
74
// account path -> AccountNotification
87
75
this._accountNotifications = {};
77
// Define features we want
78
this._accountManager = Tp.AccountManager.dup();
79
let factory = this._accountManager.get_factory();
80
factory.add_account_features([Tp.Account.get_feature_quark_connection()]);
81
factory.add_connection_features([Tp.Connection.get_feature_quark_contact_list()]);
82
factory.add_channel_features([Tp.Channel.get_feature_quark_contacts()]);
83
factory.add_contact_features([Tp.ContactFeature.ALIAS,
84
Tp.ContactFeature.AVATAR_DATA,
85
Tp.ContactFeature.PRESENCE,
86
Tp.ContactFeature.SUBSCRIPTION_STATES]);
89
88
// Set up a SimpleObserver, which will call _observeChannels whenever a
90
89
// channel matching its filters is detected.
91
90
// The second argument, recover, means _observeChannels will be run
92
91
// for any existing channel as well.
93
this._accountManager = Tp.AccountManager.dup();
94
92
this._tpClient = new Shell.TpClient({ 'account-manager': this._accountManager,
95
93
'name': 'GnomeShell',
96
94
'uniquify-name': true })
117
115
throw new Error('Couldn\'t register Telepathy client. Error: \n' + e);
121
118
// Watch subscription requests and connection errors
122
119
this._subscriptionSource = null;
123
120
this._accountSource = null;
124
let factory = this._accountManager.get_factory();
125
factory.add_account_features([Tp.Account.get_feature_quark_connection()]);
126
factory.add_connection_features([Tp.Connection.get_feature_quark_contact_list()]);
127
factory.add_contact_features([Tp.ContactFeature.SUBSCRIPTION_STATES,
128
Tp.ContactFeature.ALIAS,
129
Tp.ContactFeature.AVATAR_DATA]);
131
122
this._accountManager.connect('account-validity-changed',
132
123
Lang.bind(this, this._accountValidityChanged));
137
128
_observeChannels: function(observer, account, conn, channels,
138
129
dispatchOp, requests, context) {
139
// If the self_contact doesn't have the ALIAS, make sure
140
// to fetch it before trying to grab the channels.
141
let self_contact = conn.get_self_contact();
142
if (self_contact.has_feature(Tp.ContactFeature.ALIAS)) {
143
this._finishObserveChannels(account, conn, channels, context);
145
Shell.get_self_contact_features(conn,
147
Lang.bind(this, function() {
148
this._finishObserveChannels(account, conn, channels, context);
154
_finishObserveChannels: function(account, conn, channels, context) {
155
130
let len = channels.length;
156
131
for (let i = 0; i < len; i++) {
157
132
let channel = channels[i];
162
137
targetHandleType != Tp.HandleType.CONTACT)
165
/* Request a TpContact */
166
Shell.get_tp_contacts(conn, [targetHandle],
168
Lang.bind(this, function (connection, contacts, failed) {
169
if (contacts.length < 1)
172
/* We got the TpContact */
173
this._createChatSource(account, conn, channel, contacts[0]);
140
this._createChatSource(account, conn, channel, channel.get_target_contact());
177
143
context.accept();
201
167
_handleChannels: function(handler, account, conn, channels,
202
168
requests, user_action_time, context) {
203
this._handlingChannels(account, conn, channels);
169
this._handlingChannels(account, conn, channels, true);
204
170
context.accept();
207
_handlingChannels: function(account, conn, channels) {
173
_handlingChannels: function(account, conn, channels, notify) {
208
174
let len = channels.length;
209
175
for (let i = 0; i < len; i++) {
210
176
let channel = channels[i];
218
if (this._tpClient.is_handling_channel(channel)) {
184
// 'notify' will be true when coming from an actual HandleChannels
185
// call, and not when from a successful Claim call. The point is
186
// we don't want to notify for a channel we just claimed which
187
// has no new messages (for example, a new channel which only has
188
// a delivery notification). We rely on _displayPendingMessages()
189
// and _messageReceived() to notify for new messages.
191
// But we should still notify from HandleChannels because the
192
// Telepathy spec states that handlers must foreground channels
193
// in HandleChannels calls which are already being handled.
195
if (notify && this._tpClient.is_handling_channel(channel)) {
219
196
// We are already handling the channel, display the source
220
197
let source = this._chatSources[channel.get_object_path()];
227
204
_displayRoomInvitation: function(conn, channel, dispatchOp, context) {
228
205
// We can only approve the rooms if we have been invited to it
229
let selfHandle = channel.group_get_self_handle();
230
if (selfHandle == 0) {
206
let selfContact = channel.group_get_self_contact();
207
if (selfContact == null) {
231
208
Shell.decline_dispatch_op(context, 'Not invited to the room');
235
let [invited, inviter, reason, msg] = channel.group_get_local_pending_info(selfHandle);
212
let [invited, inviter, reason, msg] = channel.group_get_local_pending_contact_info(selfContact);
237
214
Shell.decline_dispatch_op(context, 'Not invited to the room');
241
// Request a TpContact for the inviter
242
Shell.get_tp_contacts(conn, [inviter],
244
Lang.bind(this, this._createRoomInviteSource, channel, context, dispatchOp));
249
_createRoomInviteSource: function(connection, contacts, failed, channel, context, dispatchOp) {
250
if (contacts.length < 1) {
251
Shell.decline_dispatch_op(context, 'Failed to get inviter');
255
// We got the TpContact
257
218
// FIXME: We don't have a 'chat room' icon (bgo #653737) use
258
219
// system-users for now as Empathy does.
259
220
let source = new ApproverSource(dispatchOp, _("Invitation"),
260
221
Gio.icon_new_for_string('system-users'));
261
222
Main.messageTray.add(source);
263
let notif = new RoomInviteNotification(source, dispatchOp, channel, contacts[0]);
224
let notif = new RoomInviteNotification(source, dispatchOp, channel, inviter);
264
225
source.notify(notif);
265
226
context.accept();
273
234
if (chanType == Tp.IFACE_CHANNEL_TYPE_TEXT)
274
235
this._approveTextChannel(account, conn, channel, dispatchOp, context);
275
else if (chanType == Tp.IFACE_CHANNEL_TYPE_STREAMED_MEDIA ||
276
chanType == 'org.freedesktop.Telepathy.Channel.Type.Call.DRAFT')
236
else if (chanType == Tp.IFACE_CHANNEL_TYPE_CALL)
277
237
this._approveCall(account, conn, channel, dispatchOp, context);
278
238
else if (chanType == Tp.IFACE_CHANNEL_TYPE_FILE_TRANSFER)
279
239
this._approveFileTransfer(account, conn, channel, dispatchOp, context);
288
248
Lang.bind(this, function(dispatchOp, result) {
290
250
dispatchOp.claim_with_finish(result);
291
this._handlingChannels(account, conn, [channel]);
251
this._handlingChannels(account, conn, [channel], false);
293
253
throw new Error('Failed to Claim channel: ' + err);
302
262
_approveCall: function(account, conn, channel, dispatchOp, context) {
303
let [targetHandle, targetHandleType] = channel.get_handle();
305
Shell.get_tp_contacts(conn, [targetHandle],
307
Lang.bind(this, this._createAudioVideoSource, channel, context, dispatchOp));
312
_createAudioVideoSource: function(connection, contacts, failed, channel, context, dispatchOp) {
313
if (contacts.length < 1) {
314
Shell.decline_dispatch_op(context, 'Failed to get inviter');
318
263
let isVideo = false;
320
265
let props = channel.borrow_immutable_properties();
322
if (props['org.freedesktop.Telepathy.Channel.Type.Call.DRAFT.InitialVideo'] ||
323
props[Tp.PROP_CHANNEL_TYPE_STREAMED_MEDIA_INITIAL_VIDEO])
267
if (props[Tp.PROP_CHANNEL_TYPE_CALL_INITIAL_VIDEO])
326
270
// We got the TpContact
329
273
Gio.icon_new_for_string('audio-input-microphone'));
330
274
Main.messageTray.add(source);
332
let notif = new AudioVideoNotification(source, dispatchOp, channel, contacts[0], isVideo);
276
let notif = new AudioVideoNotification(source, dispatchOp, channel,
277
channel.get_target_contact(), isVideo);
333
278
source.notify(notif);
334
279
context.accept();
337
282
_approveFileTransfer: function(account, conn, channel, dispatchOp, context) {
338
let [targetHandle, targetHandleType] = channel.get_handle();
340
Shell.get_tp_contacts(conn, [targetHandle],
342
Lang.bind(this, this._createFileTransferSource, channel, context, dispatchOp));
347
_createFileTransferSource: function(connection, contacts, failed, channel, context, dispatchOp) {
348
if (contacts.length < 1) {
349
Shell.decline_dispatch_op(context, 'Failed to get file sender');
353
283
// Use the icon of the file being transferred
354
284
let gicon = Gio.content_type_get_icon(channel.get_mime_type());
357
287
let source = new ApproverSource(dispatchOp, _("File Transfer"), gicon);
358
288
Main.messageTray.add(source);
360
let notif = new FileTransferNotification(source, dispatchOp, channel, contacts[0]);
290
let notif = new FileTransferNotification(source, dispatchOp, channel,
291
channel.get_target_contact());
361
292
source.notify(notif);
362
293
context.accept();
481
412
return this._accountSource;
485
function ChatSource(account, conn, channel, contact, client) {
486
this._init(account, conn, channel, contact, client);
489
ChatSource.prototype = {
490
__proto__: MessageTray.Source.prototype,
416
const ChatSource = new Lang.Class({
418
Extends: MessageTray.Source,
492
420
_init: function(account, conn, channel, contact, client) {
493
MessageTray.Source.prototype._init.call(this, contact.get_alias());
421
this.parent(contact.get_alias());
495
423
this.isChat = true;
508
436
this._notification.setUrgency(MessageTray.Urgency.HIGH);
509
437
this._notifyTimeoutId = 0;
511
// We ack messages when the message box is collapsed if user has
512
// interacted with it before and so read the messages:
513
// - user clicked on it the tray
514
// - user expanded the notification by hovering over the toaster notification
515
this._shouldAck = false;
517
this.connect('summary-item-clicked', Lang.bind(this, this._summaryItemClicked));
518
this._notification.connect('expanded', Lang.bind(this, this._notificationExpanded));
519
this._notification.connect('collapsed', Lang.bind(this, this._notificationCollapsed));
439
// We ack messages when the user expands the new notification or views the summary
440
// notification, in which case the notification is also expanded.
441
this._notification.connect('expanded', Lang.bind(this, this._ackMessages));
521
443
this._presence = contact.get_presence_type();
740
662
if (presence == Tp.ConnectionPresenceType.AVAILABLE) {
741
663
msg = _("%s is online.").format(title);
742
664
shouldNotify = (this._presence == Tp.ConnectionPresenceType.OFFLINE);
743
} else if (presence == Tp.ConnectionPresenceType.OFFLINE ||
744
presence == Tp.ConnectionPresenceType.EXTENDED_AWAY) {
665
} else if (presence == Tp.ConnectionPresenceType.OFFLINE) {
745
666
presence = Tp.ConnectionPresenceType.OFFLINE;
746
667
msg = _("%s is offline.").format(title);
747
668
shouldNotify = (this._presence != Tp.ConnectionPresenceType.OFFLINE);
748
} else if (presence == Tp.ConnectionPresenceType.AWAY) {
669
} else if (presence == Tp.ConnectionPresenceType.AWAY ||
670
presence == Tp.ConnectionPresenceType.EXTENDED_AWAY) {
749
671
msg = _("%s is away.").format(title);
750
672
shouldNotify = false;
751
673
} else if (presence == Tp.ConnectionPresenceType.BUSY) {
780
702
// 'pending-message-removed' for each one.
781
703
this._channel.ack_all_pending_messages_async(Lang.bind(this, function(src, result) {
782
704
this._channel.ack_all_pending_messages_finish(result);}));
785
_summaryItemClicked: function(source, button) {
789
this._shouldAck = true;
792
_notificationExpanded: function() {
793
this._shouldAck = true;
796
_notificationCollapsed: function() {
800
this._shouldAck = false;
804
function ChatNotification(source) {
808
ChatNotification.prototype = {
809
__proto__: MessageTray.Notification.prototype,
708
const ChatNotification = new Lang.Class({
709
Name: 'ChatNotification',
710
Extends: MessageTray.Notification,
811
712
_init: function(source) {
812
MessageTray.Notification.prototype._init.call(this, source, source.title, null, { customContent: true });
713
this.parent(source, source.title, null, { customContent: true });
813
714
this.setResident(true);
815
716
this._responseEntry = new St.Entry({ style_class: 'chat-response',
938
839
let body = highlighter.actor;
940
841
let styles = props.styles;
941
for (let i = 0; i < styles.length; i ++)
842
for (let i = 0; i < styles.length; i++)
942
843
body.add_style_class_name(styles[i]);
944
845
let group = props.group;
1092
993
this.source.setChatState(Tp.ChannelChatState.ACTIVE);
1097
function ApproverSource(dispatchOp, text, gicon) {
1098
this._init(dispatchOp, text, gicon);
1101
ApproverSource.prototype = {
1102
__proto__: MessageTray.Source.prototype,
998
const ApproverSource = new Lang.Class({
999
Name: 'ApproverSource',
1000
Extends: MessageTray.Source,
1104
1002
_init: function(dispatchOp, text, gicon) {
1105
MessageTray.Source.prototype._init.call(this, text);
1107
1005
this._gicon = gicon;
1108
1006
this._setSummaryIcon(this.createNotificationIcon());
1131
1029
icon_type: St.IconType.FULLCOLOR,
1132
1030
icon_size: this.ICON_SIZE });
1136
function RoomInviteNotification(source, dispatchOp, channel, inviter) {
1137
this._init(source, dispatchOp, channel, inviter);
1140
RoomInviteNotification.prototype = {
1141
__proto__: MessageTray.Notification.prototype,
1034
const RoomInviteNotification = new Lang.Class({
1035
Name: 'RoomInviteNotification',
1036
Extends: MessageTray.Notification,
1143
1038
_init: function(source, dispatchOp, channel, inviter) {
1144
MessageTray.Notification.prototype._init.call(this,
1146
/* translators: argument is a room name like
1147
* room@jabber.org for example. */
1148
_("Invitation to %s").format(channel.get_identifier()),
1150
{ customContent: true });
1040
/* translators: argument is a room name like
1041
* room@jabber.org for example. */
1042
_("Invitation to %s").format(channel.get_identifier()),
1044
{ customContent: true });
1151
1045
this.setResident(true);
1153
1047
/* translators: first argument is the name of a contact and the second
1174
1068
this.destroy();
1180
function AudioVideoNotification(source, dispatchOp, channel, contact, isVideo) {
1181
this._init(source, dispatchOp, channel, contact, isVideo);
1184
AudioVideoNotification.prototype = {
1185
__proto__: MessageTray.Notification.prototype,
1074
const AudioVideoNotification = new Lang.Class({
1075
Name: 'AudioVideoNotification',
1076
Extends: MessageTray.Notification,
1187
1078
_init: function(source, dispatchOp, channel, contact, isVideo) {
1188
1079
let title = '';
1194
1085
/* translators: argument is a contact name like Alice for example. */
1195
1086
title = _("Call from %s").format(contact.get_alias());
1197
MessageTray.Notification.prototype._init.call(this,
1201
{ customContent: true });
1088
this.parent(source, title, null, { customContent: true });
1202
1089
this.setResident(true);
1204
1091
this.addButton('reject', _("Reject"));
1221
1108
this.destroy();
1226
1113
// File Transfer
1227
function FileTransferNotification(source, dispatchOp, channel, contact) {
1228
this._init(source, dispatchOp, channel, contact);
1231
FileTransferNotification.prototype = {
1232
__proto__: MessageTray.Notification.prototype,
1114
const FileTransferNotification = new Lang.Class({
1115
Name: 'FileTransferNotification',
1116
Extends: MessageTray.Notification,
1234
1118
_init: function(source, dispatchOp, channel, contact) {
1235
MessageTray.Notification.prototype._init.call(this,
1237
/* To translators: The first parameter is
1238
* the contact's alias and the second one is the
1239
* file name. The string will be something
1240
* like: "Alice is sending you test.ogg"
1242
_("%s is sending you %s").format(contact.get_alias(),
1243
channel.get_filename()),
1245
{ customContent: true });
1120
/* To translators: The first parameter is
1121
* the contact's alias and the second one is the
1122
* file name. The string will be something
1123
* like: "Alice is sending you test.ogg"
1125
_("%s is sending you %s").format(contact.get_alias(),
1126
channel.get_filename()),
1128
{ customContent: true });
1246
1129
this.setResident(true);
1248
1131
this.addButton('decline', _("Decline"));
1264
1147
this.destroy();
1269
1152
// A notification source that can embed multiple notifications
1270
function MultiNotificationSource(title, icon) {
1271
this._init(title, icon);
1274
MultiNotificationSource.prototype = {
1275
__proto__: MessageTray.Source.prototype,
1153
const MultiNotificationSource = new Lang.Class({
1154
Name: 'MultiNotificationSource',
1155
Extends: MessageTray.Source,
1277
1157
_init: function(title, icon) {
1278
MessageTray.Source.prototype._init.call(this, title);
1280
1160
this._icon = icon;
1281
1161
this._setSummaryIcon(this.createNotificationIcon());
1285
1165
notify: function(notification) {
1286
MessageTray.Source.prototype.notify.call(this, notification);
1166
this.parent(notification);
1288
1168
this._nbNotifications += 1;
1299
1179
createNotificationIcon: function() {
1300
return new St.Icon({ gicon: Shell.util_icon_from_string(this._icon),
1180
return new St.Icon({ gicon: Gio.icon_new_for_string(this._icon),
1301
1181
icon_type: St.IconType.FULLCOLOR,
1302
1182
icon_size: this.ICON_SIZE });
1306
1186
// Subscription request
1307
function SubscriptionRequestNotification(source, contact) {
1308
this._init(source, contact);
1311
SubscriptionRequestNotification.prototype = {
1312
__proto__: MessageTray.Notification.prototype,
1187
const SubscriptionRequestNotification = new Lang.Class({
1188
Name: 'SubscriptionRequestNotification',
1189
Extends: MessageTray.Notification,
1314
1191
_init: function(source, contact) {
1315
MessageTray.Notification.prototype._init.call(this, source,
1316
/* To translators: The parameter is the contact's alias */
1317
_("%s would like permission to see when you are online").format(contact.get_alias()),
1318
null, { customContent: true });
1193
/* To translators: The parameter is the contact's alias */
1194
_("%s would like permission to see when you are online").format(contact.get_alias()),
1195
null, { customContent: true });
1320
1197
this._contact = contact;
1321
1198
this._connection = contact.get_connection();
1398
1275
if (publish != Tp.SubscriptionState.ASK)
1399
1276
this.destroy();
1404
function AccountNotification(source, account, connectionError) {
1405
this._init(source, account, connectionError);
1408
1280
// Messages from empathy/libempathy/empathy-utils.c
1409
1281
// create_errors_to_message_hash()
1444
1316
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_LOST)]
1445
1317
= _("Connection has been lost");
1446
1318
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ALREADY_CONNECTED)]
1447
= _("This resource is already connected to the server");
1319
= _("This account is already connected to the server");
1448
1320
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_REPLACED)]
1449
1321
= _("Connection has been replaced by a new connection using the same resource");
1450
1322
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.REGISTRATION_EXISTS)]
1457
1329
= _("Certificate uses an insecure cipher algorithm or is cryptographically weak");
1458
1330
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_LIMIT_EXCEEDED)]
1459
1331
= _("The length of the server certificate, or the depth of the server certificate chain, exceed the limits imposed by the cryptography library");
1332
_connectionErrorMessages['org.freedesktop.DBus.Error.NoReply']
1333
= _("Internal error");
1461
AccountNotification.prototype = {
1462
__proto__: MessageTray.Notification.prototype,
1335
const AccountNotification = new Lang.Class({
1336
Name: 'AccountNotification',
1337
Extends: MessageTray.Notification,
1464
1339
_init: function(source, account, connectionError) {
1465
MessageTray.Notification.prototype._init.call(this, source,
1466
/* translators: argument is the account name, like
1467
* name@jabber.org for example. */
1468
_("Connection to %s failed").format(account.get_display_name()),
1469
null, { customContent: true });
1341
/* translators: argument is the account name, like
1342
* name@jabber.org for example. */
1343
_("Connection to %s failed").format(account.get_display_name()),
1344
null, { customContent: true });
1471
1346
this._label = new St.Label();
1472
1347
this.addActor(this._label);