groupchat_control.py 84.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2005-2007 Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
#                    Alex Mauer <hawke AT hawkesnest.net>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
#                         Travis Shirk <travis AT pobox.com>
# Copyright (C) 2007-2008 Julien Pivotto <roidelapluie AT gmail.com>
#                         Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
#                    Jonathan Schleifer <js-gajim AT webkeks.org>
11
# Copyright (C) 2018 Marcin Mielniczuk <marmistrz dot dev at zoho dot eu>
12 13 14 15 16 17 18 19 20 21 22 23 24 25
#
# 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/>.
26

nicfit's avatar
nicfit committed
27
import time
28
import base64
Philipp Hörist's avatar
Philipp Hörist committed
29
import logging
Philipp Hörist's avatar
Philipp Hörist committed
30

Philipp Hörist's avatar
Philipp Hörist committed
31
import nbxmpp
32 33
from nbxmpp.protocol import InvalidJid
from nbxmpp.protocol import validate_resourcepart
Philipp Hörist's avatar
Philipp Hörist committed
34
from nbxmpp.const import StatusCode
Philipp Hörist's avatar
Philipp Hörist committed
35 36
from nbxmpp.const import Affiliation
from nbxmpp.const import PresenceType
37
from nbxmpp.util import is_error_result
38

39
from gi.repository import Gtk
Dicson's avatar
Dicson committed
40
from gi.repository import Gdk
Yann Leboulanger's avatar
Yann Leboulanger committed
41
from gi.repository import GLib
42
from gi.repository import Gio
Philipp Hörist's avatar
Philipp Hörist committed
43

André's avatar
André committed
44 45 46 47
from gajim import gtkgui_helpers
from gajim import gui_menu_builder
from gajim import message_control
from gajim import vcard
48

Philipp Hörist's avatar
Philipp Hörist committed
49
from gajim.common.const import AvatarSize
André's avatar
André committed
50
from gajim.common import events
51
from gajim.common import app
André's avatar
André committed
52
from gajim.common import helpers
53
from gajim.common.helpers import event_filter
54
from gajim.common.helpers import to_user_string
André's avatar
André committed
55
from gajim.common import ged
56
from gajim.common.i18n import _
57
from gajim.common import contacts
58
from gajim.common.const import Chatstate
59
from gajim.common.const import MUCJoinedState
60

André's avatar
André committed
61
from gajim.chat_control_base import ChatControlBase
62

André's avatar
André committed
63 64
from gajim.command_system.implementation.hosts import GroupChatCommands
from gajim.common.connection_handlers_events import GcMessageOutgoingEvent
65

66 67
from gajim.gtk.dialogs import DialogButton
from gajim.gtk.dialogs import NewConfirmationCheckDialog
68
from gajim.gtk.dialogs import ErrorDialog
69
from gajim.gtk.dialogs import NewConfirmationDialog
70
from gajim.gtk.filechoosers import AvatarChooserDialog
71
from gajim.gtk.groupchat_config import GroupchatConfig
72
from gajim.gtk.adhoc import AdHocCommand
73
from gajim.gtk.dataform import DataFormWidget
74
from gajim.gtk.groupchat_info import GroupChatInfoScrolled
75
from gajim.gtk.groupchat_roster import GroupchatRoster
76
from gajim.gtk.util import NickCompletionGenerator
77
from gajim.gtk.util import get_icon_name
78

Philipp Hörist's avatar
Philipp Hörist committed
79

80 81
log = logging.getLogger('gajim.groupchat_control')

82

83
class GroupchatControl(ChatControlBase):
84 85 86 87 88 89
    TYPE_ID = message_control.TYPE_GC

    # Set a command host to bound to. Every command given through a group chat
    # will be processed with this command host.
    COMMAND_HOST = GroupChatCommands

90
    def __init__(self, parent_win, contact, muc_data, acct):
91
        ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
92
                                 'groupchat_control', contact, acct)
93
        self.force_non_minimizable = False
94 95
        self.is_anonymous = True

Philipp Hörist's avatar
Philipp Hörist committed
96 97 98
        self.emoticons_button = self.xml.get_object('emoticons_button')
        self.toggle_emoticons()

99 100 101
        formattings_button = self.xml.get_object('formattings_button')
        formattings_button.set_sensitive(False)

102 103 104 105 106 107 108 109 110 111 112 113 114
        self.room_jid = self.contact.jid
        self._muc_data = muc_data

        # Stores nickname we want to kick
        self._kick_nick = None

        # Stores nickname we want to ban
        self._ban_jid = None

        self.roster = GroupchatRoster(self.account, self.room_jid, self)
        self.xml.roster_revealer.add(self.roster)
        self.roster.connect('row-activated', self._on_roster_row_activated)

115
        if parent_win is not None:
116
            # On AutoJoin with minimize Groupchats are created without parent
117
            # Tooltip Window and Actions have to be created with parent
118
            self.roster.enable_tooltips()
119
            self.add_actions()
120
            GLib.idle_add(self.update_actions)
121 122 123
            self.scale_factor = parent_win.window.get_scale_factor()
        else:
            self.scale_factor = app.interface.roster.scale_factor
lovetox's avatar
lovetox committed
124

125 126
        if not app.config.get('hide_groupchat_banner'):
            self.xml.banner_eventbox.set_no_show_all(False)
127 128 129 130 131

        # muc attention flag (when we are mentioned in a muc)
        # if True, the room has mentioned us
        self.attention_flag = False

Philipp Hörist's avatar
Philipp Hörist committed
132 133 134
        # True if we initiated room destruction
        self._wait_for_destruction = False

135 136 137
        # sorted list of nicks who mentioned us (last at the end)
        self.attention_list = []
        self.nick_hits = []
138
        self._nick_completion = NickCompletionGenerator(muc_data.nick)
139 140 141 142 143
        self.last_key_tabs = False

        self.name_label = self.xml.get_object('banner_name_label')
        self.event_box = self.xml.get_object('banner_eventbox')

Philipp Hörist's avatar
Philipp Hörist committed
144
        self.setup_seclabel()
145

146 147 148 149 150
        # Send file
        self.sendfile_button = self.xml.get_object('sendfile_button')
        self.sendfile_button.set_action_name('win.send-file-' + \
                                             self.control_id)

