chat_control.py 59.8 KB
Newer Older
Philipp Hörist's avatar
Philipp Hörist committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
#                         Nikos Kouremenos <kourem AT gmail.com>
#                         Travis Shirk <travis AT pobox.com>
# Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
#                    Julien Pivotto <roidelapluie AT gmail.com>
# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
#                         Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
nicfit's avatar
added    
nicfit committed
25

Philipp Hörist's avatar
Philipp Hörist committed
26
27
28
from typing import ClassVar  # pylint: disable=unused-import
from typing import Type  # pylint: disable=unused-import

nkour's avatar
nkour committed
29
import os
nicfit's avatar
nicfit committed
30
import time
Philipp Hörist's avatar
Philipp Hörist committed
31

32
from gi.repository import Gtk
Philipp Hörist's avatar
Philipp Hörist committed
33
from gi.repository import Gio
34
from gi.repository import Pango
Yann Leboulanger's avatar
Yann Leboulanger committed
35
from gi.repository import GLib
36
from gi.repository import Gdk
37

Philipp Hörist's avatar
Philipp Hörist committed
38
from nbxmpp.namespaces import Namespace
Philipp Hörist's avatar
Philipp Hörist committed
39

40
from gajim.common import app
André's avatar
André committed
41
42
43
from gajim.common import helpers
from gajim.common import ged
from gajim.common import i18n
44
from gajim.common.i18n import _
45
from gajim.common.helpers import AdditionalDataDict
46
from gajim.common.helpers import open_uri
47
from gajim.common.helpers import geo_provider_from_location
48
from gajim.common.helpers import event_filter
Philipp Hörist's avatar
Philipp Hörist committed
49
from gajim.common.helpers import open_file
André's avatar
André committed
50
from gajim.common.contacts import GC_Contact
51
52
53
from gajim.common.const import AvatarSize
from gajim.common.const import KindConstant
from gajim.common.const import Chatstate
54
from gajim.common.const import PEPEventType
nicfit's avatar
added    
nicfit committed
55

56
57
58
59
from gajim import gtkgui_helpers
from gajim import gui_menu_builder
from gajim import dialogs

60
from gajim.gtk.dialogs import DialogButton
61
from gajim.gtk.dialogs import NewConfirmationDialog
62
from gajim.gtk.add_contact import AddNewContactWindow
Philipp Hörist's avatar
Philipp Hörist committed
63
from gajim.gtk.util import get_cursor
64
from gajim.gtk.util import format_mood
Philipp Hörist's avatar
Philipp Hörist committed
65
from gajim.gtk.util import format_activity
Philipp Hörist's avatar
Philipp Hörist committed
66
from gajim.gtk.util import format_tune
Philipp Hörist's avatar
Philipp Hörist committed
67
from gajim.gtk.util import format_location
Philipp Hörist's avatar
Philipp Hörist committed
68
from gajim.gtk.util import get_activity_icon_name
69
from gajim.gtk.util import make_href_markup
70
from gajim.gtk.const import ControlType
71

André's avatar
André committed
72
from gajim.command_system.implementation.hosts import ChatCommands
Philipp Hörist's avatar
Philipp Hörist committed
73
from gajim.command_system.framework import CommandHost  # pylint: disable=unused-import
André's avatar
André committed
74
from gajim.chat_control_base import ChatControlBase
75

76
77
78
79
80
81
82
83
84
85
86
87
88
89

class JingleState:
    __slots__ = ('sid', 'state', 'available', 'banner_image', 'action', 'set_state', 'update')

    def __init__(self, state, banner_image, set_state, update):
        self.sid = None
        self.state = state
        self.available = False
        self.banner_image = banner_image
        self.action = None
        self.set_state = set_state
        self.update = update


90
################################################################################
91
class ChatControl(ChatControlBase):
92
93
94
95
    """
    A control for standard 1-1 chat
    """
    (
96
97
98
99
100
        JINGLE_STATE_NULL,
        JINGLE_STATE_CONNECTING,
        JINGLE_STATE_CONNECTION_RECEIVED,
        JINGLE_STATE_CONNECTED,
        JINGLE_STATE_ERROR
101
    ) = range(5)
102

103
    _type = ControlType.CHAT
104
105
106
107
    old_msg_kind = None # last kind of the printed message

    # Set a command host to bound to. Every command given through a chat will be
    # processed with this command host.
Philipp Hörist's avatar
Philipp Hörist committed
108
    COMMAND_HOST = ChatCommands  # type: ClassVar[Type[CommandHost]]
109

110
    def __init__(self, parent_win, contact, acct, session, resource=None):
111
112
113
114
115
116
        ChatControlBase.__init__(self,
                                 parent_win,
                                 'chat_control',
                                 contact,
                                 acct,
                                 resource)
117

118
119
        self.last_recv_message_id = None
        self.last_recv_message_marks = None
120
        self.last_message_timestamp = None
Philipp Hörist's avatar
Philipp Hörist committed
121

Philipp Hörist's avatar
Philipp Hörist committed
122
        self.toggle_emoticons()
123

124
125
        if not app.config.get('hide_chat_banner'):
            self.xml.banner_eventbox.set_no_show_all(False)
126

127
128
        self.xml.sendfile_button.set_action_name(
            'win.send-file-%s' % self.control_id)
129

