chat_control.py 63.7 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
Philipp Hörist's avatar
Philipp Hörist committed
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
46
from gajim.common.helpers import open_uri
47
from gajim.common.helpers import geo_provider_from_location
André's avatar
André committed
48
from gajim.common.contacts import GC_Contact
49 50 51
from gajim.common.const import AvatarSize
from gajim.common.const import KindConstant
from gajim.common.const import Chatstate
52
from gajim.common.const import PEPEventType
nicfit's avatar
added  
nicfit committed
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

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

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

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

    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.
Philipp Hörist's avatar
Philipp Hörist committed
93
    COMMAND_HOST = ChatCommands  # type: ClassVar[Type[CommandHost]]
94

95
    def __init__(self, parent_win, contact, acct, session, resource=None):
96
        ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
97
            'chat_control', contact, acct, resource)
98

99 100
        self.last_recv_message_id = None
        self.last_recv_message_marks = None
101
        self.last_message_timestamp = None
Philipp Hörist's avatar
Philipp Hörist committed
102

103
        self._formattings_button = self.xml.get_object('formattings_button')
Philipp Hörist's avatar
Philipp Hörist committed
104 105
        self.emoticons_button = self.xml.get_object('emoticons_button')
        self.toggle_emoticons()
106 107

        self.widget_set_visible(self.xml.get_object('banner_eventbox'),
108
            app.config.get('hide_chat_banner'))
109 110

        self.authentication_button = self.xml.get_object(
111
            'authentication_button')
112
        id_ = self.authentication_button.connect('clicked',
113
            self._on_authentication_button_clicked)
114 115
        self.handlers[id_] = self.authentication_button

116 117 118 119
        self.sendfile_button = self.xml.get_object('sendfile_button')
        self.sendfile_button.set_action_name('win.send-file-' + \
                                             self.control_id)

120 121 122
        # Add lock image to show chat encryption
        self.lock_image = self.xml.get_object('lock_image')

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

        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
132 133
        self.audio_state = self.JINGLE_STATE_NULL
        self.audio_available = False
134
        self.video_sid = None
135 136
        self.video_state = self.JINGLE_STATE_NULL
        self.video_available = False
137 138 139 140 141 142 143

        self.update_toolbar()
        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 168 169 170 171 172 173 174 175

        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

        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

176
        self.info_bar = Gtk.InfoBar()
177
        content_area = self.info_bar.get_content_area()
178
        self.info_bar_label = Gtk.Label()
179
        self.info_bar_label.set_use_markup(True)
180 181
        self.info_bar_label.set_halign(Gtk.Align.START)
        self.info_bar_label.set_valign(Gtk.Align.START)
182 183 184
        content_area.add(self.info_bar_label)
        self.info_bar.set_no_show_all(True)
        widget = self.xml.get_object('vbox2')
185
        widget.pack_start(self.info_bar, False, True, 5)
186 187 188 189 190 191 192
        widget.reorder_child(self.info_bar, 1)

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

        self.subscribe_events()

193 194 195 196 197
        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
198
            session = app.connections[self.account].find_controlless_session(
199
                self.contact.jid, resource)
200

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

        # Enable encryption if needed
        self.no_autonegotiation = False
Philipp Hörist's avatar
Philipp Hörist committed
208
        self.add_actions()
209
        self.update_ui()
Philipp Hörist's avatar
Philipp Hörist committed
210 211 212 213
        self.set_lock_image()

        self.encryption_menu = self.xml.get_object('encryption_menu')
        self.encryption_menu.set_menu_model(
214 215
            gui_menu_builder.get_encryption_menu(
                self.control_id, self.type_id, self.account == 'Local'))
216
        self.set_encryption_menu_icon()
217 218 219 220
        # restore previous conversation
        self.restore_conversation()
        self.msg_textview.grab_focus()

221 222 223 224
        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
225 226
        app.ged.register_event_handler('activity-received', ged.GUI1,
            self._on_activity_received)
Philipp Hörist's avatar
Philipp Hörist committed
227 228
        app.ged.register_event_handler('tune-received', ged.GUI1,
            self._on_tune_received)
