~darkxst/ubuntu/quantal/gnome-shell/lp1128804

« back to all changes in this revision

Viewing changes to js/ui/components/telepathyClient.js

  • Committer: Package Import Robot
  • Author(s): Tim Lunn
  • Date: 2012-10-09 20:42:33 UTC
  • mfrom: (57.1.7 quantal)
  • Revision ID: package-import@ubuntu.com-20121009204233-chcl8989muuzfpws
Tags: 3.6.0-0ubuntu3
* debian/patches/ubuntu-lightdm-user-switching.patch
  - Fix user switching when running lightdm.  LP: #1064269
 

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 
2
 
 
3
const Gio = imports.gi.Gio;
 
4
const GLib = imports.gi.GLib;
 
5
const Lang = imports.lang;
 
6
const Mainloop = imports.mainloop;
 
7
const Shell = imports.gi.Shell;
 
8
const Signals = imports.signals;
 
9
const St = imports.gi.St;
 
10
const Tpl = imports.gi.TelepathyLogger;
 
11
const Tp = imports.gi.TelepathyGLib;
 
12
 
 
13
const History = imports.misc.history;
 
14
const Main = imports.ui.main;
 
15
const MessageTray = imports.ui.messageTray;
 
16
const Params = imports.misc.params;
 
17
const PopupMenu = imports.ui.popupMenu;
 
18
 
 
19
// See Notification.appendMessage
 
20
const SCROLLBACK_IMMEDIATE_TIME = 60; // 1 minute
 
21
const SCROLLBACK_RECENT_TIME = 15 * 60; // 15 minutes
 
22
const SCROLLBACK_RECENT_LENGTH = 20;
 
23
const SCROLLBACK_IDLE_LENGTH = 5;
 
24
 
 
25
// See Source._displayPendingMessages
 
26
const SCROLLBACK_HISTORY_LINES = 10;
 
27
 
 
28
// See Notification._onEntryChanged
 
29
const COMPOSING_STOP_TIMEOUT = 5;
 
30
 
 
31
const NotificationDirection = {
 
32
    SENT: 'chat-sent',
 
33
    RECEIVED: 'chat-received'
 
34
};
 
35
 
 
36
function makeMessageFromTpMessage(tpMessage, direction) {
 
37
    let [text, flags] = tpMessage.to_text();
 
38
 
 
39
    let timestamp = tpMessage.get_sent_timestamp();
 
40
    if (timestamp == 0)
 
41
        timestamp = tpMessage.get_received_timestamp();
 
42
 
 
43
    return {
 
44
        messageType: tpMessage.get_message_type(),
 
45
        text: text,
 
46
        sender: tpMessage.sender.alias,
 
47
        timestamp: timestamp,
 
48
        direction: direction
 
49
    };
 
50
}
 
51
 
 
52
 
 
53
function makeMessageFromTplEvent(event) {
 
54
    let sent = event.get_sender().get_entity_type() == Tpl.EntityType.SELF;
 
55
    let direction = sent ? NotificationDirection.SENT : NotificationDirection.RECEIVED;
 
56
 
 
57
    return {
 
58
        messageType: event.get_message_type(),
 
59
        text: event.get_message(),
 
60
        sender: event.get_sender().get_alias(),
 
61
        timestamp: event.get_timestamp(),
 
62
        direction: direction
 
63
    };
 
64
}
 
65
 
 
66
const TelepathyClient = new Lang.Class({
 
67
    Name: 'TelepathyClient',
 
68
 
 
69
    _init: function() {
 
70
        // channel path -> ChatSource
 
71
        this._chatSources = {};
 
72
        this._chatState = Tp.ChannelChatState.ACTIVE;
 
73
 
 
74
        // account path -> AccountNotification
 
75
        this._accountNotifications = {};
 
76
 
 
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]);
 
87
 
 
88
        // Set up a SimpleObserver, which will call _observeChannels whenever a
 
89
        // channel matching its filters is detected.
 
90
        // The second argument, recover, means _observeChannels will be run
 
91
        // for any existing channel as well.
 
92
        this._tpClient = new Shell.TpClient({ name: 'GnomeShell',
 
93
                                              account_manager: this._accountManager,
 
94
                                              uniquify_name: true });
 
95
        this._tpClient.set_observe_channels_func(
 
96
            Lang.bind(this, this._observeChannels));
 
97
        this._tpClient.set_approve_channels_func(
 
98
            Lang.bind(this, this._approveChannels));
 
99
        this._tpClient.set_handle_channels_func(
 
100
            Lang.bind(this, this._handleChannels));
 
101
 
 
102
        // Watch subscription requests and connection errors
 
103
        this._subscriptionSource = null;
 
104
        this._accountSource = null;
 
105
 
 
106
        // Workaround for gjs not supporting GPtrArray in signals.
 
107
        // See BGO bug #653941 for context.
 
108
        this._tpClient.set_contact_list_changed_func(
 
109
            Lang.bind(this, this._contactListChanged));
 
110
 
 
111
        // Allow other clients (such as Empathy) to pre-empt our channels if
 
112
        // needed
 
113
        this._tpClient.set_delegated_channels_callback(
 
114
            Lang.bind(this, this._delegatedChannelsCb));
 
115
    },
 
116
 
 
117
    enable: function() {
 
118
        try {
 
119
            this._tpClient.register();
 
120
        } catch (e) {
 
121
            throw new Error('Couldn\'t register Telepathy client. Error: \n' + e);
 
122
        }
 
123
 
 
124
        this._accountManagerValidityChangedId = this._accountManager.connect('account-validity-changed',
 
125
                                                                             Lang.bind(this, this._accountValidityChanged));
 
126
 
 
127
        if (!this._accountManager.is_prepared(Tp.AccountManager.get_feature_quark_core()))
 
128
            this._accountManager.prepare_async(null, Lang.bind(this, this._accountManagerPrepared));
 
129
    },
 
130
 
 
131
    disable: function() {
 
132
        this._tpClient.unregister();
 
133
        this._accountManager.disconnect(this._accountManagerValidityChangedId);
 
134
        this._accountManagerValidityChangedId = 0;
 
135
    },
 
136
 
 
137
    _observeChannels: function(observer, account, conn, channels,
 
138
                               dispatchOp, requests, context) {
 
139
        let len = channels.length;
 
140
        for (let i = 0; i < len; i++) {
 
141
            let channel = channels[i];
 
142
            let [targetHandle, targetHandleType] = channel.get_handle();
 
143
 
 
144
            if (channel.get_invalidated())
 
145
              continue;
 
146
 
 
147
            /* Only observe contact text channels */
 
148
            if ((!(channel instanceof Tp.TextChannel)) ||
 
149
               targetHandleType != Tp.HandleType.CONTACT)
 
150
               continue;
 
151
 
 
152
            this._createChatSource(account, conn, channel, channel.get_target_contact());
 
153
        }
 
154
 
 
155
        context.accept();
 
156
    },
 
157
 
 
158
    _createChatSource: function(account, conn, channel, contact) {
 
159
        if (this._chatSources[channel.get_object_path()])
 
160
            return;
 
161
 
 
162
        let source = new ChatSource(account, conn, channel, contact, this._tpClient);
 
163
 
 
164
        this._chatSources[channel.get_object_path()] = source;
 
165
        source.connect('destroy', Lang.bind(this,
 
166
                       function() {
 
167
                           if (this._tpClient.is_handling_channel(channel)) {
 
168
                               // The chat box has been destroyed so it can't
 
169
                               // handle the channel any more.
 
170
                               channel.close_async(function(src, result) {
 
171
                                   channel.close_finish(result);
 
172
                               });
 
173
                           }
 
174
 
 
175
                           delete this._chatSources[channel.get_object_path()];
 
176
                       }));
 
177
    },
 