151 152 153 154 155 156 157 158 159 160 161
        # Encryption
        self.lock_image = self.xml.get_object('lock_image')
        self.authentication_button = self.xml.get_object(
            'authentication_button')
        id_ = self.authentication_button.connect('clicked',
            self._on_authentication_button_clicked)
        self.handlers[id_] = self.authentication_button
        self.set_lock_image()

        self.encryption_menu = self.xml.get_object('encryption_menu')
        self.encryption_menu.set_menu_model(
162
            gui_menu_builder.get_encryption_menu(self.control_id, self.type_id))
163
        self.set_encryption_menu_icon()
164

165 166 167
        # Banner
        self.banner_actionbar = self.xml.get_object('banner_actionbar')
        self.hide_roster_button = Gtk.Button.new_from_icon_name(
168
            'go-next-symbolic', Gtk.IconSize.MENU)
169
        self.hide_roster_button.set_valign(Gtk.Align.CENTER)
170 171 172 173
        self.hide_roster_button.connect('clicked',
                                        lambda *args: self.show_roster())
        self.banner_actionbar.pack_end(self.hide_roster_button)

174 175 176
        # Holds CaptchaRequest widget
        self._captcha_request = None

177 178 179 180 181
        # MUC Info
        self._subject_data = None
        self._muc_info_box = GroupChatInfoScrolled(self.account, {'width': 600})
        self.xml.info_grid.attach(self._muc_info_box, 0, 0, 1, 1)

182 183 184
        self.control_menu = gui_menu_builder.get_groupchat_menu(self.control_id,
                                                                self.account,
                                                                self.room_jid)
Philipp Hörist's avatar
Philipp Hörist committed
185 186
        settings_menu = self.xml.get_object('settings_menu')
        settings_menu.set_menu_model(self.control_menu)
187

Philipp Hörist's avatar
Philipp Hörist committed
188
        self._event_handlers = [
189
            ('muc-creation-failed', ged.GUI1, self._on_muc_creation_failed),
190
            ('muc-joined', ged.GUI1, self._on_muc_joined),
191
            ('muc-join-failed', ged.GUI1, self._on_muc_join_failed),
Philipp Hörist's avatar
Philipp Hörist committed
192 193 194 195 196 197 198 199
            ('muc-user-joined', ged.GUI1, self._on_user_joined),
            ('muc-user-left', ged.GUI1, self._on_user_left),
            ('muc-nickname-changed', ged.GUI1, self._on_nickname_changed),
            ('muc-self-presence', ged.GUI1, self._on_self_presence),
            ('muc-self-kicked', ged.GUI1, self._on_self_kicked),
            ('muc-user-affiliation-changed', ged.GUI1, self._on_affiliation_changed),
            ('muc-user-status-show-changed', ged.GUI1, self._on_status_show_changed),
            ('muc-user-role-changed', ged.GUI1, self._on_role_changed),
Philipp Hörist's avatar
Philipp Hörist committed
200 201
            ('muc-destroyed', ged.GUI1, self._on_destroyed),
            ('muc-presence-error', ged.GUI1, self._on_presence_error),
202
            ('muc-password-required', ged.GUI1, self._on_password_required),
Philipp Hörist's avatar
Philipp Hörist committed
203 204 205
            ('muc-config-changed', ged.GUI1, self._on_config_changed),
            ('muc-subject', ged.GUI1, self._on_subject),
            ('muc-captcha-challenge', ged.GUI1, self._on_captcha_challenge),
206
            ('muc-captcha-error', ged.GUI1, self._on_captcha_error),
207
            ('muc-voice-request', ged.GUI1, self._on_voice_request),
208 209
            ('muc-disco-update', ged.GUI1, self._on_disco_update),
            ('muc-configuration-finished', ged.GUI1, self._on_configuration_finished),
210
            ('muc-configuration-failed', ged.GUI1, self._on_configuration_failed),
Philipp Hörist's avatar
Philipp Hörist committed
211 212 213 214 215 216
            ('gc-message-received', ged.GUI1, self._nec_gc_message_received),
            ('mam-decrypted-message-received', ged.GUI1, self._nec_mam_decrypted_message_received),
            ('update-room-avatar', ged.GUI1, self._nec_update_room_avatar),
            ('signed-in', ged.GUI1, self._nec_signed_in),
            ('decrypted-message-received', ged.GUI2, self._nec_decrypted_message_received),
            ('gc-stanza-message-outgoing', ged.OUT_POSTCORE, self._message_sent),
217
            ('bookmarks-received', ged.GUI2, self._on_bookmarks_received),
Philipp Hörist's avatar
Philipp Hörist committed
218 219 220 221 222
        ]

        for handler in self._event_handlers:
            app.ged.register_event_handler(*handler)

223
        self.is_connected = False
224 225 226
        # disable win, we are not connected yet
        ChatControlBase.got_disconnected(self)

227 228
        # Stack
        self.xml.stack.show_all()
229 230
        self.xml.stack.set_visible_child_name('progress')
        self.xml.progress_spinner.start()
231

232 233
        self.update_ui()
        self.widget.show_all()
234 235 236 237 238

        if app.config.get('hide_groupchat_occupants_list'):
            # Roster is shown by default, so toggle the roster button to hide it
            self.show_roster()

239 240
        # PluginSystem: adding GUI extension point for this GroupchatControl
        # instance object
241
        app.plugin_manager.gui_extension_point('groupchat_control', self)
242

243 244 245 246
    @property
    def nick(self):
        return self._muc_data.nick

247 248 249 250 251 252
    @property
    def subject(self):
        if self._subject_data is None:
            return ''
        return self._subject_data.subject

253 254 255 256
    @property
    def room_name(self):
        return self.contact.get_shown_name()

257 258 259 260
    @property
    def disco_info(self):
        return app.logger.get_last_disco_info(self.contact.jid)

261
    def add_actions(self):
262
        super().add_actions()
263
        actions = [
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
            ('change-subject-', None, self._on_change_subject),
            ('change-nickname-', None, self._on_change_nick),
            ('disconnect-', None, self._on_disconnect),
            ('destroy-', None, self._on_destroy_room),
            ('configure-', None, self._on_configure_room),
            ('request-voice-', None, self._on_request_voice),
            ('upload-avatar-', None, self._on_upload_avatar),
            ('information-', None, self._on_information),
            ('contact-information-', 's', self._on_contact_information),
            ('execute-command-', 's', self._on_execute_command),
            ('block-', 's', self._on_block),
            ('unblock-', 's', self._on_unblock),
            ('ban-', 's', self._on_ban),
            ('kick-', 's', self._on_kick),
            ('change-role-', 'as', self._on_change_role),
            ('change-affiliation-', 'as', self._on_change_affiliation),
280
        ]