Philipp Hörist's avatar
Philipp Hörist committed
229 230
        app.ged.register_event_handler('location-received', ged.GUI1,
            self._on_location_received)
Philipp Hörist's avatar
Philipp Hörist committed
231 232
        app.ged.register_event_handler('update-client-info', ged.GUI1,
            self._on_update_client_info)
Philipp Hörist's avatar
Philipp Hörist committed
233 234 235 236
        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)
237
        app.ged.register_event_handler('chatstate-received', ged.GUI1,
238
            self._nec_chatstate_received)
239
        app.ged.register_event_handler('caps-update', ged.GUI1,
240
            self._nec_caps_received)
241
        app.ged.register_event_handler('message-sent', ged.OUT_POSTCORE,
242
            self._message_sent)
243 244 245
        app.ged.register_event_handler(
            'mam-decrypted-message-received',
            ged.GUI1, self._nec_mam_decrypted_message_received)
246 247 248
        app.ged.register_event_handler(
            'decrypted-message-received',
            ged.GUI1, self._nec_decrypted_message_received)
249 250 251
        app.ged.register_event_handler(
            'receipt-received',
            ged.GUI1, self._receipt_received)
252

253
        # PluginSystem: adding GUI extension point for this ChatControl
254
        # instance object
255
        app.plugin_manager.gui_extension_point('chat_control', self)
Philipp Hörist's avatar
Philipp Hörist committed
256 257 258
        self.update_actions()

    def add_actions(self):
259
        super().add_actions()
Philipp Hörist's avatar
Philipp Hörist committed
260 261 262 263 264 265 266 267 268 269 270 271
        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)

272
        self.audio_action = Gio.SimpleAction.new_stateful(
Philipp Hörist's avatar
Philipp Hörist committed
273 274
            'toggle-audio-' + self.control_id, None,
            GLib.Variant.new_boolean(False))
275 276
        self.audio_action.connect('change-state', self._on_audio)
        self.parent_win.window.add_action(self.audio_action)
Philipp Hörist's avatar
Philipp Hörist committed
277

278
        self.video_action = Gio.SimpleAction.new_stateful(
Philipp Hörist's avatar
Philipp Hörist committed
279 280
            'toggle-video-' + self.control_id,
            None, GLib.Variant.new_boolean(False))
281 282
        self.video_action.connect('change-state', self._on_video)
        self.parent_win.window.add_action(self.video_action)
Philipp Hörist's avatar
Philipp Hörist committed
283

284
        default_chatstate = app.config.get('send_chatstate_default')
285
        chatstate = app.config.get_per(
286
            'contacts', self.contact.jid, 'send_chatstate', default_chatstate)
287 288 289 290 291 292 293 294

        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
295 296 297
    def update_actions(self):
        win = self.parent_win.window
        online = app.account_is_connected(self.account)
298
        con = app.connections[self.account]
Philipp Hörist's avatar
Philipp Hörist committed
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317

        # 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)

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

        # 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)

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

        # Set File Transfer Button tooltip
338 339 340
        if online and (httpupload.get_enabled() or jingle.get_enabled()):
            tooltip_text = _('Send File…')
        else:
341 342
            tooltip_text = _('No File Transfer available')
        self.sendfile_button.set_tooltip_text(tooltip_text)
Philipp Hörist's avatar
Philipp Hörist committed
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360

        # 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):
361
        AddNewContactWindow(self.account, self.contact.jid)
Philipp Hörist's avatar
Philipp Hörist committed
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380

    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')
381

382 383 384 385 386
    def _on_send_chatstate(self, action, param):
        action.set_state(param)
        app.config.set_per('contacts', self.contact.jid,
                           'send_chatstate', param.get_string())

387 388 389 390
    def subscribe_events(self):
        """
        Register listeners to the events class
        """
391 392
        app.events.event_added_subscribe(self.on_event_added)
        app.events.event_removed_subscribe(self.on_event_removed)
393 394 395 396 397

    def unsubscribe_events(self):
        """
        Unregister listeners to the events class
        """
398 399
        app.events.event_added_unsubscribe(self.on_event_added)
        app.events.event_removed_unsubscribe(self.on_event_removed)
400

401
    def _update_toolbar(self):
402
        # Formatting