178
 
 
179
    _handleChannels: function(handler, account, conn, channels,
 
180
                              requests, user_action_time, context) {
 
181
        this._handlingChannels(account, conn, channels, true);
 
182
        context.accept();
 
183
    },
 
184
 
 
185
    _handlingChannels: function(account, conn, channels, notify) {
 
186
        let len = channels.length;
 
187
        for (let i = 0; i < len; i++) {
 
188
            let channel = channels[i];
 
189
 
 
190
            // We can only handle text channel, so close any other channel
 
191
            if (!(channel instanceof Tp.TextChannel)) {
 
192
                channel.close_async(null);
 
193
                continue;
 
194
            }
 
195
 
 
196
            if (channel.get_invalidated())
 
197
              continue;
 
198
 
 
199
            // 'notify' will be true when coming from an actual HandleChannels
 
200
            // call, and not when from a successful Claim call. The point is
 
201
            // we don't want to notify for a channel we just claimed which
 
202
            // has no new messages (for example, a new channel which only has
 
203
            // a delivery notification). We rely on _displayPendingMessages()
 
204
            // and _messageReceived() to notify for new messages.
 
205
 
 
206
            // But we should still notify from HandleChannels because the
 
207
            // Telepathy spec states that handlers must foreground channels
 
208
            // in HandleChannels calls which are already being handled.
 
209
 
 
210
            if (notify && this._tpClient.is_handling_channel(channel)) {
 
211
                // We are already handling the channel, display the source
 
212
                let source = this._chatSources[channel.get_object_path()];
 
213
                if (source)
 
214
                    source.notify();
 
215
            }
 
216
        }
 
217
    },
 
218
 
 
219
    _displayRoomInvitation: function(conn, channel, dispatchOp, context) {
 
220
        // We can only approve the rooms if we have been invited to it
 
221
        let selfContact = channel.group_get_self_contact();
 
222
        if (selfContact == null) {
 
223
            context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
 
224
                                        message: 'Not invited to the room' }));
 
225
            return;
 
226
        }
 
227
 
 
228
        let [invited, inviter, reason, msg] = channel.group_get_local_pending_contact_info(selfContact);
 
229
        if (!invited) {
 
230
            context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
 
231
                                        message: 'Not invited to the room' }));
 
232
            return;
 
233
        }
 
234
 
 
235
        // FIXME: We don't have a 'chat room' icon (bgo #653737) use
 
236
        // system-users for now as Empathy does.
 
237
        let source = new ApproverSource(dispatchOp, _("Invitation"),
 
238
                                        Gio.icon_new_for_string('system-users'));
 
239
        Main.messageTray.add(source);
 
240
 
 
241
        let notif = new RoomInviteNotification(source, dispatchOp, channel, inviter);
 
242
        source.notify(notif);
 
243
        context.accept();
 
244
    },
 
245
 
 
246
    _approveChannels: function(approver, account, conn, channels,
 
247
                               dispatchOp, context) {
 
248
        let channel = channels[0];
 
249
        let chanType = channel.get_channel_type();
 
250
 
 
251
        if (channel.get_invalidated()) {
 
252
            context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
 
253
                                        message: 'Channel is invalidated' }));
 
254
            return;
 
255
        }
 
256
 
 
257
        if (chanType == Tp.IFACE_CHANNEL_TYPE_TEXT)
 
258
            this._approveTextChannel(account, conn, channel, dispatchOp, context);
 
259
        else if (chanType == Tp.IFACE_CHANNEL_TYPE_CALL)
 
260
            this._approveCall(account, conn, channel, dispatchOp, context);
 
261
        else if (chanType == Tp.IFACE_CHANNEL_TYPE_FILE_TRANSFER)
 
262
            this._approveFileTransfer(account, conn, channel, dispatchOp, context);
 
263
        else
 
264
            context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
 
265
                                        message: 'Unsupported channel type' }));
 
266
    },
 
267
 
 
268
    _approveTextChannel: function(account, conn, channel, dispatchOp, context) {
 
269
        let [targetHandle, targetHandleType] = channel.get_handle();
 
270
 
 
271
        if (targetHandleType == Tp.HandleType.CONTACT) {
 
272
            // Approve private text channels right away as we are going to handle it
 
273
            dispatchOp.claim_with_async(this._tpClient,
 
274
                                        Lang.bind(this, function(dispatchOp, result) {
 
275
                try {
 
276
                    dispatchOp.claim_with_finish(result);
 
277
                    this._handlingChannels(account, conn, [channel], false);
 
278
                } catch (err) {
 
279
                    throw new Error('Failed to Claim channel: ' + err);
 
280
                }}));
 
281
 
 
282
            context.accept();
 
283
        } else {
 
284
            this._displayRoomInvitation(conn, channel, dispatchOp, context);
 
285
        }
 
286
    },
 
287
 
 
288
    _approveCall: function(account, conn, channel, dispatchOp, context) {
 
289
        let isVideo = false;
 
290
 
 
291
        let props = channel.borrow_immutable_properties();
 
292
 
 
293
        if (props[Tp.PROP_CHANNEL_TYPE_CALL_INITIAL_VIDEO])
 
294
          isVideo = true;
 
295
 
 
296
        // We got the TpContact
 
297
        let source = new ApproverSource(dispatchOp, _("Call"), isVideo ?
 
298
                                        Gio.icon_new_for_string('camera-web') :
 
299
                                        Gio.icon_new_for_string('audio-input-microphone'));
 
300
        Main.messageTray.add(source);
 
301
 
 
302
        let notif = new AudioVideoNotification(source, dispatchOp, channel,
 
303
            channel.get_target_contact(), isVideo);
 
304
        source.notify(notif);
 
305
        context.accept();
 
306
    },
 
307
 
 
308
    _approveFileTransfer: function(account, conn, channel, dispatchOp, context) {
 
309
        // Use the icon of the file being transferred
 
310
        let gicon = Gio.content_type_get_icon(channel.get_mime_type());
 
311
 
 
312
        // We got the TpContact
 
313
        let source = new ApproverSource(dispatchOp, _("File Transfer"), gicon);
 
314
        Main.messageTray.add(source);
 
315
 
 
316
        let notif = new FileTransferNotification(source, dispatchOp, channel,
 
317
            channel.get_target_contact());
 
318
        source.notify(notif);
 
319
        context.accept();
 
320
    },
 
321
 
 
322
    _delegatedChannelsCb: function(client, channels) {
 
323
        // Nothing to do as we don't make a distinction between observed and
 
324
        // handled channels.
 
325
    },
 
326
 
 
327
    _accountManagerPrepared: function(am, result) {
 
328
        am.prepare_finish(result);
 
329
 
 
330
        let accounts = am.get_valid_accounts();
 
331
        for (let i = 0; i < accounts.length; i++) {
 
332
            this._accountValidityChanged(am, accounts[i], true);
 
333
        }
 
334
    },
 
335
 
 
336
    _accountValidityChanged: function(am, account, valid) {
 
337
        if (!valid)
 
338
            return;
 
339
 
 
340
        // It would be better to connect to "status-changed" but we cannot.
 
341
        // See discussion in https://bugzilla.gnome.org/show_bug.cgi?id=654159
 
342
        account.connect("notify::connection-status",
 
343
                        Lang.bind(this, this._accountConnectionStatusNotifyCb));
 
344
 
 
345
        account.connect('notify::connection',
 
346
                        Lang.bind(this, this._connectionChanged));
 
347
        this._connectionChanged(account);
 
348
    },
 
349
 
 
350
    _connectionChanged: function(account) {
 
351
        let conn = account.get_connection();
 
352
        if (conn == null)
 
353
            return;
 
354
 
 
355
        this._tpClient.grab_contact_list_changed(conn);
 
356
        if (conn.get_contact_list_state() == Tp.ContactListState.SUCCESS) {
 
357
            this._contactListChanged(conn, conn.dup_contact_list(), []);
 
358
        }
 
359
    },
 