Philipp Hörist's avatar
Philipp Hörist committed
130
131
        # Menu for the HeaderBar
        self.control_menu = gui_menu_builder.get_singlechat_menu(
132
            self.control_id, self.account, self.contact.jid)
133

134
135
136
        # Settings menu
        self.xml.settings_menu.set_menu_model(self.control_menu)

137
138
139
140
        self.jingle = {
            'audio': JingleState(self.JINGLE_STATE_NULL, self.xml.audio_banner_image, self.set_audio_state, self.update_audio),
            'video': JingleState(self.JINGLE_STATE_NULL, self.xml.video_banner_image, self.set_video_state, self.update_video),
        }
141
142
143
144
145
146

        self.update_toolbar()
        self.update_all_pep_types()
        self.show_avatar()

        # Hook up signals
147
        widget = self.xml.location_eventbox
148
        id_ = widget.connect('button-release-event',
149
                             self.on_location_eventbox_button_release_event)
150
        self.handlers[id_] = widget
151
        id_ = widget.connect('enter-notify-event',
152
                             self.on_location_eventbox_enter_notify_event)
153
154
        self.handlers[id_] = widget
        id_ = widget.connect('leave-notify-event',
155
                             self.on_location_eventbox_leave_notify_event)
156
        self.handlers[id_] = widget
157
158
159
160
161
162
163
164

        for key in ('1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'):
            widget = self.xml.get_object(key + '_button')
            id_ = widget.connect('pressed', self.on_num_button_pressed, key)
            self.handlers[id_] = widget
            id_ = widget.connect('released', self.on_num_button_released)
            self.handlers[id_] = widget

165
        widget = self.xml.mic_hscale
166
167
168
        id_ = widget.connect('value_changed', self.on_mic_hscale_value_changed)
        self.handlers[id_] = widget

169
        widget = self.xml.sound_hscale
170
171
        id_ = widget.connect('value_changed',
                             self.on_sound_hscale_value_changed)
172
173
        self.handlers[id_] = widget

174
        self.info_bar = Gtk.InfoBar()
175
        content_area = self.info_bar.get_content_area()
176
        self.info_bar_label = Gtk.Label()
177
        self.info_bar_label.set_use_markup(True)
178
179
        self.info_bar_label.set_halign(Gtk.Align.START)
        self.info_bar_label.set_valign(Gtk.Align.START)
180
        self.info_bar_label.set_ellipsize(Pango.EllipsizeMode.END)
181
182
        content_area.add(self.info_bar_label)
        self.info_bar.set_no_show_all(True)
183
184
185

        self.xml.vbox2.pack_start(self.info_bar, False, True, 5)
        self.xml.vbox2.reorder_child(self.info_bar, 1)
186
187
188
189
190
191

        # List of waiting infobar messages
        self.info_bar_queue = []

        self.subscribe_events()

192
193
194
195
196
        if not session:
            # Don't use previous session if we want to a specific resource
            # and it's not the same
            if not resource:
                resource = contact.resource
197
            session = app.connections[self.account].find_controlless_session(
198
                self.contact.jid, resource)
199

Philipp Hörist's avatar
Philipp Hörist committed
200
        self.setup_seclabel()
201
202
203
204
        if session:
            session.control = self
            self.session = session

Philipp Hörist's avatar
Philipp Hörist committed
205
        self.add_actions()
206
        self.update_ui()
Philipp Hörist's avatar
Philipp Hörist committed
207
208
        self.set_lock_image()

209
        self.xml.encryption_menu.set_menu_model(
210
            gui_menu_builder.get_encryption_menu(
211
                self.control_id, self._type, self.account == 'Local'))
212
        self.set_encryption_menu_icon()
213
214
215
216
        # restore previous conversation
        self.restore_conversation()
        self.msg_textview.grab_focus()

217
        # pylint: disable=line-too-long
Philipp Hörist's avatar
Philipp Hörist committed
218
        self.register_events([
219
220
221
222
223
224
            ('nickname-received', ged.GUI1, self._on_nickname_received),
            ('mood-received', ged.GUI1, self._on_mood_received),
            ('activity-received', ged.GUI1, self._on_activity_received),
            ('tune-received', ged.GUI1, self._on_tune_received),
            ('location-received', ged.GUI1, self._on_location_received),
            ('update-client-info', ged.GUI1, self._on_update_client_info),
225
226
227
228
229
            ('chatstate-received', ged.GUI1, self._on_chatstate_received),
            ('caps-update', ged.GUI1, self._on_caps_update),
            ('message-sent', ged.OUT_POSTCORE, self._on_message_sent),
            ('mam-decrypted-message-received', ged.GUI1, self._on_mam_decrypted_message_received),
            ('decrypted-message-received', ged.GUI1, self._on_decrypted_message_received),
230
231
232
            ('receipt-received', ged.GUI1, self._receipt_received),
            ('message-error', ged.GUI1, self._on_message_error),
            ('zeroconf-error', ged.GUI1, self._on_zeroconf_error),
Philipp Hörist's avatar
Philipp Hörist committed
233
        ])
234

235
        if self._type.is_chat:
Emmanuel Gil Peyrot's avatar
Emmanuel Gil Peyrot committed
236
            # Don’t connect this when PrivateChatControl is used
237
            self.register_event('update-roster-avatar', ged.GUI1, self._on_update_roster_avatar)
