chat_control.py 63.8 KB
Newer Older
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
nicfit committed
25

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
31

32
from gi.repository import Gtk
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 37 38 39
from nbxmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC
from nbxmpp.protocol import NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO
from nbxmpp.protocol import NS_JINGLE_ICE_UDP, NS_JINGLE_FILE_TRANSFER_5

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
André's avatar
André committed
46
from gajim.common.contacts import GC_Contact
47 48 49
from gajim.common.const import AvatarSize
from gajim.common.const import KindConstant
from gajim.common.const import Chatstate
50
from gajim.common.const import PEPEventType
nicfit's avatar
nicfit committed
51

52 53 54 55 56 57 58
from gajim import gtkgui_helpers
from gajim import gui_menu_builder
from gajim import message_control
from gajim import dialogs

from gajim.gtk.dialogs import ConfirmationDialog
from gajim.gtk.add_contact import AddNewContactWindow
59
from gajim.gtk.util import get_icon_name
60
from gajim.gtk.util import get_cursor
61 62
from gajim.gtk.util import ensure_proper_control
from gajim.gtk.util import format_mood
Philipp Hörist's avatar
Philipp Hörist committed
63
from gajim.gtk.util import format_activity
Philipp Hörist's avatar
Philipp Hörist committed
64
from gajim.gtk.util import format_tune
Philipp Hörist's avatar
Philipp Hörist committed
65
from gajim.gtk.util import get_activity_icon_name
66

André's avatar
André committed
67
from gajim.command_system.implementation.hosts import ChatCommands
68
from gajim.command_system.framework import CommandHost  # pylint: disable=unused-import
André's avatar
André committed
69
from gajim.chat_control_base import ChatControlBase
70

71
################################################################################
72
class ChatControl(ChatControlBase):
73 74 75 76
    """
    A control for standard 1-1 chat
    """
    (
77
            JINGLE_STATE_NULL,
78 79 80 81
            JINGLE_STATE_CONNECTING,
            JINGLE_STATE_CONNECTION_RECEIVED,
            JINGLE_STATE_CONNECTED,
            JINGLE_STATE_ERROR
82
    ) = range(5)
83 84 85 86 87 88

    TYPE_ID = message_control.TYPE_CHAT
    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.
89
    COMMAND_HOST = ChatCommands  # type: ClassVar[Type[CommandHost]]
90

91
    def __init__(self, parent_win, contact, acct, session, resource=None):
92
        ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
93
            'chat_control', contact, acct, resource)
94

95 96
        self.last_recv_message_id = None
        self.last_recv_message_marks = None
97
        self.last_message_timestamp = None
98

99
        self._formattings_button = self.xml.get_object('formattings_button')
Philipp Hörist's avatar
Philipp Hörist committed
100 101
        self.emoticons_button = self.xml.get_object('emoticons_button')
        self.toggle_emoticons()
102 103

        self.widget_set_visible(self.xml.get_object('banner_eventbox'),
104
            app.config.get('hide_chat_banner'))
105 106

        self.authentication_button = self.xml.get_object(
107
            'authentication_button')
108
        id_ = self.authentication_button.connect('clicked',
109
            self._on_authentication_button_clicked)
110 111
        self.handlers[id_] = self.authentication_button

112 113 114 115
        self.sendfile_button = self.xml.get_object('sendfile_button')
        self.sendfile_button.set_action_name('win.send-file-' + \
                                             self.control_id)

116 117 118
        # Add lock image to show chat encryption
        self.lock_image = self.xml.get_object('lock_image')

119 120
        # Menu for the HeaderBar
        self.control_menu = gui_menu_builder.get_singlechat_menu(
121
            self.control_id, self.account, self.contact.jid)
Philipp Hörist's avatar
Philipp Hörist committed
122 123
        settings_menu = self.xml.get_object('settings_menu')
        settings_menu.set_menu_model(self.control_menu)
124 125 126 127

        self._audio_banner_image = self.xml.get_object('audio_banner_image')
        self._video_banner_image = self.xml.get_object('video_banner_image')
        self.audio_sid = None
128 129
        self.audio_state = self.JINGLE_STATE_NULL
        self.audio_available = False
130
        self.video_sid = None
131 132
        self.video_state = self.JINGLE_STATE_NULL
        self.video_available = False
133 134 135 136

        self.update_toolbar()

        self._pep_images = {}
137
        self._pep_images['geoloc'] = self.xml.get_object('location_image')
138 139 140 141 142 143
        self.update_all_pep_types()

        self.show_avatar()

        # Hook up signals
        widget = self.xml.get_object('avatar_eventbox')
Philipp Hörist's avatar
Philipp Hörist committed
144
        widget.set_property('height-request', AvatarSize.CHAT)
145 146

        id_ = widget.connect('button-press-event',
147
            self.on_avatar_eventbox_button_press_event)
148 149 150 151
        self.handlers[id_] = widget

        widget = self.xml.get_object('location_eventbox')
        id_ = widget.connect('button-release-event',
152
            self.on_location_eventbox_button_release_event)
153
        self.handlers[id_] = widget
154 155 156 157 158 159
        id_ = widget.connect('enter-notify-event',
            self.on_location_eventbox_enter_notify_event)
        self.handlers[id_] = widget
        id_ = widget.connect('leave-notify-event',
            self.on_location_eventbox_leave_notify_event)
        self.handlers[id_] = widget
160 161 162 163 164 165 166 167

        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

168
        self.dtmf_window = self.xml.get_object('dtmf_window')
Dicson's avatar
Dicson committed
169
        self.dtmf_window.get_child().set_direction(Gtk.TextDirection.LTR)
170 171 172 173 174 175 176 177
        id_ = self.dtmf_window.connect('focus-out-event',
            self.on_dtmf_window_focus_out_event)
        self.handlers[id_] = self.dtmf_window

        widget = self.xml.get_object('dtmf_button')
        id_ = widget.connect('clicked', self.on_dtmf_button_clicked)
        self.handlers[id_] = widget