360
 
 
361
    _contactListChanged: function(conn, added, removed) {
 
362
        for (let i = 0; i < added.length; i++) {
 
363
            let contact = added[i];
 
364
 
 
365
            contact.connect('subscription-states-changed',
 
366
                            Lang.bind(this, this._subscriptionStateChanged));
 
367
            this._subscriptionStateChanged(contact);
 
368
        }
 
369
    },
 
370
 
 
371
    _subscriptionStateChanged: function(contact) {
 
372
        if (contact.get_publish_state() != Tp.SubscriptionState.ASK)
 
373
            return;
 
374
 
 
375
        /* Implicitly accept publish requests if contact is already subscribed */
 
376
        if (contact.get_subscribe_state() == Tp.SubscriptionState.YES ||
 
377
            contact.get_subscribe_state() == Tp.SubscriptionState.ASK) {
 
378
 
 
379
            contact.authorize_publication_async(function(src, result) {
 
380
                src.authorize_publication_finish(result)});
 
381
 
 
382
            return;
 
383
        }
 
384
 
 
385
        /* Display notification to ask user to accept/reject request */
 
386
        let source = this._ensureSubscriptionSource();
 
387
 
 
388
        let notif = new SubscriptionRequestNotification(source, contact);
 
389
        source.notify(notif);
 
390
    },
 
391
 
 
392
    _ensureSubscriptionSource: function() {
 
393
        if (this._subscriptionSource == null) {
 
394
            this._subscriptionSource = new MessageTray.Source(_("Subscription request"),
 
395
                                                              'gtk-dialog-question');
 
396
            Main.messageTray.add(this._subscriptionSource);
 
397
            this._subscriptionSource.connect('destroy', Lang.bind(this, function () {
 
398
                this._subscriptionSource = null;
 
399
            }));
 
400
        }
 
401
 
 
402
        return this._subscriptionSource;
 
403
    },
 
404
 
 
405
    _accountConnectionStatusNotifyCb: function(account) {
 
406
        let connectionError = account.connection_error;
 
407
 
 
408
        if (account.connection_status != Tp.ConnectionStatus.DISCONNECTED ||
 
409
            connectionError == Tp.error_get_dbus_name(Tp.Error.CANCELLED)) {
 
410
            return;
 
411
        }
 
412
 
 
413
        let notif = this._accountNotifications[account.get_object_path()];
 
414
        if (notif)
 
415
            return;
 
416
 
 
417
        /* Display notification that account failed to connect */
 
418
        let source = this._ensureAccountSource();
 
419
 
 
420
        notif = new AccountNotification(source, account, connectionError);
 
421
        this._accountNotifications[account.get_object_path()] = notif;
 
422
        notif.connect('destroy', Lang.bind(this, function() {
 
423
            delete this._accountNotifications[account.get_object_path()];
 
424
        }));
 
425
        source.notify(notif);
 
426
    },
 
427
 
 
428
    _ensureAccountSource: function() {
 
429
        if (this._accountSource == null) {
 
430
            this._accountSource = new MessageTray.Source(_("Connection error"),
 
431
                                                         'gtk-dialog-error');
 
432
            Main.messageTray.add(this._accountSource);
 
433
            this._accountSource.connect('destroy', Lang.bind(this, function () {
 
434
                this._accountSource = null;
 
435
            }));
 
436
        }
 
437
 
 
438
        return this._accountSource;
 
439
    }
 
440
});
 