281 282

        for action in actions:
283 284 285 286
            action_name, variant, func = action
            if variant is not None:
                variant = GLib.VariantType.new(variant)
            act = Gio.SimpleAction.new(action_name + self.control_id, variant)
287 288 289
            act.connect("activate", func)
            self.parent_win.window.add_action(act)

290 291
        minimize = app.config.get_per(
            'rooms', self.contact.jid, 'minimize_on_close', True)
292 293

        act = Gio.SimpleAction.new_stateful(
294 295 296 297 298 299 300 301 302 303 304 305
            'minimize-on-close-' + self.control_id, None,
            GLib.Variant.new_boolean(minimize))
        act.connect('change-state', self._on_minimize_on_close)
        self.parent_win.window.add_action(act)

        minimize = app.config.get_per(
            'rooms', self.contact.jid, 'minimize_on_autojoin', True)

        act = Gio.SimpleAction.new_stateful(
            'minimize-on-autojoin-' + self.control_id, None,
            GLib.Variant.new_boolean(minimize))
        act.connect('change-state', self._on_minimize_on_autojoin)
306 307
        self.parent_win.window.add_action(act)

308
        default_muc_chatstate = app.config.get('send_chatstate_muc_default')
309
        chatstate = app.config.get_per(
310
            'rooms', self.contact.jid, 'send_chatstate', default_muc_chatstate)
311 312 313 314 315 316 317 318

        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)

319
        # Enable notify on all for private rooms
320 321 322 323
        members_only = False
        if self.disco_info is not None:
            members_only = self.disco_info.muc_is_members_only

324
        value = app.config.get_per(
325
            'rooms', self.contact.jid, 'notify_on_all_messages', members_only)
326 327 328 329 330 331 332

        act = Gio.SimpleAction.new_stateful(
            'notify-on-message-' + self.control_id,
            None, GLib.Variant.new_boolean(value))
        act.connect('change-state', self._on_notify_on_all_messages)
        self.parent_win.window.add_action(act)

333
        status_default = app.config.get('print_status_muc_default')
334 335
        value = app.config.get_per('rooms', self.contact.jid,
                                   'print_status', status_default)
Philipp Hörist's avatar
Philipp Hörist committed
336 337 338 339 340 341 342

        act = Gio.SimpleAction.new_stateful(
            'print-status-' + self.control_id,
            None, GLib.Variant.new_boolean(value))
        act.connect('change-state', self._on_print_status)
        self.parent_win.window.add_action(act)

343
        join_default = app.config.get('print_join_left_default')
344 345
        value = app.config.get_per('rooms', self.contact.jid,
                                   'print_join_left', join_default)
Philipp Hörist's avatar
Philipp Hörist committed
346 347 348 349 350 351 352

        act = Gio.SimpleAction.new_stateful(
            'print-join-left-' + self.control_id,
            None, GLib.Variant.new_boolean(value))
        act.connect('change-state', self._on_print_join_left)
        self.parent_win.window.add_action(act)

353 354 355 356 357 358 359 360 361 362 363
        archive_info = app.logger.get_archive_infos(self.contact.jid)
        threshold = helpers.get_sync_threshold(self.contact.jid,
                                               archive_info)

        inital = GLib.Variant.new_string(str(threshold))
        act = Gio.SimpleAction.new_stateful(
            'choose-sync-' + self.control_id,
            inital.get_type(), inital)
        act.connect('change-state', self._on_sync_threshold)
        self.parent_win.window.add_action(act)

364 365 366
    def update_actions(self):
        if self.parent_win is None:
            return
Philipp Hörist's avatar
Philipp Hörist committed
367

368 369
        contact = app.contacts.get_gc_contact(
            self.account, self.room_jid, self.nick)
370
        con = app.connections[self.account]
371 372

        # Destroy Room
373 374
        self._get_action('destroy-').set_enabled(self.is_connected and
                                                 contact.affiliation.is_owner)
375 376

        # Configure Room
377
        self._get_action('configure-').set_enabled(
Philipp Hörist's avatar
Philipp Hörist committed
378 379
            self.is_connected and contact.affiliation in (Affiliation.ADMIN,
                                                          Affiliation.OWNER))
380

381
        self._get_action('request-voice-').set_enabled(self.is_connected and
382
                                                       contact.role.is_visitor)
383 384

        # Change Subject
385
        subject_change = self._is_subject_change_allowed()
386
        self._get_action('change-subject-').set_enabled(self.is_connected and
387
                                                        subject_change)
388 389

        # Change Nick
390
        self._get_action('change-nickname-').set_enabled(self.is_connected)
391

392
        # Execute command
393
        self._get_action('execute-command-').set_enabled(self.is_connected)
394

395
        # Send file (HTTP File Upload)
396 397 398 399 400
        httpupload = self._get_action(
            'send-file-httpupload-')
        httpupload.set_enabled(self.is_connected and
                               con.get_module('HTTPUpload').available)
        self._get_action('send-file-').set_enabled(httpupload.get_enabled())
401

402
        if self.is_connected and httpupload.get_enabled():
403
            tooltip_text = _('Send File…')
404 405 406 407
            max_file_size = con.get_module('HTTPUpload').max_file_size
            if max_file_size is not None:
                max_file_size = max_file_size / (1024 * 1024)
                tooltip_text = _('Send File (max. %s MiB)…') % max_file_size
408 409
        else:
            tooltip_text = _('No File Transfer available')
410 411
        self.sendfile_button.set_tooltip_text(tooltip_text)

412
        # Upload Avatar
413 414 415
        vcard_support = False
        if self.disco_info is not None:
            vcard_support = self.disco_info.supports(nbxmpp.NS_VCARD)
416 417 418 419
        self._get_action('upload-avatar-').set_enabled(
            self.is_connected and
            vcard_support and
            contact.affiliation.is_owner)
420

421 422 423 424 425 426 427 428 429 430 431 432 433 434
        # Print join/left
        join_default = app.config.get('print_join_left_default')
        value = app.config.get_per('rooms', self.contact.jid,
                                   'print_join_left', join_default)
        self._get_action('print-join-left-').set_state(
            GLib.Variant.new_boolean(value))

        # Print join/left
        status_default = app.config.get('print_status_muc_default')
        value = app.config.get_per('rooms', self.contact.jid,
                                   'print_status', status_default)
        self._get_action('print-status-').set_state(
            GLib.Variant.new_boolean(value))