178 179 180 181 182 183 184 185
        widget = self.xml.get_object('mic_hscale')
        id_ = widget.connect('value_changed', self.on_mic_hscale_value_changed)
        self.handlers[id_] = widget

        widget = self.xml.get_object('sound_hscale')
        id_ = widget.connect('value_changed', self.on_sound_hscale_value_changed)
        self.handlers[id_] = widget

186
        self.info_bar = Gtk.InfoBar()
187
        content_area = self.info_bar.get_content_area()
188
        self.info_bar_label = Gtk.Label()
189
        self.info_bar_label.set_use_markup(True)
190 191
        self.info_bar_label.set_halign(Gtk.Align.START)
        self.info_bar_label.set_valign(Gtk.Align.START)
192 193 194
        content_area.add(self.info_bar_label)
        self.info_bar.set_no_show_all(True)
        widget = self.xml.get_object('vbox2')
195
        widget.pack_start(self.info_bar, False, True, 5)
196 197 198 199 200 201 202
        widget.reorder_child(self.info_bar, 1)

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

        self.subscribe_events()

203 204 205 206 207
        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
208
            session = app.connections[self.account].find_controlless_session(
209
                self.contact.jid, resource)
210

Philipp Hörist's avatar
Philipp Hörist committed
211
        self.setup_seclabel()
212 213 214 215 216 217
        if session:
            session.control = self
            self.session = session

        # Enable encryption if needed
        self.no_autonegotiation = False
218
        self.add_actions()
219
        self.update_ui()
220 221 222 223
        self.set_lock_image()

        self.encryption_menu = self.xml.get_object('encryption_menu')
        self.encryption_menu.set_menu_model(
224 225
            gui_menu_builder.get_encryption_menu(
                self.control_id, self.type_id, self.account == 'Local'))
226
        self.set_encryption_menu_icon()
227 228 229 230
        # restore previous conversation
        self.restore_conversation()
        self.msg_textview.grab_focus()

231
        app.ged.register_event_handler('pep-received', ged.GUI1,
232
            self._nec_pep_received)
233 234 235 236
        app.ged.register_event_handler('nickname-received', ged.GUI1,
            self._on_nickname_received)
        app.ged.register_event_handler('mood-received', ged.GUI1,
            self._on_mood_received)
Philipp Hörist's avatar
Philipp Hörist committed
237 238
        app.ged.register_event_handler('activity-received', ged.GUI1,
            self._on_activity_received)
Philipp Hörist's avatar
Philipp Hörist committed
239 240
        app.ged.register_event_handler('tune-received', ged.GUI1,
            self._on_tune_received)
Philipp Hörist's avatar
Philipp Hörist committed
241 242 243 244
        if self.TYPE_ID == message_control.TYPE_CHAT:
            # Dont connect this when PrivateChatControl is used
            app.ged.register_event_handler('update-roster-avatar', ged.GUI1,
                self._nec_update_avatar)
245
        app.ged.register_event_handler('chatstate-received', ged.GUI1,
246
            self._nec_chatstate_received)
247
        app.ged.register_event_handler('caps-update', ged.GUI1,
248
            self._nec_caps_received)
249
        app.ged.register_event_handler('message-sent', ged.OUT_POSTCORE,
250
            self._message_sent)
251 252 253
        app.ged.register_event_handler(
            'mam-decrypted-message-received',
            ged.GUI1, self._nec_mam_decrypted_message_received)
254 255 256
        app.ged.register_event_handler(
            'decrypted-message-received',
            ged.GUI1, self._nec_decrypted_message_received)
257 258 259
        app.ged.register_event_handler(
            'receipt-received',
            ged.GUI1, self._receipt_received)
260

261
        # PluginSystem: adding GUI extension point for this ChatControl
262
        # instance object
263
        app.plugin_manager.gui_extension_point('chat_control', self)
264 265 266
        self.update_actions()

    def add_actions(self):
267
        super().add_actions()
268 269 270 271 272 273 274 275 276 277 278 279
        actions = [
            ('invite-contacts-', self._on_invite_contacts),
            ('add-to-roster-', self._on_add_to_roster),
            ('information-', self._on_information),
            ]

        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)

280
        self.audio_action = Gio.SimpleAction.new_stateful(
281 282
            'toggle-audio-' + self.control_id, None,
            GLib.Variant.new_boolean(False))
283 284
        self.audio_action.connect('change-state', self._on_audio)
        self.parent_win.window.add_action(self.audio_action)
285

286
        self.video_action = Gio.SimpleAction.new_stateful(
287 288
            'toggle-video-' + self.control_id,
            None, GLib.Variant.new_boolean(False))
289 290
        self.video_action.connect('change-state', self._on_video)
        self.parent_win.window.add_action(self.video_action)
291 292 293 294

    def update_actions(self):
        win = self.parent_win.window
        online = app.account_is_connected(self.account)
295
        con = app.connections[self.account]
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314

        # Add to roster
        if not isinstance(self.contact, GC_Contact) \
        and _('Not in Roster') in self.contact.groups and \
        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)

        # Audio
        win.lookup_action('toggle-audio-' + self.control_id).set_enabled(
            online and self.audio_available)

        # Video
        win.lookup_action('toggle-video-' + self.control_id).set_enabled(
            online and self.video_available)

315 316 317 318
        # Send file (HTTP File Upload)
        httpupload = win.lookup_action(
            'send-file-httpupload-' + self.control_id)
        httpupload.set_enabled(
319
            online and con.get_module('HTTPUpload').available)
320 321 322 323 324 325 326 327 328

        # Send file (Jingle)
        jingle_conditions = (
            (self.contact.supports(NS_FILE) or
             self.contact.supports(NS_JINGLE_FILE_TRANSFER_5)) and
             self.contact.show != 'offline')
        jingle = win.lookup_action('send-file-jingle-' + self.control_id)
        jingle.set_enabled(online and jingle_conditions)

329
        # Send file