441
 
 
442
const ChatSource = new Lang.Class({
 
443
    Name: 'ChatSource',
 
444
    Extends: MessageTray.Source,
 
445
 
 
446
    _init: function(account, conn, channel, contact, client) {
 
447
        this._account = account;
 
448
        this._contact = contact;
 
449
        this._client = client;
 
450
 
 
451
        this.parent(contact.get_alias());
 
452
 
 
453
        this.isChat = true;
 
454
        this._pendingMessages = [];
 
455
 
 
456
        this._conn = conn;
 
457
        this._channel = channel;
 
458
        this._closedId = this._channel.connect('invalidated', Lang.bind(this, this._channelClosed));
 
459
 
 
460
        this._notification = new ChatNotification(this);
 
461
        this._notification.setUrgency(MessageTray.Urgency.HIGH);
 
462
        this._notifyTimeoutId = 0;
 
463
 
 
464
        // We ack messages when the user expands the new notification or views the summary
 
465
        // notification, in which case the notification is also expanded.
 
466
        this._notification.connect('expanded', Lang.bind(this, this._ackMessages));
 
467
 
 
468
        this._presence = contact.get_presence_type();
 
469
 
 
470
        this._sentId = this._channel.connect('message-sent', Lang.bind(this, this._messageSent));
 
471
        this._receivedId = this._channel.connect('message-received', Lang.bind(this, this._messageReceived));
 
472
        this._pendingId = this._channel.connect('pending-message-removed', Lang.bind(this, this._pendingRemoved));
 
473
 
 
474
        this._notifyAliasId = this._contact.connect('notify::alias', Lang.bind(this, this._updateAlias));
 
475
        this._notifyAvatarId = this._contact.connect('notify::avatar-file', Lang.bind(this, this._updateAvatarIcon));
 
476
        this._presenceChangedId = this._contact.connect('presence-changed', Lang.bind(this, this._presenceChanged));
 
477
 
 
478
        // Add ourselves as a source.
 
479
        Main.messageTray.add(this);
 
480
        this.pushNotification(this._notification);
 
481
 
 
482
        this._getLogMessages();
 
483
    },
 
484
 
 
485
    buildRightClickMenu: function() {
 
486
        let item;
 
487
 
 
488
        let rightClickMenu = this.parent();
 
489
        item = new PopupMenu.PopupMenuItem('');
 
490
        item.actor.connect('notify::mapped', Lang.bind(this, function() {
 
491
            item.label.set_text(this.isMuted ? _("Unmute") : _("Mute"));
 
492
        }));
 
493
        item.connect('activate', Lang.bind(this, function() {
 
494
            this.setMuted(!this.isMuted);
 
495
            this.emit('done-displaying-content');
 
496
        }));
 
497
        rightClickMenu.add(item.actor);
 
498
        return rightClickMenu;
 
499
    },
 
500
 
 
501
    _updateAlias: function() {
 
502
        let oldAlias = this.title;
 
503
        let newAlias = this._contact.get_alias();
 
504
 
 
505
        if (oldAlias == newAlias)
 
506
            return;
 
507
 
 
508
        this.setTitle(newAlias);
 
509
        this._notification.appendAliasChange(oldAlias, newAlias);
 
510
    },
 
511
 
 
512
    createIcon: function(size) {
 
513
        this._iconBox = new St.Bin({ style_class: 'avatar-box' });
 
514
        this._iconBox._size = size;
 
515
        let textureCache = St.TextureCache.get_default();
 
516
        let file = this._contact.get_avatar_file();
 
517
 
 
518
        if (file) {
 
519
            let uri = file.get_uri();
 
520
            this._iconBox.child = textureCache.load_uri_async(uri, this._iconBox._size, this._iconBox._size);
 
521
        } else {
 
522
            this._iconBox.child = new St.Icon({ icon_name: 'avatar-default',
 
523
                                                icon_size: this._iconBox._size });
 
524
        }
 
525
 
 
526
        return this._iconBox;
 
527
    },
 
528
 
 
529
    createSecondaryIcon: function() {
 
530
        let iconBox = new St.Bin();
 
531
        iconBox.child = new St.Icon({ style_class: 'secondary-icon' });
 
532
        let presenceType = this._contact.get_presence_type();
 
533
 
 
534
        switch (presenceType) {
 
535
            case Tp.ConnectionPresenceType.AVAILABLE:
 
536
                iconBox.child.icon_name = 'user-available';
 
537
                break;
 
538
            case Tp.ConnectionPresenceType.BUSY:
 
539
                iconBox.child.icon_name = 'user-busy';
 
540
                break;
 
541
            case Tp.ConnectionPresenceType.OFFLINE:
 
542
                iconBox.child.icon_name = 'user-offline';
 
543
                break;
 
544
            case Tp.ConnectionPresenceType.HIDDEN:
 
545
                iconBox.child.icon_name = 'user-invisible';
 
546
                break;
 
547
            case Tp.ConnectionPresenceType.AWAY:
 
548
                iconBox.child.icon_name = 'user-away';
 
549
                break;
 
550
            case Tp.ConnectionPresenceType.EXTENDED_AWAY:
 
551
                iconBox.child.icon_name = 'user-idle';
 
552
                break;
 
553
            default:
 
554
                iconBox.child.icon_name = 'user-offline';
 
555
       }
 
556
       return iconBox;
 
557
    },
 
558
 
 
559
    _updateAvatarIcon: function() {
 
560
        this.iconUpdated();
 
561
        this._notification.update(this._notification.title, null, { customContent: true });
 
562
    },
 
563
 
 
564
    open: function(notification) {
 
565
          if (this._client.is_handling_channel(this._channel)) {
 
566
              // We are handling the channel, try to pass it to Empathy
 
567
              this._client.delegate_channels_async([this._channel], global.get_current_time(), '', null);
 
568
          }
 
569
          else {
 
570
              // We are not the handler, just ask to present the channel
 
571
              let dbus = Tp.DBusDaemon.dup();
 
572
              let cd = Tp.ChannelDispatcher.new(dbus);
 
573
 
 
574
              cd.present_channel_async(this._channel, global.get_current_time(), null);
 
575
          }
 
576
    },
 
577
 
 
578
    _getLogMessages: function() {
 
579
        let logManager = Tpl.LogManager.dup_singleton();
 
580
        let entity = Tpl.Entity.new_from_tp_contact(this._contact, Tpl.EntityType.CONTACT);
 
581
 
 
582
        logManager.get_filtered_events_async(this._account, entity,
 
583
                                             Tpl.EventTypeMask.TEXT, SCROLLBACK_HISTORY_LINES,
 
584
                                             null, Lang.bind(this, this._displayPendingMessages));
 
585
    },
 
586
 
 
587
    _displayPendingMessages: function(logManager, result) {
 
588
        let [success, events] = logManager.get_filtered_events_finish(result);
 
589
 
 
590
        let logMessages = events.map(makeMessageFromTplEvent);
 
591
 
 
592
        let pendingTpMessages = this._channel.get_pending_messages();
 
593
        let pendingMessages = [];
 
594
 
 
595
        for (let i = 0; i < pendingTpMessages.length; i++) {
 
596
            let message = pendingTpMessages[i];
 
597
 
 
598
            if (message.get_message_type() == Tp.ChannelTextMessageType.DELIVERY_REPORT)
 
599
                continue;
 
600
 
 
601
            pendingMessages.push(makeMessageFromTpMessage(message, NotificationDirection.RECEIVED));
 
602
 
 
603
            this._pendingMessages.push(message);
 
604
        }
 
605
 
 
606
        this.countUpdated();
 
607
 
 
608
        let showTimestamp = false;
 
609
 
 
610
        for (let i = 0; i < logMessages.length; i++) {
 
611
            let logMessage = logMessages[i];
 
612
            let isPending = false;
 
613
 
 
614
            // Skip any log messages that are also in pendingMessages
 
615
            for (let j = 0; j < pendingMessages.length; j++) {
 
616
                let pending = pendingMessages[j];
 
617
                if (logMessage.timestamp == pending.timestamp && logMessage.text == pending.text) {
 
618
                    isPending = true;
 
619
                    break;
 
620
                }
 
621
            }
 
622
 
 
623
            if (!isPending) {
 
624
                showTimestamp = true;
 
625
                this._notification.appendMessage(logMessage, true, ['chat-log-message']);
 
626
            }
 
627
        }
 
628
 
 
629
        if (showTimestamp)
 
630
            this._notification.appendTimestamp();
 
631
 
 
632
        for (let i = 0; i < pendingMessages.length; i++)
 
633
            this._notification.appendMessage(pendingMessages[i], true);
 
634
 
 
635
        if (pendingMessages.length > 0)
 
636
            this.notify();
 
637
    },
 
638
 
 
639
    _channelClosed: function() {
 
640
        this._channel.disconnect(this._closedId);
 
641
        this._channel.disconnect(this._receivedId);
 
642
        this._channel.disconnect(this._pendingId);
 
643
        this._channel.disconnect(this._sentId);
 
644
 
 
645
        this._contact.disconnect(this._notifyAliasId);
 
646
        this._contact.disconnect(this._notifyAvatarId);
 
647
        this._contact.disconnect(this._presenceChangedId);
 
648
 
 
649
        this.destroy();
 
650
    },
 
651
 
 
652
    /* All messages are new messages for Telepathy sources */
 
653
    get count() {
 
654
        return this._pendingMessages.length;
 
655
    },
 
656
 
 
657
    get unseenCount() {
 
658
        return this.count;
 
659
    },
 
660
 
 
661
    get countVisible() {
 
662
        return this.count > 0;
 
663
    },
 
664
 
 
665
    _messageReceived: function(channel, message) {
 
666
        if (message.get_message_type() == Tp.ChannelTextMessageType.DELIVERY_REPORT)
 
667
            return;
 
668
 
 
669
        this._pendingMessages.push(message);
 
670
        this.countUpdated();
 
671
 
 
672
        message = makeMessageFromTpMessage(message, NotificationDirection.RECEIVED);
 
673
        this._notification.appendMessage(message);
 
674
 
 
675
        // Wait a bit before notifying for the received message, a handler
 
676
        // could ack it in the meantime.
 
677
        if (this._notifyTimeoutId != 0)
 
678
            Mainloop.source_remove(this._notifyTimeoutId);
 
679
        this._notifyTimeoutId = Mainloop.timeout_add(500,
 
680
            Lang.bind(this, this._notifyTimeout));
 
681
    },
 
682
 
 
683
    _notifyTimeout: function() {
 
684
        if (this._pendingMessages.length != 0)
 
685
            this.notify();
 
686
 
 
687
        this._notifyTimeoutId = 0;
 
688
 
 
689
        return false;
 
690
    },
 
691
 
 
692
    // This is called for both messages we send from
 
693
    // our client and other clients as well.
 
694
    _messageSent: function(channel, message, flags, token) {
 
695
        message = makeMessageFromTpMessage(message, NotificationDirection.SENT);
 
696
        this._notification.appendMessage(message);
 
697
    },
 
698
 
 
699
    notify: function() {
 
700
        this.parent(this._notification);
 
701
    },
 
702
 
 
703
    respond: function(text) {
 
704
        let type;
 
705
        if (text.slice(0, 4) == '/me ') {
 
706
            type = Tp.ChannelTextMessageType.ACTION;
 
707
            text = text.slice(4);
 
708
        } else {
 
709
            type = Tp.ChannelTextMessageType.NORMAL;
 
710
        }
 
711
 
 
712
        let msg = Tp.ClientMessage.new_text(type, text);
 
713
        this._channel.send_message_async(msg, 0, Lang.bind(this, function (src, result) {
 
714
            this._channel.send_message_finish(result); 
 
715
        }));
 
716
    },
 
717
 
 
718
    setChatState: function(state) {
 
719
        // We don't want to send COMPOSING every time a letter is typed into
 
720
        // the entry. We send the state only when it changes. Telepathy/Empathy
 
721
        // might change it behind our back if the user is using both
 
722
        // gnome-shell's entry and the Empathy conversation window. We could
 
723
        // keep track of it with the ChatStateChanged signal but it is good
 
724
        // enough right now.
 
725
        if (state != this._chatState) {
 
726
          this._chatState = state;
 
727
          this._channel.set_chat_state_async(state, null);
 
728
        }
 
729
    },
 
730
 
 
731
    _presenceChanged: function (contact, presence, status, message) {
 
732
        let msg, title;
 
733
 
 
734
        title = GLib.markup_escape_text(this.title, -1);
 
735
 
 
736
        this._notification.update(this._notification.title, null, { customContent: true, secondaryIcon: this.createSecondaryIcon() });
 
737
 
 
738
        if (message)
 
739
            msg += ' <i>(' + GLib.markup_escape_text(message, -1) + ')</i>';
 
740
    },
 
741
 
 
742
    _pendingRemoved: function(channel, message) {
 
743
        let idx = this._pendingMessages.indexOf(message);
 
744
 
 
745
        if (idx >= 0) {
 
746
            this._pendingMessages.splice(idx, 1);
 
747
            this.countUpdated();
 
748
        }
 
749
    },
 
750
 
 
751
    _ackMessages: function() {
 
752
        // Don't clear our messages here, tp-glib will send a
 
753
        // 'pending-message-removed' for each one.
 
754
        this._channel.ack_all_pending_messages_async(Lang.bind(this, function(src, result) {
 
755
            this._channel.ack_all_pending_messages_finish(result);}));
 
756
    }
 
757
});
 