435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
        self._get_action('contact-information-').set_enabled(self.is_connected)

        self._get_action('execute-command-').set_enabled(self.is_connected)

        block_supported = con.get_module('PrivacyLists').supported
        self._get_action('block-').set_enabled(self.is_connected and
                                               block_supported)

        self._get_action('unblock-').set_enabled(self.is_connected and
                                                 block_supported)

        self._get_action('ban-').set_enabled(self.is_connected)

        self._get_action('kick-').set_enabled(self.is_connected)

450 451 452 453 454 455 456 457 458 459 460
    def _is_subject_change_allowed(self):
        contact = app.contacts.get_gc_contact(
            self.account, self.room_jid, self.nick)
        if contact is None:
            return False

        if contact.affiliation in (Affiliation.OWNER, Affiliation.ADMIN):
            return True

        if self.disco_info is None:
            return False
461
        return self.disco_info.muc_subjectmod or False
462

463 464 465
    def _get_action(self, name):
        win = self.parent_win.window
        return win.lookup_action(name + self.control_id)
466

467 468 469 470
    def _show_page(self, name):
        transition = Gtk.StackTransitionType.SLIDE_DOWN
        if name == 'groupchat':
            transition = Gtk.StackTransitionType.SLIDE_UP
471
            self.msg_textview.grab_focus()
472 473 474 475
        if name == 'muc-info':
            # Set focus on the close button, otherwise one of the selectable labels
            # of the GroupchatInfo box gets focus, which means it is fully selected
            self.xml.info_close_button.grab_focus()
476 477
        self.xml.stack.set_visible_child_full(name, transition)

478 479 480
    def _get_current_page(self):
        return self.xml.stack.get_visible_child_name()

481 482
    @event_filter(['account', 'room_jid'])
    def _on_disco_update(self, _event):
483 484 485 486
        if self.parent_win is None:
            return
        win = self.parent_win.window
        self.update_actions()
487
        self.draw_banner_text()
488 489

        # After the room has been created, reevaluate threshold
490
        if self.disco_info.has_mam:
491 492 493 494 495 496
            archive_info = app.logger.get_archive_infos(self.contact.jid)
            threshold = helpers.get_sync_threshold(self.contact.jid,
                                                   archive_info)
            win.change_action_state('choose-sync-%s' % self.control_id,
                                    GLib.Variant('s', str(threshold)))

497 498 499
    # Actions

    def _on_disconnect(self, action, param):
500
        self.leave()
501

502
    def _on_information(self, action, param):
503
        self._muc_info_box.set_from_disco_info(self.disco_info)
504 505 506 507 508 509
        if self._subject_data is not None:
            self._muc_info_box.set_subject(self._subject_data.subject)
            self._muc_info_box.set_author(self._subject_data.nickname,
                                          self._subject_data.user_timestamp)
        self._show_page('muc-info')

510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539
    def _on_destroy_room(self, _action, _param):
        self.xml.destroy_reason_entry.grab_focus()
        self.xml.destroy_button.grab_default()
        self._show_page('destroy')

    def _on_destroy_alternate_changed(self, entry, _param):
        jid = entry.get_text()
        if jid:
            try:
                jid = helpers.validate_jid(jid)
            except Exception:
                icon = 'dialog-warning-symbolic'
                text = _('Invalid XMPP Address')
                self.xml.destroy_alternate_entry.set_icon_from_icon_name(
                    Gtk.EntryIconPosition.SECONDARY, icon)
                self.xml.destroy_alternate_entry.set_icon_tooltip_text(
                    Gtk.EntryIconPosition.SECONDARY, text)
                self.xml.destroy_button.set_sensitive(False)
                return
        self.xml.destroy_alternate_entry.set_icon_from_icon_name(
            Gtk.EntryIconPosition.SECONDARY, None)
        self.xml.destroy_button.set_sensitive(True)

    def _on_destroy_confirm(self, _button):
        reason = self.xml.destroy_reason_entry.get_text()
        jid = self.xml.destroy_alternate_entry.get_text()
        self._wait_for_destruction = True
        con = app.connections[self.account]
        con.get_module('MUC').destroy(self.room_jid, reason, jid)
        self._show_page('groupchat')
540

541
    def _on_configure_room(self, _action, _param):
542 543 544 545 546
        win = app.get_app_window('GroupchatConfig', self.account, self.room_jid)
        if win is not None:
            win.present()
            return

547
        contact = app.contacts.get_gc_contact(
548
            self.account, self.room_jid, self.nick)
Philipp Hörist's avatar
Philipp Hörist committed
549
        if contact.affiliation.is_owner:
550
            con = app.connections[self.account]
551 552
            con.get_module('MUC').request_config(
                self.room_jid, callback=self._on_configure_form_received)
Philipp Hörist's avatar
Philipp Hörist committed
553
        elif contact.affiliation.is_admin:
554 555 556 557 558 559
            GroupchatConfig(self.account,
                            self.room_jid,
                            contact.affiliation.value)

    def _on_configure_form_received(self, result):
        if is_error_result(result):
560
            log.info(result)
561 562
            return
        GroupchatConfig(self.account, result.jid, 'owner', result.form)
Philipp Hörist's avatar
Philipp Hörist committed
563 564 565 566 567 568 569 570 571 572

    def _on_print_join_left(self, action, param):
        action.set_state(param)
        app.config.set_per('rooms', self.contact.jid,
                           'print_join_left', param.get_boolean())

    def _on_print_status(self, action, param):
        action.set_state(param)
        app.config.set_per('rooms', self.contact.jid,
                           'print_status', param.get_boolean())
573 574 575 576 577

    def _on_request_voice(self, action, param):
        """
        Request voice in the current room
        """
578 579
        con = app.connections[self.account]
        con.get_module('MUC').request_voice(self.room_jid)
580

581
    def _on_minimize_on_close(self, action, param):
582
        action.set_state(param)
Philipp Hörist's avatar
Philipp Hörist committed
583 584
        app.config.set_per('rooms', self.contact.jid,
                           'minimize_on_close', param.get_boolean())
585

586 587
    def _on_minimize_on_autojoin(self, action, param):
        action.set_state(param)
Philipp Hörist's avatar
Philipp Hörist committed
588 589
        app.config.set_per('rooms', self.contact.jid,
                           'minimize_on_autojoin', param.get_boolean())
590

591 592 593 594 595
    def _on_send_chatstate(self, action, param):
        action.set_state(param)
        app.config.set_per('rooms', self.contact.jid,
                           'send_chatstate', param.get_string())