403 404
        # TODO: find out what encryption allows for xhtml and which not
        if self.contact.supports(NS_XHTML_IM):
405
            self._formattings_button.set_sensitive(True)
406
            self._formattings_button.set_tooltip_text(_(
407
                'Show a list of formattings'))
408 409
        else:
            self._formattings_button.set_sensitive(False)
Philipp Hörist's avatar
Philipp Hörist committed
410 411
            self._formattings_button.set_tooltip_text(
                _('This contact does not support HTML'))
412 413 414

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

    def update_all_pep_types(self):
Philipp Hörist's avatar
Philipp Hörist committed
425
        self._update_pep(PEPEventType.LOCATION)
426
        self._update_pep(PEPEventType.MOOD)
Philipp Hörist's avatar
Philipp Hörist committed
427
        self._update_pep(PEPEventType.ACTIVITY)
Philipp Hörist's avatar
Philipp Hörist committed
428
        self._update_pep(PEPEventType.TUNE)
429

430 431 432 433 434 435 436 437 438 439
    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
440 441 442
        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
443 444 445
        elif type_ == PEPEventType.TUNE:
            icon = 'audio-x-generic'
            formated_text = format_tune(*data)
Philipp Hörist's avatar
Philipp Hörist committed
446 447 448
        elif type_ == PEPEventType.LOCATION:
            icon = 'applications-internet'
            formated_text = format_location(data)
449 450 451 452 453 454 455 456

        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
457 458
        if type_ == PEPEventType.ACTIVITY:
            return self.xml.get_object('activity_image')
Philipp Hörist's avatar
Philipp Hörist committed
459 460
        if type_ == PEPEventType.TUNE:
            return self.xml.get_object('tune_image')
Philipp Hörist's avatar
Philipp Hörist committed
461 462
        if type_ == PEPEventType.LOCATION:
            return self.xml.get_object('location_image')
463 464 465 466 467

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

Philipp Hörist's avatar
Philipp Hörist committed
468 469 470 471
    @ensure_proper_control
    def _on_activity_received(self, _event):
        self._update_pep(PEPEventType.ACTIVITY)

Philipp Hörist's avatar
Philipp Hörist committed
472 473 474 475
    @ensure_proper_control
    def _on_tune_received(self, _event):
        self._update_pep(PEPEventType.TUNE)

Philipp Hörist's avatar
Philipp Hörist committed
476 477 478 479
    @ensure_proper_control
    def _on_location_received(self, _event):
        self._update_pep(PEPEventType.LOCATION)

480 481 482 483 484
    @ensure_proper_control
    def _on_nickname_received(self, _event):
        self.update_ui()
        self.parent_win.redraw_tab(self)
        self.parent_win.show_title()
485

Philipp Hörist's avatar
Philipp Hörist committed
486 487 488 489 490 491 492 493
    @ensure_proper_control
    def _on_update_client_info(self, event):
        contact = app.contacts.get_contact(
            self.account, event.jid, event.resource)
        if contact is None:
            return
        self.xml.get_object('phone_image').set_visible(contact.uses_phone)

494 495 496 497 498
    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')
499
        if state == self.JINGLE_STATE_NULL:
500 501 502 503
            banner_image.hide()
        else:
            banner_image.show()
        if state == self.JINGLE_STATE_CONNECTING:
504
            banner_image.set_from_icon_name(
505
                    'network-transmit-symbolic', Gtk.IconSize.MENU)
506
        elif state == self.JINGLE_STATE_CONNECTION_RECEIVED:
507
            banner_image.set_from_icon_name(
508
                    'network-receive-symbolic', Gtk.IconSize.MENU)
509
        elif state == self.JINGLE_STATE_CONNECTED:
510
            banner_image.set_from_icon_name(
511
                    'network-transmit-receive-symbolic', Gtk.IconSize.MENU)
512
        elif state == self.JINGLE_STATE_ERROR:
513
            banner_image.set_from_icon_name(
514
                    'network-error-symbolic', Gtk.IconSize.MENU)
515 516 517 518
        self.update_toolbar()

    def update_audio(self):
        self._update_jingle('audio')
Yann Leboulanger's avatar
Yann Leboulanger committed
519
        hbox = self.xml.get_object('audio_buttons_hbox')