758
 
 
759
const ChatNotification = new Lang.Class({
 
760
    Name: 'ChatNotification',
 
761
    Extends: MessageTray.Notification,
 
762
 
 
763
    _init: function(source) {
 
764
        this.parent(source, source.title, null, { customContent: true, secondaryIcon: source.createSecondaryIcon() });
 
765
        this.setResident(true);
 
766
 
 
767
        this._responseEntry = new St.Entry({ style_class: 'chat-response',
 
768
                                             can_focus: true });
 
769
        this._responseEntry.clutter_text.connect('activate', Lang.bind(this, this._onEntryActivated));
 
770
        this._responseEntry.clutter_text.connect('text-changed', Lang.bind(this, this._onEntryChanged));
 
771
        this.setActionArea(this._responseEntry);
 
772
 
 
773
        this._responseEntry.clutter_text.connect('key-focus-in', Lang.bind(this, function() {
 
774
            this.focused = true;
 
775
        }));
 
776
        this._responseEntry.clutter_text.connect('key-focus-out', Lang.bind(this, function() {
 
777
            this.focused = false;
 
778
            this.emit('unfocused');
 
779
        }));
 
780
 
 
781
        this._oldMaxScrollAdjustment = 0;
 
782
        this._createScrollArea();
 
783
        this._lastGroup = null;
 
784
        this._lastGroupActor = null;
 
785
 
 
786
        this._scrollArea.vscroll.adjustment.connect('changed', Lang.bind(this, function(adjustment) {
 
787
            let currentValue = adjustment.value + adjustment.page_size;
 
788
            if (currentValue == this._oldMaxScrollAdjustment)
 
789
                this.scrollTo(St.Side.BOTTOM);
 
790
            this._oldMaxScrollAdjustment = adjustment.upper;
 
791
        }));
 
792
 
 
793
        this._inputHistory = new History.HistoryManager({ entry: this._responseEntry.clutter_text });
 
794
 
 
795
        this._history = [];
 
796
        this._timestampTimeoutId = 0;
 
797
        this._composingTimeoutId = 0;
 
798
    },
 
799
 
 
800
    /**
 
801
     * appendMessage:
 
802
     * @message: An object with the properties:
 
803
     *   text: the body of the message,
 
804
     *   messageType: a #Tp.ChannelTextMessageType,
 
805
     *   sender: the name of the sender,
 
806
     *   timestamp: the time the message was sent
 
807
     *   direction: a #NotificationDirection
 
808
     * 
 
809
     * @noTimestamp: Whether to add a timestamp. If %true, no timestamp
 
810
     *   will be added, regardless of the difference since the
 
811
     *   last timestamp
 
812
     */
 
813
    appendMessage: function(message, noTimestamp) {
 
814
        let messageBody = GLib.markup_escape_text(message.text, -1);
 
815
        let styles = [message.direction];
 
816
 
 
817
        if (message.messageType == Tp.ChannelTextMessageType.ACTION) {
 
818
            let senderAlias = GLib.markup_escape_text(message.sender, -1);
 
819
            messageBody = '<i>%s</i> %s'.format(senderAlias, messageBody);
 
820
            styles.push('chat-action');
 
821
        }
 
822
 
 
823
        if (message.direction == NotificationDirection.RECEIVED) {
 
824
            this.update(this.source.title, messageBody, { customContent: true,
 
825
                                                          bannerMarkup: true });
 
826
        }
 
827
 
 
828
        let group = (message.direction == NotificationDirection.RECEIVED ?
 
829
                     'received' : 'sent');
 
830
 
 
831
        this._append({ body: messageBody,
 
832
                       group: group,
 
833
                       styles: styles,
 
834
                       timestamp: message.timestamp,
 
835
                       noTimestamp: noTimestamp });
 
836
    },
 
837
 
 
838
    _filterMessages: function() {
 
839
        if (this._history.length < 1)
 
840
            return;
 
841
 
 
842
        let lastMessageTime = this._history[0].time;
 
843
        let currentTime = (Date.now() / 1000);
 
844
 
 
845
        // Keep the scrollback from growing too long. If the most
 
846
        // recent message (before the one we just added) is within
 
847
        // SCROLLBACK_RECENT_TIME, we will keep
 
848
        // SCROLLBACK_RECENT_LENGTH previous messages. Otherwise
 
849
        // we'll keep SCROLLBACK_IDLE_LENGTH messages.
 
850
 
 
851
        let maxLength = (lastMessageTime < currentTime - SCROLLBACK_RECENT_TIME) ?
 
852
            SCROLLBACK_IDLE_LENGTH : SCROLLBACK_RECENT_LENGTH;
 
853
 
 
854
        let filteredHistory = this._history.filter(function(item) { return item.realMessage });
 
855
        if (filteredHistory.length > maxLength) {
 
856
            let lastMessageToKeep = filteredHistory[maxLength];
 
857
            let expired = this._history.splice(this._history.indexOf(lastMessageToKeep));
 
858
            for (let i = 0; i < expired.length; i++)
 
859
                expired[i].actor.destroy();
 
860
        }
 
861
 
 
862
        let groups = this._contentArea.get_children();
 
863
        for (let i = 0; i < groups.length; i++) {
 
864
            let group = groups[i];
 
865
            if (group.get_n_children() == 0)
 
866
                group.destroy();
 
867
        }
 
868
    },
 
869
 
 
870
    /**
 
871
     * _append:
 
872
     * @props: An object with the properties:
 
873
     *  body: The text of the message.
 
874
     *  group: The group of the message, one of:
 
875
     *         'received', 'sent', 'meta'.
 
876
     *  styles: Style class names for the message to have.
 
877
     *  timestamp: The timestamp of the message.
 
878
     *  noTimestamp: suppress timestamp signal?
 
879
     *  childProps: props to add the actor with.
 
880
     */
 
881
    _append: function(props) {
 
882
        let currentTime = (Date.now() / 1000);
 
883
        props = Params.parse(props, { body: null,
 
884
                                      group: null,
 
885
                                      styles: [],
 
886
                                      timestamp: currentTime,
 
887
                                      noTimestamp: false,
 
888
                                      childProps: null });
 
889
 
 
890
        // Reset the old message timeout
 
891
        if (this._timestampTimeoutId)
 
892
            Mainloop.source_remove(this._timestampTimeoutId);
 
893
 
 
894
        let highlighter = new MessageTray.URLHighlighter(props.body,
 
895
                                                         true,  // line wrap?
 
896
                                                         true); // allow markup?
 
897
 
 
898
        let body = highlighter.actor;
 
899
 
 
900
        let styles = props.styles;
 
901
        for (let i = 0; i < styles.length; i++)
 
902
            body.add_style_class_name(styles[i]);
 
903
 
 
904
        let group = props.group;
 
905
        if (group != this._lastGroup) {
 
906
            let style = 'chat-group-' + group;
 
907
            this._lastGroup = group;
 
908
            this._lastGroupActor = new St.BoxLayout({ style_class: style,
 
909
                                                      vertical: true });
 
910
            this.addActor(this._lastGroupActor);
 
911
        }
 
912
 
 
913
        this._lastGroupActor.add(body, props.childProps);
 
914
 
 
915
        this.updated();
 
916
 
 
917
        let timestamp = props.timestamp;
 
918
        this._history.unshift({ actor: body, time: timestamp,
 
919
                                realMessage: group != 'meta' });
 
920
 
 
921
        if (!props.noTimestamp) {
 
922
            if (timestamp < currentTime - SCROLLBACK_IMMEDIATE_TIME)
 
923
                this.appendTimestamp();
 
924
            else
 
925
                // Schedule a new timestamp in SCROLLBACK_IMMEDIATE_TIME
 
926
                // from the timestamp of the message.
 
927
                this._timestampTimeoutId = Mainloop.timeout_add_seconds(
 
928
                    SCROLLBACK_IMMEDIATE_TIME - (currentTime - timestamp),
 
929
                    Lang.bind(this, this.appendTimestamp));
 
930
        }
 
931
 
 
932
        this._filterMessages();
 
933
    },
 
934
 
 
935
    _formatTimestamp: function(date) {
 
936
        let now = new Date();
 
937
 
 
938
        var daysAgo = (now.getTime() - date.getTime()) / (24 * 60 * 60 * 1000);
 
939
 
 
940
        let format;
 
941
 
 
942
        // Show a week day and time if date is in the last week
 
943
        if (daysAgo < 1 || (daysAgo < 7 && now.getDay() != date.getDay())) {
 
944
            /* Translators: this is a time format string followed by a date.
 
945
             If applicable, replace %X with a strftime format valid for your
 
946
             locale, without seconds. */
 
947
            // xgettext:no-c-format
 
948
            format = _("Sent at <b>%X</b> on <b>%A</b>");
 
949
 
 
950
        } else if (date.getYear() == now.getYear()) {
 
951
            /* Translators: this is a time format in the style of "Wednesday, May 25",
 
952
             shown when you get a chat message in the same year. */
 
953
            // xgettext:no-c-format
 
954
            format = _("Sent on <b>%A</b>, <b>%B %d</b>");
 
955
        } else {
 
956
            /* Translators: this is a time format in the style of "Wednesday, May 25, 2012",
 
957
             shown when you get a chat message in a different year. */
 
958
            // xgettext:no-c-format
 
959
            format = _("Sent on <b>%A</b>, <b>%B %d</b>, %Y");
 
960
        }
 
961
 
 
962
        return date.toLocaleFormat(format);
 
963
    },
 
964
 
 
965
    appendTimestamp: function() {
 
966
        let lastMessageTime = this._history[0].time;
 
967
        let lastMessageDate = new Date(lastMessageTime * 1000);
 
968
 
 
969
        let timeLabel = this._append({ body: this._formatTimestamp(lastMessageDate),
 
970
                                       group: 'meta',
 
971
                                       styles: ['chat-meta-message'],
 
972
                                       childProps: { expand: true, x_fill: false,
 
973
                                                     x_align: St.Align.END },
 
974
                                       noTimestamp: true,
 
975
                                       timestamp: lastMessageTime });
 
976
 
 
977
        this._filterMessages();
 
978
 
 
979
        return false;
 
980
    },
 
981
 
 
982
    appendAliasChange: function(oldAlias, newAlias) {
 
983
        oldAlias = GLib.markup_escape_text(oldAlias, -1);
 
984
        newAlias = GLib.markup_escape_text(newAlias, -1);
 
985
 
 
986
        /* Translators: this is the other person changing their old IM name to their new
 
987
           IM name. */
 
988
        let message = '<i>' + _("%s is now known as %s").format(oldAlias, newAlias) + '</i>';
 
989
 
 
990
        let label = this._append({ body: message,
 
991
                                   group: 'meta',
 
992
                                   styles: ['chat-meta-message'] });
 
993
 
 
994
        this.update(newAlias, null, { customContent: true });
 
995
 
 
996
        this._filterMessages();
 
997
    },
 
998
 
 
999
    _onEntryActivated: function() {
 
1000
        let text = this._responseEntry.get_text();
 
1001
        if (text == '')
 
1002
            return;
 
1003
 
 
1004
        this._inputHistory.addItem(text);
 
1005
 
 
1006
        // Telepathy sends out the Sent signal for us.
 
1007
        // see Source._messageSent
 
1008
        this._responseEntry.set_text('');
 
1009
        this.source.respond(text);
 
1010
    },
 
1011
 
 
1012
    _composingStopTimeout: function() {
 
1013
        this._composingTimeoutId = 0;
 
1014
 
 
1015
        this.source.setChatState(Tp.ChannelChatState.PAUSED);
 
1016
 
 
1017
        return false;
 
1018
    },
 
1019
 
 
1020
    _onEntryChanged: function() {
 
1021
        let text = this._responseEntry.get_text();
 
1022
 
 
1023
        // If we're typing, we want to send COMPOSING.
 
1024
        // If we empty the entry, we want to send ACTIVE.
 
1025
        // If we've stopped typing for COMPOSING_STOP_TIMEOUT
 
1026
        //    seconds, we want to send PAUSED.
 
1027
 
 
1028
        // Remove composing timeout.
 
1029
        if (this._composingTimeoutId > 0) {
 
1030
            Mainloop.source_remove(this._composingTimeoutId);
 
1031
            this._composingTimeoutId = 0;
 
1032
        }
 
1033
 
 
1034
        if (text != '') {
 
1035
            this.source.setChatState(Tp.ChannelChatState.COMPOSING);
 
1036
 
 
1037
            this._composingTimeoutId = Mainloop.timeout_add_seconds(
 
1038
                COMPOSING_STOP_TIMEOUT,
 
1039
                Lang.bind(this, this._composingStopTimeout));
 
1040
        } else {
 
1041
            this.source.setChatState(Tp.ChannelChatState.ACTIVE);
 
1042
        }
 
1043
    }
 
1044
});
 