238
239
        # pylint: enable=line-too-long

240
        # PluginSystem: adding GUI extension point for this ChatControl
241
        # instance object
242
        app.plugin_manager.gui_extension_point('chat_control', self)
Philipp Hörist's avatar
Philipp Hörist committed
243
244
        self.update_actions()

245
246
247
248
    @property
    def jid(self):
        return self.contact.jid

Philipp Hörist's avatar
Philipp Hörist committed
249
    def add_actions(self):
250
        super().add_actions()
Philipp Hörist's avatar
Philipp Hörist committed
251
252
253
        actions = [
            ('invite-contacts-', self._on_invite_contacts),
            ('add-to-roster-', self._on_add_to_roster),
254
            ('block-contact-', self._on_block_contact),
Philipp Hörist's avatar
Philipp Hörist committed
255
            ('information-', self._on_information),
256
        ]
Philipp Hörist's avatar
Philipp Hörist committed
257
258
259
260
261
262
263

        for action in actions:
            action_name, func = action
            act = Gio.SimpleAction.new(action_name + self.control_id, None)
            act.connect("activate", func)
            self.parent_win.window.add_action(act)

264
265
        audio = self.jingle['audio']
        audio.action = Gio.SimpleAction.new_stateful(
Philipp Hörist's avatar
Philipp Hörist committed
266
267
            'toggle-audio-' + self.control_id, None,
            GLib.Variant.new_boolean(False))
268
269
        audio.action.connect('change-state', self._on_audio)
        self.parent_win.window.add_action(audio.action)
Philipp Hörist's avatar
Philipp Hörist committed
270

271
272
        video = self.jingle['video']
        video.action = Gio.SimpleAction.new_stateful(
Philipp Hörist's avatar
Philipp Hörist committed
273
274
            'toggle-video-' + self.control_id,
            None, GLib.Variant.new_boolean(False))
275
276
        video.action.connect('change-state', self._on_video)
        self.parent_win.window.add_action(video.action)
Philipp Hörist's avatar
Philipp Hörist committed
277

278
        default_chatstate = app.config.get('send_chatstate_default')
279
        chatstate = app.config.get_per(
280
            'contacts', self.contact.jid, 'send_chatstate', default_chatstate)
281
282
283
284
285
286
287
288

        act = Gio.SimpleAction.new_stateful(
            'send-chatstate-' + self.control_id,
            GLib.VariantType.new("s"),
            GLib.Variant("s", chatstate))
        act.connect('change-state', self._on_send_chatstate)
        self.parent_win.window.add_action(act)

Philipp Hörist's avatar
Philipp Hörist committed
289
290
291
    def update_actions(self):
        win = self.parent_win.window
        online = app.account_is_connected(self.account)
Philipp Hörist's avatar
Philipp Hörist committed
292
        con = app.connections[self.account]
Philipp Hörist's avatar
Philipp Hörist committed
293
294
295

        # Add to roster
        if not isinstance(self.contact, GC_Contact) \
296
        and _('Not in contact list') in self.contact.groups and \
Philipp Hörist's avatar
Philipp Hörist committed
297
298
299
300
301
302
303
        app.connections[self.account].roster_supported and online:
            win.lookup_action(
                'add-to-roster-' + self.control_id).set_enabled(True)
        else:
            win.lookup_action(
                'add-to-roster-' + self.control_id).set_enabled(False)

304
305
306
307
308
        # Block contact
        win.lookup_action(
            'block-contact-' + self.control_id).set_enabled(
                online and con.get_module('Blocking').supported)

Philipp Hörist's avatar
Philipp Hörist committed
309
310
        # Audio
        win.lookup_action('toggle-audio-' + self.control_id).set_enabled(
311
            online and self.jingle['audio'].available)
Philipp Hörist's avatar
Philipp Hörist committed
312
313
314

        # Video
        win.lookup_action('toggle-video-' + self.control_id).set_enabled(
315
            online and self.jingle['video'].available)
Philipp Hörist's avatar
Philipp Hörist committed
316

317
318
319
320
        # Send file (HTTP File Upload)
        httpupload = win.lookup_action(
            'send-file-httpupload-' + self.control_id)
        httpupload.set_enabled(
Philipp Hörist's avatar
Philipp Hörist committed
321
            online and con.get_module('HTTPUpload').available)
322
323

        # Send file (Jingle)
Philipp Hörist's avatar
Philipp Hörist committed
324
        jingle_support = self.contact.supports(Namespace.JINGLE_FILE_TRANSFER_5)
Philipp Hörist's avatar
Philipp Hörist committed
325
        jingle_conditions = jingle_support and self.contact.show != 'offline'
326
327
328
        jingle = win.lookup_action('send-file-jingle-' + self.control_id)
        jingle.set_enabled(online and jingle_conditions)

Philipp Hörist's avatar
Philipp Hörist committed
329
        # Send file
330
331
        win.lookup_action(
            'send-file-' + self.control_id).set_enabled(
332
                jingle.get_enabled() or httpupload.get_enabled())
333
334

        # Set File Transfer Button tooltip
335
336
337
        if online and (httpupload.get_enabled() or jingle.get_enabled()):
            tooltip_text = _('Send File…')
        else:
338
            tooltip_text = _('No File Transfer available')
339
        self.xml.sendfile_button.set_tooltip_text(tooltip_text)
