gajim-plural/gajim/groupchat_control.py

3069 lines
123 KiB
Python

# 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>
# Copyright (C) 2018 Marcin Mielniczuk <marmistrz dot dev at zoho dot eu>
#
# 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/>.
from typing import Optional
import time
import locale
import base64
import logging
from enum import IntEnum, unique
import nbxmpp
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import Pango
from gi.repository import GLib
from gi.repository import Gio
from gajim import gtkgui_helpers
from gajim import gui_menu_builder
from gajim import message_control
from gajim import vcard
from gajim import dataforms_widget
from gajim.common.const import AvatarSize
from gajim.common.caps_cache import muc_caps_cache
from gajim.common import events
from gajim.common import app
from gajim.common import helpers
from gajim.common.modules import dataforms
from gajim.common import ged
from gajim.common import i18n
from gajim.common.i18n import _
from gajim.common import contacts
from gajim.common.const import StyleAttr
from gajim.common.const import Chatstate
from gajim.chat_control import ChatControl
from gajim.chat_control_base import ChatControlBase
from gajim.command_system.implementation.hosts import PrivateChatCommands
from gajim.command_system.implementation.hosts import GroupChatCommands
from gajim.common.connection_handlers_events import GcMessageOutgoingEvent
from gajim.gtk.dialogs import ErrorDialog
from gajim.gtk.dialogs import InputTextDialog
from gajim.gtk.dialogs import ConfirmationDialogCheck
from gajim.gtk.dialogs import DoubleInputDialog
from gajim.gtk.dialogs import InputDialog
from gajim.gtk.dialogs import ChangeNickDialog
from gajim.gtk.filechoosers import AvatarChooserDialog
from gajim.gtk.add_contact import AddNewContactWindow
from gajim.gtk.tooltips import GCTooltip
from gajim.gtk.groupchat_config import GroupchatConfig
from gajim.gtk.adhoc_commands import CommandWindow
from gajim.gtk.util import NickCompletionGenerator
from gajim.gtk.util import get_icon_name
from gajim.gtk.util import get_affiliation_surface
from gajim.gtk.util import get_builder
log = logging.getLogger('gajim.groupchat_control')
@unique
class Column(IntEnum):
IMG = 0 # image to show state (online, new message etc)
NICK = 1 # contact nickname or ROLE name
TYPE = 2 # type of the row ('contact' or 'role')
TEXT = 3 # text shown in the cellrenderer
AVATAR_IMG = 4 # avatar of the contact
class PrivateChatControl(ChatControl):
TYPE_ID = message_control.TYPE_PM
# Set a command host to bound to. Every command given through a private chat
# will be processed with this command host.
COMMAND_HOST = PrivateChatCommands
def __init__(self, parent_win, gc_contact, contact, account, session):
room_jid = gc_contact.room_jid
self.room_ctrl = app.interface.msg_win_mgr.get_gc_control(room_jid,
account)
if room_jid in app.interface.minimized_controls[account]:
self.room_ctrl = app.interface.minimized_controls[account][room_jid]
if self.room_ctrl:
self.room_name = self.room_ctrl.name
else:
self.room_name = room_jid
self.gc_contact = gc_contact
ChatControl.__init__(self, parent_win, contact, account, session)
self.TYPE_ID = 'pm'
app.ged.register_event_handler('update-gc-avatar', ged.GUI1,
self._nec_update_avatar)
app.ged.register_event_handler('caps-update', ged.GUI1,
self._nec_caps_received_pm)
app.ged.register_event_handler('gc-presence-received', ged.GUI1,
self._nec_gc_presence_received)
def get_our_nick(self):
return self.room_ctrl.nick
def shutdown(self):
super(PrivateChatControl, self).shutdown()
app.ged.remove_event_handler('update-gc-avatar', ged.GUI1,
self._nec_update_avatar)
app.ged.remove_event_handler('caps-update', ged.GUI1,
self._nec_caps_received_pm)
app.ged.remove_event_handler('gc-presence-received', ged.GUI1,
self._nec_gc_presence_received)
def _nec_caps_received_pm(self, obj):
if obj.conn.name != self.account or \
obj.fjid != self.gc_contact.get_full_jid():
return
self.update_contact()
def _nec_gc_presence_received(self, obj):
if obj.conn.name != self.account:
return
if obj.fjid != self.full_jid:
return
if '303' in obj.status_code:
self.print_conversation(_('%(nick)s is now known as '
'%(new_nick)s') % {'nick': obj.nick, 'new_nick': obj.new_nick},
'status')
gc_c = app.contacts.get_gc_contact(obj.conn.name, obj.room_jid,
obj.new_nick)
c = gc_c.as_contact()
self.gc_contact = gc_c
self.contact = c
self.draw_banner()
old_jid = obj.room_jid + '/' + obj.nick
new_jid = obj.room_jid + '/' + obj.new_nick
app.interface.msg_win_mgr.change_key(old_jid, new_jid,
obj.conn.name)
else:
self.contact.show = obj.show
self.contact.status = obj.status
self.gc_contact.show = obj.show
self.gc_contact.status = obj.status
uf_show = helpers.get_uf_show(obj.show)
self.print_conversation(_('%(nick)s is now %(status)s') % {
'nick': obj.nick, 'status': uf_show}, 'status')
if obj.status:
self.print_conversation(' (', 'status', simple=True)
self.print_conversation('%s' % (obj.status), 'status',
simple=True)
self.print_conversation(')', 'status', simple=True)
self.parent_win.redraw_tab(self)
self.update_ui()
def send_message(self, message, xhtml=None, process_commands=True,
attention=False):
"""
Call this method to send the message
"""
message = helpers.remove_invalid_xml_chars(message)
if not message:
return
# We need to make sure that we can still send through the room and that
# the recipient did not go away
contact = app.contacts.get_first_contact_from_jid(self.account,
self.contact.jid)
if not contact:
# contact was from pm in MUC
room, nick = app.get_room_and_nick_from_fjid(self.contact.jid)
gc_contact = app.contacts.get_gc_contact(self.account, room, nick)
if not gc_contact:
ErrorDialog(
_('Sending private message failed'),
#in second %s code replaces with nickname
_('You are no longer in group chat "%(room)s" or '
'"%(nick)s" has left.') % {'room': '\u200E' + room,
'nick': nick}, transient_for=self.parent_win.window)
return
ChatControl.send_message(self, message, xhtml=xhtml,
process_commands=process_commands, attention=attention)
def update_ui(self):
if self.contact.show == 'offline':
self.got_disconnected()
else:
self.got_connected()
ChatControl.update_ui(self)
def _nec_update_avatar(self, obj):
if obj.contact != self.gc_contact:
return
self.show_avatar()
def show_avatar(self):
if not app.config.get('show_avatar_in_chat'):
return
scale = self.parent_win.window.get_scale_factor()
surface = app.interface.get_avatar(
self.gc_contact.avatar_sha, AvatarSize.CHAT, scale)
image = self.xml.get_object('avatar_image')
image.set_from_surface(surface)
def update_contact(self):
self.contact = self.gc_contact.as_contact()
def got_disconnected(self):
ChatControl.got_disconnected(self)
class GroupchatControl(ChatControlBase):
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
def __init__(self, parent_win, contact, nick, acct, is_continued=False):
ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
'groupchat_control', contact, acct)
self.force_non_minimizable = False
self.is_continued = is_continued
self.is_anonymous = True
self.join_time = 0
# Controls the state of autorejoin.
# None - autorejoin is neutral.
# False - autorejoin is to be prevented (gets reset to initial state in
# got_connected()).
# int - autorejoin is being active and working (gets reset to initial
# state in got_connected()).
self.autorejoin = None
# Keep error dialog instance to be sure to have only once at a time
self.error_dialog = None
# Source id for saving the handle position
self._handle_timeout_id = None
self.emoticons_button = self.xml.get_object('emoticons_button')
self.toggle_emoticons()
formattings_button = self.xml.get_object('formattings_button')
formattings_button.set_sensitive(False)
self._state_change_handler_id = None
if parent_win is not None:
# On AutoJoin with minimize Groupchats are created without parent
# Tooltip Window and Actions have to be created with parent
self.set_tooltip()
self.add_actions()
GLib.idle_add(self.update_actions)
self.scale_factor = parent_win.window.get_scale_factor()
self._connect_window_state_change(parent_win)
else:
self.scale_factor = app.interface.roster.scale_factor
widget = self.xml.get_object('list_treeview')
id_ = widget.connect('row_expanded', self.on_list_treeview_row_expanded)
self.handlers[id_] = widget
id_ = widget.connect('row_collapsed',
self.on_list_treeview_row_collapsed)
self.handlers[id_] = widget
id_ = widget.connect('row_activated',
self.on_list_treeview_row_activated)
self.handlers[id_] = widget
id_ = widget.connect('button_press_event',
self.on_list_treeview_button_press_event)
self.handlers[id_] = widget
id_ = widget.connect('key_press_event',
self.on_list_treeview_key_press_event)
self.handlers[id_] = widget
self.room_jid = self.contact.jid
self.nick = nick
self.new_nick = ''
bm_module = app.connections[self.account].get_module('Bookmarks')
self.name = bm_module.get_name_from_bookmark(self.room_jid)
self.contact.name = self.name
self.widget_set_visible(self.xml.get_object('banner_eventbox'),
app.config.get('hide_groupchat_banner'))
self._last_selected_contact = None # None or holds jid, account tuple
# muc attention flag (when we are mentioned in a muc)
# if True, the room has mentioned us
self.attention_flag = False
# sorted list of nicks who mentioned us (last at the end)
self.attention_list = []
self.nick_hits = []
self._nick_completion = NickCompletionGenerator(self.nick)
self.last_key_tabs = False
self.subject = ''
# nickname coloring
self.gc_count_nicknames_colors = -1
self.gc_custom_colors = {}
self.number_of_colors = len(app.config.get('gc_nicknames_colors').\
split(':'))
self.name_label = self.xml.get_object('banner_name_label')
self.event_box = self.xml.get_object('banner_eventbox')
self.list_treeview = self.xml.get_object('list_treeview')
selection = self.list_treeview.get_selection()
id_ = selection.connect('changed',
self.on_list_treeview_selection_changed)
self.handlers[id_] = selection
id_ = self.list_treeview.connect('style-set',
self.on_list_treeview_style_set)
self.handlers[id_] = self.list_treeview
# flag that stops hpaned position event
# when the handle gets resized in another control
self._resize_from_another_muc = False
self.hpaned = self.xml.get_object('hpaned')
# set the position of the current hpaned
hpaned_position = app.config.get('gc-hpaned-position')
self.hpaned.set_position(hpaned_position)
# Holds the Gtk.TreeRowReference for each contact
self._contact_refs = {}
# Holds the Gtk.TreeRowReference for each role
self._role_refs = {}
#status_image, shown_nick, type, nickname, avatar
self.columns = [str, str, str, str, Gtk.Image]
self.model = Gtk.TreeStore(*self.columns)
self.model.set_sort_func(Column.NICK, self.tree_compare_iters)
# columns
column = Gtk.TreeViewColumn()
# list of renderers with attributes / properties in the form:
# (name, renderer_object, expand?, attribute_name, attribute_value,
# cell_data_func, func_arg)
self.renderers_list = []
# Number of renderers plugins added
self.nb_ext_renderers = 0
self.renderers_propertys = {}
renderer_text = Gtk.CellRendererText()
self.renderers_propertys[renderer_text] = ('ellipsize',
Pango.EllipsizeMode.END)
self.renderers_list += (
# status img
('icon', Gtk.CellRendererPixbuf(), False,
'icon_name', Column.IMG, self._cell_data_func, 'status'),
# contact name
('name', renderer_text, True,
'markup', Column.TEXT, self._cell_data_func, 'name'))
# avatar img
avatar_renderer = ('avatar', Gtk.CellRendererPixbuf(),
False, None, Column.AVATAR_IMG,
self._cell_data_func, 'avatar')
if app.config.get('avatar_position_in_roster') == 'right':
self.renderers_list.append(avatar_renderer)
else:
self.renderers_list.insert(0, avatar_renderer)
self.fill_column(column)
self.list_treeview.append_column(column)
# workaround to avoid gtk arrows to be shown
column = Gtk.TreeViewColumn() # 2nd COLUMN
renderer = Gtk.CellRendererPixbuf()
column.pack_start(renderer, False)
self.list_treeview.append_column(column)
column.set_visible(False)
self.list_treeview.set_expander_column(column)
self.setup_seclabel()
self.form_widget = None
# Send file
self.sendfile_button = self.xml.get_object('sendfile_button')
self.sendfile_button.set_action_name('win.send-file-' + \
self.control_id)
# 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(
gui_menu_builder.get_encryption_menu(self.control_id, self.type_id))
self.set_encryption_menu_icon()
# Banner
self.banner_actionbar = self.xml.get_object('banner_actionbar')
self.hide_roster_button = Gtk.Button.new_from_icon_name(
'go-next-symbolic', Gtk.IconSize.MENU)
self.hide_roster_button.connect('clicked',
lambda *args: self.show_roster())
self.subject_button = Gtk.MenuButton()
self.subject_button.set_image(Gtk.Image.new_from_icon_name(
'go-down-symbolic', Gtk.IconSize.MENU))
self.subject_button.set_popover(SubjectPopover())
self.subject_button.set_no_show_all(True)
self.banner_actionbar.pack_end(self.hide_roster_button)
self.banner_actionbar.pack_start(self.subject_button)
# GC Roster tooltip
self.gc_tooltip = GCTooltip()
self.control_menu = gui_menu_builder.get_groupchat_menu(self.control_id,
self.account,
self.room_jid)
settings_menu = self.xml.get_object('settings_menu')
settings_menu.set_menu_model(self.control_menu)
app.ged.register_event_handler('gc-presence-received', ged.GUI1,
self._nec_gc_presence_received)
app.ged.register_event_handler('gc-message-received', ged.GUI1,
self._nec_gc_message_received)
app.ged.register_event_handler('mam-decrypted-message-received',
ged.GUI1, self._nec_mam_decrypted_message_received)
app.ged.register_event_handler('vcard-published', ged.GUI1,
self._nec_vcard_published)
app.ged.register_event_handler('update-gc-avatar', ged.GUI1,
self._nec_update_avatar)
app.ged.register_event_handler('update-room-avatar', ged.GUI1,
self._nec_update_room_avatar)
app.ged.register_event_handler('gc-subject-received', ged.GUI1,
self._nec_gc_subject_received)
app.ged.register_event_handler('gc-config-changed-received', ged.GUI1,
self._nec_gc_config_changed_received)
app.ged.register_event_handler('signed-in', ged.GUI1,
self._nec_signed_in)
app.ged.register_event_handler('decrypted-message-received', ged.GUI2,
self._nec_decrypted_message_received)
app.ged.register_event_handler('gc-stanza-message-outgoing', ged.OUT_POSTCORE,
self._message_sent)
self.is_connected = False
# disable win, we are not connected yet
ChatControlBase.got_disconnected(self)
self.update_ui()
self.widget.show_all()
if app.config.get('hide_groupchat_occupants_list'):
# Roster is shown by default, so toggle the roster button to hide it
self.show_roster()
# PluginSystem: adding GUI extension point for this GroupchatControl
# instance object
app.plugin_manager.gui_extension_point('groupchat_control', self)
def add_actions(self):
super().add_actions()
actions = [
('change-subject-', self._on_change_subject),
('change-nick-', self._on_change_nick),
('disconnect-', self._on_disconnect),
('destroy-', self._on_destroy_room),
('configure-', self._on_configure_room),
('bookmark-', self._on_bookmark_room),
('request-voice-', self._on_request_voice),
('execute-command-', self._on_execute_command),
('upload-avatar-', self._on_upload_avatar),
]
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)
non_minimized_gc = app.config.get_per(
'accounts', self.account, 'non_minimized_gc').split()
value = self.contact.jid not in non_minimized_gc
act = Gio.SimpleAction.new_stateful(
'minimize-' + self.control_id, None,
GLib.Variant.new_boolean(value))
act.connect('change-state', self._on_minimize)
self.parent_win.window.add_action(act)
value = app.config.get_per(
'rooms', self.contact.jid, 'notify_on_all_messages')
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)
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)
def update_actions(self):
if self.parent_win is None:
return
win = self.parent_win.window
contact = app.contacts.get_gc_contact(
self.account, self.room_jid, self.nick)
con = app.connections[self.account]
# Destroy Room
win.lookup_action('destroy-' + self.control_id).set_enabled(
self.is_connected and contact.affiliation == 'owner')
# Configure Room
win.lookup_action('configure-' + self.control_id).set_enabled(
self.is_connected and contact.affiliation in ('admin', 'owner'))
# Bookmarks
con = app.connections[self.account]
bookmark_support = con.get_module('Bookmarks').available
bookmarked = self.room_jid in con.get_module('Bookmarks').bookmarks
win.lookup_action('bookmark-' + self.control_id).set_enabled(
self.is_connected and bookmark_support and not bookmarked)
# Request Voice
role = self.get_role(self.nick)
win.lookup_action('request-voice-' + self.control_id).set_enabled(
self.is_connected and role == 'visitor')
# Change Subject
subject = False
if contact is not None:
subject = muc_caps_cache.is_subject_change_allowed(
self.room_jid, contact.affiliation)
win.lookup_action('change-subject-' + self.control_id).set_enabled(
self.is_connected and subject)
# Change Nick
win.lookup_action('change-nick-' + self.control_id).set_enabled(
self.is_connected)
# Execute command
win.lookup_action('execute-command-' + self.control_id).set_enabled(
self.is_connected)
# Send file (HTTP File Upload)
httpupload = win.lookup_action(
'send-file-httpupload-' + self.control_id)
httpupload.set_enabled(
self.is_connected and con.get_module('HTTPUpload').available)
win.lookup_action('send-file-' + self.control_id).set_enabled(
httpupload.get_enabled())
if self.is_connected and httpupload.get_enabled():
tooltip_text = _('Send File…')
else:
tooltip_text = _('No File Transfer available')
self.sendfile_button.set_tooltip_text(tooltip_text)
# Upload Avatar
vcard_support = muc_caps_cache.supports(self.room_jid, nbxmpp.NS_VCARD)
win.lookup_action('upload-avatar-' + self.control_id).set_enabled(
self.is_connected and vcard_support and contact.affiliation == 'owner')
# Sync Threshold
has_mam = muc_caps_cache.has_mam(self.room_jid)
win.lookup_action('choose-sync-' + self.control_id).set_enabled(has_mam)
def _cell_data_func(self, column, renderer, model, iter_, user_data):
# Background color has to be rendered for all cells
theme = app.config.get('roster_theme')
has_parent = bool(model.iter_parent(iter_))
if has_parent:
bgcolor = app.css_config.get_value('.gajim-contact-row', StyleAttr.BACKGROUND)
renderer.set_property('cell-background', bgcolor)
else:
bgcolor = app.css_config.get_value('.gajim-group-row', StyleAttr.BACKGROUND)
renderer.set_property('cell-background', bgcolor)
if user_data == 'status':
self._status_cell_data_func(column, renderer, model, iter_, has_parent)
elif user_data == 'name':
self._text_cell_data_func(column, renderer, model, iter_, has_parent, theme)
elif user_data == 'avatar':
self._avatar_cell_data_func(column, renderer, model, iter_, has_parent)
def _status_cell_data_func(self, column, renderer, model, iter_, has_parent):
renderer.set_property('width', 26)
icon_name = model[iter_][Column.IMG]
if ':' in icon_name:
icon_name, affiliation = icon_name.split(':')
surface = get_affiliation_surface(
icon_name, affiliation, self.scale_factor)
renderer.set_property('icon_name', None)
renderer.set_property('surface', surface)
else:
renderer.set_property('surface', None)
renderer.set_property('icon_name', icon_name)
def _avatar_cell_data_func(self, column, renderer, model, iter_, has_parent):
image = model[iter_][Column.AVATAR_IMG]
if image is None:
renderer.set_property('surface', None)
else:
surface = image.get_property('surface')
renderer.set_property('surface', surface)
renderer.set_property('xalign', 0.5)
if has_parent:
renderer.set_property('visible', True)
renderer.set_property('width', AvatarSize.ROSTER)
else:
renderer.set_property('visible', False)
def _text_cell_data_func(self, column, renderer, model, iter_, has_parent, theme):
# cell data func is global, because we don't want it to keep
# reference to GroupchatControl instance (self)
if has_parent:
color = app.css_config.get_value('.gajim-contact-row', StyleAttr.COLOR)
renderer.set_property('foreground', color)
desc = app.css_config.get_font('.gajim-contact-row')
renderer.set_property('font-desc', desc)
else:
color = app.css_config.get_value('.gajim-group-row', StyleAttr.COLOR)
renderer.set_property('foreground', color)
desc = app.css_config.get_font('.gajim-group-row')
renderer.set_property('font-desc', desc)
def _on_room_created(self):
if self.parent_win is None:
return
win = self.parent_win.window
self.update_actions()
# After the room has been created, reevaluate threshold
if muc_caps_cache.has_mam(self.contact.jid):
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)))
def _connect_window_state_change(self, parent_win):
if self._state_change_handler_id is None:
id_ = parent_win.window.connect('notify::is-maximized',
self._on_window_state_change)
self._state_change_handler_id = id_
# Actions
def _on_change_subject(self, action, param):
def on_ok(subject):
# Note, we don't update self.subject since we don't know whether it
# will work yet
con = app.connections[self.account]
con.get_module('MUC').set_subject(self.room_jid, subject)
InputTextDialog(_('Changing Subject'),
_('Please specify the new subject:'), input_str=self.subject,
ok_handler=on_ok, transient_for=self.parent_win.window)
def _on_change_nick(self, action, param):
if 'change_nick_dialog' in app.interface.instances:
app.interface.instances['change_nick_dialog'].dialog.present()
else:
title = _('Changing Nickname')
prompt = _('Please specify the new nickname you want to use:')
app.interface.instances['change_nick_dialog'] = \
ChangeNickDialog(self.account, self.room_jid, title,
prompt, change_nick=True, transient_for=self.parent_win.window)
def _on_disconnect(self, action, param):
self.force_non_minimizable = True
self.parent_win.remove_tab(self, self.parent_win.CLOSE_COMMAND)
self.force_non_minimizable = False
def _on_destroy_room(self, action, param):
def on_ok(reason, jid):
if jid:
# Test jid
try:
jid = helpers.parse_jid(jid)
except Exception:
ErrorDialog(
_('Invalid group chat JID'),
_('The group chat JID has not allowed characters.'))
return
con = app.connections[self.account]
con.get_module('MUC').destroy(self.room_jid, reason, jid)
con.get_module('Bookmarks').bookmarks.pop(self.room_jid, None)
con.get_module('Bookmarks').store_bookmarks()
gui_menu_builder.build_bookmark_menu(self.account)
self.force_non_minimizable = True
self.parent_win.remove_tab(self, self.parent_win.CLOSE_COMMAND)
self.force_non_minimizable = False
# Ask for a reason
DoubleInputDialog(_('Destroying %s') % '\u200E' + \
self.room_jid, _('You are going to remove this room permanently.'
'\nYou may specify a reason below:'),
_('You may also enter an alternate venue:'), ok_handler=on_ok,
transient_for=self.parent_win.window)
def _on_configure_room(self, _action, _param):
contact = app.contacts.get_gc_contact(
self.account, self.room_jid, self.nick)
if contact.affiliation == 'owner':
con = app.connections[self.account]
con.get_module('MUC').request_config(self.room_jid)
elif contact.affiliation == 'admin':
win = app.get_app_window(
'GroupchatConfig', self.account, self.room_jid)
if win is not None:
win.present()
else:
GroupchatConfig(self.account,
self.room_jid,
contact.affiliation)
def _on_bookmark_room(self, action, param):
"""
Bookmark the room, without autojoin and not minimized
"""
password = app.gc_passwords.get(self.room_jid, '')
con = app.connections[self.account]
con.get_module('Bookmarks').add_bookmark(self.name,
self.room_jid,
True,
True,
password,
self.nick)
self.update_actions()
def _on_request_voice(self, action, param):
"""
Request voice in the current room
"""
con = app.connections[self.account]
con.get_module('MUC').request_voice(self.room_jid)
def _on_minimize(self, action, param):
"""
When a groupchat is minimized, unparent the tab, put it in roster etc
"""
action.set_state(param)
non_minimized_gc = app.config.get_per(
'accounts', self.account, 'non_minimized_gc').split()
minimize = param.get_boolean()
if minimize:
non_minimized_gc.remove(self.contact.jid)
else:
non_minimized_gc.append(self.contact.jid)
app.config.set_per('accounts', self.account,
'non_minimized_gc', ' '.join(non_minimized_gc))
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())
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)
def _on_execute_command(self, action, param):
"""
Execute AdHoc commands on the current room
"""
CommandWindow(self.account, self.room_jid)
def _on_upload_avatar(self, action, param):
def _on_accept(filename):
sha = app.interface.save_avatar(filename, publish=True)
if sha is None:
ErrorDialog(
_('Could not load image'),
transient_for=self.parent_win.window)
return
publish = app.interface.get_avatar(sha, publish=True)
avatar = base64.b64encode(publish).decode('utf-8')
con = app.connections[self.account]
con.get_module('VCardTemp').upload_room_avatar(
self.room_jid, avatar)
AvatarChooserDialog(_on_accept,
transient_for=self.parent_win.window,
modal=True)
def show_roster(self):
new_state = not self.hpaned.get_child2().is_visible()
image = self.hide_roster_button.get_image()
if new_state:
self.hpaned.get_child2().show()
image.set_from_icon_name('go-next-symbolic', Gtk.IconSize.MENU)
else:
self.hpaned.get_child2().hide()
image.set_from_icon_name('go-previous-symbolic', Gtk.IconSize.MENU)
def on_groupchat_maximize(self):
self.set_tooltip()
self.add_actions()
self.update_actions()
self.set_lock_image()
self._connect_window_state_change(self.parent_win)
def set_tooltip(self):
widget = self.xml.get_object('list_treeview')
if widget.get_tooltip_window():
return
widget.set_has_tooltip(True)
id_ = widget.connect('query-tooltip', self.query_tooltip)
self.handlers[id_] = widget
def query_tooltip(self, widget, x_pos, y_pos, keyboard_mode, tooltip):
try:
row = self.list_treeview.get_path_at_pos(x_pos, y_pos)[0]
except TypeError:
self.gc_tooltip.clear_tooltip()
return False
if not row:
self.gc_tooltip.clear_tooltip()
return False
iter_ = None
try:
iter_ = self.model.get_iter(row)
except Exception:
self.gc_tooltip.clear_tooltip()
return False
typ = self.model[iter_][Column.TYPE]
nick = self.model[iter_][Column.NICK]
if typ != 'contact':
self.gc_tooltip.clear_tooltip()
return False
contact = app.contacts.get_gc_contact(
self.account, self.room_jid, nick)
if not contact:
self.gc_tooltip.clear_tooltip()
return False
value, widget = self.gc_tooltip.get_tooltip(contact)
tooltip.set_custom(widget)
return value
def fill_column(self, col):
for rend in self.renderers_list:
col.pack_start(rend[1], rend[2])
if rend[0] not in ('avatar', 'icon'):
col.add_attribute(rend[1], rend[3], rend[4])
col.set_cell_data_func(rend[1], rend[5], rend[6])
# set renderers properties
for renderer in self.renderers_propertys:
renderer.set_property(self.renderers_propertys[renderer][0],
self.renderers_propertys[renderer][1])
def tree_compare_iters(self, model, iter1, iter2, data=None):
"""
Compare two iterators to sort them
"""
type1 = model[iter1][Column.TYPE]
type2 = model[iter2][Column.TYPE]
if not type1 or not type2:
return 0
nick1 = model[iter1][Column.NICK]
nick2 = model[iter2][Column.NICK]
if not nick1 or not nick2:
return 0
if type1 == 'role':
return locale.strcoll(nick1, nick2)
if type1 == 'contact':
gc_contact1 = app.contacts.get_gc_contact(self.account,
self.room_jid, nick1)
if not gc_contact1:
return 0
if type2 == 'contact':
gc_contact2 = app.contacts.get_gc_contact(self.account,
self.room_jid, nick2)
if not gc_contact2:
return 0
if type1 == 'contact' and type2 == 'contact' and \
app.config.get('sort_by_show_in_muc'):
cshow = {'chat':0, 'online': 1, 'away': 2, 'xa': 3, 'dnd': 4,
'invisible': 5, 'offline': 6, 'error': 7}
show1 = cshow[gc_contact1.show]
show2 = cshow[gc_contact2.show]
if show1 < show2:
return -1
if show1 > show2:
return 1
# We compare names
name1 = gc_contact1.get_shown_name()
name2 = gc_contact2.get_shown_name()
return locale.strcoll(name1.lower(), name2.lower())
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)
item = Gtk.SeparatorMenuItem.new()
menu.prepend(item)
item = Gtk.MenuItem.new_with_label(_('Insert Nickname'))
menu.prepend(item)
submenu = Gtk.Menu()
item.set_submenu(submenu)
for nick in sorted(app.contacts.get_nick_list(self.account,
self.room_jid)):
item = Gtk.MenuItem.new_with_label(nick)
item.set_use_underline(False)
submenu.append(item)
id_ = item.connect('activate', self.append_nick_in_msg_textview,
nick)
self.handlers[id_] = item
menu.show_all()
def resize_occupant_treeview(self, position):
if self.hpaned.get_position() == position:
return
self._resize_from_another_muc = True
self.hpaned.set_position(position)
def _reset_flag():
self._resize_from_another_muc = False
# Reset the flag when everything will be redrawn, and in particular when
# on_treeview_size_allocate will have been called.
GLib.timeout_add(500, _reset_flag)
def _on_window_state_change(self, win, param):
# Add with timeout, because state change happens before
# the hpaned notifys us about a new handle position
GLib.timeout_add(100, self._check_for_resize)
def _on_hpaned_release_button(self, hpaned, event):
if event.get_button()[1] != 1:
# We want only to catch the left mouse button
return
self._check_for_resize()
def _check_for_resize(self):
# Check if we have a new position
pos = self.hpaned.get_position()
if pos == app.config.get('gc-hpaned-position'):
return
# Save new position
self._remove_handle_timeout()
app.config.set('gc-hpaned-position', pos)
# Resize other MUC rosters
for account in app.gc_connected:
for room_jid in [i for i in app.gc_connected[account] if \
app.gc_connected[account][i] and i != self.room_jid]:
ctrl = app.interface.msg_win_mgr.get_gc_control(room_jid,
account)
if not ctrl and room_jid in \
app.interface.minimized_controls[account]:
ctrl = app.interface.minimized_controls[account][room_jid]
if ctrl and app.config.get('one_message_window') != 'never':
ctrl.resize_occupant_treeview(pos)
def _on_hpaned_handle_change(self, hpaned, param):
if self._resize_from_another_muc:
return
# Window was resized, save new handle pos
pos = hpaned.get_position()
if pos != app.config.get('gc-hpaned-position'):
self._remove_handle_timeout(renew=True)
def _remove_handle_timeout(self, renew=False):
if self._handle_timeout_id is not None:
GLib.source_remove(self._handle_timeout_id)
self._handle_timeout_id = None
if renew:
pos = self.hpaned.get_position()
self._handle_timeout_id = GLib.timeout_add_seconds(
2, self._save_handle_position, pos)
def _save_handle_position(self, pos):
self._handle_timeout_id = None
app.config.set('gc-hpaned-position', pos)
def iter_contact_rows(self):
"""
Iterate over all contact rows in the tree model
"""
role_iter = self.model.get_iter_first()
while role_iter:
contact_iter = self.model.iter_children(role_iter)
while contact_iter:
yield self.model[contact_iter]
contact_iter = self.model.iter_next(contact_iter)
role_iter = self.model.iter_next(role_iter)
def on_list_treeview_style_set(self, treeview, style):
"""
When style (theme) changes, redraw all contacts
"""
# Get the room_jid from treeview
for contact in self.iter_contact_rows():
nick = contact[Column.NICK]
self.draw_contact(nick)
def on_list_treeview_selection_changed(self, selection):
model, selected_iter = selection.get_selected()
self.draw_contact(self.nick)
if self._last_selected_contact is not None:
self.draw_contact(self._last_selected_contact)
if selected_iter is None:
self._last_selected_contact = None
return
contact = model[selected_iter]
nick = contact[Column.NICK]
self._last_selected_contact = nick
if contact[Column.TYPE] != 'contact':
return
self.draw_contact(nick, selected=True, focus=True)
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
color = 'tab-muc-directed-msg'
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:
color = 'tab-muc-msg'
if self.is_continued:
# if this is a continued conversation
label_str = self.get_continued_conversation_name()
else:
label_str = self.name
label_str = GLib.markup_escape_text(label_str)
# count waiting highlighted messages
unread = ''
num_unread = self.get_nb_unread()
if num_unread == 1:
unread = '*'
elif num_unread > 1:
unread = '[' + str(num_unread) + ']'
label_str = unread + label_str
return (label_str, color)
def get_tab_image(self, count_unread=True):
tab_image = None
if self.is_connected:
tab_image = get_icon_name('muc-active')
else:
tab_image = get_icon_name('muc-inactive')
return tab_image
def update_ui(self):
ChatControlBase.update_ui(self)
for nick in app.contacts.get_nick_list(self.account, self.room_jid):
self.draw_contact(nick)
def set_lock_image(self):
encryption_state = {'visible': self.encryption is not None,
'enc_type': self.encryption,
'authenticated': False}
if self.encryption:
app.plugin_manager.extension_point(
'encryption_state' + self.encryption, self, encryption_state)
self._show_lock_image(**encryption_state)
def _show_lock_image(self, visible, enc_type='', authenticated=False):
"""
Set lock icon visibility and create tooltip
"""
if authenticated:
authenticated_string = _('and authenticated')
self.lock_image.set_from_icon_name(
'security-high', Gtk.IconSize.MENU)
else:
authenticated_string = _('and NOT authenticated')
self.lock_image.set_from_icon_name(
'security-low', Gtk.IconSize.MENU)
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):
app.plugin_manager.extension_point(
'encryption_dialog' + self.encryption, self)
def _change_style(self, model, path, iter_, option):
model[iter_][Column.NICK] = model[iter_][Column.NICK]
def change_roster_style(self):
self.model.foreach(self._change_style, None)
def repaint_themed_widgets(self):
ChatControlBase.repaint_themed_widgets(self)
self.change_roster_style()
def _update_banner_state_image(self):
banner_status_img = self.xml.get_object('gc_banner_status_image')
if self.is_connected:
if self.contact.avatar_sha:
surface = app.interface.get_avatar(self.contact.avatar_sha,
AvatarSize.ROSTER,
self.scale_factor)
banner_status_img.set_from_surface(surface)
return
icon = get_icon_name('muc-active')
else:
icon = get_icon_name('muc-inactive')
banner_status_img.set_from_icon_name(icon, Gtk.IconSize.DND)
def get_continued_conversation_name(self):
"""
Get the name of a continued conversation. Will return Continued
Conversation if there isn't any other contact in the room
"""
nicks = []
for nick in app.contacts.get_nick_list(self.account,
self.room_jid):
if nick != self.nick:
nicks.append(nick)
if nicks != []:
title = ', '
title = _('Conversation with ') + title.join(nicks)
else:
title = _('Continued conversation')
return title
def draw_banner_text(self):
"""
Draw the text in the fat line at the top of the window that houses the
room jid, subject
"""
self.name_label.set_ellipsize(Pango.EllipsizeMode.END)
if self.is_continued:
name = self.get_continued_conversation_name()
else:
name = self.room_jid
self.name_label.set_text(name)
if self.subject:
subject = GLib.markup_escape_text(self.subject)
subject_text = self.urlfinder.sub(self.make_href, subject)
self.subject_button.get_popover().set_text(subject_text)
def _nec_vcard_published(self, obj):
if obj.conn.name != self.account:
return
show = app.SHOW_LIST[obj.conn.connected]
status = obj.conn.status
obj.conn.send_gc_status(self.nick, self.room_jid, show, status)
def _nec_update_avatar(self, obj):
if obj.contact.room_jid != self.room_jid:
return
app.log('avatar').debug('Draw Groupchat Avatar: %s %s',
obj.contact.name, obj.contact.avatar_sha)
self.draw_avatar(obj.contact)
def _nec_update_room_avatar(self, obj):
if obj.jid != self.room_jid:
return
self._update_banner_state_image()
def _nec_mam_decrypted_message_received(self, obj):
if obj.conn.name != self.account:
return
if not obj.groupchat:
return
if obj.archive_jid != self.room_jid:
return
self.print_conversation(
obj.msgtxt, contact=obj.nick,
tim=obj.timestamp, correct_id=obj.correct_id,
encrypted=obj.encrypted,
msg_stanza_id=obj.message_id,
additional_data=obj.additional_data)
def _nec_gc_message_received(self, obj):
if obj.room_jid != self.room_jid or obj.conn.name != self.account:
return
if obj.captcha_form:
if self.form_widget:
self.form_widget.hide()
self.form_widget.destroy()
self.btn_box.destroy()
dataform = dataforms.extend_form(node=obj.captcha_form)
self.form_widget = dataforms_widget.DataFormWidget(dataform)
def on_send_dataform_clicked(widget):
if not self.form_widget:
return
form_node = self.form_widget.data_form.get_purged()
form_node.type_ = 'submit'
obj.conn.send_captcha(self.room_jid, form_node)
self.form_widget.hide()
self.form_widget.destroy()
self.btn_box.destroy()
self.form_widget = None
del self.btn_box
self.form_widget.connect('validated', on_send_dataform_clicked)
self.form_widget.show_all()
vbox = self.xml.get_object('gc_textviews_vbox')
vbox.pack_start(self.form_widget, False, True, 0)
valid_button = Gtk.Button(stock=Gtk.STOCK_OK)
valid_button.connect('clicked', on_send_dataform_clicked)
self.btn_box = Gtk.HButtonBox()
self.btn_box.set_layout(Gtk.ButtonBoxStyle.END)
self.btn_box.pack_start(valid_button, True, True, 0)
self.btn_box.show_all()
vbox.pack_start(self.btn_box, False, False, 0)
if self.parent_win:
self.parent_win.redraw_tab(self, 'attention')
else:
self.attention_flag = True
if '100' in obj.status_code:
# Room is not anonymous
self.is_anonymous = False
if not obj.nick:
# message from server
self.print_conversation(
obj.msgtxt, tim=obj.timestamp,
xhtml=obj.xhtml_msgtxt, displaymarking=obj.displaymarking,
additional_data=obj.additional_data)
else:
# message from someone
if obj.delayed:
# don't print xhtml if it's an old message.
# Like that xhtml messages are grayed too.
self.print_old_conversation(
obj.msgtxt, contact=obj.nick,
tim=obj.timestamp, xhtml=None, encrypted=obj.encrypted,
displaymarking=obj.displaymarking, msg_stanza_id=obj.id_,
additional_data=obj.additional_data)
else:
if obj.nick == self.nick:
self.last_sent_txt = obj.msgtxt
self.print_conversation(
obj.msgtxt, contact=obj.nick,
tim=obj.timestamp, xhtml=obj.xhtml_msgtxt,
displaymarking=obj.displaymarking, encrypted=obj.encrypted,
correct_id=obj.correct_id, msg_stanza_id=obj.id_,
additional_data=obj.additional_data)
obj.needs_highlight = self.needs_visual_notification(obj.msgtxt)
def on_private_message(self, nick, sent, msg, tim, xhtml, session, msg_log_id=None,
encrypted=False, displaymarking=None):
# Do we have a queue?
fjid = self.room_jid + '/' + nick
no_queue = len(app.events.get_events(self.account, fjid)) == 0
event = events.PmEvent(msg, '', 'incoming', tim, encrypted, '',
msg_log_id, xhtml=xhtml, session=session, form_node=None,
displaymarking=displaymarking, sent_forwarded=sent)
app.events.add_event(self.account, fjid, event)
autopopup = app.config.get('autopopup')
autopopupaway = app.config.get('autopopupaway')
iter_ = self.get_contact_iter(nick)
path = self.model.get_path(iter_)
if not autopopup or (not autopopupaway and \
app.connections[self.account].connected > 2):
if no_queue: # We didn't have a queue: we change icons
transport = None
if app.jid_is_transport(self.room_jid):
transport = app.get_transport_name_from_jid(self.room_jid)
self.model[iter_][Column.IMG] = get_icon_name(
'event', transport=transport)
if self.parent_win:
self.parent_win.show_title()
self.parent_win.redraw_tab(self)
else:
self._start_private_message(nick)
# Scroll to line
path_ = path.copy()
path_.up()
self.list_treeview.expand_row(path_, False)
self.list_treeview.scroll_to_cell(path)
self.list_treeview.set_cursor(path)
contact = app.contacts.get_contact_with_highest_priority(
self.account, self.room_jid)
if contact:
app.interface.roster.draw_contact(self.room_jid, self.account)
def get_contact_iter(self, nick: str) -> Optional[Gtk.TreeIter]:
try:
ref = self._contact_refs[nick]
except KeyError:
return None
path = ref.get_path()
if path is None:
return None
return self.model.get_iter(path)
def print_old_conversation(self, text, contact='', tim=None, xhtml=None,
displaymarking=None, msg_stanza_id=None, encrypted=None, additional_data=None):
if additional_data is None:
additional_data = {}
if contact:
if contact == self.nick: # it's us
kind = 'outgoing'
else:
kind = 'incoming'
else:
kind = 'status'
if app.config.get('restored_messages_small'):
small_attr = ['small']
else:
small_attr = []
ChatControlBase.print_conversation_line(self, text, kind, contact, tim,
small_attr, small_attr + ['restored_message'],
small_attr + ['restored_message'], count_as_new=False, xhtml=xhtml,
displaymarking=displaymarking, msg_stanza_id=msg_stanza_id,
encrypted=encrypted, additional_data=additional_data)
def print_conversation(self, text, contact='', tim=None, xhtml=None,
graphics=True, displaymarking=None, correct_id=None, msg_stanza_id=None,
encrypted=None, additional_data=None):
"""
Print a line in the conversation
If contact is set: it's a message from someone or an info message
(contact = 'info' in such a case).
If contact is not set: it's a message from the server or help.
"""
if additional_data is None:
additional_data = {}
other_tags_for_name = []
other_tags_for_text = []
if contact:
if contact == self.nick: # it's us
kind = 'outgoing'
elif contact == 'info':
kind = 'info'
contact = None
else:
kind = 'incoming'
# muc-specific chatstate
if self.parent_win:
self.parent_win.redraw_tab(self, 'newmsg')
else:
kind = 'status'
if kind == 'incoming': # it's a message NOT from us
# highlighting and sounds
highlight, _sound = self.highlighting_for_message(text, tim)
if contact in self.gc_custom_colors:
other_tags_for_name.append('gc_nickname_color_' + \
str(self.gc_custom_colors[contact]))
else:
self.gc_count_nicknames_colors += 1
if self.gc_count_nicknames_colors == self.number_of_colors:
self.gc_count_nicknames_colors = 0
self.gc_custom_colors[contact] = \
self.gc_count_nicknames_colors
other_tags_for_name.append('gc_nickname_color_' + \
str(self.gc_count_nicknames_colors))
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')
self._nick_completion.record_message(contact, highlight)
if text.startswith('/me ') or text.startswith('/me\n'):
other_tags_for_text.append('gc_nickname_color_' + \
str(self.gc_custom_colors[contact]))
self.check_and_possibly_add_focus_out_line()
ChatControlBase.print_conversation_line(self, text, kind, contact, tim,
other_tags_for_name, [], other_tags_for_text, xhtml=xhtml,
graphics=graphics, displaymarking=displaymarking,
correct_id=correct_id, msg_stanza_id=msg_stanza_id, encrypted=encrypted,
additional_data=additional_data)
def get_nb_unread(self):
type_events = ['printed_marked_gc_msg']
if app.config.get('notify_on_all_muc_messages') or \
app.config.get_per('rooms', self.room_jid, 'notify_on_all_messages'):
type_events.append('printed_gc_msg')
nb = len(app.events.get_events(self.account, self.room_jid,
type_events))
nb += self.get_nb_unread_pm()
return nb
def get_nb_unread_pm(self):
nb = 0
for nick in app.contacts.get_nick_list(self.account, self.room_jid):
nb += len(app.events.get_events(self.account, self.room_jid + \
'/' + nick, ['pm']))
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
"""
highlight, sound = (None, None)
# Are any of the defined highlighting words in the text?
if self.needs_visual_notification(text):
highlight = True
if app.config.get_per('soundevents', 'muc_message_highlight',
'enabled'):
sound = 'highlight'
# Do we play a sound on every muc message?
elif app.config.get_per('soundevents', 'muc_message_received', \
'enabled'):
sound = 'received'
# Is it a history message? Don't want sound-floods when we join.
if tim is not None and time.mktime(time.localtime()) - tim > 1:
sound = None
return (highlight, sound)
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
"""
win = app.interface.msg_win_mgr.get_window(self.room_jid,
self.account)
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
self.conv_textview.show_focus_out_line()
def needs_visual_notification(self, text):
"""
Check text to see whether any of the words in (muc_highlight_words and
nick) appear
"""
special_words = app.config.get('muc_highlight_words').split(';')
special_words.append(self.nick)
con = app.connections[self.account]
special_words.append(con.get_own_jid().getStripped())
# 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)
while found_here > -1:
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
def set_subject(self, subject):
self.subject = subject
self.draw_banner_text()
def _nec_gc_subject_received(self, event):
if event.conn.name != self.account:
return
if event.jid != self.room_jid:
return
if self.subject == event.subject:
# Probably a rejoin, we already showed that subject
return
self.set_subject(event.subject)
text = _('%(nick)s has set the subject to %(subject)s') % {
'nick': event.nickname, 'subject': event.subject}
if event.delayed:
date = time.strftime('%d-%m-%Y %H:%M:%S',
time.localtime(event.timestamp))
text = '%s - %s' % (text, date)
just_joined = self.join_time > time.time() - 10
if app.config.get('show_subject_on_join') or not just_joined:
self.print_conversation(text)
if event.subject == '':
self.subject_button.hide()
else:
self.subject_button.show()
def _nec_gc_config_changed_received(self, obj):
# statuscode is a list
# http://www.xmpp.org/extensions/xep-0045.html#roomconfig-notify
# http://www.xmpp.org/extensions/xep-0045.html#registrar-statuscodes...
# -init
if obj.room_jid != self.room_jid or obj.conn.name != self.account:
return
changes = []
if '100' in obj.status_code:
# Can be a presence (see chg_contact_status in groupchat_control.py)
changes.append(_('Any occupant is allowed to see your full JID'))
self.is_anonymous = False
if '102' in obj.status_code:
changes.append(_('Room now shows unavailable members'))
if '103' in obj.status_code:
changes.append(_('Room now does not show unavailable members'))
if '104' in obj.status_code:
changes.append(_('A setting not related to privacy has been '
'changed'))
app.connections[self.account].get_module('Discovery').disco_muc(
self.room_jid, self.update_actions, update=True)
if '170' in obj.status_code:
# Can be a presence (see chg_contact_status in groupchat_control.py)
changes.append(_('Room logging is now enabled'))
if '171' in obj.status_code:
changes.append(_('Room logging is now disabled'))
if '172' in obj.status_code:
changes.append(_('Room is now non-anonymous'))
self.is_anonymous = False
if '173' in obj.status_code:
changes.append(_('Room is now semi-anonymous'))
self.is_anonymous = True
if '174' in obj.status_code:
changes.append(_('Room is now fully anonymous'))
self.is_anonymous = True
for change in changes:
self.print_conversation(change)
def _nec_signed_in(self, obj):
if obj.conn.name != self.account:
return
password = app.gc_passwords.get(self.room_jid, '')
obj.conn.join_gc(self.nick, self.room_jid, password, rejoin=True)
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
frm = ''
if obj.sent:
frm = 'out'
obj.session.control.print_conversation(obj.msgtxt, frm,
tim=obj.timestamp, xhtml=obj.xhtml, encrypted=obj.encrypted,
displaymarking=obj.displaymarking, msg_stanza_id=obj.id_,
correct_id=obj.correct_id)
else:
# otherwise pass it off to the control to be queued
self.on_private_message(nick, obj.sent, obj.msgtxt, obj.timestamp,
obj.xhtml, self.session, msg_log_id=obj.msg_log_id,
encrypted=obj.encrypted, displaymarking=obj.displaymarking)
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':
self.print_conversation(_('Ping? (%s)') % nick)
elif obj.name == 'ping-reply':
self.print_conversation(
_('Pong! (%(nick)s %(delay)s s.)') % {'nick': nick,
'delay': obj.seconds})
elif obj.name == 'ping-error':
self.print_conversation(_('Error.'))
@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
def got_connected(self):
self.join_time = time.time()
# Make autorejoin stop.
if self.autorejoin:
GLib.source_remove(self.autorejoin)
self.autorejoin = None
if muc_caps_cache.has_mam(self.room_jid):
# Request MAM
con = app.connections[self.account]
con.get_module('MAM').request_archive_on_muc_join(
self.room_jid)
self.is_connected = True
ChatControlBase.got_connected(self)
# Sort model and assign it to treeview
self.model.set_sort_column_id(Column.NICK, Gtk.SortType.ASCENDING)
self.list_treeview.set_model(self.model)
self.list_treeview.expand_all()
# 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)
formattings_button = self.xml.get_object('formattings_button')
formattings_button.set_sensitive(True)
self.update_actions()
def got_disconnected(self):
formattings_button = self.xml.get_object('formattings_button')
formattings_button.set_sensitive(False)
self.model.set_sort_column_id(Gtk.TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
Gtk.SortType.ASCENDING)
self.list_treeview.set_model(None)
self._contact_refs = {}
self._role_refs = {}
self.model.clear()
nick_list = app.contacts.get_nick_list(self.account, self.room_jid)
for nick in nick_list:
# Update pm chat window
fjid = self.room_jid + '/' + nick
gc_contact = app.contacts.get_gc_contact(self.account,
self.room_jid, nick)
ctrl = app.interface.msg_win_mgr.get_control(fjid, self.account)
if ctrl:
gc_contact.show = 'offline'
gc_contact.status = ''
ctrl.update_ui()
if ctrl.parent_win:
ctrl.parent_win.redraw_tab(ctrl)
app.contacts.remove_gc_contact(self.account, gc_contact)
self.is_connected = False
ChatControlBase.got_disconnected(self)
# 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)
# Autorejoin stuff goes here.
# Notice that we don't need to activate autorejoin if connection is lost
# or in progress.
if self.autorejoin is None and app.account_is_connected(self.account):
ar_to = app.config.get('muc_autorejoin_timeout')
if ar_to:
self.autorejoin = GLib.timeout_add_seconds(ar_to, self.rejoin)
self.update_actions()
def rejoin(self):
if not self.autorejoin:
return False
password = app.gc_passwords.get(self.room_jid, '')
app.connections[self.account].join_gc(self.nick, self.room_jid,
password, rejoin=True)
return True
def draw_roster(self):
self.model.clear()
for nick in app.contacts.get_nick_list(self.account, self.room_jid):
gc_contact = app.contacts.get_gc_contact(self.account,
self.room_jid, nick)
self.add_contact_to_roster(nick, gc_contact.show, gc_contact.role,
gc_contact.affiliation, gc_contact.status, gc_contact.jid)
self.draw_all_roles()
# Recalculate column width for ellipsizin
self.list_treeview.columns_autosize()
def on_send_pm(self, widget=None, model=None, iter_=None, nick=None,
msg=None):
"""
Open a chat window and if msg is not None - send private message to a
contact in a room
"""
if nick is None:
nick = model[iter_][Column.NICK]
ctrl = self._start_private_message(nick)
if ctrl and msg:
ctrl.send_message(msg)
def draw_contact(self, nick, selected=False, focus=False):
iter_ = self.get_contact_iter(nick)
if not iter_:
return
gc_contact = app.contacts.get_gc_contact(
self.account, self.room_jid, nick)
if app.events.get_events(self.account, self.room_jid + '/' + nick):
icon_name = get_icon_name('event')
else:
icon_name = get_icon_name(gc_contact.show)
name = GLib.markup_escape_text(gc_contact.name)
# Strike name if blocked
fjid = self.room_jid + '/' + nick
if helpers.jid_is_blocked(self.account, fjid):
name = '<span strikethrough="true">%s</span>' % name
status = gc_contact.status
# add status msg, if not empty, under contact name in the treeview
if status and app.config.get('show_status_msgs_in_roster'):
status = status.strip()
if status != '':
status = helpers.reduce_chars_newlines(status, max_lines=1)
# escape markup entities and make them small italic and fg color
name += ('\n<span size="small" style="italic" alpha="70%">'
'{}</span>'.format(GLib.markup_escape_text(status)))
if (gc_contact.affiliation != 'none' and
app.config.get('show_affiliation_in_groupchat')):
icon_name += ':%s' % gc_contact.affiliation
self.model[iter_][Column.IMG] = icon_name
self.model[iter_][Column.TEXT] = name
def draw_avatar(self, gc_contact):
if not app.config.get('show_avatars_in_roster'):
return
iter_ = self.get_contact_iter(gc_contact.name)
if not iter_:
return
surface = app.interface.get_avatar(
gc_contact.avatar_sha, AvatarSize.ROSTER, self.scale_factor)
image = Gtk.Image.new_from_surface(surface)
self.model[iter_][Column.AVATAR_IMG] = image
def draw_role(self, role):
role_iter = self.get_role_iter(role)
if not role_iter:
return
role_name = helpers.get_uf_role(role, plural=True)
if app.config.get('show_contacts_number'):
nbr_role, nbr_total = app.contacts.get_nb_role_total_gc_contacts(
self.account, self.room_jid, role)
role_name += ' (%s/%s)' % (repr(nbr_role), repr(nbr_total))
self.model[role_iter][Column.TEXT] = role_name
def draw_all_roles(self):
for role in ('visitor', 'participant', 'moderator'):
self.draw_role(role)
def _change_nick(self, new_nick: str) -> None:
self.nick = new_nick
self._nick_completion.change_nick(new_nick)
def _nec_gc_presence_received(self, obj):
if obj.room_jid != self.room_jid or obj.conn.name != self.account:
return
if obj.ptype == 'error':
return
role = obj.role
if not role:
role = 'visitor'
affiliation = obj.affiliation
if not affiliation:
affiliation = 'none'
newly_created = False
nick = i18n.direction_mark + obj.nick
nick_jid = nick + i18n.direction_mark
# Set to true if role or affiliation have changed
right_changed = False
if obj.real_jid:
# delete resource
simple_jid = app.get_jid_without_resource(obj.real_jid)
nick_jid += ' (%s)' % simple_jid
con = app.connections[self.account]
bookmarks = con.get_module('Bookmarks').bookmarks
bookmark = bookmarks.get(self.room_jid, None)
if bookmark is None or not bookmark['print_status']:
print_status = app.config.get('print_status_in_muc')
else:
print_status = bookmark['print_status']
# status_code
# http://www.xmpp.org/extensions/xep-0045.html#registrar-statuscodes-\
# init
if obj.status_code and obj.nick == self.nick:
if '110' in obj.status_code:
if not self.is_connected:
# We just join the room
self.print_conversation(
_('You (%s) joined the room') % self.nick,
'info', graphics=False)
if self.room_jid in app.automatic_rooms[self.account] and \
app.automatic_rooms[self.account][self.room_jid]['invities']:
if self.room_jid not in app.interface.instances[
self.account]['gc_config']:
if obj.affiliation == 'owner':
# We need to configure the room if it's a new one.
# We cannot know it's a new one. Status 201 is not
# sent by all servers.
con = app.connections[self.account]
con.get_module('MUC').request_config(self.room_jid)
elif 'continue_tag' in app.automatic_rooms[
self.account][self.room_jid]:
# We just need to invite contacts
for jid in app.automatic_rooms[self.account][
self.room_jid]['invities']:
obj.conn.get_module('MUC').invite(self.room_jid, jid)
self.print_conversation(_('%(jid)s has been '
'invited in this room') % {'jid': jid},
graphics=False)
if '100' in obj.status_code:
# Can be a message (see handle_event_gc_config_change in
# app.py)
self.print_conversation(
_('Any occupant is allowed to see your full JID'))
self.is_anonymous = False
if '170' in obj.status_code:
# Can be a message (see handle_event_gc_config_change in
# app.py)
self.print_conversation(_('Room logging is enabled'))
if '201' in obj.status_code:
app.connections[self.account].get_module('Discovery').disco_muc(
self.room_jid, self._on_room_created, update=True)
self.print_conversation(_('A new room has been created'))
if '210' in obj.status_code:
self.print_conversation(\
_('The server has assigned or modified your roomnick'))
if obj.show in ('offline', 'error'):
if obj.status_code:
if '333' in obj.status_code:
# Handle 333 before 307, some MUCs add both
if print_status != 'none':
if obj.nick == self.nick:
s = _('%s kicked us due to an error' % self.room_jid)
else:
s = _('%s has left due to an error' % nick)
if obj.reason:
s += ' (%s)' % obj.reason
self.print_conversation(s, 'info', graphics=False)
elif '307' in obj.status_code:
if obj.actor is None: # do not print 'kicked by None'
s = _('%(nick)s has been kicked: %(reason)s') % {
'nick': nick, 'reason': obj.reason}
else:
s = _('%(nick)s has been kicked by %(who)s: '
'%(reason)s') % {'nick': nick, 'who': obj.actor,
'reason': obj.reason}
self.print_conversation(s, 'info', graphics=False)
if obj.nick == self.nick and not app.config.get(
'muc_autorejoin_on_kick'):
self.autorejoin = False
elif '301' in obj.status_code:
if obj.actor is None: # do not print 'banned by None'
s = _('%(nick)s has been banned: %(reason)s') % {
'nick': nick, 'reason': obj.reason}
else:
s = _('%(nick)s has been banned by %(who)s: '
'%(reason)s') % {'nick': nick, 'who': obj.actor,
'reason': obj.reason}
self.print_conversation(s, 'info', graphics=False)
if obj.nick == self.nick:
self.autorejoin = False
elif '303' in obj.status_code: # Someone changed their nick
if obj.new_nick == self.new_nick or obj.nick == self.nick:
# We changed our nick
self.change_nick(obj.new_nick)
self.new_nick = ''
s = _('You are now known as %s') % self.nick
else:
s = _('%(nick)s is now known as %(new_nick)s') % {
'nick': nick, 'new_nick': obj.new_nick}
tv = self.conv_textview
if obj.nick in tv.last_received_message_id:
tv.last_received_message_id[obj.new_nick] = \
tv.last_received_message_id[obj.nick]
del tv.last_received_message_id[obj.nick]
# We add new nick to muc roster here, so we don't see
# that "new_nick has joined the room" when he just changed
# nick.
# add_contact_to_roster will be called a second time
# after that, but that doesn't hurt
self.add_contact_to_roster(obj.new_nick, obj.show, role,
affiliation, obj.status, obj.real_jid)
self._nick_completion.contact_renamed(nick, obj.new_nick)
# keep nickname color
if obj.nick in self.gc_custom_colors:
self.gc_custom_colors[obj.new_nick] = \
self.gc_custom_colors[obj.nick]
self.print_conversation(s, 'info', graphics=False)
elif '321' in obj.status_code:
s = _('%(nick)s has been removed from the room '
'(%(reason)s)') % {'nick': nick,
'reason': _('affiliation changed')}
self.print_conversation(s, 'info', graphics=False)
elif '322' in obj.status_code:
s = _('%(nick)s has been removed from the room '
'(%(reason)s)') % {'nick': nick,
'reason': _('room configuration changed to '
'members-only')}
self.print_conversation(s, 'info', graphics=False)
elif '332' in obj.status_code:
s = _('%(nick)s has been removed from the room '
'(%(reason)s)') % {'nick': nick,
'reason': _('system shutdown')}
self.print_conversation(s, 'info', graphics=False)
# Room has been destroyed.
elif 'destroyed' in obj.status_code:
self.autorejoin = False
self.print_conversation(obj.reason, 'info', graphics=False)
if not app.events.get_events(
self.account, jid=obj.fjid, types=['pm']):
self.remove_contact(obj.nick)
self.draw_all_roles()
else:
c = app.contacts.get_gc_contact(self.account, self.room_jid,
obj.nick)
c.show = obj.show
c.status = obj.status
if obj.nick == self.nick and (not obj.status_code or \
'303' not in obj.status_code): # We became offline
self.got_disconnected()
contact = app.contacts.\
get_contact_with_highest_priority(self.account,
self.room_jid)
if contact:
app.interface.roster.draw_contact(self.room_jid,
self.account)
if self.parent_win:
self.parent_win.redraw_tab(self)
else:
iter_ = self.get_contact_iter(obj.nick)
if not iter_:
if '210' in obj.status_code:
# Server changed our nick
self.change_nick(obj.nick)
s = _('You are now known as %s') % nick
self.print_conversation(s, 'info', graphics=False)
iter_ = self.add_contact_to_roster(obj.nick, obj.show, role,
affiliation, obj.status, obj.real_jid, obj.avatar_sha)
newly_created = True
self.draw_all_roles()
if obj.status_code and '201' in obj.status_code:
# We just created the room
con = app.connections[self.account]
con.get_module('MUC').request_config(self.room_jid)
else:
gc_c = app.contacts.get_gc_contact(self.account,
self.room_jid, obj.nick)
if not gc_c:
log.error('%s has an iter, but no gc_contact instance',
obj.nick)
return
actual_affiliation = gc_c.affiliation
if affiliation != actual_affiliation:
if obj.actor:
st = _('** Affiliation of %(nick)s has been set to '
'%(affiliation)s by %(actor)s') % {'nick': nick_jid,
'affiliation': affiliation, 'actor': obj.actor}
else:
st = _('** Affiliation of %(nick)s has been set to '
'%(affiliation)s') % {'nick': nick_jid,
'affiliation': affiliation}
if obj.reason:
st += ' (%s)' % obj.reason
self.print_conversation(st, graphics=False)
right_changed = True
actual_role = self.get_role(obj.nick)
if role != actual_role:
self.remove_contact(obj.nick)
self.add_contact_to_roster(obj.nick, obj.show, role,
affiliation, obj.status, obj.real_jid)
self.draw_role(actual_role)
self.draw_role(role)
if obj.actor:
st = _('** Role of %(nick)s has been set to %(role)s '
'by %(actor)s') % {'nick': nick_jid, 'role': role,
'actor': obj.actor}
else:
st = _('** Role of %(nick)s has been set to '
'%(role)s') % {'nick': nick_jid, 'role': role}
if obj.reason:
st += ' (%s)' % obj.reason
self.print_conversation(st, graphics=False)
right_changed = True
else:
if gc_c.show == obj.show and gc_c.status == obj.status and \
gc_c.affiliation == affiliation: # no change
return
gc_c.show = obj.show
gc_c.affiliation = affiliation
gc_c.status = obj.status
self.draw_contact(obj.nick)
if self.is_connected and obj.nick != self.nick \
and (not obj.status_code or '303' not in obj.status_code) and not \
right_changed:
st = ''
if obj.show == 'offline' and print_status in ('all', 'in_and_out') \
and (not obj.status_code or '307' not in obj.status_code):
st = _('%s has left') % nick_jid
if obj.reason:
st += ' [%s]' % obj.reason
else:
if newly_created and print_status in ('all', 'in_and_out'):
st = _('%s has joined the group chat') % nick_jid
elif print_status == 'all':
st = _('%(nick)s is now %(status)s') % {'nick': nick_jid,
'status': helpers.get_uf_show(obj.show)}
if st:
if obj.status:
st += ' (' + obj.status + ')'
self.print_conversation(st, graphics=False)
# Update Actions
if obj.status_code:
if '110' in obj.status_code:
self.update_actions()
def add_contact_to_roster(self, nick, show, role, affiliation, status,
jid='', avatar_sha=None):
role_name = helpers.get_uf_role(role, plural=True)
resource = ''
if jid:
jids = jid.split('/', 1)
j = jids[0]
if len(jids) > 1:
resource = jids[1]
else:
j = ''
name = nick
# Add Contact
gc_contact = app.contacts.create_gc_contact(
room_jid=self.room_jid, account=self.account,
name=nick, show=show, status=status, role=role,
affiliation=affiliation, jid=j, resource=resource,
avatar_sha=avatar_sha)
app.contacts.add_gc_contact(self.account, gc_contact)
# Create Role
role_iter = self.get_role_iter(role)
if not role_iter:
icon_name = get_icon_name('closed')
ext_columns = [None] * self.nb_ext_renderers
row = [icon_name, role, 'role', role_name, None] + ext_columns
role_iter = self.model.append(None, row)
self._role_refs[role] = Gtk.TreeRowReference(
self.model, self.model.get_path(role_iter))
self.draw_all_roles()
# Avatar
image = None
if app.config.get('show_avatars_in_roster'):
surface = app.interface.get_avatar(
avatar_sha, AvatarSize.ROSTER, self.scale_factor)
image = Gtk.Image.new_from_surface(surface)
# Add to model
ext_columns = [None] * self.nb_ext_renderers
row = [None, nick, 'contact', name, image] + ext_columns
iter_ = self.model.append(role_iter, row)
self._contact_refs[nick] = Gtk.TreeRowReference(
self.model, self.model.get_path(iter_))
self.draw_contact(nick)
if nick == self.nick: # we became online
self.got_connected()
if self.list_treeview.get_model():
self.list_treeview.expand_row(
(self.model.get_path(role_iter)), False)
if self.is_continued:
self.draw_banner_text()
return iter_
def get_role_iter(self, role: str) -> Optional[Gtk.TreeIter]:
try:
ref = self._role_refs[role]
except KeyError:
return None
path = ref.get_path()
if path is None:
return None
return self.model.get_iter(path)
def remove_contact(self, nick):
"""
Remove a user from the contacts_list
"""
iter_ = self.get_contact_iter(nick)
if not iter_:
return
gc_contact = app.contacts.get_gc_contact(
self.account, self.room_jid, nick)
if gc_contact:
app.contacts.remove_gc_contact(self.account, gc_contact)
parent_iter = self.model.iter_parent(iter_)
if parent_iter is None:
# This is not a child, should never happen
return
self.model.remove(iter_)
del self._contact_refs[nick]
if self.model.iter_n_children(parent_iter) == 0:
role = self.model[parent_iter][Column.NICK]
del self._role_refs[role]
self.model.remove(parent_iter)
def _message_sent(self, obj):
if not obj.message:
return
if obj.account != self.account:
return
if obj.jid != self.room_jid:
return
# we'll save sent message text when we'll receive it in
# _nec_gc_message_received
self.last_sent_msg = obj.stanza_id
if self.correcting:
self.correcting = False
gtkgui_helpers.remove_css_class(
self.msg_textview, 'gajim-msg-correcting')
def send_message(self, message, xhtml=None, process_commands=True):
"""
Call this function to send our message
"""
if not message:
return
if self.encryption:
self.sendmessage = True
app.plugin_manager.extension_point(
'send_message' + self.encryption, self)
if not self.sendmessage:
return
if process_commands and self.process_as_command(message):
return
message = helpers.remove_invalid_xml_chars(message)
if not message:
return
label = self.get_seclabel()
if message != '' or message != '\n':
self.save_message(message, 'sent')
if self.correcting and self.last_sent_msg:
correct_id = self.last_sent_msg
else:
correct_id = None
con = app.connections[self.account]
chatstate = con.get_module('Chatstate').get_active_chatstate(
self.contact)
# Send the message
app.nec.push_outgoing_event(GcMessageOutgoingEvent(
None, account=self.account, jid=self.room_jid, message=message,
xhtml=xhtml, label=label, chatstate=chatstate,
correct_id=correct_id, automatic_message=False))
self.msg_textview.get_buffer().set_text('')
self.msg_textview.grab_focus()
def get_role(self, nick):
gc_contact = app.contacts.get_gc_contact(
self.account, self.room_jid, nick)
if gc_contact:
return gc_contact.role
return 'visitor'
def minimizable(self):
if self.force_non_minimizable:
return False
if self.contact.jid not in app.config.get_per('accounts', self.account,
'non_minimized_gc').split(' '):
return True
return False
def minimize(self, status='offline'):
# Minimize it
win = app.interface.msg_win_mgr.get_window(self.contact.jid,
self.account)
ctrl = win.get_control(self.contact.jid, self.account)
ctrl_page = win.notebook.page_num(ctrl.widget)
control = win.notebook.get_nth_page(ctrl_page)
win.notebook.remove_page(ctrl_page)
control.unparent()
ctrl.parent_win = None
con = app.connections[self.account]
con.get_module('Chatstate').set_chatstate(self.contact, Chatstate.INACTIVE)
app.interface.roster.minimize_groupchat(
self.account, self.contact.jid, status=self.subject)
del win._controls[self.account][self.contact.jid]
def shutdown(self, status='offline'):
# PluginSystem: calling shutdown of super class (ChatControlBase)
# to let it remove it's GUI extension points
super(GroupchatControl, self).shutdown()
# PluginSystem: removing GUI extension points connected with
# GrouphatControl instance object
app.plugin_manager.remove_gui_extension_point('groupchat_control',
self)
# Preventing autorejoin from being activated
self.autorejoin = False
app.ged.remove_event_handler('gc-presence-received', ged.GUI1,
self._nec_gc_presence_received)
app.ged.remove_event_handler('gc-message-received', ged.GUI1,
self._nec_gc_message_received)
app.ged.remove_event_handler('vcard-published', ged.GUI1,
self._nec_vcard_published)
app.ged.remove_event_handler('update-gc-avatar', ged.GUI1,
self._nec_update_avatar)
app.ged.remove_event_handler('update-room-avatar', ged.GUI1,
self._nec_update_room_avatar)
app.ged.remove_event_handler('gc-subject-received', ged.GUI1,
self._nec_gc_subject_received)
app.ged.remove_event_handler('gc-config-changed-received', ged.GUI1,
self._nec_gc_config_changed_received)
app.ged.remove_event_handler('signed-in', ged.GUI1,
self._nec_signed_in)
app.ged.remove_event_handler('decrypted-message-received', ged.GUI2,
self._nec_decrypted_message_received)
app.ged.remove_event_handler('mam-decrypted-message-received',
ged.GUI1, self._nec_mam_decrypted_message_received)
app.ged.remove_event_handler('gc-stanza-message-outgoing', ged.OUT_POSTCORE,
self._message_sent)
if self.is_connected:
app.connections[self.account].send_gc_status(self.nick,
self.room_jid, show='offline', status=status)
nick_list = app.contacts.get_nick_list(self.account, self.room_jid)
for nick in nick_list:
# Update pm chat window
fjid = self.room_jid + '/' + nick
ctrl = app.interface.msg_win_mgr.get_gc_control(fjid,
self.account)
if ctrl:
contact = app.contacts.get_gc_contact(self.account,
self.room_jid, nick)
contact.show = 'offline'
contact.status = ''
ctrl.update_ui()
ctrl.parent_win.redraw_tab(ctrl)
# They can already be removed by the destroy function
if self.room_jid in app.contacts.get_gc_list(self.account):
app.contacts.remove_room(self.account, self.room_jid)
del app.gc_connected[self.account][self.room_jid]
# Save hpaned position
app.config.set('gc-hpaned-position', self.hpaned.get_position())
# remove all register handlers on wigets, created by self.xml
# to prevent circular references among objects
for i in list(self.handlers.keys()):
if self.handlers[i].handler_is_connected(i):
self.handlers[i].disconnect(i)
del self.handlers[i]
# Remove unread events from systray
app.events.remove_events(self.account, self.room_jid)
def safe_shutdown(self):
if self.minimizable():
return True
includes = app.config.get('confirm_close_muc_rooms').split(' ')
excludes = app.config.get('noconfirm_close_muc_rooms').split(' ')
# whether to ask for confirmation before closing muc
if (app.config.get('confirm_close_muc') or self.room_jid in includes)\
and self.is_connected and self.room_jid not in excludes:
return False
return True
def allow_shutdown(self, method, on_yes, on_no, on_minimize):
if self.minimizable():
on_minimize(self)
return
if method == self.parent_win.CLOSE_ESC:
iter_ = self.list_treeview.get_selection().get_selected()[1]
if iter_:
self.list_treeview.get_selection().unselect_all()
on_no(self)
return
includes = app.config.get('confirm_close_muc_rooms').split(' ')
excludes = app.config.get('noconfirm_close_muc_rooms').split(' ')
# whether to ask for confirmation before closing muc
if (app.config.get('confirm_close_muc') or self.room_jid in includes)\
and self.is_connected and self.room_jid \
not in excludes:
def on_ok(clicked):
if clicked:
# user does not want to be asked again
app.config.set('confirm_close_muc', False)
on_yes(self)
def on_cancel(clicked):
if clicked:
# user does not want to be asked again
app.config.set('confirm_close_muc', False)
on_no(self)
pritext = _('Are you sure you want to leave group chat "%s"?')\
% self.name
sectext = _('If you close this window, you will be disconnected '
'from this group chat.')
ConfirmationDialogCheck(pritext, sectext,
_('_Do not ask me again'), on_response_ok=on_ok,
on_response_cancel=on_cancel,
transient_for=self.parent_win.window)
return
on_yes(self)
def set_control_active(self, state):
self.conv_textview.allow_focus_out_line = True
self.attention_flag = False
ChatControlBase.set_control_active(self, state)
if not state:
# add the focus-out line to the tab we are leaving
self.check_and_possibly_add_focus_out_line()
# Sending active to undo unread state
self.parent_win.redraw_tab(self, 'active')
def get_specific_unread(self):
# returns the number of the number of unread msgs
# for room_jid & number of unread private msgs with each contact
# that we have
nb = 0
for nick in app.contacts.get_nick_list(self.account, self.room_jid):
fjid = self.room_jid + '/' + nick
nb += len(app.events.get_events(self.account, fjid))
# gc can only have messages as event
return nb
def _on_change_subject_menuitem_activate(self, widget):
def on_ok(subject):
# Note, we don't update self.subject since we don't know whether it
# will work yet
con = app.connections[self.account]
con.get_module('MUC').set_subject(self.room_jid, subject)
InputTextDialog(_('Changing Subject'),
_('Please specify the new subject:'), input_str=self.subject,
ok_handler=on_ok, transient_for=self.parent_win.window)
def _on_drag_data_received(self, widget, context, x, y, selection,
target_type, timestamp):
if not selection.get_data():
return
# get contact info
contact = contacts.Contact(jid=self.room_jid, account=self.account)
if target_type == self.TARGET_TYPE_URI_LIST:
# file drag and drop (handled in chat_control_base)
self.drag_data_file_transfer(contact, selection, self)
else:
# Invite contact to groupchat
treeview = app.interface.roster.tree
model = treeview.get_model()
data = selection.get_data()
path = treeview.get_selection().get_selected_rows()[1][0]
iter_ = model.get_iter(path)
type_ = model[iter_][2]
if type_ != 'contact': # source is not a contact
return
contact_jid = data
con = app.connections[self.account]
con.get_module('MUC').invite(self.room_jid, contact_jid)
self.print_conversation(_('%(jid)s has been invited in this room') %
{'jid': contact_jid}, graphics=False)
def _jid_not_blocked(self, bare_jid: str) -> bool:
fjid = self.room_jid + '/' + bare_jid
return not helpers.jid_is_blocked(self.account, fjid)
def _on_message_textview_key_press_event(self, widget, event):
res = ChatControlBase._on_message_textview_key_press_event(self, widget,
event)
if res:
return True
if event.keyval == Gdk.KEY_Tab: # TAB
message_buffer = widget.get_buffer()
start_iter, end_iter = message_buffer.get_bounds()
cursor_position = message_buffer.get_insert()
end_iter = message_buffer.get_iter_at_mark(cursor_position)
text = message_buffer.get_text(start_iter, end_iter, False)
splitted_text = text.split()
# nick completion
# check if tab is pressed with empty message
if splitted_text: # if there are any words
begin = splitted_text[-1] # last word we typed
else:
begin = ''
gc_refer_to_nick_char = app.config.get('gc_refer_to_nick_char')
with_refer_to_nick_char = False
after_nick_len = 1 # the space that is printed after we type [Tab]
# first part of this if : works fine even if refer_to_nick_char
if gc_refer_to_nick_char and text.endswith(
gc_refer_to_nick_char + ' '):
with_refer_to_nick_char = True
after_nick_len = len(gc_refer_to_nick_char + ' ')
if self.nick_hits and self.last_key_tabs and \
text[:-after_nick_len].endswith(self.nick_hits[0]):
# we should cycle
# Previous nick in list may had a space inside, so we check text
# and not splitted_text and store it into 'begin' var
self.nick_hits.append(self.nick_hits[0])
begin = self.nick_hits.pop(0)
else:
list_nick = app.contacts.get_nick_list(self.account,
self.room_jid)
list_nick = list(filter(self._jid_not_blocked, list_nick))
log.debug("Nicks to be considered for autosuggestions: %s",
list_nick)
self.nick_hits = self._nick_completion.generate_suggestions(
nicks=list_nick, beginning=begin)
log.debug("Nicks filtered for autosuggestions: %s",
self.nick_hits)
if self.nick_hits:
if len(splitted_text) < 2 or with_refer_to_nick_char:
# This is the 1st word of the line or no word or we are cycling
# at the beginning, possibly with a space in one nick
add = gc_refer_to_nick_char + ' '
else:
add = ' '
start_iter = end_iter.copy()
if self.last_key_tabs and with_refer_to_nick_char or (text and \
text[-1] == ' '):
# have to accommodate for the added space from last
# completion
# gc_refer_to_nick_char may be more than one char!
start_iter.backward_chars(len(begin) + len(add))
elif self.last_key_tabs and not app.config.get(
'shell_like_completion'):
# have to accommodate for the added space from last
# completion
start_iter.backward_chars(len(begin) + \
len(gc_refer_to_nick_char))
else:
start_iter.backward_chars(len(begin))
message_buffer.delete(start_iter, end_iter)
# get a shell-like completion
# if there's more than one nick for this completion, complete
# only the part that all these nicks have in common
if app.config.get('shell_like_completion') and \
len(self.nick_hits) > 1:
end = False
completion = ''
add = "" # if nick is not complete, don't add anything
while not end and len(completion) < len(self.nick_hits[0]):
completion = self.nick_hits[0][:len(completion)+1]
for nick in self.nick_hits:
if completion.lower() not in nick.lower():
end = True
completion = completion[:-1]
break
# if the current nick matches a COMPLETE existing nick,
# and if the user tab TWICE, complete that nick (with the
# "add")
if self.last_key_tabs:
for nick in self.nick_hits:
if nick == completion:
# The user seems to want this nick, so
# complete it as if it were the only nick
# available
add = gc_refer_to_nick_char + ' '
else:
completion = self.nick_hits[0]
message_buffer.insert_at_cursor(completion + add)
self.last_key_tabs = True
return True
self.last_key_tabs = False
def on_list_treeview_key_press_event(self, widget, event):
if event.keyval == Gdk.KEY_Escape:
selection = widget.get_selection()
iter_ = selection.get_selected()[1]
if iter_:
widget.get_selection().unselect_all()
return True
def on_list_treeview_row_expanded(self, widget, iter_, path):
"""
When a row is expanded: change the icon of the arrow
"""
model = widget.get_model()
model[iter_][Column.IMG] = get_icon_name('opened')
def on_list_treeview_row_collapsed(self, widget, iter_, path):
"""
When a row is collapsed: change the icon of the arrow
"""
model = widget.get_model()
model[iter_][Column.IMG] = get_icon_name('closed')
def kick(self, widget, nick):
"""
Kick a user
"""
def on_ok(reason):
con = app.connections[self.account]
con.get_module('MUC').set_role(self.room_jid, nick, 'none', reason)
# ask for reason
InputDialog(_('Kicking %s') % nick,
_('You may specify a reason below:'), ok_handler=on_ok,
transient_for=self.parent_win.window)
def mk_menu(self, event, iter_):
"""
Make contact's popup menu
"""
nick = self.model[iter_][Column.NICK]
c = app.contacts.get_gc_contact(self.account, self.room_jid, nick)
fjid = self.room_jid + '/' + nick
jid = c.jid
target_affiliation = c.affiliation
target_role = c.role
# looking for user's affiliation and role
user_nick = self.nick
user_affiliation = app.contacts.get_gc_contact(self.account,
self.room_jid, user_nick).affiliation
user_role = self.get_role(user_nick)
# making menu from gtk builder
xml = get_builder('gc_occupants_menu.ui')
# these conditions were taken from JEP 0045
item = xml.get_object('kick_menuitem')
if user_role != 'moderator' or \
(user_affiliation == 'admin' and target_affiliation == 'owner') or \
(user_affiliation == 'member' and target_affiliation in ('admin',
'owner')) or (user_affiliation == 'none' and target_affiliation != \
'none'):
item.set_sensitive(False)
id_ = item.connect('activate', self.kick, nick)
self.handlers[id_] = item
item = xml.get_object('voice_checkmenuitem')
item.set_active(target_role != 'visitor')
if user_role != 'moderator' or \
user_affiliation == 'none' or \
(user_affiliation == 'member' and target_affiliation != 'none') or \
target_affiliation in ('admin', 'owner'):
item.set_sensitive(False)
id_ = item.connect('activate', self.on_voice_checkmenuitem_activate,
nick)
self.handlers[id_] = item
item = xml.get_object('moderator_checkmenuitem')
item.set_active(target_role == 'moderator')
if not user_affiliation in ('admin', 'owner') or \
target_affiliation in ('admin', 'owner'):
item.set_sensitive(False)
id_ = item.connect('activate', self.on_moderator_checkmenuitem_activate,
nick)
self.handlers[id_] = item
item = xml.get_object('ban_menuitem')
if not user_affiliation in ('admin', 'owner') or \
(target_affiliation in ('admin', 'owner') and\
user_affiliation != 'owner'):
item.set_sensitive(False)
id_ = item.connect('activate', self.ban, jid)
self.handlers[id_] = item
item = xml.get_object('member_checkmenuitem')
item.set_active(target_affiliation != 'none')
if not user_affiliation in ('admin', 'owner') or \
(user_affiliation != 'owner' and target_affiliation in ('admin',
'owner')):
item.set_sensitive(False)
id_ = item.connect('activate', self.on_member_checkmenuitem_activate,
jid)
self.handlers[id_] = item
item = xml.get_object('admin_checkmenuitem')
item.set_active(target_affiliation in ('admin', 'owner'))
if not user_affiliation == 'owner':
item.set_sensitive(False)
id_ = item.connect('activate', self.on_admin_checkmenuitem_activate,
jid)
self.handlers[id_] = item
item = xml.get_object('owner_checkmenuitem')
item.set_active(target_affiliation == 'owner')
if not user_affiliation == 'owner':
item.set_sensitive(False)
id_ = item.connect('activate', self.on_owner_checkmenuitem_activate,
jid)
self.handlers[id_] = item
item = xml.get_object('invite_menuitem')
if jid and c.name != self.nick:
bookmarked = False
contact = app.contacts.get_contact(self.account, jid, c.resource)
if contact and contact.supports(nbxmpp.NS_CONFERENCE):
bookmarked = True
gui_menu_builder.build_invite_submenu(item, ((c, self.account),),
ignore_rooms=[self.room_jid], show_bookmarked=bookmarked)
else:
item.set_sensitive(False)
item = xml.get_object('information_menuitem')
id_ = item.connect('activate', self.on_info, nick)
self.handlers[id_] = item
item = xml.get_object('history_menuitem')
item.set_action_name('app.browse-history')
dict_ = {'jid': GLib.Variant('s', fjid),
'account': GLib.Variant('s', self.account)}
variant = GLib.Variant('a{sv}', dict_)
item.set_action_target_value(variant)
item = xml.get_object('add_to_roster_menuitem')
our_jid = app.get_jid_from_account(self.account)
if not jid or jid == our_jid or not app.connections[self.account].\
roster_supported:
item.set_sensitive(False)
else:
id_ = item.connect('activate', self.on_add_to_roster, jid)
self.handlers[id_] = item
item = xml.get_object('execute_command_menuitem')
id_ = item.connect('activate', self._on_execute_command_occupant, nick)
self.handlers[id_] = item
item = xml.get_object('block_menuitem')
item2 = xml.get_object('unblock_menuitem')
if not app.connections[self.account].get_module('PrivacyLists').supported:
item2.set_no_show_all(True)
item.set_no_show_all(True)
item.hide()
item2.hide()
elif helpers.jid_is_blocked(self.account, fjid):
item.set_no_show_all(True)
item.hide()
id_ = item2.connect('activate', self.on_unblock, nick)
self.handlers[id_] = item2
else:
id_ = item.connect('activate', self.on_block, nick)
self.handlers[id_] = item
item2.set_no_show_all(True)
item2.hide()
item = xml.get_object('send_private_message_menuitem')
id_ = item.connect('activate', self.on_send_pm, self.model, iter_)
self.handlers[id_] = item
item = xml.get_object('send_file_menuitem')
if not c.resource:
item.set_sensitive(False)
else:
item.set_sensitive(False)
# ToDo: integrate HTTP File Upload
id_ = item.connect('activate', self._on_send_file_jingle, c)
self.handlers[id_] = item
# show the popup now!
menu = xml.get_object('gc_occupants_menu')
menu.show_all()
menu.attach_to_widget(app.interface.roster.window, None)
menu.popup(None, None, None, None, event.button, event.time)
def _start_private_message(self, nick):
gc_c = app.contacts.get_gc_contact(self.account, self.room_jid, nick)
nick_jid = gc_c.get_full_jid()
ctrl = app.interface.msg_win_mgr.get_control(nick_jid, self.account)
if not ctrl:
ctrl = app.interface.new_private_chat(gc_c, self.account)
if ctrl:
ctrl.parent_win.set_active_tab(ctrl)
return ctrl
def _on_execute_command_occupant(self, widget, nick):
jid = self.room_jid + '/' + nick
CommandWindow(self.account, jid)
def on_row_activated(self, widget, path):
"""
When an iter is activated (double click or single click if gnome
is set this way)
"""
if path.get_depth() == 1: # It's a group
if widget.row_expanded(path):
widget.collapse_row(path)
else:
widget.expand_row(path, False)
else: # We want to send a private message
nick = self.model[path][Column.NICK]
self._start_private_message(nick)
def on_list_treeview_row_activated(self, widget, path, col=0):
"""
When an iter is double clicked: open the chat window
"""
if not app.single_click:
self.on_row_activated(widget, path)
def on_list_treeview_button_press_event(self, widget, event):
"""
Popup user's group's or agent menu
"""
try:
pos = widget.get_path_at_pos(int(event.x), int(event.y))
path, x = pos[0], pos[2]
except TypeError:
widget.get_selection().unselect_all()
return
if event.button == 3: # right click
widget.get_selection().select_path(path)
iter_ = self.model.get_iter(path)
if path.get_depth() == 2:
self.mk_menu(event, iter_)
return True
if event.button == 2: # middle click
widget.get_selection().select_path(path)
iter_ = self.model.get_iter(path)
if path.get_depth() == 2:
nick = self.model[iter_][Column.NICK]
self._start_private_message(nick)
return True
if event.button == 1: # left click
if app.single_click and not event.get_state() & Gdk.ModifierType.SHIFT_MASK:
self.on_row_activated(widget, path)
return True
iter_ = self.model.get_iter(path)
nick = self.model[iter_][Column.NICK]
if not nick in app.contacts.get_nick_list(
self.account, self.room_jid):
# it's a group
if x < 27:
if widget.row_expanded(path):
widget.collapse_row(path)
else:
widget.expand_row(path, False)
elif event.get_state() & Gdk.ModifierType.SHIFT_MASK:
self.append_nick_in_msg_textview(self.msg_textview, nick)
self.msg_textview.grab_focus()
return True
def append_nick_in_msg_textview(self, widget, nick):
self.msg_textview.remove_placeholder()
message_buffer = self.msg_textview.get_buffer()
start_iter, end_iter = message_buffer.get_bounds()
cursor_position = message_buffer.get_insert()
end_iter = message_buffer.get_iter_at_mark(cursor_position)
text = message_buffer.get_text(start_iter, end_iter, False)
start = ''
if text: # Cursor is not at first position
if not text[-1] in (' ', '\n', '\t'):
start = ' '
add = ' '
else:
gc_refer_to_nick_char = app.config.get('gc_refer_to_nick_char')
add = gc_refer_to_nick_char + ' '
message_buffer.insert_at_cursor(start + nick + add)
def grant_voice(self, widget, nick):
"""
Grant voice privilege to a user
"""
con = app.connections[self.account]
con.get_module('MUC').set_role(self.room_jid, nick, 'participant')
def revoke_voice(self, widget, nick):
"""
Revoke voice privilege to a user
"""
con = app.connections[self.account]
con.get_module('MUC').set_role(self.room_jid, nick, 'visitor')
def grant_moderator(self, widget, nick):
"""
Grant moderator privilege to a user
"""
con = app.connections[self.account]
con.get_module('MUC').set_role(self.room_jid, nick, 'moderator')
def revoke_moderator(self, widget, nick):
"""
Revoke moderator privilege to a user
"""
con = app.connections[self.account]
con.get_module('MUC').set_role(self.room_jid, nick, 'participant')
def ban(self, widget, jid):
"""
Ban a user
"""
def on_ok(reason):
con = app.connections[self.account]
con.get_module('MUC').set_affiliation(
self.room_jid,
{jid: {'affiliation': 'outcast',
'reason': reason}})
# to ban we know the real jid. so jid is not fakejid
nick = app.get_nick_from_jid(jid)
# ask for reason
InputDialog(_('Banning %s') % nick,
_('You may specify a reason below:'), ok_handler=on_ok,
transient_for=self.parent_win.window)
def grant_membership(self, widget, jid):
"""
Grant membership privilege to a user
"""
con = app.connections[self.account]
con.get_module('MUC').set_affiliation(
self.room_jid,
{jid: {'affiliation': 'member'}})
def revoke_membership(self, widget, jid):
"""
Revoke membership privilege to a user
"""
con = app.connections[self.account]
con.get_module('MUC').set_affiliation(
self.room_jid,
{jid: {'affiliation': 'none'}})
def grant_admin(self, widget, jid):
"""
Grant administrative privilege to a user
"""
con = app.connections[self.account]
con.get_module('MUC').set_affiliation(
self.room_jid,
{jid: {'affiliation': 'admin'}})
def revoke_admin(self, widget, jid):
"""
Revoke administrative privilege to a user
"""
con = app.connections[self.account]
con.get_module('MUC').set_affiliation(
self.room_jid,
{jid: {'affiliation': 'member'}})
def grant_owner(self, widget, jid):
"""
Grant owner privilege to a user
"""
con = app.connections[self.account]
con.get_module('MUC').set_affiliation(
self.room_jid,
{jid: {'affiliation': 'owner'}})
def revoke_owner(self, widget, jid):
"""
Revoke owner privilege to a user
"""
con = app.connections[self.account]
con.get_module('MUC').set_affiliation(
self.room_jid,
{jid: {'affiliation': 'admin'}})
def on_info(self, widget, nick):
"""
Call vcard_information_window class to display user's information
"""
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_add_to_roster(self, widget, jid):
AddNewContactWindow(self.account, jid)
def on_block(self, widget, nick):
fjid = self.room_jid + '/' + nick
con = app.connections[self.account]
con.get_module('PrivacyLists').block_gc_contact(fjid)
self.draw_contact(nick)
def on_unblock(self, widget, nick):
fjid = self.room_jid + '/' + nick
con = app.connections[self.account]
con.get_module('PrivacyLists').unblock_gc_contact(fjid)
self.draw_contact(nick)
def on_voice_checkmenuitem_activate(self, widget, nick):
if widget.get_active():
self.grant_voice(widget, nick)
else:
self.revoke_voice(widget, nick)
def on_moderator_checkmenuitem_activate(self, widget, nick):
if widget.get_active():
self.grant_moderator(widget, nick)
else:
self.revoke_moderator(widget, nick)
def on_member_checkmenuitem_activate(self, widget, jid):
if widget.get_active():
self.grant_membership(widget, jid)
else:
self.revoke_membership(widget, jid)
def on_admin_checkmenuitem_activate(self, widget, jid):
if widget.get_active():
self.grant_admin(widget, jid)
else:
self.revoke_admin(widget, jid)
def on_owner_checkmenuitem_activate(self, widget, jid):
if widget.get_active():
self.grant_owner(widget, jid)
else:
self.revoke_owner(widget, jid)
class SubjectPopover(Gtk.Popover):
def __init__(self):
Gtk.Popover.__init__(self)
self.set_name('SubjectPopover')
scrolledwindow = Gtk.ScrolledWindow()
scrolledwindow.set_max_content_height(250)
scrolledwindow.set_propagate_natural_height(True)
scrolledwindow.set_propagate_natural_width(True)
scrolledwindow.set_policy(Gtk.PolicyType.NEVER,
Gtk.PolicyType.AUTOMATIC)
self.label = Gtk.Label()
self.label.set_line_wrap(True)
self.label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
self.label.set_max_width_chars(80)
scrolledwindow.add(self.label)
box = Gtk.Box()
box.add(scrolledwindow)
box.show_all()
self.add(box)
self.connect_after('show', self._after_show)
def set_text(self, text):
self.label.set_markup(text)
def _after_show(self, *args):
# Gtk Bug: If we set selectable True, on show
# everything inside the Label is selected.
# So we switch after show to False and again to True
self.label.set_selectable(False)
self.label.set_selectable(True)