1045
 
 
1046
const ApproverSource = new Lang.Class({
 
1047
    Name: 'ApproverSource',
 
1048
    Extends: MessageTray.Source,
 
1049
 
 
1050
    _init: function(dispatchOp, text, gicon) {
 
1051
        this._gicon = gicon;
 
1052
 
 
1053
        this.parent(text);
 
1054
 
 
1055
        this._dispatchOp = dispatchOp;
 
1056
 
 
1057
        // Destroy the source if the channel dispatch operation is invalidated
 
1058
        // as we can't approve any more.
 
1059
        this._invalidId = dispatchOp.connect('invalidated',
 
1060
                                             Lang.bind(this, function(domain, code, msg) {
 
1061
            this.destroy();
 
1062
        }));
 
1063
    },
 
1064
 
 
1065
    destroy: function() {
 
1066
        if (this._invalidId != 0) {
 
1067
            this._dispatchOp.disconnect(this._invalidId);
 
1068
            this._invalidId = 0;
 
1069
        }
 
1070
 
 
1071
        this.parent();
 
1072
    },
 
1073
 
 
1074
    createIcon: function(size) {
 
1075
        return new St.Icon({ gicon: this._gicon,
 
1076
                             icon_size: size });
 
1077
    }
 
1078
});
 
1079
 
 
1080
const RoomInviteNotification = new Lang.Class({
 
1081
    Name: 'RoomInviteNotification',
 
1082
    Extends: MessageTray.Notification,
 
1083
 
 
1084
    _init: function(source, dispatchOp, channel, inviter) {
 
1085
        this.parent(source,
 
1086
                    /* translators: argument is a room name like
 
1087
                     * room@jabber.org for example. */
 
1088
                    _("Invitation to %s").format(channel.get_identifier()),
 
1089
                    null,
 
1090
                    { customContent: true });
 
1091
        this.setResident(true);
 
1092
 
 
1093
        /* translators: first argument is the name of a contact and the second
 
1094
         * one the name of a room. "Alice is inviting you to join room@jabber.org
 
1095
         * for example. */
 
1096
        this.addBody(_("%s is inviting you to join %s").format(inviter.get_alias(), channel.get_identifier()));
 
1097
 
 
1098
        this.addButton('decline', _("Decline"));
 
1099
        this.addButton('accept', _("Accept"));
 
1100
 
 
1101
        this.connect('action-invoked', Lang.bind(this, function(self, action) {
 
1102
            switch (action) {
 
1103
            case 'decline':
 
1104
                dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
 
1105
                                                '', function(src, result) {
 
1106
                    src.leave_channels_finish(result)});
 
1107
                break;
 
1108
            case 'accept':
 
1109
                dispatchOp.handle_with_time_async('', global.get_current_time(),
 
1110
                                                  function(src, result) {
 
1111
                    src.handle_with_time_finish(result)});
 
1112
                break;
 
1113
            }
 
1114
            this.destroy();
 
1115
        }));
 
1116
    }
 
1117
});
 