520 521
        if self.audio_state == self.JINGLE_STATE_CONNECTED:
            # Set volume from config
522 523
            input_vol = app.config.get('audio_input_volume')
            output_vol = app.config.get('audio_output_volume')
524 525 526 527 528
            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
Yann Leboulanger's avatar
Yann Leboulanger committed
529 530
            hbox.set_no_show_all(False)
            hbox.show_all()
531
        elif not self.audio_sid:
Yann Leboulanger's avatar
Yann Leboulanger committed
532 533
            hbox.set_no_show_all(True)
            hbox.hide()
534 535 536 537 538 539 540 541

    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()
542 543 544 545
        # 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]
546
        # update events
547
        app.events.change_jid(self.account, old_full_jid, new_full_jid)
548 549 550
        # update MessageWindow._controls
        self.parent_win.change_jid(self.account, old_full_jid, new_full_jid)

551 552 553 554 555 556
    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')

557 558 559
    def _set_jingle_state(self, jingle_type, state, sid=None, reason=None):
        if jingle_type not in ('audio', 'video'):
            return
560
        if state in ('connecting', 'connected', 'stop', 'error') and reason:
Philipp Hörist's avatar
Philipp Hörist committed
561
            info = _('%(type)s state : %(state)s, reason: %(reason)s') % {
562
                    'type': jingle_type.capitalize(), 'state': state, 'reason': reason}
Philipp Hörist's avatar
Philipp Hörist committed
563
            self.print_conversation(info, 'info')
564

565
        states = {'connecting': self.JINGLE_STATE_CONNECTING,
566 567
                'connection_received': self.JINGLE_STATE_CONNECTION_RECEIVED,
                'connected': self.JINGLE_STATE_CONNECTED,
568
                'stop': self.JINGLE_STATE_NULL,
569 570
                'error': self.JINGLE_STATE_ERROR}

571
        jingle_state = states[state]
572
        if getattr(self, jingle_type + '_state') == jingle_state or state == 'error':
573 574 575 576 577 578
            return

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

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

580
        if jingle_state == self.JINGLE_STATE_NULL:
581 582 583 584
            setattr(self, jingle_type + '_sid', None)
        if state in ('connection_received', 'connecting'):
            setattr(self, jingle_type + '_sid', sid)

585 586
        v = GLib.Variant.new_boolean(jingle_state != self.JINGLE_STATE_NULL)
        getattr(self, jingle_type + '_action').change_state(v)
587 588 589 590 591 592 593 594 595 596

        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):