Philipp Hörist's avatar
Philipp Hörist committed
340
341
342
343
344
345

        # Convert to GC
        if app.config.get_per('accounts', self.account, 'is_zeroconf'):
            win.lookup_action(
                'invite-contacts-' + self.control_id).set_enabled(False)
        else:
Philipp Hörist's avatar
Philipp Hörist committed
346
            if self.contact.supports(Namespace.MUC) and online:
Philipp Hörist's avatar
Philipp Hörist committed
347
348
349
350
351
352
353
354
355
356
                win.lookup_action(
                    'invite-contacts-' + self.control_id).set_enabled(True)
            else:
                win.lookup_action(
                    'invite-contacts-' + self.control_id).set_enabled(False)

        # Information
        win.lookup_action(
            'information-' + self.control_id).set_enabled(online)

357
358
359
    def focus(self):
        self.msg_textview.grab_focus()

360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
    def delegate_action(self, action):
        res = super().delegate_action(action)
        if res == Gdk.EVENT_STOP:
            return res

        if action == 'show-contact-info':
            self.parent_win.window.lookup_action(
                'information-%s' % self.control_id).activate()
            return Gdk.EVENT_STOP

        if action == 'send-file':
            if app.interface.msg_win_mgr.mode == \
            app.interface.msg_win_mgr.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER:
                app.interface.roster.tree.grab_focus()
                return Gdk.EVENT_PROPAGATE

            self.parent_win.window.lookup_action(
                'send-file-%s' % self.control_id).activate()
            return Gdk.EVENT_STOP

        return Gdk.EVENT_PROPAGATE

382
    def _on_add_to_roster(self, _action, _param):
383
        AddNewContactWindow(self.account, self.contact.jid)
Philipp Hörist's avatar
Philipp Hörist committed
384

385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
    def _on_block_contact(self, _action, _param):
        def _block_contact(report=None):
            con = app.connections[self.account]
            con.get_module('Blocking').block([self.contact.jid], report=report)

            self.parent_win.remove_tab(self, None, force=True)
            if _('Not in contact list') in self.contact.get_shown_groups():
                app.interface.roster.remove_contact(
                    self.contact.jid, self.account, force=True, backend=True)
                return
            app.interface.roster.draw_contact(self.contact.jid, self.account)

        NewConfirmationDialog(
            _('Block Contact'),
            _('Really block this contact?'),
            _('You will appear offline for this contact and you will '
              'not receive further messages.'),
            [DialogButton.make('Cancel'),
             DialogButton.make('OK',
                               text=_('_Report Spam'),
                               callback=_block_contact,
                               kwargs={'report': 'spam'}),
             DialogButton.make('Remove',
                               text=_('_Block'),
                               callback=_block_contact)],
            modal=False).show()

412
    def _on_information(self, _action, _param):
Philipp Hörist's avatar
Philipp Hörist committed
413
414
        app.interface.roster.on_info(None, self.contact, self.account)

415
    def _on_invite_contacts(self, _action, _param):
Philipp Hörist's avatar
Philipp Hörist committed
416
417
418
419
420
421
422
423
424
425
426
427
428
429
        """
        User wants to invite some friends to chat
        """
        dialogs.TransformChatToMUC(self.account, [self.contact.jid])

    def _on_audio(self, action, param):
        action.set_state(param)
        state = param.get_boolean()
        self.on_jingle_button_toggled(state, 'audio')

    def _on_video(self, action, param):
        action.set_state(param)
        state = param.get_boolean()
        self.on_jingle_button_toggled(state, 'video')
430

431
432
433
434
435
    def _on_send_chatstate(self, action, param):
        action.set_state(param)
        app.config.set_per('contacts', self.contact.jid,
                           'send_chatstate', param.get_string())

436
437
438
439
    def subscribe_events(self):
        """
        Register listeners to the events class
        """
440
441
        app.events.event_added_subscribe(self.on_event_added)
        app.events.event_removed_subscribe(self.on_event_removed)
442
443
444
445
446

    def unsubscribe_events(self):
        """
        Unregister listeners to the events class
        """
447
448
        app.events.event_added_unsubscribe(self.on_event_added)
        app.events.event_removed_unsubscribe(self.on_event_removed)
449

450
    def _update_toolbar(self):
451
        # Formatting
452
        # TODO: find out what encryption allows for xhtml and which not
Philipp Hörist's avatar
Philipp Hörist committed
453
        if self.contact.supports(Namespace.XHTML_IM):
454
455
            self.xml.formattings_button.set_sensitive(True)
            self.xml.formattings_button.set_tooltip_text(_(
456
                'Show a list of formattings'))
457
        else:
458
459
            self.xml.formattings_button.set_sensitive(False)
            self.xml.formattings_button.set_tooltip_text(
Philipp Hörist's avatar
Philipp Hörist committed
460
                _('This contact does not support HTML'))
461
462

        # Jingle detection
463
464
        jingle_audio = self.jingle['audio']
        jingle_video = self.jingle['video']
Philipp Hörist's avatar
Philipp Hörist committed
465
        if self.contact.supports(Namespace.JINGLE_ICE_UDP) and \
466
        app.is_installed('FARSTREAM') and self.contact.resource:
Philipp Hörist's avatar
Philipp Hörist committed
467
468
469
470
            jingle_audio.available = self.contact.supports(
                Namespace.JINGLE_RTP_AUDIO)
            jingle_video.available = self.contact.supports(
                Namespace.JINGLE_RTP_VIDEO)
471
        else:
472
            if jingle_video.available or jingle_audio.available:
473
                self.stop_jingle()
474
475
            jingle_video.available = False
            jingle_audio.available = False
476
477

    def update_all_pep_types(self):
Philipp Hörist's avatar
Philipp Hörist committed
478
        self._update_pep(PEPEventType.LOCATION)
479
        self._update_pep(PEPEventType.MOOD)
Philipp Hörist's avatar
Philipp Hörist committed
480
        self._update_pep(PEPEventType.ACTIVITY)
Philipp Hörist's avatar
Philipp Hörist committed
481
        self._update_pep(PEPEventType.TUNE)
482

483
484
485
486
487
488
489
490
491
492
    def _update_pep(self, type_):
        image = self._get_pep_widget(type_)
        data = self.contact.pep.get(type_)
        if data is None:
            image.hide()
            return

        if type_ == PEPEventType.MOOD:
            icon = 'mood-%s' % data.mood
            formated_text = format_mood(*data)
Philipp Hörist's avatar
Philipp Hörist committed
493
494
495
        elif type_ == PEPEventType.ACTIVITY:
            icon = get_activity_icon_name(data.activity, data.subactivity)
            formated_text = format_activity(*data)
Philipp Hörist's avatar
Philipp Hörist committed
496
497
498
        elif type_ == PEPEventType.TUNE:
            icon = 'audio-x-generic'
            formated_text = format_tune(*data)
Philipp Hörist's avatar
Philipp Hörist committed
499
500
501
        elif type_ == PEPEventType.LOCATION:
            icon = 'applications-internet'
            formated_text = format_location(data)
502
503
504
505
506
507
508

        image.set_from_icon_name(icon, Gtk.IconSize.MENU)
        image.set_tooltip_markup(formated_text)
        image.show()

    def _get_pep_widget(self, type_):
        if type_ == PEPEventType.MOOD:
509
            return self.xml.mood_image
Philipp Hörist's avatar
Philipp Hörist committed
510
        if type_ == PEPEventType.ACTIVITY:
511
            return self.xml.activity_image
Philipp Hörist's avatar
Philipp Hörist committed
512
        if type_ == PEPEventType.TUNE:
513
            return self.xml.tune_image
Philipp Hörist's avatar
Philipp Hörist committed
514
        if type_ == PEPEventType.LOCATION:
515
            return self.xml.location_image
516
        return None
517

518
    @event_filter(['account', 'jid'])
519
520
521
    def _on_mood_received(self, _event):
        self._update_pep(PEPEventType.MOOD)

522
    @event_filter(['account', 'jid'])
Philipp Hörist's avatar
Philipp Hörist committed
523
524
525
    def _on_activity_received(self, _event):
        self._update_pep(PEPEventType.ACTIVITY)

526
    @event_filter(['account', 'jid'])
Philipp Hörist's avatar
Philipp Hörist committed
527
528
529
    def _on_tune_received(self, _event):
        self._update_pep(PEPEventType.TUNE)

530
    @event_filter(['account', 'jid'])
Philipp Hörist's avatar
Philipp Hörist committed
531
532
533
    def _on_location_received(self, _event):
        self._update_pep(PEPEventType.LOCATION)

534
    @event_filter(['account', 'jid'])
535
536
537
538
    def _on_nickname_received(self, _event):
        self.update_ui()
        self.parent_win.redraw_tab(self)
        self.parent_win.show_title()
539

540
    @event_filter(['account', 'jid'])
Philipp Hörist's avatar
Philipp Hörist committed
541
542
543
544
545
    def _on_update_client_info(self, event):
        contact = app.contacts.get_contact(
            self.account, event.jid, event.resource)
        if contact is None:
            return
546
        self.xml.phone_image.set_visible(contact.uses_phone)
Philipp Hörist's avatar
Philipp Hörist committed
547

548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
    @event_filter(['account'])
    def _on_chatstate_received(self, event):
        if self._type.is_privatechat:
            if event.contact != self.gc_contact:
                return
        else:
            if event.contact.jid != self.contact.jid:
                return

        self.draw_banner_text()

        # update chatstate in tab for this chat
        if event.contact.is_gc_contact:
            chatstate = event.contact.chatstate
        else:
            chatstate = app.contacts.get_combined_chatstate(
                self.account, self.contact.jid)
        self.parent_win.redraw_tab(self, chatstate)

    @event_filter(['account'])
    def _on_caps_update(self, event):
        if self._type.is_chat and event.jid != self.contact.jid:
            return
        if self._type.is_privatechat and event.fjid != self.contact.jid:
            return
        self.update_ui()

    @event_filter(['account'])
    def _on_mam_decrypted_message_received(self, event):
        if event.properties.type.is_groupchat:
            return

        if event.properties.is_muc_pm:
            if not event.properties.jid == self.contact.get_full_jid():
                return
        else:
            if not event.properties.jid.bareMatch(self.contact.jid):
                return

        kind = '' # incoming
        if event.kind == KindConstant.CHAT_MSG_SENT:
            kind = 'outgoing'

        self.add_message(event.msgtxt,
                         kind,
                         tim=event.properties.mam.timestamp,
                         correct_id=event.correct_id,
                         message_id=event.properties.id,
                         additional_data=event.additional_data)

    @event_filter(['account'])
    def _on_decrypted_message_received(self, event):
        if not event.msgtxt:
            return True

        if event.session.control != self:
            return

        typ = ''
        if event.properties.is_sent_carbon:
            typ = 'out'

        self.add_message(event.msgtxt,
                         typ,
                         tim=event.properties.timestamp,
                         subject=event.properties.subject,
                         displaymarking=event.displaymarking,
                         msg_log_id=event.msg_log_id,
                         message_id=event.properties.id,
                         correct_id=event.correct_id,
                         additional_data=event.additional_data)
        if event.msg_log_id:
            pw = self.parent_win
            end = self.conv_textview.autoscroll
            if not pw or (pw.get_active_control() and self \
            == pw.get_active_control() and pw.is_active() and end):
                app.logger.set_read_messages([event.msg_log_id])

    @event_filter(['account', 'jid'])
    def _on_message_error(self, event):
        self.conv_textview.show_error(event.message_id, event.error)

    @event_filter(['account', 'jid'])
    def _on_message_sent(self, event):
        if not event.message:
            return