596 597 598 599 600
    def _on_notify_on_all_messages(self, action, param):
        action.set_state(param)
        app.config.set_per('rooms', self.contact.jid,
                           'notify_on_all_messages', param.get_boolean())

601 602 603 604 605
    def _on_sync_threshold(self, action, param):
        threshold = param.get_string()
        action.set_state(param)
        app.logger.set_archive_infos(self.contact.jid, sync_threshold=threshold)

606 607 608 609 610 611
    def _on_execute_command(self, _action, param):
        jid = self.room_jid
        nick = param.get_string()
        if nick:
            jid += '/' + nick
        AdHocCommand(self.account, jid)
612

613 614
    def _on_upload_avatar(self, action, param):
        def _on_accept(filename):
615 616
            data, sha = app.interface.avatar_storage.prepare_for_publish(
                filename)
617
            if sha is None:
618
                ErrorDialog(
619 620 621 622
                    _('Could not load image'),
                    transient_for=self.parent_win.window)
                return

623
            avatar = base64.b64encode(data).decode('utf-8')
624 625
            con = app.connections[self.account]
            con.get_module('VCardTemp').upload_room_avatar(
626 627 628 629 630 631
                self.room_jid, avatar)

        AvatarChooserDialog(_on_accept,
                            transient_for=self.parent_win.window,
                            modal=True)

632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687
    def _on_contact_information(self, _action, param):
        nick = param.get_string()
        gc_contact = app.contacts.get_gc_contact(self.account,
                                                 self.room_jid,
                                                 nick)
        contact = gc_contact.as_contact()
        if contact.jid in app.interface.instances[self.account]['infos']:
            app.interface.instances[self.account]['infos'][contact.jid].\
                window.present()
        else:
            app.interface.instances[self.account]['infos'][contact.jid] = \
                vcard.VcardWindow(contact, self.account, gc_contact)

    def _on_block(self, _action, param):
        nick = param.get_string()
        fjid = self.room_jid + '/' + nick
        con = app.connections[self.account]
        con.get_module('PrivacyLists').block_gc_contact(fjid)
        self.roster.draw_contact(nick)

    def _on_unblock(self, _action, param):
        nick = param.get_string()
        fjid = self.room_jid + '/' + nick
        con = app.connections[self.account]
        con.get_module('PrivacyLists').unblock_gc_contact(fjid)
        self.roster.draw_contact(nick)

    def _on_kick(self, _action, param):
        nick = param.get_string()
        self._kick_nick = nick
        self.xml.kick_label.set_text(_('Kick %s' % nick))
        self.xml.kick_reason_entry.grab_focus()
        self.xml.kick_participant_button.grab_default()
        self._show_page('kick')

    def _on_ban(self, _action, param):
        jid = param.get_string()
        self._ban_jid = jid
        nick = app.get_nick_from_jid(jid)
        self.xml.ban_label.set_text(_('Ban %s' % nick))
        self.xml.ban_reason_entry.grab_focus()
        self.xml.ban_participant_button.grab_default()
        self._show_page('ban')

    def _on_change_role(self, _action, param):
        nick, role = param.get_strv()
        con = app.connections[self.account]
        con.get_module('MUC').set_role(self.room_jid, nick, role)

    def _on_change_affiliation(self, _action, param):
        jid, affiliation = param.get_strv()
        con = app.connections[self.account]
        con.get_module('MUC').set_affiliation(
            self.room_jid,
            {jid: {'affiliation': affiliation}})

688
    def show_roster(self):
689 690
        show = not self.xml.roster_revealer.get_reveal_child()
        icon = 'go-next-symbolic' if show else 'go-previous-symbolic'
691
        image = self.hide_roster_button.get_image()
692 693 694 695 696 697 698
        image.set_from_icon_name(icon, Gtk.IconSize.MENU)

        transition = Gtk.RevealerTransitionType.SLIDE_RIGHT
        if show:
            transition = Gtk.RevealerTransitionType.SLIDE_LEFT
        self.xml.roster_revealer.set_transition_type(transition)
        self.xml.roster_revealer.set_reveal_child(show)
699

700
    def on_groupchat_maximize(self):
701
        self.roster.enable_tooltips()
702 703
        self.add_actions()
        self.update_actions()
704
        self.set_lock_image()
705
        self.draw_banner_text()
706

707 708
    def _on_roster_row_activated(self, _roster, nick):
        self._start_private_message(nick)
709 710 711 712 713 714 715

    def on_msg_textview_populate_popup(self, textview, menu):
        """
        Override the default context menu and we prepend Clear
        and the ability to insert a nick
        """
        ChatControlBase.on_msg_textview_populate_popup(self, textview, menu)
Dicson's avatar
Dicson committed
716
        item = Gtk.SeparatorMenuItem.new()
717 718
        menu.prepend(item)

719
        item = Gtk.MenuItem.new_with_label(_('Insert Nickname'))
720
        menu.prepend(item)
721
        submenu = Gtk.Menu()
722 723
        item.set_submenu(submenu)

724
        for nick in sorted(app.contacts.get_nick_list(self.account,
725
        self.room_jid)):
726 727
            item = Gtk.MenuItem.new_with_label(nick)
            item.set_use_underline(False)
728
            submenu.append(item)
729 730
            id_ = item.connect('activate', self.append_nick_in_msg_textview,
                nick)
731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746
            self.handlers[id_] = item

        menu.show_all()

    def get_tab_label(self, chatstate):
        """
        Markup the label if necessary. Returns a tuple such as: (new_label_str,
        color) either of which can be None if chatstate is given that means we
        have HE SENT US a chatstate
        """

        has_focus = self.parent_win.window.get_property('has-toplevel-focus')
        current_tab = self.parent_win.get_active_control() == self
        color = None
        if chatstate == 'attention' and (not has_focus or not current_tab):
            self.attention_flag = True
Philipp Hörist's avatar
Philipp Hörist committed
747
            color = 'tab-muc-directed-msg'
748 749 750 751 752 753
        elif chatstate == 'active' or (current_tab and has_focus):
            self.attention_flag = False
            # get active color from gtk
            color = 'active'
        elif chatstate == 'newmsg' and (not has_focus or not current_tab) \
        and not self.attention_flag:
Philipp Hörist's avatar
Philipp Hörist committed
754
            color = 'tab-muc-msg'
755

756
        label_str = GLib.markup_escape_text(self.room_name)
757 758 759 760 761 762 763

        # count waiting highlighted messages
        unread = ''
        num_unread = self.get_nb_unread()
        if num_unread == 1:
            unread = '*'
        elif num_unread > 1:
764
            unread = '[' + str(num_unread) + ']'
765 766 767 768 769
        label_str = unread + label_str
        return (label_str, color)

    def get_tab_image(self, count_unread=True):
        tab_image = None
770
        if self.is_connected:
771
            tab_image = get_icon_name('muc-active')
772
        else:
773
            tab_image = get_icon_name('muc-inactive')
774 775
        return tab_image

776
    def set_lock_image(self):
777
        encryption_state = {'visible': self.encryption is not None,
778 779 780
                            'enc_type': self.encryption,
                            'authenticated': False}

781
        if self.encryption:
782
            app.plugin_manager.extension_point(
783
                'encryption_state' + self.encryption, self, encryption_state)
784 785 786

        self._show_lock_image(**encryption_state)

787
    def _show_lock_image(self, visible, enc_type='', authenticated=False):
788 789 790 791 792
        """
        Set lock icon visibility and create tooltip
        """
        if authenticated:
            authenticated_string = _('and authenticated')
793
            self.lock_image.set_from_icon_name(
794
                'security-high-symbolic', Gtk.IconSize.MENU)
795 796
        else:
            authenticated_string = _('and NOT authenticated')
797
            self.lock_image.set_from_icon_name(
798
                'security-low-symbolic', Gtk.IconSize.MENU)
799 800 801 802 803 804 805 806 807

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

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

    def _on_authentication_button_clicked(self, widget):
808
        app.plugin_manager.extension_point(
809 810
            'encryption_dialog' + self.encryption, self)

811
    def _update_banner_state_image(self):
812 813 814 815 816 817 818
        surface = app.interface.avatar_storage.get_muc_surface(
            self.account,
            self.contact.jid,
            AvatarSize.CHAT,
            self.scale_factor)

        self.xml.gc_banner_status_image.set_from_surface(surface)
819 820 821 822

    def draw_banner_text(self):
        """
        Draw the text in the fat line at the top of the window that houses the
823
        room jid
824
        """
825
        self.name_label.set_text(self.room_name)
826

827 828 829 830 831
    def _nec_update_room_avatar(self, obj):
        if obj.jid != self.room_jid:
            return
        self._update_banner_state_image()

832 833 834 835 836 837 838
    @event_filter(['account'])
    def _on_bookmarks_received(self, _event):
        if self.parent_win is None:
            return
        self.parent_win.redraw_tab(self)
        self.draw_banner_text()

839
    @event_filter(['account', 'room_jid'])
840 841 842 843 844 845 846 847 848 849 850
    def _on_voice_request(self, event):
        def on_approve():
            con = app.connections[self.account]
            con.get_module('MUC').approve_voice_request(self.room_jid,
                                                        event.voice_request)
        NewConfirmationDialog(
            _('Voice Request'),
            _('Voice Request'),
            _('<b>%s</b> from <b>%s</b> requests voice') % (
                event.voice_request.nick, self.room_name),
            [DialogButton.make('Cancel'),
851 852
             DialogButton.make('Accept',
                               text=_('_Approve'),
853 854
                               callback=on_approve)],
            modal=False).show()
855

856
    @event_filter(['account'])
Philipp Hörist's avatar
Philipp Hörist committed
857 858 859
    def _nec_mam_decrypted_message_received(self, obj):
        if not obj.groupchat:
            return
860
        if obj.archive_jid != self.room_jid:
Philipp Hörist's avatar
Philipp Hörist committed
861
            return
862
        self.add_message(
Philipp Hörist's avatar
Philipp Hörist committed
863
            obj.msgtxt, contact=obj.nick,
864 865
            tim=obj.timestamp, correct_id=obj.correct_id,
            encrypted=obj.encrypted,
866
            message_id=obj.message_id,
Philipp Hörist's avatar
Philipp Hörist committed
867 868
            additional_data=obj.additional_data)

869
    @event_filter(['account', 'room_jid'])
870 871
    def _nec_gc_message_received(self, obj):
        if not obj.nick:
872
            # message from server
873
            self.add_message(
874 875 876
                obj.msgtxt, tim=obj.timestamp,
                xhtml=obj.xhtml_msgtxt, displaymarking=obj.displaymarking,
                additional_data=obj.additional_data)
877
        else:
878 879 880 881 882 883
            if obj.nick == self.nick:
                self.last_sent_txt = obj.msgtxt
            self.add_message(
                obj.msgtxt, contact=obj.nick,
                tim=obj.timestamp, xhtml=obj.xhtml_msgtxt,
                displaymarking=obj.displaymarking, encrypted=obj.encrypted,
884
                correct_id=obj.correct_id, message_id=obj.message_id,
885
                additional_data=obj.additional_data)
886
        obj.needs_highlight = self.needs_visual_notification(obj.msgtxt)
887

888
    def on_private_message(self, nick, sent, msg, tim, xhtml, session, msg_log_id=None,
889
    encrypted=False, displaymarking=None):
890 891 892
        # Do we have a queue?
        fjid = self.room_jid + '/' + nick

893 894
        event = events.PmEvent(msg, '', 'incoming', tim, encrypted, '',
            msg_log_id, xhtml=xhtml, session=session, form_node=None,
895
            displaymarking=displaymarking, sent_forwarded=sent)
896
        app.events.add_event(self.account, fjid, event)
897

898 899
        autopopup = app.config.get('autopopup')
        autopopupaway = app.config.get('autopopupaway')
900
        if not autopopup or (not autopopupaway and \
901
        app.connections[self.account].connected > 2):
902
            self.roster.draw_contact(nick)
903 904 905 906 907
            if self.parent_win:
                self.parent_win.show_title()
                self.parent_win.redraw_tab(self)
        else:
            self._start_private_message(nick)
908

909
        contact = app.contacts.get_contact_with_highest_priority(
910
            self.account, self.room_jid)
911
        if contact:
912
            app.interface.roster.draw_contact(self.room_jid, self.account)
913

914
    def add_message(self, text, contact='', tim=None, xhtml=None,
Philipp Hörist's avatar
Philipp Hörist committed
915 916
                    displaymarking=None, correct_id=None, message_id=None,
                    encrypted=None, additional_data=None):
917
        """
Philipp Hörist's avatar
Philipp Hörist committed
918
        Add message to the ConversationsTextview
919

Philipp Hörist's avatar
Philipp Hörist committed
920
        If contact is set: it's a message from someone
921 922
        If contact is not set: it's a message from the server or help.
        """