597
        session = app.connections[self.account].get_jingle_session(
598 599 600 601 602 603 604 605 606
                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()

Yann Leboulanger's avatar
Yann Leboulanger committed
607
    def on_mic_hscale_value_changed(self, widget, value):
608 609
        self._get_audio_content().set_mic_volume(value / 100)
        # Save volume to config
610
        app.config.set('audio_input_volume', value)
611

Yann Leboulanger's avatar
Yann Leboulanger committed
612
    def on_sound_hscale_value_changed(self, widget, value):
613 614
        self._get_audio_content().set_out_volume(value / 100)
        # Save volume to config
615
        app.config.set('audio_output_volume', value)
616 617 618 619 620 621

    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
622 623 624 625 626 627 628
            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()
629 630
            if sha is not None:
                gui_menu_builder.show_save_as_menu(sha, name)
631 632 633
        return True

    def on_location_eventbox_button_release_event(self, widget, event):
634
        if 'geoloc' in self.contact.pep:
Philipp Hörist's avatar
Philipp Hörist committed
635
            location = self.contact.pep['geoloc'].data
636 637 638
            if 'lat' in location and 'lon' in location:
                uri = geo_provider_from_location(location['lat'],
                                                 location['lon'])
639
                open_uri(uri)
640

641 642 643 644
    def on_location_eventbox_leave_notify_event(self, widget, event):
        """
        Just moved the mouse so show the cursor
        """
645
        cursor = get_cursor('LEFT_PTR')
Yann Leboulanger's avatar
Yann Leboulanger committed
646
        self.parent_win.window.get_window().set_cursor(cursor)
647 648

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

652 653 654 655 656 657
    def update_ui(self):
        # The name banner is drawn here
        ChatControlBase.update_ui(self)
        self.update_toolbar()

    def _update_banner_state_image(self):
658 659
        contact = app.contacts.get_contact_with_highest_priority(
            self.account, self.contact.jid)
660 661 662 663 664 665
        if not contact or self.resource:
            # For transient contacts
            contact = self.contact
        show = contact.show

        # Set banner image
666
        icon = get_icon_name(show)
667
        banner_status_img = self.xml.get_object('banner_status_image')
668
        banner_status_img.set_from_icon_name(icon, Gtk.IconSize.DND)
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683

    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:
684 685 686
            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
687
        name = i18n.direction_mark + GLib.markup_escape_text(name)
688 689 690 691 692

        # 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 = ''
693
        for account in app.contacts.get_accounts():
694 695 696 697
            if account == self.account:
                continue
            if acct_info: # We already found a contact with same nick
                break
698
            for jid in app.contacts.get_jid_list(account):
699
                other_contact_ = \
700
                    app.contacts.get_first_contact_from_jid(account, jid)
701 702 703
                if other_contact_.get_shown_name() == \
                self.contact.get_shown_name():
                    acct_info = i18n.direction_mark + ' (%s)' % \
704 705
                        GLib.markup_escape_text(
                            app.get_account_label(self.account))
706 707 708 709
                    break

        status = contact.status
        if status is not None:
710 711
            banner_name_label.set_ellipsize(Pango.EllipsizeMode.END)
            self.banner_status_label.set_ellipsize(Pango.EllipsizeMode.END)
712
            status_reduced = helpers.reduce_chars_newlines(status, max_lines=1)
713 714
        else:
            status_reduced = ''
Yann Leboulanger's avatar
Yann Leboulanger committed
715
        status_escaped = GLib.markup_escape_text(status_reduced)
716

717 718 719 720 721
        if self.TYPE_ID == 'pm':
            cs = self.gc_contact.chatstate
        else:
            cs = app.contacts.get_combined_chatstate(
                self.account, self.contact.jid)
722 723 724

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

Philipp Hörist's avatar
Philipp Hörist committed
726 727
            label_text = '<span>%s</span><span size="x-small" weight="light">%s %s</span>' \
                % (name, acct_info, chatstate)
728
            if acct_info:
729
                acct_info = i18n.direction_mark + ' ' + acct_info
730 731
            label_tooltip = '%s%s %s' % (name, acct_info, chatstate)
        else:
Philipp Hörist's avatar
Philipp Hörist committed
732 733
            label_text = '<span>%s</span><span size="x-small" weight="light">%s</span>' % \
                    (name, acct_info)
734
            if acct_info:
735
                acct_info = i18n.direction_mark + ' ' + acct_info
736 737 738 739
            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
740
            status_text = '<span size="x-small" weight="light">%s</span>' % status_text
741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757
            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
758 759
        setattr(self, jingle_type + '_sid', None)
        setattr(self, jingle_type + '_state', self.JINGLE_STATE_NULL)
760
        session = app.connections[self.account].get_jingle_session(
761 762 763 764 765
                self.contact.get_full_jid(), sid)
        if session:
            content = session.get_content(jingle_type)
            if content:
                session.remove_content(content.creator, content.name)
766 767
        v = GLib.Variant.new_boolean(False)
        getattr(self, jingle_type + '_action').change_state(v)
768
        getattr(self, 'update_' + jingle_type)()
769

Philipp Hörist's avatar
Philipp Hörist committed
770 771
    def on_jingle_button_toggled(self, state, jingle_type):
        if state:
772
            if getattr(self, jingle_type + '_state') == \
773
            self.JINGLE_STATE_NULL:
774 775 776
                if jingle_type == 'video':
                    video_hbox = self.xml.get_object('video_hbox')
                    video_hbox.set_no_show_all(False)
777
                    if app.config.get('video_see_self'):
778 779
                        fixed = self.xml.get_object('outgoing_fixed')
                        fixed.set_no_show_all(False)
780
                        video_hbox.show_all()
781 782
                        out_da = self.xml.get_object('outgoing_drawingarea')
                        out_da.realize()
783
                        if os.name == 'nt':
784
                            out_xid = out_da.get_window().handle
785
                        else:
786
                            out_xid = out_da.get_window().get_xid()
787 788
                    else:
                        out_xid = None
789
                    video_hbox.show_all()
790 791 792
                    in_da = self.xml.get_object('incoming_drawingarea')
                    in_da.realize()
                    in_xid = in_da.get_window().get_xid()
793
                    sid = app.connections[self.account].start_video(
794 795
                        self.contact.get_full_jid(), in_xid, out_xid)
                else:
796
                    sid = getattr(app.connections[self.account],
797 798 799
                        'start_' + jingle_type)(self.contact.get_full_jid())
                getattr(self, 'set_' + jingle_type + '_state')('connecting', sid)
        else:
800 801 802 803 804
            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)