330 331 332 333 334
        win.lookup_action(
            'send-file-' + self.control_id).set_enabled(
            jingle.get_enabled() or httpupload.get_enabled())

        # Set File Transfer Button tooltip
335 336 337
        if online and (httpupload.get_enabled() or jingle.get_enabled()):
            tooltip_text = _('Send File…')
        else:
338 339
            tooltip_text = _('No File Transfer available')
        self.sendfile_button.set_tooltip_text(tooltip_text)
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357

        # 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:
            if self.contact.supports(NS_MUC) and online:
                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)

    def _on_add_to_roster(self, action, param):
358
        AddNewContactWindow(self.account, self.contact.jid)
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377

    def _on_information(self, action, param):
        app.interface.roster.on_info(None, self.contact, self.account)

    def _on_invite_contacts(self, action, param):
        """
        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')
378

379 380 381 382
    def subscribe_events(self):
        """
        Register listeners to the events class
        """
383 384
        app.events.event_added_subscribe(self.on_event_added)
        app.events.event_removed_subscribe(self.on_event_removed)
385 386 387 388 389

    def unsubscribe_events(self):
        """
        Unregister listeners to the events class
        """
390 391
        app.events.event_added_unsubscribe(self.on_event_added)
        app.events.event_removed_unsubscribe(self.on_event_removed)
392

393
    def _update_toolbar(self):
394
        # Formatting
395 396
        # TODO: find out what encryption allows for xhtml and which not
        if self.contact.supports(NS_XHTML_IM):
397
            self._formattings_button.set_sensitive(True)
398
            self._formattings_button.set_tooltip_text(_(
399
                'Show a list of formattings'))
400 401
        else:
            self._formattings_button.set_sensitive(False)
402
            if self.contact.supports(NS_XHTML_IM):
403 404
                self._formattings_button.set_tooltip_text(_('Formatting is not '
                    'available so long as GPG is active'))
405
            else:
406 407
                self._formattings_button.set_tooltip_text(_('This contact does '
                    'not support HTML'))
408 409 410

        # Jingle detection
        if self.contact.supports(NS_JINGLE_ICE_UDP) and \
411
        app.is_installed('FARSTREAM') and self.contact.resource:
412 413
            self.audio_available = self.contact.supports(NS_JINGLE_RTP_AUDIO)
            self.video_available = self.contact.supports(NS_JINGLE_RTP_VIDEO)
414
        else:
415 416 417 418
            if self.video_available or self.audio_available:
                self.stop_jingle()
            self.video_available = False
            self.audio_available = False
419 420 421 422

    def update_all_pep_types(self):
        for pep_type in self._pep_images:
            self.update_pep(pep_type)
423
        self._update_pep(PEPEventType.MOOD)
Philipp Hörist's avatar
Philipp Hörist committed
424
        self._update_pep(PEPEventType.ACTIVITY)
Philipp Hörist's avatar
Philipp Hörist committed
425
        self._update_pep(PEPEventType.TUNE)
426 427 428 429 430 431 432 433 434

    def update_pep(self, pep_type):
        if isinstance(self.contact, GC_Contact):
            return
        if pep_type not in self._pep_images:
            return
        pep = self.contact.pep
        img = self._pep_images[pep_type]
        if pep_type in pep:
435 436 437 438 439
            icon = gtkgui_helpers.get_pep_icon(pep[pep_type])
            if isinstance(icon, str):
                img.set_from_icon_name(icon, Gtk.IconSize.MENU)
            else:
                img.set_from_pixbuf(icon)
440
            img.set_tooltip_markup(pep[pep_type].as_markup_text())
441 442 443 444
            img.show()
        else:
            img.hide()

445 446 447 448 449 450
    def _nec_pep_received(self, obj):
        if obj.conn.name != self.account:
            return
        if obj.jid != self.contact.jid:
            return

451 452 453 454 455 456 457 458 459 460 461 462
        self.update_pep(obj.pep_type)

    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
463 464 465
        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
466 467 468
        elif type_ == PEPEventType.TUNE:
            icon = 'audio-x-generic'
            formated_text = format_tune(*data)
469 470 471 472 473 474 475 476

        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:
            return self.xml.get_object('mood_image')
Philipp Hörist's avatar
Philipp Hörist committed
477 478
        if type_ == PEPEventType.ACTIVITY:
            return self.xml.get_object('activity_image')
Philipp Hörist's avatar
Philipp Hörist committed
479 480
        if type_ == PEPEventType.TUNE:
            return self.xml.get_object('tune_image')
481 482 483 484 485

    @ensure_proper_control
    def _on_mood_received(self, _event):
        self._update_pep(PEPEventType.MOOD)

Philipp Hörist's avatar
Philipp Hörist committed
486 487 488 489
    @ensure_proper_control
    def _on_activity_received(self, _event):
        self._update_pep(PEPEventType.ACTIVITY)

Philipp Hörist's avatar
Philipp Hörist committed
490 491 492 493
    @ensure_proper_control
    def _on_tune_received(self, _event):
        self._update_pep(PEPEventType.TUNE)

494 495 496 497 498
    @ensure_proper_control
    def _on_nickname_received(self, _event):
        self.update_ui()
        self.parent_win.redraw_tab(self)
        self.parent_win.show_title()
499

500 501 502 503 504
    def _update_jingle(self, jingle_type):
        if jingle_type not in ('audio', 'video'):
            return
        banner_image = getattr(self, '_' + jingle_type + '_banner_image')
        state = getattr(self, jingle_type + '_state')
505
        if state == self.JINGLE_STATE_NULL:
506 507 508 509
            banner_image.hide()
        else:
            banner_image.show()
        if state == self.JINGLE_STATE_CONNECTING:
510 511
            banner_image.set_from_icon_name(
                    Gtk.STOCK_CONVERT, Gtk.IconSize.MENU)
512
        elif state == self.JINGLE_STATE_CONNECTION_RECEIVED:
513 514
            banner_image.set_from_icon_name(
                    "network-workgroup", Gtk.IconSize.MENU)
515
        elif state == self.JINGLE_STATE_CONNECTED:
516 517
            banner_image.set_from_icon_name(
                    Gtk.STOCK_CONNECT, Gtk.IconSize.MENU)
518
        elif state == self.JINGLE_STATE_ERROR:
519 520
            banner_image.set_from_icon_name(
                    "dialog-warning", Gtk.IconSize.MENU)
521 522 523 524
        self.update_toolbar()

    def update_audio(self):
        self._update_jingle('audio')
525
        hbox = self.xml.get_object('audio_buttons_hbox')
526 527
        if self.audio_state == self.JINGLE_STATE_CONNECTED:
            # Set volume from config
528 529
            input_vol = app.config.get('audio_input_volume')
            output_vol = app.config.get('audio_output_volume')
530 531 532 533 534
            input_vol = max(min(input_vol, 100), 0)
            output_vol = max(min(output_vol, 100), 0)
            self.xml.get_object('mic_hscale').set_value(input_vol)
            self.xml.get_object('sound_hscale').set_value(output_vol)
            # Show vbox
535 536
            hbox.set_no_show_all(False)
            hbox.show_all()
537
        elif not self.audio_sid:
538 539
            hbox.set_no_show_all(True)
            hbox.hide()
540 541 542 543 544 545 546 547

    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()
548 549 550 551
        # 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]
552
        # update events
553
        app.events.change_jid(self.account, old_full_jid, new_full_jid)
554 555 556
        # update MessageWindow._controls
        self.parent_win.change_jid(self.account, old_full_jid, new_full_jid)

557 558 559 560 561 562
    def stop_jingle(self, sid=None, reason=None):
        if self.audio_sid and sid in (self.audio_sid, None):
            self.close_jingle_content('audio')
        if self.video_sid and sid in (self.video_sid, None):
            self.close_jingle_content('video')

563 564 565
    def _set_jingle_state(self, jingle_type, state, sid=None, reason=None):
        if jingle_type not in ('audio', 'video'):
            return
566
        if state in ('connecting', 'connected', 'stop', 'error') and reason:
567
            info = _('%(type)s state : %(state)s, reason: %(reason)s') % {
568
                    'type': jingle_type.capitalize(), 'state': state, 'reason': reason}
569
            self.print_conversation(info, 'info')
570

571
        states = {'connecting': self.JINGLE_STATE_CONNECTING,
572 573
                'connection_received': self.JINGLE_STATE_CONNECTION_RECEIVED,
                'connected': self.JINGLE_STATE_CONNECTED,
574
                'stop': self.JINGLE_STATE_NULL,
575 576
                'error': self.JINGLE_STATE_ERROR}

577
        jingle_state = states[state]
578
        if getattr(self, jingle_type + '_state') == jingle_state or state == 'error':
579 580 581 582 583 584
            return

        if state == 'stop' and getattr(self, jingle_type + '_sid') not in (None, sid):
            return

        setattr(self, jingle_type + '_state', jingle_state)
585

586
        if jingle_state == self.JINGLE_STATE_NULL:
587 588 589 590
            setattr(self, jingle_type + '_sid', None)
        if state in ('connection_received', 'connecting'):
            setattr(self, jingle_type + '_sid', sid)

591 592
        v = GLib.Variant.new_boolean(jingle_state != self.JINGLE_STATE_NULL)
        getattr(self, jingle_type + '_action').change_state(v)
593 594 595 596 597 598 599 600 601 602

        getattr(self, 'update_' + jingle_type)()

    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):
603
        session = app.connections[self.account].get_jingle_session(
604 605 606 607 608 609 610 611 612
                self.contact.get_full_jid(), self.audio_sid)
        return session.get_content('audio')

    def on_num_button_pressed(self, widget, num):
        self._get_audio_content()._start_dtmf(num)

    def on_num_button_released(self, released):
        self._get_audio_content()._stop_dtmf()

613 614 615 616 617 618 619
    def on_dtmf_button_clicked(self, widget):
        self.dtmf_window.show_all()

    def on_dtmf_window_focus_out_event(self, widget, event):
        self.dtmf_window.hide()

    def on_mic_hscale_value_changed(self, widget, value):
620 621
        self._get_audio_content().set_mic_volume(value / 100)
        # Save volume to config
622
        app.config.set('audio_input_volume', value)
623

624
    def on_sound_hscale_value_changed(self, widget, value):
625 626
        self._get_audio_content().set_out_volume(value / 100)
        # Save volume to config
627
        app.config.set('audio_output_volume', value)
628 629 630 631 632 633

    def on_avatar_eventbox_button_press_event(self, widget, event):
        """
        If right-clicked, show popup
        """
        if event.button == 3: # right click
Philipp Hörist's avatar
Philipp Hörist committed
634 635 636 637 638 639 640
            if self.TYPE_ID == message_control.TYPE_CHAT:
                sha = app.contacts.get_avatar_sha(
                    self.account, self.contact.jid)
                name = self.contact.get_shown_name()
            else:
                sha = self.gc_contact.avatar_sha
                name = self.gc_contact.get_shown_name()
641
            gui_menu_builder.show_save_as_menu(sha, name)
642 643 644
        return True

    def on_location_eventbox_button_release_event(self, widget, event):
645
        if 'geoloc' in self.contact.pep:
Philipp Hörist's avatar
Philipp Hörist committed
646
            location = self.contact.pep['geoloc'].data
647
            if ('lat' in location) and ('lon' in location):
648
                uri = 'https://www.openstreetmap.org/?' + \
649 650 651 652
                        'mlat=%(lat)s&mlon=%(lon)s&zoom=16' % {'lat': location['lat'],
                        'lon': location['lon']}
                helpers.launch_browser_mailer('url', uri)

653 654 655 656
    def on_location_eventbox_leave_notify_event(self, widget, event):
        """
        Just moved the mouse so show the cursor
        """
657
        cursor = get_cursor('LEFT_PTR')
Yann Leboulanger's avatar
Yann Leboulanger committed
658
        self.parent_win.window.get_window().set_cursor(cursor)
659 660

    def on_location_eventbox_enter_notify_event(self, widget, event):
661
        cursor = get_cursor('HAND2')
Yann Leboulanger's avatar
Yann Leboulanger committed
662
        self.parent_win.window.get_window().set_cursor(cursor)
663

664 665 666 667 668 669
    def update_ui(self):
        # The name banner is drawn here
        ChatControlBase.update_ui(self)
        self.update_toolbar()

    def _update_banner_state_image(self):
670 671
        contact = app.contacts.get_contact_with_highest_priority(
            self.account, self.contact.jid)
672 673 674 675 676 677
        if not contact or self.resource:
            # For transient contacts
            contact = self.contact
        show = contact.show

        # Set banner image
678
        icon = get_icon_name(show)
679
        banner_status_img = self.xml.get_object('banner_status_image')
680
        banner_status_img.set_from_icon_name(icon, Gtk.IconSize.DND)
681 682 683 684 685 686 687 688 689 690 691 692 693 694 695

    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
        jid = contact.jid

        banner_name_label = self.xml.get_object('banner_name_label')

        name = contact.get_shown_name()
        if self.resource:
            name += '/' + self.resource
        if self.TYPE_ID == message_control.TYPE_PM:
696 697 698
            name = i18n.direction_mark +  _(
                '%(nickname)s from group chat %(room_name)s') % \
                {'nickname': name, 'room_name': self.room_name}
Yann Leboulanger's avatar
Yann Leboulanger committed
699
        name = i18n.direction_mark + GLib.markup_escape_text(name)
700 701 702 703 704

        # We know our contacts nick, but if another contact has the same nick
        # in another account we need to also display the account.
        # except if we are talking to two different resources of the same contact
        acct_info = ''
705
        for account in app.contacts.get_accounts():
706 707 708 709
            if account == self.account:
                continue
            if acct_info: # We already found a contact with same nick
                break
710
            for jid in app.contacts.get_jid_list(account):
711
                other_contact_ = \
712
                    app.contacts.get_first_contact_from_jid(account, jid)
713 714 715
                if other_contact_.get_shown_name() == \
                self.contact.get_shown_name():
                    acct_info = i18n.direction_mark + ' (%s)' % \
716 717
                        GLib.markup_escape_text(
                            app.get_account_label(self.account))
718 719 720 721
                    break

        status = contact.status
        if status is not None:
722 723
            banner_name_label.set_ellipsize(Pango.EllipsizeMode.END)
            self.banner_status_label.set_ellipsize(Pango.EllipsizeMode.END)
724
            status_reduced = helpers.reduce_chars_newlines(status, max_lines=1)
725 726
        else:
            status_reduced = ''
Yann Leboulanger's avatar
Yann Leboulanger committed
727
        status_escaped = GLib.markup_escape_text(status_reduced)
728

729 730 731 732 733
        if self.TYPE_ID == 'pm':
            cs = self.gc_contact.chatstate
        else:
            cs = app.contacts.get_combined_chatstate(
                self.account, self.contact.jid)
734 735 736

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

Philipp Hörist's avatar
Philipp Hörist committed
738 739
            label_text = '<span>%s</span><span size="x-small" weight="light">%s %s</span>' \
                % (name, acct_info, chatstate)
740
            if acct_info:
741
                acct_info = i18n.direction_mark + ' ' + acct_info
742 743
            label_tooltip = '%s%s %s' % (name, acct_info, chatstate)
        else:
Philipp Hörist's avatar
Philipp Hörist committed
744 745
            label_text = '<span>%s</span><span size="x-small" weight="light">%s</span>' % \
                    (name, acct_info)
746
            if acct_info:
747
                acct_info = i18n.direction_mark + ' ' + acct_info
748 749 750 751
            label_tooltip = '%s%s' % (name, acct_info)

        if status_escaped:
            status_text = self.urlfinder.sub(self.make_href, status_escaped)
Philipp Hörist's avatar
Philipp Hörist committed
752
            status_text = '<span size="x-small" weight="light">%s</span>' % status_text
753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769
            self.banner_status_label.set_tooltip_text(status)
            self.banner_status_label.set_no_show_all(False)
            self.banner_status_label.show()
        else:
            status_text = ''
            self.banner_status_label.hide()
            self.banner_status_label.set_no_show_all(True)

        self.banner_status_label.set_markup(status_text)
        # setup the label that holds name and jid
        banner_name_label.set_markup(label_text)
        banner_name_label.set_tooltip_text(label_tooltip)

    def close_jingle_content(self, jingle_type):
        sid = getattr(self, jingle_type + '_sid')
        if not sid:
            return
770 771
        setattr(self, jingle_type + '_sid', None)
        setattr(self, jingle_type + '_state', self.JINGLE_STATE_NULL)
772
        session = app.connections[self.account].get_jingle_session(
773 774 775 776 777
                self.contact.get_full_jid(), sid)
        if session:
            content = session.get_content(jingle_type)
            if content:
                session.remove_content(content.creator, content.name)
778 779
        v = GLib.Variant.new_boolean(False)
        getattr(self, jingle_type + '_action').change_state(v)
780
        getattr(self, 'update_' + jingle_type)()
781

782 783
    def on_jingle_button_toggled(self, state, jingle_type):
        if state:
784
            if getattr(self, jingle_type + '_state') == \
785
            self.JINGLE_STATE_NULL:
786 787 788
                if jingle_type == 'video':
                    video_hbox = self.xml.get_object('video_hbox')
                    video_hbox.set_no_show_all(False)
789
                    if app.config.get('video_see_self'):
790 791
                        fixed = self.xml.get_object('outgoing_fixed')
                        fixed.set_no_show_all(False)
792
                        video_hbox.show_all()
793 794
                        out_da = self.xml.get_object('outgoing_drawingarea')
                        out_da.realize()
795
                        if os.name == 'nt':
796
                            out_xid = out_da.get_window().handle
797
                        else:
798
                            out_xid = out_da.get_window().get_xid()
799 800
                    else:
                        out_xid = None
801
                    video_hbox.show_all()
802 803 804
                    in_da = self.xml.get_object('incoming_drawingarea')
                    in_da.realize()
                    in_xid = in_da.get_window().get_xid()
805
                    sid = app.connections[self.account].start_video(
806 807
                        self.contact.get_full_jid(), in_xid, out_xid)
                else:
808
                    sid = getattr(app.connections[self.account],
809 810 811
                        'start_' + jingle_type)(self.contact.get_full_jid())
                getattr(self, 'set_' + jingle_type + '_state')('connecting', sid)
        else:
812 813 814 815 816
            video_hbox = self.xml.get_object('video_hbox')
            video_hbox.set_no_show_all(True)
            video_hbox.hide()
            fixed = self.xml.get_object('outgoing_fixed')
            fixed.set_no_show_all(True)
817 818
            self.close_jingle_content(jingle_type)

819
    def set_lock_image(self):
820
        encryption_state = {'visible': self.encryption is not None,
821 822 823
                            'enc_type': self.encryption,
                            'authenticated': False}

824
        if self.encryption:
825
            app.plugin_manager.extension_point(
826
                'encryption_state' + self.encryption, self, encryption_state)
827 828

        self._show_lock_image(**encryption_state)
829

830
    def _show_lock_image(self, visible, enc_type='', authenticated=False):
831 832 833 834 835
        """
        Set lock icon visibility and create tooltip
        """
        if authenticated:
            authenticated_string = _('and authenticated')
836 837
            self.lock_image.set_from_icon_name(
                'security-high', Gtk.IconSize.MENU)
838 839
        else:
            authenticated_string = _('and NOT authenticated')
840 841
            self.lock_image.set_from_icon_name(
                'security-low', Gtk.IconSize.MENU)
842

843
        tooltip = _('%(type)s encryption is active %(authenticated)s.') % {'type': enc_type, 'authenticated': authenticated_string}
844 845 846

        self.authentication_button.set_tooltip_text(tooltip)
        self.widget_set_visible(self.authentication_button, not visible)
847
        self.lock_image.set_sensitive(visible)
848 849

    def _on_authentication_button_clicked(self, widget):
850
        if self.encryption:
851
            app.plugin_manager.extension_point(
852
                'encryption_dialog' + self.encryption, self)
853

854 855 856
    def _nec_mam_decrypted_message_received(self, obj):
        if obj.conn.name != self.account:
            return
857 858 859 860 861 862 863

        if obj.muc_pm:
            if not obj.with_ == self.contact.get_full_jid():
                return
        else:
            if not obj.with_.bareMatch(self.contact.jid):
                return
864

865
        kind = '' # incoming
866 867 868
        if obj.kind == KindConstant.CHAT_MSG_SENT:
            kind = 'outgoing'

869 870 871
        self.print_conversation(
            obj.msgtxt, kind, tim=obj.timestamp,
            encrypted=obj.encrypted, correct_id=obj.correct_id,
872 873
            msg_stanza_id=obj.message_id, additional_data=obj.additional_data)

874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903
    def _nec_decrypted_message_received(self, obj):
        if not obj.msgtxt:
            return True
        if obj.conn.name != self.account:
            return
        if obj.mtype != 'chat':
            return
        if obj.session.control != self:
            return

        typ = ''
        xep0184_id = None
        if obj.mtype == 'error':
            typ = 'error'
        if obj.forwarded and obj.sent:
            typ = 'out'
            if obj.jid != app.get_jid_from_account(obj.conn.name):
                xep0184_id = obj.id_
        self.print_conversation(obj.msgtxt, typ,
            tim=obj.timestamp, encrypted=obj.encrypted, subject=obj.subject,
            xhtml=obj.xhtml, displaymarking=obj.displaymarking,
            msg_log_id=obj.msg_log_id, msg_stanza_id=obj.id_, correct_id=obj.correct_id,
            xep0184_id=xep0184_id, additional_data=obj.additional_data)
        if obj.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([obj.msg_log_id])

904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924
    def _message_sent(self, obj):
        if obj.conn.name != self.account:
            return
        if obj.jid != self.contact.jid:
            return
        if not obj.message:
            return

        self.last_sent_msg = obj.stanza_id
        id_ = obj.msg_iq.getID()
        xep0184_id = None
        if self.contact.jid != app.get_jid_from_account(self.account):
            if app.config.get_per('accounts', self.account, 'request_receipt'):
                xep0184_id = id_
        if obj.label:
            displaymarking = obj.label.getTag('displaymarking')
        else:
            displaymarking = None
        if self.correcting:
            self.correcting = False
            gtkgui_helpers.remove_css_class(
Philipp Hörist's avatar
Philipp Hörist committed
925
                self.msg_textview, 'gajim-msg-correcting')
926 927 928 929 930 931 932

        self.print_conversation(obj.message, self.contact.jid, tim=obj.timestamp,
            encrypted=obj.encrypted, xep0184_id=xep0184_id, xhtml=obj.xhtml,
            displaymarking=displaymarking, msg_stanza_id=id_,
            correct_id=obj.correct_id,
            additional_data=obj.additional_data)

933 934
    def send_message(self, message, keyID='', xhtml=None,
                     process_commands=True, attention=False):
935 936 937
        """
        Send a message to contact
        """
938 939 940

        if self.encryption:
            self.sendmessage = True
941
            app.plugin_manager.extension_point(
942 943 944 945
                    'send_message' + self.encryption, self)
            if not self.sendmessage:
                return

946
        message = helpers.remove_invalid_xml_chars(message)
947 948 949 950
        if message in ('', None, '\n'):
            return None

        contact = self.contact
951 952
        keyID = contact.keyID

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

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

964
    def print_conversation(self, text, frm='', tim=None, encrypted=None,
965
    subject=None, xhtml=None, simple=False, xep0184_id=None,
966
    displaymarking=None, msg_log_id=None, correct_id=None,
967
    msg_stanza_id=None, additional_data=None):
968 969 970 971 972 973 974 975
        """
        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
                status and error is mainly that with error, msg count as a new message
                (in systray and in control).
        If frm is set to info: it's a information message.