1118
 
 
1119
// Audio Video
 
1120
const AudioVideoNotification = new Lang.Class({
 
1121
    Name: 'AudioVideoNotification',
 
1122
    Extends: MessageTray.Notification,
 
1123
 
 
1124
    _init: function(source, dispatchOp, channel, contact, isVideo) {
 
1125
        let title = '';
 
1126
 
 
1127
        if (isVideo)
 
1128
             /* translators: argument is a contact name like Alice for example. */
 
1129
            title = _("Video call from %s").format(contact.get_alias());
 
1130
        else
 
1131
             /* translators: argument is a contact name like Alice for example. */
 
1132
            title = _("Call from %s").format(contact.get_alias());
 
1133
 
 
1134
        this.parent(source, title, null, { customContent: true });
 
1135
        this.setResident(true);
 
1136
 
 
1137
        this.addButton('reject', _("Reject"));
 
1138
        /* translators: this is a button label (verb), not a noun */
 
1139
        this.addButton('answer', _("Answer"));
 
1140
 
 
1141
        this.connect('action-invoked', Lang.bind(this, function(self, action) {
 
1142
            switch (action) {
 
1143
            case 'reject':
 
1144
                dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
 
1145
                                                '', function(src, result) {
 
1146
                    src.leave_channels_finish(result)});
 
1147
                break;
 
1148
            case 'answer':
 
1149
                dispatchOp.handle_with_time_async('', global.get_current_time(),
 
1150
                                                  function(src, result) {
 
1151
                    src.handle_with_time_finish(result)});
 
1152
                break;
 
1153
            }
 
1154
            this.destroy();
 
1155
        }));
 
1156
    }
 
1157
});
 
1158
 
 
1159
// File Transfer
 
1160
const FileTransferNotification = new Lang.Class({
 
1161
    Name: 'FileTransferNotification',
 
1162
    Extends: MessageTray.Notification,
 
1163
 
 
1164
    _init: function(source, dispatchOp, channel, contact) {
 
1165
        this.parent(source,
 
1166
                    /* To translators: The first parameter is
 
1167
                     * the contact's alias and the second one is the
 
1168
                     * file name. The string will be something
 
1169
                     * like: "Alice is sending you test.ogg"
 
1170
                     */
 
1171
                    _("%s is sending you %s").format(contact.get_alias(),
 
1172
                                                     channel.get_filename()),
 
1173
                    null,
 
1174
                    { customContent: true });
 
1175
        this.setResident(true);
 
1176
 
 
1177
        this.addButton('decline', _("Decline"));
 
1178
        this.addButton('accept', _("Accept"));
 
1179
 
 
1180
        this.connect('action-invoked', Lang.bind(this, function(self, action) {
 
1181
            switch (action) {
 
1182
            case 'decline':
 
1183
                dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE,
 
1184
                                                '', function(src, result) {
 
1185
                    src.leave_channels_finish(result)});
 
1186
                break;
 
1187
            case 'accept':
 
1188
                dispatchOp.handle_with_time_async('', global.get_current_time(),
 
1189
                                                  function(src, result) {
 
1190
                    src.handle_with_time_finish(result)});
 
1191
                break;
 
1192
            }
 
1193
            this.destroy();
 
1194
        }));
 
1195
    }
 
1196
});
 
1197
 
 
1198
// Subscription request
 
1199
const SubscriptionRequestNotification = new Lang.Class({
 
1200
    Name: 'SubscriptionRequestNotification',
 
1201
    Extends: MessageTray.Notification,
 
1202
 
 
1203
    _init: function(source, contact) {
 
1204
        this.parent(source,
 
1205
                    /* To translators: The parameter is the contact's alias */
 
1206
                    _("%s would like permission to see when you are online").format(contact.get_alias()),
 
1207
                    null, { customContent: true });
 
1208
 
 
1209
        this._contact = contact;
 
1210
        this._connection = contact.get_connection();
 
1211
 
 
1212
        let layout = new St.BoxLayout({ vertical: false });
 
1213
 
 
1214
        // Display avatar
 
1215
        let iconBox = new St.Bin({ style_class: 'avatar-box' });
 
1216
        iconBox._size = 48;
 
1217
 
 
1218
        let textureCache = St.TextureCache.get_default();
 
1219
        let file = contact.get_avatar_file();
 
1220
 
 
1221
        if (file) {
 
1222
            let uri = file.get_uri();
 
1223
            iconBox.child = textureCache.load_uri_async(uri, iconBox._size, iconBox._size);
 
1224
        }
 
1225
        else {
 
1226
            iconBox.child = new St.Icon({ icon_name: 'avatar-default',
 
1227
                                          icon_size: iconBox._size });
 
1228
        }
 
1229
 
 
1230
        layout.add(iconBox);
 
1231
 
 
1232
        // subscription request message
 
1233
        let label = new St.Label({ style_class: 'subscription-message',
 
1234
                                   text: contact.get_publish_request() });
 
1235
 
 
1236
        layout.add(label);
 
1237
 
 
1238
        this.addActor(layout);
 
1239
 
 
1240
        this.addButton('decline', _("Decline"));
 
1241
        this.addButton('accept', _("Accept"));
 
1242
 
 
1243
        this.connect('action-invoked', Lang.bind(this, function(self, action) {
 
1244
            switch (action) {
 
1245
            case 'decline':
 
1246
                contact.remove_async(function(src, result) {
 
1247
                    src.remove_finish(result)});
 
1248
                break;
 
1249
            case 'accept':
 
1250
                // Authorize the contact and request to see his status as well
 
1251
                contact.authorize_publication_async(function(src, result) {
 
1252
                    src.authorize_publication_finish(result)});
 
1253
 
 
1254
                contact.request_subscription_async('', function(src, result) {
 
1255
                    src.request_subscription_finish(result)});
 
1256
                break;
 
1257
            }
 
1258
 
 
1259
            // rely on _subscriptionStatesChangedCb to destroy the
 
1260
            // notification
 
1261
        }));
 
1262
 
 
1263
        this._changedId = contact.connect('subscription-states-changed',
 
1264
            Lang.bind(this, this._subscriptionStatesChangedCb));
 
1265
        this._invalidatedId = this._connection.connect('invalidated',
 
1266
            Lang.bind(this, this.destroy));
 
1267
    },
 
1268
 
 
1269
    destroy: function() {
 
1270
        if (this._changedId != 0) {
 
1271
            this._contact.disconnect(this._changedId);
 
1272
            this._changedId = 0;
 
1273
        }
 
1274
 
 
1275
        if (this._invalidatedId != 0) {
 
1276
            this._connection.disconnect(this._invalidatedId);
 
1277
            this._invalidatedId = 0;
 
1278
        }
 
1279
 
 
1280
        this.parent();
 
1281
    },
 
1282
 
 
1283
    _subscriptionStatesChangedCb: function(contact, subscribe, publish, msg) {
 
1284
        // Destroy the notification if the subscription request has been
 
1285
        // answered
 
1286
        if (publish != Tp.SubscriptionState.ASK)
 
1287
            this.destroy();
 
1288
    }
 
1289
});
 