Philipp Hörist's avatar
Philipp Hörist committed
635
636
        self.last_sent_msg = event.message_id
        message_id = event.message_id
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677

        if event.label:
            displaymarking = event.label.getTag('displaymarking')
        else:
            displaymarking = None
        if self.correcting:
            self.correcting = False
            gtkgui_helpers.remove_css_class(
                self.msg_textview, 'gajim-msg-correcting')

        self.add_message(event.message,
                         self.contact.jid,
                         tim=event.timestamp,
                         displaymarking=displaymarking,
                         message_id=message_id,
                         correct_id=event.correct_id,
                         additional_data=event.additional_data)

    @event_filter(['account', 'jid'])
    def _receipt_received(self, event):
        self.conv_textview.show_receipt(event.receipt_id)

    @event_filter(['account', 'jid'])
    def _on_zeroconf_error(self, event):
        self.add_status_message(event.message)

    @event_filter(['account', 'jid'])
    def _on_update_roster_avatar(self, obj):
        self.show_avatar()

    def _nec_ping(self, event):
        if self.contact != event.contact:
            return
        if event.name == 'ping-sent':
            self.add_info_message(_('Ping?'))
        elif event.name == 'ping-reply':
            self.add_info_message(
                _('Pong! (%s seconds)') % event.seconds)
        elif event.name == 'ping-error':
            self.add_info_message(_('Error.'))

678
679
680
681
    def _update_jingle(self, jingle_type: str) -> None:
        jingle = self.jingle[jingle_type]
        banner_image = jingle.banner_image
        state = jingle.state
682
        if state == self.JINGLE_STATE_NULL:
683
684
685
686
            banner_image.hide()
        else:
            banner_image.show()
        if state == self.JINGLE_STATE_CONNECTING:
687
688
            banner_image.set_from_icon_name('network-transmit-symbolic',
                                            Gtk.IconSize.MENU)
689
        elif state == self.JINGLE_STATE_CONNECTION_RECEIVED:
690
691
            banner_image.set_from_icon_name('network-receive-symbolic',
                                            Gtk.IconSize.MENU)
692
        elif state == self.JINGLE_STATE_CONNECTED:
693
694
            banner_image.set_from_icon_name('network-transmit-receive-symbolic',
                                            Gtk.IconSize.MENU)
695
        elif state == self.JINGLE_STATE_ERROR:
696
697
            banner_image.set_from_icon_name('network-error-symbolic',
                                            Gtk.IconSize.MENU)
698
699
700
701
        self.update_toolbar()

    def update_audio(self):
        self._update_jingle('audio')
702
        hbox = self.xml.audio_buttons_hbox
703
        if self.jingle['audio'].state == self.JINGLE_STATE_CONNECTED:
704
            # Set volume from config
705
706
            input_vol = app.config.get('audio_input_volume')
            output_vol = app.config.get('audio_output_volume')
707
708
            input_vol = max(min(input_vol, 100), 0)
            output_vol = max(min(output_vol, 100), 0)
709
710
            self.xml.mic_hscale.set_value(input_vol)
            self.xml.sound_hscale.set_value(output_vol)
711
            # Show vbox
Yann Leboulanger's avatar
Yann Leboulanger committed
712
713
            hbox.set_no_show_all(False)
            hbox.show_all()
714
        elif not self.jingle['audio'].sid:
Yann Leboulanger's avatar
Yann Leboulanger committed
715
716
            hbox.set_no_show_all(True)
            hbox.hide()
717
718
719
720
721
722
723
724

    def update_video(self):
        self._update_jingle('video')

    def change_resource(self, resource):
        old_full_jid = self.get_full_jid()
        self.resource = resource
        new_full_jid = self.get_full_jid()
725
726
727
728
        # update app.last_message_time
        if old_full_jid in app.last_message_time[self.account]:
            app.last_message_time[self.account][new_full_jid] = \
                    app.last_message_time[self.account][old_full_jid]
729
        # update events
730
        app.events.change_jid(self.account, old_full_jid, new_full_jid)
731
732
733
        # update MessageWindow._controls
        self.parent_win.change_jid(self.account, old_full_jid, new_full_jid)