Alexander Krotov's avatar
Alexander Krotov committed
976
        If frm is set to print_queue: it is incoming from queue.
977
        If frm is set to another value: it's an outgoing message.
Alexander Krotov's avatar
Alexander Krotov committed
978
        If frm is not set: it's an incoming message.
979 980 981
        """
        contact = self.contact

982
        if additional_data is None:
983
            additional_data = AdditionalDataDict()
984

985
        if frm == 'status':
986
            if not app.config.get('print_status_in_chats'):
987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004
                return
            kind = 'status'
            name = ''
        elif frm == 'error':
            kind = 'error'
            name = ''
        elif frm == 'info':
            kind = 'info'
            name = ''
        else:
            if not frm:
                kind = 'incoming'
                name = contact.get_shown_name()
            elif frm == 'print_queue': # incoming message, but do not update time
                kind = 'incoming_queue'
                name = contact.get_shown_name()
            else:
                kind = 'outgoing'
1005
                name = self.get_our_nick()
1006
                if not xhtml and not encrypted and \
1007
                app.config.get('rst_formatting_outgoing_messages'):
André's avatar
André committed
1008
                    from gajim.common.rst_xhtml_generator import create_xhtml
1009 1010 1011 1012
                    xhtml = create_xhtml(text)
                    if xhtml:
                        xhtml = '<body xmlns="%s">%s</body>' % (NS_XHTML, xhtml)
        ChatControlBase.print_conversation_line(self, text, kind, name, tim,
1013 1014
            subject=subject, old_kind=self.old_msg_kind, xhtml=xhtml,
            simple=simple, xep0184_id=xep0184_id, displaymarking=displaymarking,
1015
            msg_log_id=msg_log_id, msg_stanza_id=msg_stanza_id,
1016 1017
            correct_id=correct_id, additional_data=additional_data,
            encrypted=encrypted)
1018 1019 1020 1021 1022
        if text.startswith('/me ') or text.startswith('/me\n'):
            self.old_msg_kind = None
        else:
            self.old_msg_kind = kind

1023 1024 1025 1026 1027 1028 1029 1030
    def _receipt_received(self, event):
        if event.conn.name != self.account:
            return
        if event.jid != self.contact.jid:
            return

        self.conv_textview.show_xep0184_ack(event.receipt_id)

1031
    def get_tab_label(self):
1032 1033 1034 1035 1036
        unread = ''
        if self.resource:
            jid = self.contact.get_full_jid()
        else:
            jid = self.contact.jid
1037
        num_unread = len(app.events.get_events(self.account, jid,
1038
                ['printed_' + self.type_id, self.type_id]))
1039
        if num_unread == 1 and not app.config.get('show_unread_tab_icon'):
1040 1041
            unread = '*'
        elif num_unread > 1:
1042
            unread = '[' + str(num_unread) + ']'
1043 1044 1045 1046

        name = self.contact.get_shown_name()
        if self.resource:
            name += '/' + self.resource
Yann Leboulanger's avatar
Yann Leboulanger committed
1047
        label_str = GLib.markup_escape_text(name)
1048 1049
        if num_unread: # if unread, text in the label becomes bold
            label_str = '<b>' + unread + label_str + '</b>'
1050
        return label_str
1051 1052 1053 1054 1055 1056

    def get_tab_image(self, count_unread=True):
        if self.resource:
            jid = self.contact.get_full_jid()
        else:
            jid = self.contact.jid
1057

1058
        if app.config.get('show_avatar_in_tabs'):
1059 1060 1061 1062 1063
            scale = self.parent_win.window.get_scale_factor()
            surface = app.contacts.get_avatar(
                self.account, jid, AvatarSize.TAB, scale)
            if surface is not None:
                return surface
1064

1065
        if count_unread:
1066
            num_unread = len(app.events.get_events(self.account, jid,
1067 1068 1069
                    ['printed_' + self.type_id, self.type_id]))
        else:
            num_unread = 0
1070 1071 1072 1073

        transport = None
        if app.jid_is_transport(jid):
            transport = app.get_transport_name_from_jid(jid)
1074

1075
        if num_unread and app.config.get('show_unread_tab_icon'):
1076
            icon_name = get_icon_name('event', transport=transport)
1077
        else:
1078
            contact = app.contacts.get_contact_with_highest_priority(
1079 1080 1081 1082
                    self.account, self.contact.jid)
            if not contact or self.resource:
                # For transient contacts
                contact = self.contact
1083
            icon_name = get_icon_name(contact.show, transport=transport)
1084

1085
        return icon_name
1086 1087 1088 1089

    def prepare_context_menu(self, hide_buttonbar_items=False):
        """
        Set compact view menuitem active state sets active and sensitivity state