1290
 
 
1291
// Messages from empathy/libempathy/empathy-utils.c
 
1292
// create_errors_to_message_hash()
 
1293
 
 
1294
/* Translator note: these should be the same messages that are
 
1295
 * used in Empathy, so just copy and paste from there. */
 
1296
let _connectionErrorMessages = {};
 
1297
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.NETWORK_ERROR)]
 
1298
  = _("Network error");
 
1299
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.AUTHENTICATION_FAILED)]
 
1300
  = _("Authentication failed");
 
1301
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ENCRYPTION_ERROR)]
 
1302
  = _("Encryption error");
 
1303
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_NOT_PROVIDED)]
 
1304
  = _("Certificate not provided");
 
1305
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_UNTRUSTED)]
 
1306
  = _("Certificate untrusted");
 
1307
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_EXPIRED)]
 
1308
  = _("Certificate expired");
 
1309
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_NOT_ACTIVATED)]
 
1310
  = _("Certificate not activated");
 
1311
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_HOSTNAME_MISMATCH)]
 
1312
  = _("Certificate hostname mismatch");
 
1313
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_FINGERPRINT_MISMATCH)]
 
1314
  = _("Certificate fingerprint mismatch");
 
1315
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_SELF_SIGNED)]
 
1316
  = _("Certificate self-signed");
 
1317
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CANCELLED)]
 
1318
  = _("Status is set to offline");
 
1319
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ENCRYPTION_NOT_AVAILABLE)]
 
1320
  = _("Encryption is not available");
 
1321
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_INVALID)]
 
1322
  = _("Certificate is invalid");
 
1323
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_REFUSED)]
 
1324
  = _("Connection has been refused");
 
1325
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_FAILED)]
 
1326
  = _("Connection can't be established");
 
1327
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_LOST)]
 
1328
  = _("Connection has been lost");
 
1329
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ALREADY_CONNECTED)]
 
1330
  = _("This account is already connected to the server");
 
1331
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_REPLACED)]
 
1332
  = _("Connection has been replaced by a new connection using the same resource");
 
1333
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.REGISTRATION_EXISTS)]
 
1334
  = _("The account already exists on the server");
 
1335
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.SERVICE_BUSY)]
 
1336
  = _("Server is currently too busy to handle the connection");
 
1337
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_REVOKED)]
 
1338
  = _("Certificate has been revoked");
 
1339
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_INSECURE)]
 
1340
  = _("Certificate uses an insecure cipher algorithm or is cryptographically weak");
 
1341
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_LIMIT_EXCEEDED)]
 
1342
  = _("The length of the server certificate, or the depth of the server certificate chain, exceed the limits imposed by the cryptography library");
 
1343
_connectionErrorMessages['org.freedesktop.DBus.Error.NoReply']
 
1344
  = _("Internal error");
 
1345
 
 
1346
const AccountNotification = new Lang.Class({
 
1347
    Name: 'AccountNotification',
 
1348
    Extends: MessageTray.Notification,
 
1349
 
 
1350
    _init: function(source, account, connectionError) {
 
1351
        this.parent(source,
 
1352
                    /* translators: argument is the account name, like
 
1353
                     * name@jabber.org for example. */
 
1354
                    _("Connection to %s failed").format(account.get_display_name()),
 
1355
                    null, { customContent: true });
 
1356
 
 
1357
        this._label = new St.Label();
 
1358
        this.addActor(this._label);
 
1359
        this._updateMessage(connectionError);
 
1360
 
 
1361
        this._account = account;
 
1362
 
 
1363
        this.addButton('reconnect', _("Reconnect"));
 
1364
        this.addButton('edit', _("Edit account"));
 
1365
 
 
1366
        this.connect('action-invoked', Lang.bind(this, function(self, action) {
 
1367
            switch (action) {
 
1368
            case 'reconnect':
 
1369
                // If it fails again, a new notification should pop up with the
 
1370
                // new error.
 
1371
                account.reconnect_async(null);
 
1372
                break;
 
1373
            case 'edit':
 
1374
                let cmd = '/usr/bin/empathy-accounts'
 
1375
                        + ' --select-account=%s'
 
1376
                        .format(account.get_path_suffix());
 
1377
                let app_info = Gio.app_info_create_from_commandline(cmd, null, 0);
 
1378
                app_info.launch([], global.create_app_launch_context());
 
1379
                break;
 
1380
            }
 
1381
            this.destroy();
 
1382
        }));
 
1383
 
 
1384
        this._enabledId = account.connect('notify::enabled',
 
1385
                                          Lang.bind(this, function() {
 
1386
                                              if (!account.is_enabled())
 
1387
                                                  this.destroy();
 
1388
                                          }));
 
1389
 
 
1390
        this._invalidatedId = account.connect('invalidated',
 
1391
                                              Lang.bind(this, this.destroy));
 
1392
 
 
1393
        this._connectionStatusId = account.connect('notify::connection-status',
 
1394
            Lang.bind(this, function() {
 
1395
                let status = account.connection_status;
 
1396
                if (status == Tp.ConnectionStatus.CONNECTED) {
 
1397
                    this.destroy();
 
1398
                } else if (status == Tp.ConnectionStatus.DISCONNECTED) {
 
1399
                    this._updateMessage(account.connection_error);
 
1400
                }
 
1401
            }));
 
1402
    },
 
1403
 
 
1404
    _updateMessage: function(connectionError) {
 
1405
        let message;
 
1406
        if (connectionError in _connectionErrorMessages) {
 
1407
            message = _connectionErrorMessages[connectionError];
 
1408
        } else {
 
1409
            message = _("Unknown reason");
 
1410
        }
 
1411
        this._label.set_text(message);
 
1412
    },
 
1413
 
 
1414
    destroy: function() {
 
1415
        if (this._enabledId != 0) {
 
1416
            this._account.disconnect(this._enabledId);
 
1417
            this._enabledId = 0;
 
1418
        }
 
1419
 
 
1420
        if (this._invalidatedId != 0) {
 
1421
            this._account.disconnect(this._invalidatedId);
 
1422
            this._invalidatedId = 0;
 
1423
        }
 
1424
 
 
1425
        if (this._connectionStatusId != 0) {
 
1426
            this._account.disconnect(this._connectionStatusId);
 
1427
            this._connectionStatusId = 0;
 
1428
        }
 
1429
 
 
1430
        this.parent();
 
1431
    }
 
1432
});
 
1433
const Component = TelepathyClient;