734
    def stop_jingle(self, sid=None, reason=None):
735
736
737
        audio_sid = self.jingle['audio'].sid
        video_sid = self.jingle['video'].sid
        if audio_sid and sid in (audio_sid, None):
738
            self.close_jingle_content('audio')
739
        if video_sid and sid in (video_sid, None):
740
741
            self.close_jingle_content('video')

742
743
744
    def _set_jingle_state(self, jingle_type: str, state: str, sid: str = None,
                          reason: str = None) -> None:
        jingle = self.jingle[jingle_type]
745
        if state in ('connecting', 'connected', 'stop', 'error') and reason:
Philipp Hörist's avatar
Philipp Hörist committed
746
            info = _('%(type)s state : %(state)s, reason: %(reason)s') % {
747
748
749
                'type': jingle_type.capitalize(),
                'state': state,
                'reason': reason}
750
            self.add_info_message(info)
751

752
        states = {'connecting': self.JINGLE_STATE_CONNECTING,
753
754
755
756
                  'connection_received': self.JINGLE_STATE_CONNECTION_RECEIVED,
                  'connected': self.JINGLE_STATE_CONNECTED,
                  'stop': self.JINGLE_STATE_NULL,
                  'error': self.JINGLE_STATE_ERROR}
757

758
        jingle_state = states[state]
759
        if jingle.state == jingle_state or state == 'error':
760
761
            return

762
        if (state == 'stop' and jingle.sid not in (None, sid)):
763
764
            return

765
        new_sid = None
766
        if jingle_state == self.JINGLE_STATE_NULL:
767
            new_sid = None
768
        if state in ('connection_received', 'connecting'):
769
770
771
772
            new_sid = sid

        jingle.state = jingle_state
        jingle.sid = new_sid
773

774
        var = GLib.Variant.new_boolean(jingle_state != self.JINGLE_STATE_NULL)
775
        jingle.action.change_state(var)
776

777
        jingle.update()
778
779
780
781
782
783
784
785

    def set_audio_state(self, state, sid=None, reason=None):
        self._set_jingle_state('audio', state, sid=sid, reason=reason)

    def set_video_state(self, state, sid=None, reason=None):
        self._set_jingle_state('video', state, sid=sid, reason=reason)

    def _get_audio_content(self):
786
787
        con = app.connections[self.account]
        session = con.get_module('Jingle').get_jingle_session(
788
            self.contact.get_full_jid(), self.jingle['audio'].sid)
789
790
        return session.get_content('audio')

791
    def on_num_button_pressed(self, _widget, num):
792
793
        self._get_audio_content()._start_dtmf(num)

794
    def on_num_button_released(self, _released):
795
796
        self._get_audio_content()._stop_dtmf()

797
    def on_mic_hscale_value_changed(self, _widget, value):
798
799
        self._get_audio_content().set_mic_volume(value / 100)
        # Save volume to config
800
        app.config.set('audio_input_volume', value)
801

802
    def on_sound_hscale_value_changed(self, _widget, value):
803
804
        self._get_audio_content().set_out_volume(value / 100)
        # Save volume to config
805
        app.config.set('audio_output_volume', value)
806

807
    def on_location_eventbox_button_release_event(self, _widget, _event):
808
        if 'geoloc' in self.contact.pep:
Philipp Hörist's avatar
Philipp Hörist committed
809
            location = self.contact.pep['geoloc'].data
810
811
812
            if 'lat' in location and 'lon' in location:
                uri = geo_provider_from_location(location['lat'],
                                                 location['lon'])
813
                open_uri(uri)
814

815
    def on_location_eventbox_leave_notify_event(self, _widget, _event):
816
817
818
        """
        Just moved the mouse so show the cursor
        """
819
        cursor = get_cursor('default')
Yann Leboulanger's avatar
Yann Leboulanger committed
820
        self.parent_win.window.get_window().set_cursor(cursor)
821

822
    def on_location_eventbox_enter_notify_event(self, _widget, _event):
823
        cursor = get_cursor('pointer')
Yann Leboulanger's avatar
Yann Leboulanger committed
824
        self.parent_win.window.get_window().set_cursor(cursor)
825

826
827
828
829
    def update_ui(self):
        # The name banner is drawn here
        ChatControlBase.update_ui(self)
        self.update_toolbar()
830
        self.show_avatar()
831
832
833
834
835
836
837
838
839
840

    def draw_banner_text(self):
        """
        Draw the text in the fat line at the top of the window that houses the
        name, jid
        """
        contact = self.contact
        name = contact.get_shown_name()
        if self.resource:
            name += '/' + self.resource
841
        if self._type.is_privatechat:
842
            name = i18n.direction_mark + _(
843
844
                '%(nickname)s from group chat %(room_name)s') % \
                {'nickname': name, 'room_name': self.room_name}
Yann Leboulanger's avatar
Yann Leboulanger committed
845
        name = i18n.direction_mark + GLib.markup_escape_text(name)
846
847
848

        status = contact.status
        if status is not None:
849
            status_reduced = helpers.reduce_chars_newlines(status, max_lines=1)
850
851
        else:
            status_reduced = ''
Yann Leboulanger's avatar
Yann Leboulanger committed
852
        status_escaped = GLib.markup_escape_text(status_reduced)
853

854
        if self._type.is_privatechat:
855
856
857
858
            cs = self.gc_contact.chatstate
        else:
            cs = app.contacts.get_combined_chatstate(
                self.account, self.contact.jid)
859
860
861

        if app.config.get('show_chatstate_in_banner'):
            chatstate = helpers.get_uf_chatstate(cs)
862

863
864
865
            label_text = '<span>%s</span><span size="x-small" weight="light"> %s</span>' % \
                (name, chatstate)
            label_tooltip = '%s %s' % (name, chatstate)
866
        else:
867
868
            label_text = '<span>%s</span>' % name
            label_tooltip = name
869
870

        if status_escaped:
871
            status_text = make_href_markup(status_escaped)
Philipp Hörist's avatar
Philipp Hörist committed
872
            status_text = '<span size="x-small" weight="light">%s</span>' % status_text
873
874
875
            self.xml.banner_label.set_tooltip_text(status)
            self.xml.banner_label.set_no_show_all(False)
            self.xml.banner_label.show()
876
877
        else:
            status_text = ''
878
879
            self.xml.banner_label.hide()
            self.xml.banner_label.set_no_show_all(True)
880

881
        self.xml.banner_label.set_markup(status_text)
882
        # setup the label that holds name and jid
883
884
        self.xml.banner_name_label.set_markup(label_text)
        self.xml.banner_name_label.set_tooltip_text(label_tooltip)
885

886
887
888
    def close_jingle_content(self, jingle_type: str) -> None:
        jingle = self.jingle[jingle_type]
        if not jingle.sid:
889
            return
890
891
892
893

        jingle.sid = None
        jingle.state = self.JINGLE_STATE_NULL

894
895
        con = app.connections[self.account]
        session = con.get_module('Jingle').get_jingle_session(
896
            self.contact.get_full_jid(), jingle.sid)
897
898
899
900
        if session:
            content = session.get_content(jingle_type)
            if content:
                session.remove_content(content.creator, content.name)
901
        var = GLib.Variant.new_boolean(False)
902
903
904

        jingle.action.change_state(var)
        jingle.update()
905

Philipp Hörist's avatar
Philipp Hörist committed
906
    def on_jingle_button_toggled(self, state, jingle_type):
907
        con = app.connections[self.account]
Philipp Hörist's avatar
Philipp Hörist committed
908
        if state:
909
910
            if self.jingle[jingle_type].state == self.JINGLE_STATE_NULL:
                con = app.connections[self.account]
911
                if jingle_type == 'video':
912
                    video_hbox = self.xml.video_hbox
913
                    video_hbox.set_no_show_all(False)
914
                    if app.config.get('video_see_self'):
915
                        fixed = self.xml.outgoing_fixed
916
                        fixed.set_no_show_all(False)
917
                        video_hbox.show_all()
918
                    video_hbox.show_all()
919
                    sid = con.get_module('Jingle').start_video(
920
                        self.contact.get_full_jid())
921

922
                else:
923
924
925
                    sid = con.get_module('Jingle').start_audio(
                        self.contact.get_full_jid())

926
                self.jingle[jingle_type].set_state('connecting', sid)
927
        else:
928
            video_hbox = self.xml.video_hbox
929
930
            video_hbox.set_no_show_all(True)
            video_hbox.hide()
931
            fixed = self.xml.outgoing_fixed
932
            fixed.set_no_show_all(True)
933
934
            self.close_jingle_content(jingle_type)

Philipp Hörist's avatar
Philipp Hörist committed
935
936
937
938
    def send_message(self,
                     message,
                     xhtml=None,
                     process_commands=True,
939
                     attention=False):
940
941
942
        """
        Send a message to contact
        """
Philipp Hörist's avatar
Philipp Hörist committed
943
944
945

        if self.encryption:
            self.sendmessage = True
946
947
            app.plugin_manager.extension_point('send_message' + self.encryption,
                                               self)
Philipp Hörist's avatar
Philipp Hörist committed
948
949
950
            if not self.sendmessage:
                return

951
        message = helpers.remove_invalid_xml_chars(message)
952
        if message in ('', None, '\n'):
953
            return
954

955
956
957
958
959
        ChatControlBase.send_message(self,
                                     message,
                                     type_='chat',
                                     xhtml=xhtml,
                                     process_commands=process_commands,
960
                                     attention=attention)
961

962
    def get_our_nick(self):
963
        return app.nicks[self.account]
964

Philipp Hörist's avatar
Philipp Hörist committed
965
966
967
968
969
970
971
972
973
974
975
    def add_message(self,
                    text,
                    frm='',
                    tim=None,
                    subject=None,
                    displaymarking=None,
                    msg_log_id=None,
                    correct_id=None,
                    message_id=None,
                    additional_data=None,
                    error=None):
976
977
978
979
980
        """
        Print a line in the conversation

        If frm is set to status: it's a status message.
        if frm is set to error: it's an error message. The difference between
981
982
            status and error is mainly that with error, msg count as a new
            message (in systray and in control).
983
        If frm is set to info: it's a information message.
Alexander Krotov's avatar
Alexander Krotov committed
984
        If frm is set to print_queue: it is incoming from queue.
985
        If frm is set to another value: it's an outgoing message.
Alexander Krotov's avatar
Alexander Krotov committed
986
        If frm is not set: it's an incoming message.
987
988
989
        """
        contact = self.contact