Philipp Hörist's avatar
Philipp Hörist committed
923

924 925
        other_tags_for_name = []
        other_tags_for_text = []
926

927 928 929 930
        if not contact:
            # Message from the server
            kind = 'status'
        elif contact == self.nick: # it's us
931
            kind = 'outgoing'
932
        else:
933 934 935 936
            kind = 'incoming'
            # muc-specific chatstate
            if self.parent_win:
                self.parent_win.redraw_tab(self, 'newmsg')
937 938 939

        if kind == 'incoming': # it's a message NOT from us
            # highlighting and sounds
940
            highlight, _sound = self.highlighting_for_message(text, tim)
941
            other_tags_for_name.append('muc_nickname_color_%s' % contact)
942 943 944 945 946 947 948 949 950
            if highlight:
                # muc-specific chatstate
                if self.parent_win:
                    self.parent_win.redraw_tab(self, 'attention')
                else:
                    self.attention_flag = True
                other_tags_for_name.append('bold')
                other_tags_for_text.append('marked')

951
            self._nick_completion.record_message(contact, highlight)
952 953 954

            self.check_and_possibly_add_focus_out_line()

955
        ChatControlBase.add_message(self, text, kind, contact, tim,
956
            other_tags_for_name, [], other_tags_for_text, xhtml=xhtml,
Philipp Hörist's avatar
Philipp Hörist committed
957
            displaymarking=displaymarking,
958
            correct_id=correct_id, message_id=message_id, encrypted=encrypted,
959
            additional_data=additional_data)
960 961 962

    def get_nb_unread(self):
        type_events = ['printed_marked_gc_msg']
963
        if app.config.notify_for_muc(self.room_jid):
964
            type_events.append('printed_gc_msg')
965
        nb = len(app.events.get_events(self.account, self.room_jid,
966
            type_events))
967 968 969 970 971
        nb += self.get_nb_unread_pm()
        return nb

    def get_nb_unread_pm(self):
        nb = 0
972 973
        for nick in app.contacts.get_nick_list(self.account, self.room_jid):
            nb += len(app.events.get_events(self.account, self.room_jid + \
974
                '/' + nick, ['pm']))
975 976 977 978 979 980 981
        return nb

    def highlighting_for_message(self, text, tim):
        """
        Returns a 2-Tuple. The first says whether or not to highlight the text,
        the second, what sound to play
        """
982 983 984 985 986 987
        highlight, sound = None, None

        notify = app.config.notify_for_muc(self.room_jid)
        message_sound_enabled = app.config.get_per('soundevents',
                                                   'muc_message_received',
                                                   'enabled')
988 989 990 991

        # Are any of the defined highlighting words in the text?
        if self.needs_visual_notification(text):
            highlight = True
992 993 994
            if app.config.get_per('soundevents',
                                  'muc_message_highlight',
                                  'enabled'):
995 996 997
                sound = 'highlight'

        # Do we play a sound on every muc message?
998
        elif notify and message_sound_enabled:
999 1000 1001
            sound = 'received'

        # Is it a history message? Don't want sound-floods when we join.
1002
        if tim is not None and time.mktime(time.localtime()) - tim > 1:
1003 1004
            sound = None

1005
        return highlight, sound
1006 1007 1008 1009 1010 1011 1012

    def check_and_possibly_add_focus_out_line(self):
        """
        Check and possibly add focus out line for room_jid if it needs it and
        does not already have it as last event. If it goes to add this line
        - remove previous line first
        """
1013
        win = app.interface.msg_win_mgr.get_window(self.room_jid,
1014
            self.account)
1015 1016 1017 1018 1019 1020 1021
        if win and self.room_jid == win.get_active_jid() and\
        win.window.get_property('has-toplevel-focus') and\
        self.parent_win.get_active_control() == self:
            # it's the current room and it's the focused window.
            # we have full focus (we are reading it!)
            return

Philipp Hörist's avatar
Philipp Hörist committed
1022
        self.conv_textview.show_focus_out_line()
1023 1024 1025 1026 1027 1028

    def needs_visual_notification(self, text):
        """
        Check text to see whether any of the words in (muc_highlight_words and
        nick) appear
        """
1029
        special_words = app.config.get('muc_highlight_words').split(';')
1030
        special_words.append(self.nick)
1031 1032
        con = app.connections[self.account]
        special_words.append(con.get_own_jid().getStripped())
1033 1034 1035 1036 1037 1038 1039
        # Strip empties: ''.split(';') == [''] and would highlight everything.
        # Also lowercase everything for case insensitive compare.
        special_words = [word.lower() for word in special_words if word]
        text = text.lower()

        for special_word in special_words:
            found_here = text.find(special_word)
1040
            while found_here > -1:
1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051
                end_here = found_here + len(special_word)
                if (found_here == 0 or not text[found_here - 1].isalpha()) and \
                (end_here == len(text) or not text[end_here].isalpha()):
                    # It is beginning of text or char before is not alpha AND
                    # it is end of text or char after is not alpha
                    return True
                # continue searching
                start = found_here + 1
                found_here = text.find(special_word, start)
        return False

1052
    @event_filter(['account', 'room_jid'])
Philipp Hörist's avatar
Philipp Hörist committed
1053
    def _on_subject(self, event):
1054
        if self.subject == event.subject or event.is_fake:
1055 1056
            # Probably a rejoin, we already showed that subject
            return
1057 1058 1059

        self._subject_data = event

1060
        text = _('%(nick)s has set the subject to %(subject)s') % {
Philipp Hörist's avatar
Philipp Hörist committed
1061
            'nick': event.nickname, 'subject': event.subject}
1062

Philipp Hörist's avatar
Philipp Hörist committed
1063
        if event.user_timestamp:
1064
            date = time.strftime('%d-%m-%Y %H:%M:%S',
Philipp Hörist's avatar
Philipp Hörist committed
1065
                                 time.localtime(event.user_timestamp))
1066
            text = '%s - %s' % (text, date)
1067

1068
        if (app.config.get('show_subject_on_join') or
1069
                self._muc_data.state != MUCJoinedState.JOINING):
1070
            self.add_info_message(text)
1071

1072
    @event_filter(['account', 'room_jid'])
Philipp Hörist's avatar
Philipp Hörist committed
1073
    def _on_config_changed(self, event):
1074 1075
        # http://www.xmpp.org/extensions/xep-0045.html#roomconfig-notify
        changes = []