1090
        for history_menuitem (False for tranasports) and file_transfer_menuitem
1091
        and hide()/show() for add_to_roster_menuitem
1092
        """
1093
        if app.jid_is_transport(self.contact.jid):
1094 1095 1096 1097
            menu = gui_menu_builder.get_transport_menu(self.contact,
                self.account)
        else:
            menu = gui_menu_builder.get_contact_menu(self.contact, self.account,
1098 1099 1100 1101 1102 1103
                use_multiple_contacts=False, show_start_chat=False,
                show_encryption=True, control=self,
                show_buttonbar_items=not hide_buttonbar_items)
        return menu

    def shutdown(self):
1104 1105
        # PluginSystem: removing GUI extension points connected with ChatControl
        # instance object
1106
        app.plugin_manager.remove_gui_extension_point('chat_control', self)
1107

1108
        app.ged.remove_event_handler('pep-received', ged.GUI1,
1109
            self._nec_pep_received)
1110 1111 1112 1113
        app.ged.remove_event_handler('nickname-received', ged.GUI1,
            self._on_nickname_received)
        app.ged.remove_event_handler('mood-received', ged.GUI1,
            self._on_mood_received)
Philipp Hörist's avatar
Philipp Hörist committed
1114 1115
        app.ged.remove_event_handler('activity-received', ged.GUI1,
            self._on_activity_received)
Philipp Hörist's avatar
Philipp Hörist committed
1116 1117
        app.ged.remove_event_handler('tune-received', ged.GUI1,
            self._on_tune_received)
Philipp Hörist's avatar
Philipp Hörist committed
1118 1119 1120
        if self.TYPE_ID == message_control.TYPE_CHAT:
            app.ged.remove_event_handler('update-roster-avatar', ged.GUI1,
                self._nec_update_avatar)
1121
        app.ged.remove_event_handler('chatstate-received', ged.GUI1,
1122
            self._nec_chatstate_received)
1123
        app.ged.remove_event_handler('caps-update', ged.GUI1,
1124
            self._nec_caps_received)
1125
        app.ged.remove_event_handler('message-sent', ged.OUT_POSTCORE,
1126
            self._message_sent)
1127 1128 1129
        app.ged.remove_event_handler(
            'mam-decrypted-message-received',
            ged.GUI1, self._nec_mam_decrypted_message_received)
1130 1131 1132
        app.ged.remove_event_handler(
            'decrypted-message-received',
            ged.GUI1, self._nec_decrypted_message_received)
1133 1134 1135
        app.ged.remove_event_handler(
            'receipt-received',
            ged.GUI1, self._receipt_received)
1136

1137 1138
        self.unsubscribe_events()

1139
        # Send 'gone' chatstate
1140 1141
        con = app.connections[self.account]
        con.get_module('Chatstate').set_chatstate(self.contact, Chatstate.GONE)
1142 1143 1144 1145 1146 1147 1148 1149 1150

        for jingle_type in ('audio', 'video'):
            self.close_jingle_content(jingle_type)

        # disconnect self from session
        if self.session:
            self.session.control = None

        # Clean events
1151
        app.events.remove_events(self.account, self.get_full_jid(),
1152
                types=['printed_' + self.type_id, self.type_id])
1153 1154
        # Remove contact instance if contact has been removed
        key = (self.contact.jid, self.account)
1155
        roster = app.interface.roster
1156
        if key in roster.contacts_to_be_removed.keys() and \
1157 1158
        not roster.contact_has_pending_roster_events(self.contact,
        self.account):
1159 1160 1161
            backend = roster.contacts_to_be_removed[key]['backend']
            del roster.contacts_to_be_removed[key]
            roster.remove_contact(self.contact.jid, self.account, force=True,
1162
                backend=backend)
1163 1164
        # remove all register handlers on widgets, created by self.xml
        # to prevent circular references among objects
Dicson's avatar
Dicson committed
1165
        for i in list(self.handlers.keys()):
1166 1167 1168 1169 1170
            if self.handlers[i].handler_is_connected(i):
                self.handlers[i].disconnect(i)
            del self.handlers[i]
        self.conv_textview.del_handlers()
        self.msg_textview.destroy()
1171 1172 1173
        # PluginSystem: calling shutdown of super class (ChatControlBase) to let
        # it remove it's GUI extension points
        super(ChatControl, self).shutdown()
1174 1175 1176 1177 1178 1179 1180 1181

    def minimizable(self):
        return False

    def safe_shutdown(self):
        return False

    def allow_shutdown(self, method, on_yes, on_no, on_minimize):
1182
        if time.time() - app.last_message_time[self.account]\
1183 1184
        [self.get_full_jid()] < 2:
            # 2 seconds
1185

1186 1187 1188 1189 1190 1191
            def on_ok():
                on_yes(self)

            def on_cancel():
                on_no(self)

1192
            ConfirmationDialog(
1193 1194 1195 1196 1197 1198 1199
                #%s is being replaced in the code with JID
                _('You just received a new message from "%s"') % \
                self.contact.jid,
                _('If you close this tab and you have history disabled, '\
                'this message will be lost.'), on_response_ok=on_ok,
                on_response_cancel=on_cancel,
                transient_for=self.parent_win.window)
1200 1201 1202
            return
        on_yes(self)

1203 1204 1205 1206
    def _nec_chatstate_received(self, event):
        if event.account != self.account:
            return

1207 1208 1209 1210 1211 1212
        if self.TYPE_ID == 'pm':
            if event.contact != self.gc_contact:
                return
        else:
            if event.contact.jid != self.contact.jid:
                return
1213

1214
        self.draw_banner_text()
1215

1216
        # update chatstate in tab for this chat
1217 1218 1219 1220 1221
        if event.contact.is_gc_contact:
            chatstate = event.contact.chatstate
        else:
            chatstate = app.contacts.get_combined_chatstate(
                self.account, self.contact.jid)
1222
        self.parent_win.redraw_tab(self, chatstate)
1223

1224
    def _nec_caps_received(self, obj):
1225 1226 1227 1228 1229
        if obj.conn.name != self.account:
            return
        if self.TYPE_ID == 'chat' and obj.jid != self.contact.jid:
            return
        if self.TYPE_ID == 'pm' and obj.fjid != self.contact.jid:
1230 1231 1232
            return
        self.update_ui()

1233 1234 1235 1236 1237 1238 1239
    def _nec_ping(self, obj):
        if self.contact != obj.contact:
            return
        if obj.name == 'ping-sent':
            self.print_conversation(_('Ping?'), 'status')
        elif obj.name == 'ping-reply':
            self.print_conversation(
1240
                _('Pong! (%s seconds)') % obj.seconds, 'status')
1241 1242
        elif obj.name == 'ping-error':
            self.print_conversation(_('Error.'), 'status')
1243

1244
    def show_avatar(self):
1245
        if not app.config.get('show_avatar_in_chat'):