805 806
            self.close_jingle_content(jingle_type)

Philipp Hörist's avatar
Philipp Hörist committed
807
    def set_lock_image(self):
808
        encryption_state = {'visible': self.encryption is not None,
Philipp Hörist's avatar
Philipp Hörist committed
809 810 811
                            'enc_type': self.encryption,
                            'authenticated': False}

812
        if self.encryption:
813
            app.plugin_manager.extension_point(
814
                'encryption_state' + self.encryption, self, encryption_state)
Philipp Hörist's avatar
Philipp Hörist committed
815 816

        self._show_lock_image(**encryption_state)
817

818
    def _show_lock_image(self, visible, enc_type='', authenticated=False):
819 820 821 822 823
        """
        Set lock icon visibility and create tooltip
        """
        if authenticated:
            authenticated_string = _('and authenticated')
824
            self.lock_image.set_from_icon_name(
825
                'security-high-symbolic', Gtk.IconSize.MENU)
826 827
        else:
            authenticated_string = _('and NOT authenticated')
828
            self.lock_image.set_from_icon_name(
829
                'security-low-symbolic', Gtk.IconSize.MENU)
830

831 832
        tooltip = _('%(type)s encryption is active %(authenticated)s') % \
            {'type': enc_type, 'authenticated': authenticated_string}
833 834 835

        self.authentication_button.set_tooltip_text(tooltip)
        self.widget_set_visible(self.authentication_button, not visible)
836
        self.lock_image.set_sensitive(visible)
837 838

    def _on_authentication_button_clicked(self, widget):
839
        if self.encryption:
840
            app.plugin_manager.extension_point(
841
                'encryption_dialog' + self.encryption, self)
842

843 844 845
    def _nec_mam_decrypted_message_received(self, obj):
        if obj.conn.name != self.account:
            return
846 847 848 849 850 851 852

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

854
        kind = '' # incoming
855 856 857
        if obj.kind == KindConstant.CHAT_MSG_SENT:
            kind = 'outgoing'

858 859 860
        self.print_conversation(
            obj.msgtxt, kind, tim=obj.timestamp,
            encrypted=obj.encrypted, correct_id=obj.correct_id,
861 862
            msg_stanza_id=obj.message_id, additional_data=obj.additional_data)

863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892
    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])

893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913
    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
914
                self.msg_textview, 'gajim-msg-correcting')
915 916 917 918 919 920 921

        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)

Philipp Hörist's avatar
Philipp Hörist committed
922
    def send_message(self, message, xhtml=None,
923
                     process_commands=True, attention=False):
924 925 926
        """
        Send a message to contact
        """
Philipp Hörist's avatar
Philipp Hörist committed
927 928 929

        if self.encryption:
            self.sendmessage = True
930
            app.plugin_manager.extension_point(
Philipp Hörist's avatar
Philipp Hörist committed
931 932 933 934
                    'send_message' + self.encryption, self)
            if not self.sendmessage:
                return

935
        message = helpers.remove_invalid_xml_chars(message)
936 937 938
        if message in ('', None, '\n'):
            return None

939 940 941 942 943 944
        ChatControlBase.send_message(self,
                                     message,
                                     type_='chat',
                                     xhtml=xhtml,
                                     process_commands=process_commands,
                                     attention=attention)
945

946
    def get_our_nick(self):
947
        return app.nicks[self.account]