Philipp Hörist's avatar
Philipp Hörist committed
1076
        if StatusCode.SHOWING_UNAVAILABLE in event.status_codes:
1077
            changes.append(_('Group chat now shows unavailable members'))
1078

Philipp Hörist's avatar
Philipp Hörist committed
1079
        if StatusCode.NOT_SHOWING_UNAVAILABLE in event.status_codes:
1080
            changes.append(_('Group chat now does not show unavailable members'))
1081

Philipp Hörist's avatar
Philipp Hörist committed
1082
        if StatusCode.CONFIG_NON_PRIVACY_RELATED in event.status_codes:
1083
            changes.append(_('A setting not related to privacy has been '
1084
                             'changed'))
1085
            app.connections[self.account].get_module('Discovery').disco_muc(
1086
                self.room_jid)
1087

Philipp Hörist's avatar
Philipp Hörist committed
1088
        if StatusCode.CONFIG_ROOM_LOGGING in event.status_codes:
1089
            # Can be a presence (see chg_contact_status in groupchat_control.py)
1090
            changes.append(_('Conversations are stored on the server'))
1091

Philipp Hörist's avatar
Philipp Hörist committed
1092
        if StatusCode.CONFIG_NO_ROOM_LOGGING in event.status_codes:
1093
            changes.append(_('Conversations are not stored on the server'))
1094

Philipp Hörist's avatar
Philipp Hörist committed
1095
        if StatusCode.CONFIG_NON_ANONYMOUS in event.status_codes:
1096
            changes.append(_('Group chat is now non-anonymous'))
1097
            self.is_anonymous = False
1098

Philipp Hörist's avatar
Philipp Hörist committed
1099
        if StatusCode.CONFIG_SEMI_ANONYMOUS in event.status_codes:
1100
            changes.append(_('Group chat is now semi-anonymous'))
1101
            self.is_anonymous = True
1102

Philipp Hörist's avatar
Philipp Hörist committed
1103
        if StatusCode.CONFIG_FULL_ANONYMOUS in event.status_codes:
1104
            changes.append(_('Group chat is now fully anonymous'))
1105
            self.is_anonymous = True
1106 1107

        for change in changes:
1108
            self.add_info_message(change)
1109

1110 1111 1112
    def _nec_signed_in(self, obj):
        if obj.conn.name != self.account:
            return
1113
        obj.conn.get_module('MUC').join(self._muc_data)
1114

1115 1116 1117 1118 1119 1120 1121 1122
    def _nec_decrypted_message_received(self, obj):
        if obj.conn.name != self.account:
            return
        if obj.gc_control == self and obj.resource:
            # We got a pm from this room
            nick = obj.resource
            if obj.session.control:
                # print if a control is open
1123 1124 1125
                frm = ''
                if obj.sent:
                    frm = 'out'
1126
                obj.session.control.add_message(obj.msgtxt, frm,
1127
                    tim=obj.timestamp, xhtml=obj.xhtml, encrypted=obj.encrypted,
1128
                    displaymarking=obj.displaymarking, message_id=obj.message_id,
1129
                    correct_id=obj.correct_id)
1130 1131
            else:
                # otherwise pass it off to the control to be queued
1132
                self.on_private_message(nick, obj.sent, obj.msgtxt, obj.timestamp,
1133
                    obj.xhtml, self.session, msg_log_id=obj.msg_log_id,
1134
                    encrypted=obj.encrypted, displaymarking=obj.displaymarking)
1135

1136 1137 1138 1139 1140 1141
    def _nec_ping(self, obj):
        if self.contact.jid != obj.contact.room_jid:
            return

        nick = obj.contact.get_shown_name()
        if obj.name == 'ping-sent':
1142
            self.add_info_message(_('Ping? (%s)') % nick)
1143
        elif obj.name == 'ping-reply':
1144
            self.add_info_message(
1145 1146
                _('Pong! (%(nick)s %(delay)s s.)') % {'nick': nick,
                'delay': obj.seconds})
1147
        elif obj.name == 'ping-error':
1148
            self.add_info_message(_('Error.'))
1149

1150 1151 1152 1153 1154 1155 1156 1157
    @property
    def is_connected(self) -> bool:
        return app.gc_connected[self.account][self.room_jid]

    @is_connected.setter
    def is_connected(self, value: bool) -> None:
        app.gc_connected[self.account][self.room_jid] = value

1158
    def got_connected(self):
1159
        self.roster.draw()
1160

1161
        if self.disco_info.has_mam:
Philipp Hörist's avatar
Philipp Hörist committed
1162
            # Request MAM
1163 1164
            con = app.connections[self.account]
            con.get_module('MAM').request_archive_on_muc_join(
Philipp Hörist's avatar
Philipp Hörist committed
1165 1166
                self.room_jid)

1167
        self.is_connected = True
1168
        ChatControlBase.got_connected(self)
1169

1170 1171 1172 1173 1174
        # We don't redraw the whole banner here, because only icon change
        self._update_banner_state_image()
        if self.parent_win:
            self.parent_win.redraw_tab(self)

1175 1176 1177
        # Update Roster
        app.interface.roster.draw_contact(self.room_jid, self.account)

1178 1179
        formattings_button = self.xml.get_object('formattings_button')
        formattings_button.set_sensitive(True)
1180 1181

        self.update_actions()
1182

1183
    def got_disconnected(self):
1184 1185
        formattings_button = self.xml.get_object('formattings_button')
        formattings_button.set_sensitive(False)
1186

1187 1188
        self.roster.enable_sort(False)
        self.roster.clear()
1189

Philipp Hörist's avatar
Philipp Hörist committed
1190 1191 1192 1193 1194
        for contact in app.contacts.get_gc_contact_list(
                self.account, self.room_jid):
            contact.presence = PresenceType.UNAVAILABLE
            ctrl = app.interface.msg_win_mgr.get_control(contact.get_full_jid,
                                                         self.account)
1195
            if ctrl:
Philipp Hörist's avatar
Philipp Hörist committed
1196 1197 1198
                ctrl.got_disconnected()

            app.contacts.remove_gc_contact(self.account, contact)
1199

1200
        self.is_connected = False
1201
        ChatControlBase.got_disconnected(self)
Philipp Hörist's avatar
Philipp Hörist committed
1202

1203 1204 1205
        con = app.connections[self.account]
        con.get_module('Chatstate').remove_delay_timeout(self.contact)

1206 1207
        # Update Roster
        app.interface.roster.draw_contact(self.room_jid, self.account)
Philipp Hörist's avatar
Philipp Hörist committed
1208