948

949
    def print_conversation(self, text, frm='', tim=None, encrypted=None,
950
    subject=None, xhtml=None, simple=False, xep0184_id=None,
951
    displaymarking=None, msg_log_id=None, correct_id=None,
952
    msg_stanza_id=None, additional_data=None):
953 954 955 956 957 958 959 960
        """
        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
961
        If frm is set to print_queue: it is incoming from queue.
962
        If frm is set to another value: it's an outgoing message.
Alexander Krotov's avatar
Alexander Krotov committed
963
        If frm is not set: it's an incoming message.
964 965 966
        """
        contact = self.contact

967
        if additional_data is None:
968
            additional_data = AdditionalDataDict()
969

970
        if frm == 'status':
971
            if not app.config.get('print_status_in_chats'):
972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989
                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'
990
                name = self.get_our_nick()
991
                if not xhtml and not encrypted and \
992
                app.config.get('rst_formatting_outgoing_messages'):
André's avatar
André committed
993
                    from gajim.common.rst_xhtml_generator import create_xhtml
994 995 996 997
                    xhtml = create_xhtml(text)
                    if xhtml:
                        xhtml = '<body xmlns="%s">%s</body>' % (NS_XHTML, xhtml)
        ChatControlBase.print_conversation_line(self, text, kind, name, tim,
998 999
            subject=subject, old_kind=self.old_msg_kind, xhtml=xhtml,
            simple=simple, xep0184_id=xep0184_id, displaymarking=displaymarking,
1000
            msg_log_id=msg_log_id, msg_stanza_id=msg_stanza_id,
1001 1002
            correct_id=correct_id, additional_data=additional_data,
            encrypted=encrypted)
1003 1004 1005 1006 1007
        if text.startswith('/me ') or text.startswith('/me\n'):
            self.old_msg_kind = None
        else:
            self.old_msg_kind = kind

1008 1009 1010 1011 1012 1013 1014 1015
    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)

1016
    def get_tab_label(self):
1017 1018 1019 1020 1021
        unread = ''
        if self.resource:
            jid = self.contact.get_full_jid()
        else:
            jid = self.contact.jid
1022
        num_unread = len(app.events.get_events(self.account, jid,
1023
                ['printed_' + self.type_id, self.type_id]))
1024
        if num_unread == 1 and not app.config.get('show_unread_tab_icon'):
1025 1026
            unread = '*'
        elif num_unread > 1:
Yann Leboulanger's avatar
Yann Leboulanger committed
1027
            unread = '[' + str(num_unread) + ']'
1028 1029 1030 1031

        name = self.contact.get_shown_name()
        if self.resource:
            name += '/' + self.resource
Yann Leboulanger's avatar
Yann Leboulanger committed
1032
        label_str = GLib.markup_escape_text(name)
1033 1034
        if num_unread: # if unread, text in the label becomes bold
            label_str = '<b>' + unread + label_str + '</b>'
1035
        return label_str
1036 1037 1038 1039 1040 1041

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

1043
        if app.config.get('show_avatar_in_tabs'):
1044 1045 1046 1047 1048
            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
1049

1050
        if count_unread:
1051
            num_unread = len(app.events.get_events(self.account, jid,
1052 1053 1054
                    ['printed_' + self.type_id, self.type_id]))
        else:
            num_unread = 0
1055 1056 1057 1058

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

1060
        if num_unread and app.config.get('show_unread_tab_icon'):
1061
            icon_name = get_icon_name('event', transport=transport)
1062
        else:
1063
            contact = app.contacts.get_contact_with_highest_priority(
1064 1065 1066 1067
                    self.account, self.contact.jid)
            if not contact or self.resource:
                # For transient contacts
                contact = self.contact
1068
            icon_name = get_icon_name(contact.show, transport=transport)
1069

1070
        return icon_name
1071 1072 1073 1074

    def prepare_context_menu(self, hide_buttonbar_items=False):
        """
        Set compact view menuitem active state sets active and sensitivity state
1075
        for history_menuitem (False for tranasports) and file_transfer_menuitem
1076
        and hide()/show() for add_to_roster_menuitem
1077
        """
1078
        if app.jid_is_transport(self.contact.jid):