1747 lines
67 KiB
Python
1747 lines
67 KiB
Python
# Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
|
||
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
|
||
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
|
||
# Nikos Kouremenos <kourem AT gmail.com>
|
||
# Travis Shirk <travis AT pobox.com>
|
||
# Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
|
||
# Julien Pivotto <roidelapluie AT gmail.com>
|
||
# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
|
||
# Stephan Erb <steve-e AT h3c.de>
|
||
# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
|
||
#
|
||
# This file is part of Gajim.
|
||
#
|
||
# Gajim is free software; you can redistribute it and/or modify
|
||
# it under the terms of the GNU General Public License as published
|
||
# by the Free Software Foundation; version 3 only.
|
||
#
|
||
# Gajim is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU General Public License
|
||
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
||
|
||
from typing import ClassVar # pylint: disable=unused-import
|
||
from typing import Type # pylint: disable=unused-import
|
||
from typing import Optional # pylint: disable=unused-import
|
||
|
||
import os
|
||
import time
|
||
import logging
|
||
|
||
from gi.repository import Gtk
|
||
from gi.repository import Gio
|
||
from gi.repository import Pango
|
||
from gi.repository import GLib
|
||
from gi.repository import Gdk
|
||
|
||
from nbxmpp.namespaces import Namespace
|
||
|
||
from gajim.common import app
|
||
from gajim.common import helpers
|
||
from gajim.common import ged
|
||
from gajim.common import i18n
|
||
from gajim.common.i18n import _
|
||
from gajim.common.helpers import AdditionalDataDict
|
||
from gajim.common.helpers import open_uri
|
||
from gajim.common.helpers import geo_provider_from_location
|
||
from gajim.common.helpers import event_filter
|
||
from gajim.common.helpers import open_file
|
||
from gajim.common.contacts import GC_Contact
|
||
from gajim.common.const import AvatarSize
|
||
from gajim.common.const import KindConstant
|
||
from gajim.common.const import Chatstate
|
||
from gajim.common.const import PEPEventType
|
||
from gajim.common.const import JingleState
|
||
|
||
from gajim import gtkgui_helpers
|
||
from gajim import gui_menu_builder
|
||
from gajim import dialogs
|
||
|
||
from gajim.gui.gstreamer import create_gtk_widget
|
||
from gajim.gui.dialogs import DialogButton
|
||
from gajim.gui.dialogs import ConfirmationDialog
|
||
from gajim.gui.add_contact import AddNewContactWindow
|
||
from gajim.gui.util import get_cursor
|
||
from gajim.gui.util import format_mood
|
||
from gajim.gui.util import format_activity
|
||
from gajim.gui.util import format_tune
|
||
from gajim.gui.util import format_location
|
||
from gajim.gui.util import get_activity_icon_name
|
||
from gajim.gui.util import make_href_markup
|
||
from gajim.gui.const import ControlType
|
||
|
||
from gajim.command_system.implementation.hosts import ChatCommands
|
||
from gajim.command_system.framework import CommandHost # pylint: disable=unused-import
|
||
from gajim.chat_control_base import ChatControlBase
|
||
|
||
log = logging.getLogger('gajim.chat_control')
|
||
|
||
|
||
class JingleObject:
|
||
__slots__ = ('sid', 'state', 'available', 'update')
|
||
|
||
def __init__(self, state, update):
|
||
self.sid = None
|
||
self.state = state
|
||
self.available = False
|
||
self.update = update
|
||
|
||
|
||
################################################################################
|
||
class ChatControl(ChatControlBase):
|
||
"""
|
||
A control for standard 1-1 chat
|
||
"""
|
||
_type = ControlType.CHAT
|
||
old_msg_kind = None # last kind of the printed message
|
||
|
||
# Set a command host to bound to. Every command given through a chat will be
|
||
# processed with this command host.
|
||
COMMAND_HOST = ChatCommands # type: ClassVar[Type[CommandHost]]
|
||
|
||
def __init__(self, parent_win, contact, acct, session, resource=None):
|
||
ChatControlBase.__init__(self,
|
||
parent_win,
|
||
'chat_control',
|
||
contact,
|
||
acct,
|
||
resource)
|
||
|
||
self.last_recv_message_id = None
|
||
self.last_recv_message_marks = None
|
||
self.last_message_timestamp = None
|
||
|
||
self.toggle_emoticons()
|
||
|
||
if not app.settings.get('hide_chat_banner'):
|
||
self.xml.banner_eventbox.set_no_show_all(False)
|
||
|
||
self.xml.sendfile_button.set_action_name(
|
||
'win.send-file-%s' % self.control_id)
|
||
|
||
# Menu for the HeaderBar
|
||
self.control_menu = gui_menu_builder.get_singlechat_menu(
|
||
self.control_id, self.account, self.contact.jid, self._type)
|
||
|
||
# Settings menu
|
||
self.xml.settings_menu.set_menu_model(self.control_menu)
|
||
|
||
self.jingle = {
|
||
'audio': JingleObject(
|
||
JingleState.NULL,
|
||
self.update_audio),
|
||
'video': JingleObject(
|
||
JingleState.NULL,
|
||
self.update_video),
|
||
}
|
||
self._video_widget_other = None
|
||
self._video_widget_self = None
|
||
|
||
self.update_toolbar()
|
||
self.update_all_pep_types()
|
||
self._update_avatar()
|
||
|
||
# Hook up signals
|
||
widget = self.xml.location_eventbox
|
||
id_ = widget.connect('button-release-event',
|
||
self.on_location_eventbox_button_release_event)
|
||
self.handlers[id_] = widget
|
||
id_ = widget.connect('enter-notify-event',
|
||
self.on_location_eventbox_enter_notify_event)
|
||
self.handlers[id_] = widget
|
||
id_ = widget.connect('leave-notify-event',
|
||
self.on_location_eventbox_leave_notify_event)
|
||
self.handlers[id_] = widget
|
||
|
||
for key in ('1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'):
|
||
widget = self.xml.get_object(key + '_button')
|
||
id_ = widget.connect('pressed', self.on_num_button_pressed, key)
|
||
self.handlers[id_] = widget
|
||
id_ = widget.connect('released', self.on_num_button_released)
|
||
self.handlers[id_] = widget
|
||
|
||
widget = self.xml.mic_hscale
|
||
id_ = widget.connect('value_changed', self.on_mic_hscale_value_changed)
|
||
self.handlers[id_] = widget
|
||
|
||
widget = self.xml.sound_hscale
|
||
id_ = widget.connect('value_changed',
|
||
self.on_sound_hscale_value_changed)
|
||
self.handlers[id_] = widget
|
||
|
||
self.info_bar = Gtk.InfoBar()
|
||
content_area = self.info_bar.get_content_area()
|
||
self.info_bar_label = Gtk.Label()
|
||
self.info_bar_label.set_use_markup(True)
|
||
self.info_bar_label.set_halign(Gtk.Align.START)
|
||
self.info_bar_label.set_valign(Gtk.Align.START)
|
||
self.info_bar_label.set_ellipsize(Pango.EllipsizeMode.END)
|
||
content_area.add(self.info_bar_label)
|
||
self.info_bar.set_no_show_all(True)
|
||
|
||
self.xml.vbox2.pack_start(self.info_bar, False, True, 5)
|
||
self.xml.vbox2.reorder_child(self.info_bar, 1)
|
||
|
||
# List of waiting infobar messages
|
||
self.info_bar_queue = []
|
||
|
||
self.subscribe_events()
|
||
|
||
if not session:
|
||
# Don't use previous session if we want to a specific resource
|
||
# and it's not the same
|
||
if not resource:
|
||
resource = contact.resource
|
||
session = app.connections[self.account].find_controlless_session(
|
||
self.contact.jid, resource)
|
||
|
||
self.setup_seclabel()
|
||
if session:
|
||
session.control = self
|
||
self.session = session
|
||
|
||
self.add_actions()
|
||
self.update_ui()
|
||
self.set_lock_image()
|
||
|
||
self.xml.encryption_menu.set_menu_model(
|
||
gui_menu_builder.get_encryption_menu(
|
||
self.control_id, self._type, self.account == 'Local'))
|
||
self.set_encryption_menu_icon()
|
||
# restore previous conversation
|
||
self.restore_conversation()
|
||
self.msg_textview.grab_focus()
|
||
|
||
# pylint: disable=line-too-long
|
||
self.register_events([
|
||
('nickname-received', ged.GUI1, self._on_nickname_received),
|
||
('mood-received', ged.GUI1, self._on_mood_received),
|
||
('activity-received', ged.GUI1, self._on_activity_received),
|
||
('tune-received', ged.GUI1, self._on_tune_received),
|
||
('location-received', ged.GUI1, self._on_location_received),
|
||
('update-client-info', ged.GUI1, self._on_update_client_info),
|
||
('chatstate-received', ged.GUI1, self._on_chatstate_received),
|
||
('caps-update', ged.GUI1, self._on_caps_update),
|
||
('message-sent', ged.OUT_POSTCORE, self._on_message_sent),
|
||
('mam-decrypted-message-received', ged.GUI1, self._on_mam_decrypted_message_received),
|
||
('decrypted-message-received', ged.GUI1, self._on_decrypted_message_received),
|
||
('receipt-received', ged.GUI1, self._receipt_received),
|
||
('displayed-received', ged.GUI1, self._displayed_received),
|
||
('message-error', ged.GUI1, self._on_message_error),
|
||
('zeroconf-error', ged.GUI1, self._on_zeroconf_error),
|
||
])
|
||
|
||
if self._type.is_chat:
|
||
# Don’t connect this when PrivateChatControl is used
|
||
self.register_event('update-roster-avatar', ged.GUI1, self._on_update_roster_avatar)
|
||
# pylint: enable=line-too-long
|
||
|
||
# PluginSystem: adding GUI extension point for this ChatControl
|
||
# instance object
|
||
app.plugin_manager.gui_extension_point('chat_control', self)
|
||
self.update_actions()
|
||
|
||
@property
|
||
def jid(self):
|
||
return self.contact.jid
|
||
|
||
def add_actions(self):
|
||
super().add_actions()
|
||
actions = [
|
||
('invite-contacts-', self._on_invite_contacts),
|
||
('add-to-roster-', self._on_add_to_roster),
|
||
('block-contact-', self._on_block_contact),
|
||
('information-', self._on_information),
|
||
('start-call-', self._on_start_call),
|
||
]
|
||
|
||
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)
|
||
|
||
chatstate = self.contact.settings.get('send_chatstate')
|
||
|
||
act = Gio.SimpleAction.new_stateful(
|
||
'send-chatstate-' + self.control_id,
|
||
GLib.VariantType.new("s"),
|
||
GLib.Variant("s", chatstate))
|
||
act.connect('change-state', self._on_send_chatstate)
|
||
self.parent_win.window.add_action(act)
|
||
|
||
marker = self.contact.settings.get('send_marker')
|
||
|
||
act = Gio.SimpleAction.new_stateful(
|
||
f'send-marker-{self.control_id}',
|
||
None,
|
||
GLib.Variant.new_boolean(marker))
|
||
act.connect('change-state', self._on_send_marker)
|
||
self.parent_win.window.add_action(act)
|
||
|
||
def update_actions(self):
|
||
win = self.parent_win.window
|
||
online = app.account_is_connected(self.account)
|
||
con = app.connections[self.account]
|
||
|
||
# Add to roster
|
||
if not isinstance(self.contact, GC_Contact) \
|
||
and _('Not in contact list') in self.contact.groups and \
|
||
app.connections[self.account].roster_supported and online:
|
||
win.lookup_action(
|
||
'add-to-roster-' + self.control_id).set_enabled(True)
|
||
else:
|
||
win.lookup_action(
|
||
'add-to-roster-' + self.control_id).set_enabled(False)
|
||
|
||
# Block contact
|
||
win.lookup_action(
|
||
'block-contact-' + self.control_id).set_enabled(
|
||
online and con.get_module('Blocking').supported)
|
||
|
||
# Jingle AV detection
|
||
if (self.contact.supports(Namespace.JINGLE_ICE_UDP) and
|
||
app.is_installed('FARSTREAM') and self.contact.resource):
|
||
self.jingle['audio'].available = self.contact.supports(
|
||
Namespace.JINGLE_RTP_AUDIO)
|
||
self.jingle['video'].available = self.contact.supports(
|
||
Namespace.JINGLE_RTP_VIDEO)
|
||
else:
|
||
if (self.jingle['audio'].available or
|
||
self.jingle['video'].available):
|
||
self.stop_jingle()
|
||
self.jingle['audio'].available = False
|
||
self.jingle['video'].available = False
|
||
|
||
win.lookup_action(f'start-call-{self.control_id}').set_enabled(
|
||
online and (self.jingle['audio'].available or
|
||
self.jingle['video'].available))
|
||
|
||
# Send message
|
||
has_text = self.msg_textview.has_text()
|
||
win.lookup_action(
|
||
f'send-message-{self.control_id}').set_enabled(online and has_text)
|
||
|
||
# Send file (HTTP File Upload)
|
||
httpupload = win.lookup_action(
|
||
'send-file-httpupload-' + self.control_id)
|
||
httpupload.set_enabled(
|
||
online and con.get_module('HTTPUpload').available)
|
||
|
||
# Send file (Jingle)
|
||
jingle_support = self.contact.supports(Namespace.JINGLE_FILE_TRANSFER_5)
|
||
jingle_conditions = jingle_support and self.contact.show != 'offline'
|
||
jingle = win.lookup_action('send-file-jingle-' + self.control_id)
|
||
jingle.set_enabled(online and jingle_conditions)
|
||
|
||
# Send file
|
||
win.lookup_action(
|
||
'send-file-' + self.control_id).set_enabled(
|
||
jingle.get_enabled() or httpupload.get_enabled())
|
||
|
||
# Set File Transfer Button tooltip
|
||
if online and (httpupload.get_enabled() or jingle.get_enabled()):
|
||
tooltip_text = _('Send File…')
|
||
else:
|
||
tooltip_text = _('No File Transfer available')
|
||
self.xml.sendfile_button.set_tooltip_text(tooltip_text)
|
||
|
||
# Chat markers
|
||
state = GLib.Variant.new_boolean(
|
||
self.contact.settings.get('send_marker'))
|
||
win.lookup_action(
|
||
f'send-marker-{self.control_id}').change_state(state)
|
||
|
||
# Convert to GC
|
||
if app.settings.get_account_setting(self.account, 'is_zeroconf'):
|
||
win.lookup_action(
|
||
'invite-contacts-' + self.control_id).set_enabled(False)
|
||
else:
|
||
if self.contact.supports(Namespace.MUC) and online:
|
||
win.lookup_action(
|
||
'invite-contacts-' + self.control_id).set_enabled(True)
|
||
else:
|
||
win.lookup_action(
|
||
'invite-contacts-' + self.control_id).set_enabled(False)
|
||
|
||
# Information
|
||
win.lookup_action(
|
||
'information-' + self.control_id).set_enabled(online)
|
||
|
||
def remove_actions(self):
|
||
super().remove_actions()
|
||
actions = [
|
||
'invite-contacts-',
|
||
'add-to-roster-',
|
||
'block-contact-',
|
||
'information-',
|
||
'start-call-',
|
||
'send-chatstate-',
|
||
'send-marker-',
|
||
]
|
||
for action in actions:
|
||
self.parent_win.window.remove_action(f'{action}{self.control_id}')
|
||
|
||
def focus(self):
|
||
self.msg_textview.grab_focus()
|
||
|
||
def delegate_action(self, action):
|
||
res = super().delegate_action(action)
|
||
if res == Gdk.EVENT_STOP:
|
||
return res
|
||
|
||
if action == 'show-contact-info':
|
||
self.parent_win.window.lookup_action(
|
||
'information-%s' % self.control_id).activate()
|
||
return Gdk.EVENT_STOP
|
||
|
||
if action == 'send-file':
|
||
if app.interface.msg_win_mgr.mode == \
|
||
app.interface.msg_win_mgr.ONE_MSG_WINDOW_ALWAYS_WITH_ROSTER:
|
||
app.interface.roster.tree.grab_focus()
|
||
return Gdk.EVENT_PROPAGATE
|
||
|
||
self.parent_win.window.lookup_action(
|
||
'send-file-%s' % self.control_id).activate()
|
||
return Gdk.EVENT_STOP
|
||
|
||
return Gdk.EVENT_PROPAGATE
|
||
|
||
def _on_add_to_roster(self, _action, _param):
|
||
AddNewContactWindow(self.account, self.contact.jid)
|
||
|
||
def _on_block_contact(self, _action, _param):
|
||
def _block_contact(report=None):
|
||
con = app.connections[self.account]
|
||
con.get_module('Blocking').block([self.contact.jid], report=report)
|
||
|
||
self.parent_win.remove_tab(self, None, force=True)
|
||
if _('Not in contact list') in self.contact.get_shown_groups():
|
||
app.interface.roster.remove_contact(
|
||
self.contact.jid, self.account, force=True, backend=True)
|
||
return
|
||
app.interface.roster.draw_contact(self.contact.jid, self.account)
|
||
|
||
ConfirmationDialog(
|
||
_('Block Contact'),
|
||
_('Really block this contact?'),
|
||
_('You will appear offline for this contact and you will '
|
||
'not receive further messages.'),
|
||
[DialogButton.make('Cancel'),
|
||
DialogButton.make('OK',
|
||
text=_('_Report Spam'),
|
||
callback=_block_contact,
|
||
kwargs={'report': 'spam'}),
|
||
DialogButton.make('Remove',
|
||
text=_('_Block'),
|
||
callback=_block_contact)],
|
||
modal=False).show()
|
||
|
||
def _on_information(self, _action, _param):
|
||
app.interface.roster.on_info(None, self.contact, self.account)
|
||
|
||
def _on_invite_contacts(self, _action, _param):
|
||
"""
|
||
User wants to invite some friends to chat
|
||
"""
|
||
dialogs.TransformChatToMUC(self.account, [self.contact.jid])
|
||
|
||
def _on_send_chatstate(self, action, param):
|
||
action.set_state(param)
|
||
self.contact.settings.set('send_chatstate', param.get_string())
|
||
|
||
def _on_send_marker(self, action, param):
|
||
action.set_state(param)
|
||
self.contact.settings.set('send_marker', param.get_boolean())
|
||
|
||
def subscribe_events(self):
|
||
"""
|
||
Register listeners to the events class
|
||
"""
|
||
app.events.event_added_subscribe(self.on_event_added)
|
||
app.events.event_removed_subscribe(self.on_event_removed)
|
||
|
||
def unsubscribe_events(self):
|
||
"""
|
||
Unregister listeners to the events class
|
||
"""
|
||
app.events.event_added_unsubscribe(self.on_event_added)
|
||
app.events.event_removed_unsubscribe(self.on_event_removed)
|
||
|
||
def _update_toolbar(self):
|
||
# Formatting
|
||
# TODO: find out what encryption allows for xhtml and which not
|
||
if self.contact.supports(Namespace.XHTML_IM):
|
||
self.xml.formattings_button.set_sensitive(True)
|
||
self.xml.formattings_button.set_tooltip_text(_(
|
||
'Show a list of formattings'))
|
||
else:
|
||
self.xml.formattings_button.set_sensitive(False)
|
||
self.xml.formattings_button.set_tooltip_text(
|
||
_('This contact does not support HTML'))
|
||
|
||
def update_all_pep_types(self):
|
||
self._update_pep(PEPEventType.LOCATION)
|
||
self._update_pep(PEPEventType.MOOD)
|
||
self._update_pep(PEPEventType.ACTIVITY)
|
||
self._update_pep(PEPEventType.TUNE)
|
||
|
||
def _update_pep(self, type_):
|
||
image = self._get_pep_widget(type_)
|
||
data = self.contact.pep.get(type_)
|
||
if data is None:
|
||
image.hide()
|
||
return
|
||
|
||
if type_ == PEPEventType.MOOD:
|
||
icon = 'mood-%s' % data.mood
|
||
formated_text = format_mood(*data)
|
||
elif type_ == PEPEventType.ACTIVITY:
|
||
icon = get_activity_icon_name(data.activity, data.subactivity)
|
||
formated_text = format_activity(*data)
|
||
elif type_ == PEPEventType.TUNE:
|
||
icon = 'audio-x-generic'
|
||
formated_text = format_tune(*data)
|
||
elif type_ == PEPEventType.LOCATION:
|
||
icon = 'applications-internet'
|
||
formated_text = format_location(data)
|
||
|
||
image.set_from_icon_name(icon, Gtk.IconSize.MENU)
|
||
image.set_tooltip_markup(formated_text)
|
||
image.show()
|
||
|
||
def _get_pep_widget(self, type_):
|
||
if type_ == PEPEventType.MOOD:
|
||
return self.xml.mood_image
|
||
if type_ == PEPEventType.ACTIVITY:
|
||
return self.xml.activity_image
|
||
if type_ == PEPEventType.TUNE:
|
||
return self.xml.tune_image
|
||
if type_ == PEPEventType.LOCATION:
|
||
return self.xml.location_image
|
||
return None
|
||
|
||
@event_filter(['account', 'jid'])
|
||
def _on_mood_received(self, _event):
|
||
self._update_pep(PEPEventType.MOOD)
|
||
|
||
@event_filter(['account', 'jid'])
|
||
def _on_activity_received(self, _event):
|
||
self._update_pep(PEPEventType.ACTIVITY)
|
||
|
||
@event_filter(['account', 'jid'])
|
||
def _on_tune_received(self, _event):
|
||
self._update_pep(PEPEventType.TUNE)
|
||
|
||
@event_filter(['account', 'jid'])
|
||
def _on_location_received(self, _event):
|
||
self._update_pep(PEPEventType.LOCATION)
|
||
|
||
@event_filter(['account', 'jid'])
|
||
def _on_nickname_received(self, _event):
|
||
self.update_ui()
|
||
self.parent_win.redraw_tab(self)
|
||
self.parent_win.show_title()
|
||
|
||
@event_filter(['account', 'jid'])
|
||
def _on_update_client_info(self, event):
|
||
contact = app.contacts.get_contact(
|
||
self.account, event.jid, event.resource)
|
||
if contact is None:
|
||
return
|
||
self.xml.phone_image.set_visible(contact.uses_phone)
|
||
|
||
@event_filter(['account'])
|
||
def _on_chatstate_received(self, event):
|
||
if self._type.is_privatechat:
|
||
if event.contact != self.gc_contact:
|
||
return
|
||
else:
|
||
if event.contact.jid != self.contact.jid:
|
||
return
|
||
|
||
self.draw_banner_text()
|
||
|
||
# update chatstate in tab for this chat
|
||
if event.contact.is_gc_contact:
|
||
chatstate = event.contact.chatstate
|
||
else:
|
||
chatstate = app.contacts.get_combined_chatstate(
|
||
self.account, self.contact.jid)
|
||
self.parent_win.redraw_tab(self, chatstate)
|
||
|
||
@event_filter(['account'])
|
||
def _on_caps_update(self, event):
|
||
if self._type.is_chat and event.jid != self.contact.jid:
|
||
return
|
||
if self._type.is_privatechat and event.fjid != self.contact.jid:
|
||
return
|
||
self.update_ui()
|
||
|
||
@event_filter(['account'])
|
||
def _on_mam_decrypted_message_received(self, event):
|
||
if event.properties.type.is_groupchat:
|
||
return
|
||
|
||
if event.properties.is_muc_pm:
|
||
if not event.properties.jid == self.contact.get_full_jid():
|
||
return
|
||
else:
|
||
if not event.properties.jid.bare_match(self.contact.jid):
|
||
return
|
||
|
||
kind = '' # incoming
|
||
if event.kind == KindConstant.CHAT_MSG_SENT:
|
||
kind = 'outgoing'
|
||
|
||
self.add_message(event.msgtxt,
|
||
kind,
|
||
tim=event.properties.mam.timestamp,
|
||
correct_id=event.correct_id,
|
||
message_id=event.properties.id,
|
||
additional_data=event.additional_data)
|
||
|
||
@event_filter(['account'])
|
||
def _on_decrypted_message_received(self, event):
|
||
if not event.msgtxt:
|
||
return True
|
||
|
||
if event.session.control != self:
|
||
return
|
||
|
||
typ = ''
|
||
if event.properties.is_sent_carbon:
|
||
typ = 'out'
|
||
|
||
self.add_message(event.msgtxt,
|
||
typ,
|
||
tim=event.properties.timestamp,
|
||
subject=event.properties.subject,
|
||
displaymarking=event.displaymarking,
|
||
msg_log_id=event.msg_log_id,
|
||
message_id=event.properties.id,
|
||
correct_id=event.correct_id,
|
||
additional_data=event.additional_data)
|
||
if event.msg_log_id:
|
||
pw = self.parent_win
|
||
end = self.conv_textview.autoscroll
|
||
if not pw or (pw.get_active_control() and self \
|
||
== pw.get_active_control() and pw.is_active() and end):
|
||
app.storage.archive.set_read_messages([event.msg_log_id])
|
||
|
||
@event_filter(['account', 'jid'])
|
||
def _on_message_error(self, event):
|
||
self.conv_textview.show_error(event.message_id, event.error)
|
||
|
||
@event_filter(['account', 'jid'])
|
||
def _on_message_sent(self, event):
|
||
if not event.message:
|
||
return
|
||
|
||
self.last_sent_msg = event.message_id
|
||
message_id = event.message_id
|
||
|
||
if event.label:
|
||
displaymarking = event.label.displaymarking
|
||
else:
|
||
displaymarking = None
|
||
if self.correcting:
|
||
self.correcting = False
|
||
gtkgui_helpers.remove_css_class(
|
||
self.msg_textview, 'gajim-msg-correcting')
|
||
|
||
self.add_message(event.message,
|
||
self.contact.jid,
|
||
tim=event.timestamp,
|
||
displaymarking=displaymarking,
|
||
message_id=message_id,
|
||
correct_id=event.correct_id,
|
||
additional_data=event.additional_data)
|
||
|
||
@event_filter(['account', 'jid'])
|
||
def _receipt_received(self, event):
|
||
self.conv_textview.show_receipt(event.receipt_id)
|
||
|
||
@event_filter(['account', 'jid'])
|
||
def _displayed_received(self, event):
|
||
self.conv_textview.show_displayed(event.marker_id)
|
||
|
||
@event_filter(['account', 'jid'])
|
||
def _on_zeroconf_error(self, event):
|
||
self.add_status_message(event.message)
|
||
|
||
@event_filter(['account', 'jid'])
|
||
def _on_update_roster_avatar(self, obj):
|
||
self._update_avatar()
|
||
|
||
@event_filter(['account'])
|
||
def _nec_ping(self, event):
|
||
if self.contact != event.contact:
|
||
return
|
||
if event.name == 'ping-sent':
|
||
self.add_info_message(_('Ping?'))
|
||
elif event.name == 'ping-reply':
|
||
self.add_info_message(
|
||
_('Pong! (%s seconds)') % event.seconds)
|
||
elif event.name == 'ping-error':
|
||
self.add_info_message(event.error)
|
||
|
||
def change_resource(self, resource):
|
||
old_full_jid = self.get_full_jid()
|
||
self.resource = resource
|
||
new_full_jid = self.get_full_jid()
|
||
# update app.last_message_time
|
||
if old_full_jid in app.last_message_time[self.account]:
|
||
app.last_message_time[self.account][new_full_jid] = \
|
||
app.last_message_time[self.account][old_full_jid]
|
||
# update events
|
||
app.events.change_jid(self.account, old_full_jid, new_full_jid)
|
||
# update MessageWindow._controls
|
||
self.parent_win.change_jid(self.account, old_full_jid, new_full_jid)
|
||
|
||
# Jingle AV
|
||
def _on_start_call(self, *args):
|
||
audio_state = self.jingle['audio'].state
|
||
video_state = self.jingle['video'].state
|
||
if audio_state == JingleState.NULL and video_state == JingleState.NULL:
|
||
self.xml.av_box.set_no_show_all(False)
|
||
self.xml.av_box.show_all()
|
||
self.xml.jingle_audio_state.hide()
|
||
self.xml.av_start_box.show()
|
||
self.xml.av_start_mic_cam_button.set_sensitive(
|
||
self.jingle['video'].available)
|
||
self.xml.av_cam_button.set_sensitive(False)
|
||
|
||
def _on_call_with_mic(self, _button):
|
||
self._on_jingle_button_toggled(['audio'])
|
||
self.xml.av_start_box.hide()
|
||
|
||
def _on_call_with_mic_and_cam(self, _button):
|
||
self._on_jingle_button_toggled(['audio', 'video'])
|
||
self.xml.av_start_box.hide()
|
||
|
||
def _on_video(self, *args):
|
||
self._on_jingle_button_toggled(['video'])
|
||
|
||
def update_audio(self):
|
||
self.update_actions()
|
||
|
||
audio_state = self.jingle['audio'].state
|
||
video_state = self.jingle['video'].state
|
||
if self.jingle['video'].available:
|
||
self.xml.av_cam_button.set_sensitive(
|
||
video_state not in (
|
||
JingleState.CONNECTING,
|
||
JingleState.CONNECTED))
|
||
|
||
if audio_state == JingleState.NULL:
|
||
self.xml.audio_buttons_box.set_sensitive(False)
|
||
self.xml.jingle_audio_state.set_no_show_all(True)
|
||
self.xml.jingle_audio_state.hide()
|
||
self.xml.jingle_connection_state.set_text('')
|
||
self.xml.jingle_connection_spinner.stop()
|
||
self.xml.jingle_connection_spinner.hide()
|
||
|
||
if video_state == JingleState.NULL:
|
||
self.xml.av_box.set_no_show_all(True)
|
||
self.xml.av_box.hide()
|
||
else:
|
||
self.xml.jingle_connection_spinner.show()
|
||
self.xml.jingle_connection_spinner.start()
|
||
|
||
if audio_state == JingleState.CONNECTING:
|
||
self.xml.av_box.set_no_show_all(False)
|
||
self.xml.av_box.show_all()
|
||
self.xml.jingle_connection_state.set_text(
|
||
_('Calling…'))
|
||
self.xml.av_cam_button.set_sensitive(False)
|
||
|
||
elif audio_state == JingleState.CONNECTION_RECEIVED:
|
||
self.xml.jingle_connection_state.set_text(
|
||
_('Incoming Call'))
|
||
|
||
elif audio_state == JingleState.CONNECTED:
|
||
self.xml.jingle_audio_state.set_no_show_all(False)
|
||
self.xml.jingle_audio_state.show()
|
||
self.xml.jingle_connection_state.set_text('')
|
||
self.xml.jingle_connection_spinner.stop()
|
||
self.xml.jingle_connection_spinner.hide()
|
||
if self.jingle['video'].available:
|
||
self.xml.av_cam_button.set_sensitive(True)
|
||
|
||
input_vol = app.settings.get('audio_input_volume')
|
||
output_vol = app.settings.get('audio_output_volume')
|
||
self.xml.mic_hscale.set_value(max(min(input_vol, 100), 0))
|
||
self.xml.sound_hscale.set_value(max(min(output_vol, 100), 0))
|
||
self.xml.audio_buttons_box.set_sensitive(True)
|
||
|
||
elif audio_state == JingleState.ERROR:
|
||
self.xml.jingle_audio_state.hide()
|
||
self.xml.jingle_connection_state.set_text(
|
||
_('Connection Error'))
|
||
self.xml.jingle_connection_spinner.stop()
|
||
self.xml.jingle_connection_spinner.hide()
|
||
|
||
if not self.jingle['audio'].sid:
|
||
self.xml.audio_buttons_box.set_sensitive(False)
|
||
|
||
def update_video(self):
|
||
self.update_actions()
|
||
|
||
audio_state = self.jingle['audio'].state
|
||
video_state = self.jingle['video'].state
|
||
|
||
if video_state == JingleState.NULL:
|
||
self.xml.video_box.set_no_show_all(True)
|
||
self.xml.video_box.hide()
|
||
self.xml.outgoing_viewport.set_no_show_all(True)
|
||
self.xml.outgoing_viewport.hide()
|
||
if self._video_widget_other:
|
||
self._video_widget_other.destroy()
|
||
if self._video_widget_self:
|
||
self._video_widget_self.destroy()
|
||
|
||
if audio_state != JingleState.CONNECTED:
|
||
self.xml.jingle_connection_state.set_text('')
|
||
self.xml.jingle_connection_spinner.stop()
|
||
self.xml.jingle_connection_spinner.hide()
|
||
self.xml.av_cam_button.set_sensitive(True)
|
||
self.xml.av_cam_button.set_tooltip_text(_('Turn Camera on'))
|
||
self.xml.av_cam_image.set_from_icon_name(
|
||
'feather-camera-symbolic', Gtk.IconSize.BUTTON)
|
||
if audio_state == JingleState.NULL:
|
||
self.xml.av_box.set_no_show_all(True)
|
||
self.xml.av_box.hide()
|
||
else:
|
||
self.xml.jingle_connection_spinner.show()
|
||
self.xml.jingle_connection_spinner.start()
|
||
|
||
if video_state == JingleState.CONNECTING:
|
||
self.xml.jingle_connection_state.set_text(_('Calling (Video)…'))
|
||
self.xml.av_box.set_no_show_all(False)
|
||
self.xml.av_box.show_all()
|
||
self.xml.av_cam_button.set_sensitive(False)
|
||
self.xml.av_cam_button.set_tooltip_text(_('Turn Camera off'))
|
||
self.xml.av_cam_image.set_from_icon_name(
|
||
'feather-camera-off-symbolic', Gtk.IconSize.BUTTON)
|
||
|
||
elif video_state == JingleState.CONNECTION_RECEIVED:
|
||
self.xml.jingle_connection_state.set_text(
|
||
_('Incoming Call (Video)'))
|
||
self.xml.av_cam_button.set_sensitive(False)
|
||
self.xml.av_cam_button.set_tooltip_text(_('Turn Camera off'))
|
||
self.xml.av_cam_image.set_from_icon_name(
|
||
'feather-camera-off-symbolic', Gtk.IconSize.BUTTON)
|
||
|
||
elif video_state == JingleState.CONNECTED:
|
||
self.xml.video_box.set_no_show_all(False)
|
||
self.xml.video_box.show_all()
|
||
if app.settings.get('video_see_self'):
|
||
self.xml.outgoing_viewport.set_no_show_all(False)
|
||
self.xml.outgoing_viewport.show()
|
||
else:
|
||
self.xml.outgoing_viewport.set_no_show_all(True)
|
||
self.xml.outgoing_viewport.hide()
|
||
|
||
sink_other, self._video_widget_other, _name = create_gtk_widget()
|
||
sink_self, self._video_widget_self, _name = create_gtk_widget()
|
||
self.xml.incoming_viewport.add(self._video_widget_other)
|
||
self.xml.outgoing_viewport.add(self._video_widget_self)
|
||
|
||
con = app.connections[self.account]
|
||
session = con.get_module('Jingle').get_jingle_session(
|
||
self.contact.get_full_jid(), self.jingle['video'].sid)
|
||
content = session.get_content('video')
|
||
content.do_setup(sink_self, sink_other)
|
||
|
||
self.xml.jingle_connection_state.set_text('')
|
||
self.xml.jingle_connection_spinner.stop()
|
||
self.xml.jingle_connection_spinner.hide()
|
||
|
||
self.xml.av_cam_button.set_sensitive(True)
|
||
self.xml.av_cam_button.set_tooltip_text(_('Turn Camera off'))
|
||
self.xml.av_cam_image.set_from_icon_name(
|
||
'feather-camera-off-symbolic', Gtk.IconSize.BUTTON)
|
||
|
||
elif video_state == JingleState.ERROR:
|
||
self.xml.jingle_connection_state.set_text(
|
||
_('Connection Error'))
|
||
self.xml.jingle_connection_spinner.stop()
|
||
self.xml.jingle_connection_spinner.hide()
|
||
|
||
def set_jingle_state(self, jingle_type: str, state: str, sid: str = None,
|
||
reason: str = None) -> None:
|
||
jingle = self.jingle[jingle_type]
|
||
if state in (
|
||
JingleState.CONNECTING,
|
||
JingleState.CONNECTED,
|
||
JingleState.NULL,
|
||
JingleState.ERROR) and reason:
|
||
log.info('%s state: %s, reason: %s', jingle_type, state, reason)
|
||
|
||
if state in (jingle.state, JingleState.ERROR):
|
||
return
|
||
|
||
if (state == JingleState.NULL and jingle.sid not in (None, sid)):
|
||
return
|
||
|
||
new_sid = None
|
||
if state == JingleState.NULL:
|
||
new_sid = None
|
||
if state in (
|
||
JingleState.CONNECTION_RECEIVED,
|
||
JingleState.CONNECTING,
|
||
JingleState.CONNECTED):
|
||
new_sid = sid
|
||
|
||
jingle.state = state
|
||
jingle.sid = new_sid
|
||
jingle.update()
|
||
|
||
def stop_jingle(self, sid=None, reason=None):
|
||
audio_sid = self.jingle['audio'].sid
|
||
video_sid = self.jingle['video'].sid
|
||
if audio_sid and sid in (audio_sid, None):
|
||
self.close_jingle_content('audio')
|
||
if video_sid and sid in (video_sid, None):
|
||
self.close_jingle_content('video')
|
||
|
||
def close_jingle_content(self, jingle_type: str,
|
||
shutdown: Optional[bool] = False) -> None:
|
||
jingle = self.jingle[jingle_type]
|
||
if not jingle.sid:
|
||
return
|
||
|
||
con = app.connections[self.account]
|
||
session = con.get_module('Jingle').get_jingle_session(
|
||
self.contact.get_full_jid(), jingle.sid)
|
||
if session:
|
||
content = session.get_content(jingle_type)
|
||
if content:
|
||
session.remove_content(content.creator, content.name)
|
||
|
||
if not shutdown:
|
||
jingle.sid = None
|
||
jingle.state = JingleState.NULL
|
||
jingle.update()
|
||
|
||
def _on_end_call_clicked(self, _widget):
|
||
self.close_jingle_content('audio')
|
||
self.close_jingle_content('video')
|
||
self.xml.jingle_audio_state.set_no_show_all(True)
|
||
self.xml.jingle_audio_state.hide()
|
||
self.xml.av_box.set_no_show_all(True)
|
||
self.xml.av_box.hide()
|
||
|
||
def _on_jingle_button_toggled(self, jingle_types):
|
||
con = app.connections[self.account]
|
||
|
||
if all(item in jingle_types for item in ['audio', 'video']):
|
||
# Both 'audio' and 'video' in jingle_types
|
||
sid = con.get_module('Jingle').start_audio_video(
|
||
self.contact.get_full_jid())
|
||
self.set_jingle_state('audio', JingleState.CONNECTING, sid)
|
||
self.set_jingle_state('video', JingleState.CONNECTING, sid)
|
||
return
|
||
|
||
if 'audio' in jingle_types:
|
||
if self.jingle['audio'].state != JingleState.NULL:
|
||
self.close_jingle_content('audio')
|
||
else:
|
||
sid = con.get_module('Jingle').start_audio(
|
||
self.contact.get_full_jid())
|
||
self.set_jingle_state('audio', JingleState.CONNECTING, sid)
|
||
|
||
if 'video' in jingle_types:
|
||
if self.jingle['video'].state != JingleState.NULL:
|
||
self.close_jingle_content('video')
|
||
else:
|
||
sid = con.get_module('Jingle').start_video(
|
||
self.contact.get_full_jid())
|
||
self.set_jingle_state('video', JingleState.CONNECTING, sid)
|
||
|
||
def _get_audio_content(self):
|
||
con = app.connections[self.account]
|
||
session = con.get_module('Jingle').get_jingle_session(
|
||
self.contact.get_full_jid(), self.jingle['audio'].sid)
|
||
return session.get_content('audio')
|
||
|
||
def on_num_button_pressed(self, _widget, num):
|
||
self._get_audio_content().start_dtmf(num)
|
||
|
||
def on_num_button_released(self, _released):
|
||
self._get_audio_content().stop_dtmf()
|
||
|
||
def on_mic_hscale_value_changed(self, _widget, value):
|
||
self._get_audio_content().set_mic_volume(value / 100)
|
||
app.settings.set('audio_input_volume', int(value))
|
||
|
||
def on_sound_hscale_value_changed(self, _widget, value):
|
||
self._get_audio_content().set_out_volume(value / 100)
|
||
app.settings.set('audio_output_volume', int(value))
|
||
|
||
def on_location_eventbox_button_release_event(self, _widget, _event):
|
||
if 'geoloc' in self.contact.pep:
|
||
location = self.contact.pep['geoloc'].data
|
||
if 'lat' in location and 'lon' in location:
|
||
uri = geo_provider_from_location(location['lat'],
|
||
location['lon'])
|
||
open_uri(uri)
|
||
|
||
def on_location_eventbox_leave_notify_event(self, _widget, _event):
|
||
"""
|
||
Just moved the mouse so show the cursor
|
||
"""
|
||
cursor = get_cursor('default')
|
||
self.parent_win.window.get_window().set_cursor(cursor)
|
||
|
||
def on_location_eventbox_enter_notify_event(self, _widget, _event):
|
||
cursor = get_cursor('pointer')
|
||
self.parent_win.window.get_window().set_cursor(cursor)
|
||
|
||
def update_ui(self):
|
||
# The name banner is drawn here
|
||
ChatControlBase.update_ui(self)
|
||
self.update_toolbar()
|
||
self._update_avatar()
|
||
self.update_actions()
|
||
|
||
def draw_banner_text(self):
|
||
"""
|
||
Draw the text in the fat line at the top of the window that houses the
|
||
name, jid
|
||
"""
|
||
contact = self.contact
|
||
name = contact.get_shown_name()
|
||
if self.resource:
|
||
name += '/' + self.resource
|
||
if self._type.is_privatechat:
|
||
name = i18n.direction_mark + _(
|
||
'%(nickname)s from group chat %(room_name)s') % \
|
||
{'nickname': name, 'room_name': self.room_name}
|
||
name = i18n.direction_mark + GLib.markup_escape_text(name)
|
||
|
||
status = contact.status
|
||
if status is not None:
|
||
status_reduced = helpers.reduce_chars_newlines(status, max_lines=1)
|
||
else:
|
||
status_reduced = ''
|
||
status_escaped = GLib.markup_escape_text(status_reduced)
|
||
|
||
if self._type.is_privatechat:
|
||
cs = self.gc_contact.chatstate
|
||
else:
|
||
cs = app.contacts.get_combined_chatstate(
|
||
self.account, self.contact.jid)
|
||
|
||
if app.settings.get('show_chatstate_in_banner'):
|
||
chatstate = helpers.get_uf_chatstate(cs)
|
||
|
||
label_text = '<span>%s</span><span size="x-small" weight="light"> %s</span>' % \
|
||
(name, chatstate)
|
||
label_tooltip = '%s %s' % (name, chatstate)
|
||
else:
|
||
label_text = '<span>%s</span>' % name
|
||
label_tooltip = name
|
||
|
||
if status_escaped:
|
||
status_text = make_href_markup(status_escaped)
|
||
status_text = '<span size="x-small" weight="light">%s</span>' % status_text
|
||
self.xml.banner_label.set_tooltip_text(status)
|
||
self.xml.banner_label.set_no_show_all(False)
|
||
self.xml.banner_label.show()
|
||
else:
|
||
status_text = ''
|
||
self.xml.banner_label.hide()
|
||
self.xml.banner_label.set_no_show_all(True)
|
||
|
||
self.xml.banner_label.set_markup(status_text)
|
||
# setup the label that holds name and jid
|
||
self.xml.banner_name_label.set_markup(label_text)
|
||
self.xml.banner_name_label.set_tooltip_text(label_tooltip)
|
||
|
||
def send_message(self,
|
||
message,
|
||
xhtml=None,
|
||
process_commands=True,
|
||
attention=False):
|
||
"""
|
||
Send a message to contact
|
||
"""
|
||
|
||
if self.encryption:
|
||
self.sendmessage = True
|
||
app.plugin_manager.extension_point('send_message' + self.encryption,
|
||
self)
|
||
if not self.sendmessage:
|
||
return
|
||
|
||
message = helpers.remove_invalid_xml_chars(message)
|
||
if message in ('', None, '\n'):
|
||
return
|
||
|
||
ChatControlBase.send_message(self,
|
||
message,
|
||
type_='chat',
|
||
xhtml=xhtml,
|
||
process_commands=process_commands,
|
||
attention=attention)
|
||
|
||
def get_our_nick(self):
|
||
return app.nicks[self.account]
|
||
|
||
def add_message(self,
|
||
text,
|
||
frm='',
|
||
tim=None,
|
||
subject=None,
|
||
displaymarking=None,
|
||
msg_log_id=None,
|
||
correct_id=None,
|
||
message_id=None,
|
||
additional_data=None,
|
||
error=None):
|
||
"""
|
||
Print a line in the conversation
|
||
|
||
If frm is set to status: it's a status message.
|
||
if frm is set to error: it's an error message. The difference between
|
||
status and error is mainly that with error, msg count as a new
|
||
message (in systray and in control).
|
||
If frm is set to info: it's a information message.
|
||
If frm is set to print_queue: it is incoming from queue.
|
||
If frm is set to another value: it's an outgoing message.
|
||
If frm is not set: it's an incoming message.
|
||
"""
|
||
contact = self.contact
|
||
|
||
if additional_data is None:
|
||
additional_data = AdditionalDataDict()
|
||
|
||
if frm == 'error':
|
||
kind = 'error'
|
||
name = ''
|
||
else:
|
||
if not frm:
|
||
kind = 'incoming'
|
||
name = contact.get_shown_name()
|
||
elif frm == 'print_queue':
|
||
kind = 'incoming_queue'
|
||
name = contact.get_shown_name()
|
||
else:
|
||
kind = 'outgoing'
|
||
name = self.get_our_nick()
|
||
|
||
ChatControlBase.add_message(self,
|
||
text,
|
||
kind,
|
||
name,
|
||
tim,
|
||
subject=subject,
|
||
old_kind=self.old_msg_kind,
|
||
displaymarking=displaymarking,
|
||
msg_log_id=msg_log_id,
|
||
message_id=message_id,
|
||
correct_id=correct_id,
|
||
additional_data=additional_data,
|
||
error=error)
|
||
if text.startswith('/me ') or text.startswith('/me\n'):
|
||
self.old_msg_kind = None
|
||
else:
|
||
self.old_msg_kind = kind
|
||
|
||
def get_tab_label(self):
|
||
unread = ''
|
||
if self.resource:
|
||
jid = self.contact.get_full_jid()
|
||
else:
|
||
jid = self.contact.jid
|
||
num_unread = len(app.events.get_events(
|
||
self.account, jid, ['printed_%s' % self._type, str(self._type)]))
|
||
if num_unread == 1:
|
||
unread = '*'
|
||
elif num_unread > 1:
|
||
unread = '[' + str(num_unread) + ']'
|
||
|
||
name = self.contact.get_shown_name()
|
||
if self.resource:
|
||
name += '/' + self.resource
|
||
label_str = GLib.markup_escape_text(name)
|
||
if num_unread: # if unread, text in the label becomes bold
|
||
label_str = '<b>' + unread + label_str + '</b>'
|
||
return label_str
|
||
|
||
def get_tab_image(self):
|
||
scale = self.parent_win.window.get_scale_factor()
|
||
return app.contacts.get_avatar(self.account,
|
||
self.contact.jid,
|
||
AvatarSize.ROSTER,
|
||
scale,
|
||
self.contact.show)
|
||
|
||
def prepare_context_menu(self, hide_buttonbar_items=False):
|
||
"""
|
||
Set compact view menuitem active state sets active and sensitivity state
|
||
for history_menuitem (False for tranasports) and file_transfer_menuitem
|
||
and hide()/show() for add_to_roster_menuitem
|
||
"""
|
||
if app.jid_is_transport(self.contact.jid):
|
||
menu = gui_menu_builder.get_transport_menu(self.contact,
|
||
self.account)
|
||
else:
|
||
menu = gui_menu_builder.get_contact_menu(
|
||
self.contact,
|
||
self.account,
|
||
use_multiple_contacts=False,
|
||
show_start_chat=False,
|
||
show_encryption=True,
|
||
control=self,
|
||
show_buttonbar_items=not hide_buttonbar_items)
|
||
return menu
|
||
|
||
def shutdown(self):
|
||
# PluginSystem: removing GUI extension points connected with ChatControl
|
||
# instance object
|
||
app.plugin_manager.remove_gui_extension_point('chat_control', self)
|
||
|
||
self.unsubscribe_events()
|
||
|
||
self.remove_actions()
|
||
|
||
# Send 'gone' chatstate
|
||
con = app.connections[self.account]
|
||
con.get_module('Chatstate').set_chatstate(self.contact, Chatstate.GONE)
|
||
|
||
for jingle_type in ('audio', 'video'):
|
||
self.close_jingle_content(jingle_type, shutdown=True)
|
||
self.jingle.clear()
|
||
|
||
# disconnect self from session
|
||
if self.session:
|
||
self.session.control = None
|
||
|
||
# Clean events
|
||
app.events.remove_events(
|
||
self.account,
|
||
self.get_full_jid(),
|
||
types=['printed_%s' % self._type, str(self._type)])
|
||
# Remove contact instance if contact has been removed
|
||
key = (self.contact.jid, self.account)
|
||
roster = app.interface.roster
|
||
has_pending = roster.contact_has_pending_roster_events(self.contact,
|
||
self.account)
|
||
if key in roster.contacts_to_be_removed.keys() and not has_pending:
|
||
backend = roster.contacts_to_be_removed[key]['backend']
|
||
del roster.contacts_to_be_removed[key]
|
||
roster.remove_contact(self.contact.jid,
|
||
self.account,
|
||
force=True,
|
||
backend=backend)
|
||
|
||
super(ChatControl, self).shutdown()
|
||
|
||
def minimizable(self):
|
||
return False
|
||
|
||
def safe_shutdown(self):
|
||
return False
|
||
|
||
def allow_shutdown(self, method, on_yes, on_no, _on_minimize):
|
||
time_ = app.last_message_time[self.account][self.get_full_jid()]
|
||
# 2 seconds
|
||
if time.time() - time_ < 2:
|
||
no_log_for = app.settings.get_account_setting(
|
||
self.account, 'no_log_for').split()
|
||
more = ''
|
||
if self.contact.jid in no_log_for:
|
||
more = _('Note: Chat history is disabled for this contact.')
|
||
if self.account in no_log_for:
|
||
more = _('Note: Chat history is disabled for this account.')
|
||
text = _('You just received a new message from %s.\n'
|
||
'Do you want to close this tab?') % self.contact.get_shown_name()
|
||
if more:
|
||
text += '\n' + more
|
||
|
||
ConfirmationDialog(
|
||
_('Close'),
|
||
_('New Message'),
|
||
text,
|
||
[DialogButton.make('Cancel',
|
||
callback=lambda: on_no(self)),
|
||
DialogButton.make('Remove',
|
||
text=_('_Close'),
|
||
callback=lambda: on_yes(self))],
|
||
transient_for=self.parent_win.window).show()
|
||
return
|
||
on_yes(self)
|
||
|
||
def _update_avatar(self):
|
||
scale = self.parent_win.window.get_scale_factor()
|
||
surface = app.contacts.get_avatar(self.account,
|
||
self.contact.jid,
|
||
AvatarSize.CHAT,
|
||
scale,
|
||
self.contact.show)
|
||
|
||
self.xml.avatar_image.set_from_surface(surface)
|
||
|
||
def _on_drag_data_received(self, widget, context, x, y, selection,
|
||
target_type, timestamp):
|
||
if not selection.get_data():
|
||
return
|
||
|
||
if target_type == self.TARGET_TYPE_URI_LIST:
|
||
# File drag and drop (handled in chat_control_base)
|
||
self.drag_data_file_transfer(selection)
|
||
else:
|
||
# Convert single chat to MUC
|
||
treeview = app.interface.roster.tree
|
||
model = treeview.get_model()
|
||
data = selection.get_data().decode()
|
||
tree_selection = treeview.get_selection()
|
||
if tree_selection.count_selected_rows() == 0:
|
||
return
|
||
path = tree_selection.get_selected_rows()[1][0]
|
||
iter_ = model.get_iter(path)
|
||
type_ = model[iter_][2]
|
||
if type_ != 'contact': # Source is not a contact
|
||
return
|
||
dropped_jid = data
|
||
|
||
dropped_transport = app.get_transport_name_from_jid(dropped_jid)
|
||
c_transport = app.get_transport_name_from_jid(self.contact.jid)
|
||
if dropped_transport or c_transport:
|
||
return # transport contacts cannot be invited
|
||
|
||
dialogs.TransformChatToMUC(self.account,
|
||
[self.contact.jid],
|
||
[dropped_jid])
|
||
|
||
def restore_conversation(self):
|
||
jid = self.contact.jid
|
||
# don't restore lines if it's a transport
|
||
if app.jid_is_transport(jid):
|
||
return
|
||
|
||
# number of messages that are in queue and are already logged, we want
|
||
# to avoid duplication
|
||
pending = len(app.events.get_events(self.account, jid, ['chat', 'pm']))
|
||
if self.resource:
|
||
pending += len(app.events.get_events(self.account,
|
||
self.contact.get_full_jid(),
|
||
['chat', 'pm']))
|
||
|
||
rows = app.storage.archive.get_last_conversation_lines(
|
||
self.account, jid, pending)
|
||
|
||
local_old_kind = None
|
||
self.conv_textview.just_cleared = True
|
||
for row in rows: # time, kind, message, subject, additional_data
|
||
msg = row.message
|
||
additional_data = row.additional_data
|
||
if not msg: # message is empty, we don't print it
|
||
continue
|
||
if row.kind in (KindConstant.CHAT_MSG_SENT,
|
||
KindConstant.SINGLE_MSG_SENT):
|
||
kind = 'outgoing'
|
||
name = self.get_our_nick()
|
||
elif row.kind in (KindConstant.SINGLE_MSG_RECV,
|
||
KindConstant.CHAT_MSG_RECV):
|
||
kind = 'incoming'
|
||
name = self.contact.get_shown_name()
|
||
elif row.kind == KindConstant.ERROR:
|
||
kind = 'status'
|
||
name = self.contact.get_shown_name()
|
||
|
||
tim = float(row.time)
|
||
|
||
if row.subject:
|
||
msg = _('Subject: %(subject)s\n%(message)s') % \
|
||
{'subject': row.subject, 'message': msg}
|
||
ChatControlBase.add_message(self,
|
||
msg,
|
||
kind,
|
||
name,
|
||
tim,
|
||
restored=True,
|
||
old_kind=local_old_kind,
|
||
additional_data=additional_data,
|
||
message_id=row.message_id,
|
||
marker=row.marker,
|
||
error=row.error)
|
||
if (row.message.startswith('/me ') or
|
||
row.message.startswith('/me\n')):
|
||
local_old_kind = None
|
||
else:
|
||
local_old_kind = kind
|
||
if rows:
|
||
self.conv_textview.print_empty_line()
|
||
|
||
def read_queue(self):
|
||
"""
|
||
Read queue and print messages contained in it
|
||
"""
|
||
jid = self.contact.jid
|
||
jid_with_resource = jid
|
||
if self.resource:
|
||
jid_with_resource += '/' + self.resource
|
||
events = app.events.get_events(self.account, jid_with_resource)
|
||
|
||
# list of message ids which should be marked as read
|
||
message_ids = []
|
||
for event in events:
|
||
if event.type_ != str(self._type):
|
||
continue
|
||
kind = 'print_queue'
|
||
if event.sent_forwarded:
|
||
kind = 'out'
|
||
self.add_message(event.message,
|
||
kind,
|
||
tim=event.time,
|
||
subject=event.subject,
|
||
displaymarking=event.displaymarking,
|
||
correct_id=event.correct_id,
|
||
message_id=event.message_id,
|
||
additional_data=event.additional_data)
|
||
if isinstance(event.msg_log_id, int):
|
||
message_ids.append(event.msg_log_id)
|
||
|
||
if event.session and not self.session:
|
||
self.set_session(event.session)
|
||
if message_ids:
|
||
app.storage.archive.set_read_messages(message_ids)
|
||
|
||
# XEP-0333 Send <displayed> marker
|
||
con = app.connections[self.account]
|
||
con.get_module('ChatMarkers').send_displayed_marker(
|
||
self.contact,
|
||
self.last_msg_id,
|
||
self._type)
|
||
self.last_msg_id = None
|
||
app.events.remove_events(self.account,
|
||
jid_with_resource,
|
||
types=[str(self._type)])
|
||
|
||
typ = 'chat' # Is it a normal chat or a pm ?
|
||
|
||
# reset to status image in gc if it is a pm
|
||
# Is it a pm ?
|
||
room_jid, nick = app.get_room_and_nick_from_fjid(jid)
|
||
control = app.interface.msg_win_mgr.get_gc_control(room_jid,
|
||
self.account)
|
||
if control and control.is_groupchat:
|
||
control.update_ui()
|
||
control.parent_win.show_title()
|
||
typ = 'pm'
|
||
|
||
self.redraw_after_event_removed(jid)
|
||
if self.contact.show in ('offline', 'error'):
|
||
show_offline = app.settings.get('showoffline')
|
||
show_transports = app.settings.get('show_transports_group')
|
||
if (not show_transports and app.jid_is_transport(jid)) or \
|
||
(not show_offline and typ == 'chat' and \
|
||
len(app.contacts.get_contacts(self.account, jid)) < 2):
|
||
app.interface.roster.remove_to_be_removed(self.contact.jid,
|
||
self.account)
|
||
elif typ == 'pm':
|
||
control.remove_contact(nick)
|
||
|
||
def _on_convert_to_gc_menuitem_activate(self, _widget):
|
||
"""
|
||
User wants to invite some friends to chat
|
||
"""
|
||
dialogs.TransformChatToMUC(self.account, [self.contact.jid])
|
||
|
||
def got_connected(self):
|
||
ChatControlBase.got_connected(self)
|
||
# Refreshing contact
|
||
contact = app.contacts.get_contact_with_highest_priority(
|
||
self.account, self.contact.jid)
|
||
if isinstance(contact, GC_Contact):
|
||
contact = contact.as_contact()
|
||
if contact:
|
||
self.contact = contact
|
||
self.draw_banner()
|
||
self.update_actions()
|
||
|
||
def got_disconnected(self):
|
||
ChatControlBase.got_disconnected(self)
|
||
self.update_actions()
|
||
|
||
def update_status_display(self, name, uf_show, status):
|
||
self.update_ui()
|
||
self.parent_win.redraw_tab(self)
|
||
|
||
if not app.settings.get('print_status_in_chats'):
|
||
return
|
||
|
||
if status:
|
||
status = '- %s' % status
|
||
status_line = _('%(name)s is now %(show)s %(status)s') % {
|
||
'name': name, 'show': uf_show, 'status': status or ''}
|
||
self.add_status_message(status_line)
|
||
|
||
def _info_bar_show_message(self):
|
||
if self.info_bar.get_visible():
|
||
# A message is already shown
|
||
return
|
||
if not self.info_bar_queue:
|
||
return
|
||
markup, buttons, _args, type_ = self.info_bar_queue[0]
|
||
self.info_bar_label.set_markup(markup)
|
||
|
||
# Remove old buttons
|
||
area = self.info_bar.get_action_area()
|
||
for button in area.get_children():
|
||
area.remove(button)
|
||
|
||
# Add new buttons
|
||
for button in buttons:
|
||
self.info_bar.add_action_widget(button, 0)
|
||
|
||
self.info_bar.set_message_type(type_)
|
||
self.info_bar.set_no_show_all(False)
|
||
self.info_bar.show_all()
|
||
|
||
def _add_info_bar_message(self, markup, buttons, args,
|
||
type_=Gtk.MessageType.INFO):
|
||
self.info_bar_queue.append((markup, buttons, args, type_))
|
||
self._info_bar_show_message()
|
||
|
||
def _get_file_props_event(self, file_props, type_):
|
||
evs = app.events.get_events(self.account, self.contact.jid, [type_])
|
||
for ev in evs:
|
||
if ev.file_props == file_props:
|
||
return ev
|
||
return None
|
||
|
||
def _on_accept_file_request(self, _widget, file_props):
|
||
app.interface.instances['file_transfers'].on_file_request_accepted(
|
||
self.account, self.contact, file_props)
|
||
ev = self._get_file_props_event(file_props, 'file-request')
|
||
if ev:
|
||
app.events.remove_events(self.account, self.contact.jid, event=ev)
|
||
|
||
def _on_cancel_file_request(self, _widget, file_props):
|
||
con = app.connections[self.account]
|
||
con.get_module('Bytestream').send_file_rejection(file_props)
|
||
ev = self._get_file_props_event(file_props, 'file-request')
|
||
if ev:
|
||
app.events.remove_events(self.account, self.contact.jid, event=ev)
|
||
|
||
def _got_file_request(self, file_props):
|
||
"""
|
||
Show an InfoBar on top of control
|
||
"""
|
||
if app.settings.get('use_kib_mib'):
|
||
units = GLib.FormatSizeFlags.IEC_UNITS
|
||
else:
|
||
units = GLib.FormatSizeFlags.DEFAULT
|
||
|
||
markup = '<b>%s</b>\n%s' % (_('File Transfer'), file_props.name)
|
||
if file_props.desc:
|
||
markup += '\n(%s)' % file_props.desc
|
||
markup += '\n%s: %s' % (
|
||
_('Size'),
|
||
GLib.format_size_full(file_props.size, units))
|
||
button_decline = Gtk.Button.new_with_mnemonic(_('_Decline'))
|
||
button_decline.connect(
|
||
'clicked', self._on_cancel_file_request, file_props)
|
||
button_accept = Gtk.Button.new_with_mnemonic(_('_Accept'))
|
||
button_accept.connect(
|
||
'clicked', self._on_accept_file_request, file_props)
|
||
self._add_info_bar_message(
|
||
markup,
|
||
[button_decline, button_accept],
|
||
file_props,
|
||
Gtk.MessageType.QUESTION)
|
||
|
||
def _on_open_ft_folder(self, _widget, file_props):
|
||
path = os.path.split(file_props.file_name)[0]
|
||
if os.path.exists(path) and os.path.isdir(path):
|
||
open_file(path)
|
||
ev = self._get_file_props_event(file_props, 'file-completed')
|
||
if ev:
|
||
app.events.remove_events(self.account, self.contact.jid, event=ev)
|
||
|
||
def _on_ok(self, _widget, file_props, type_):
|
||
ev = self._get_file_props_event(file_props, type_)
|
||
if ev:
|
||
app.events.remove_events(self.account, self.contact.jid, event=ev)
|
||
|
||
def _got_file_completed(self, file_props):
|
||
markup = '<b>%s</b>\n%s' % (_('File Transfer Completed'),
|
||
file_props.name)
|
||
if file_props.desc:
|
||
markup += '\n(%s)' % file_props.desc
|
||
b1 = Gtk.Button.new_with_mnemonic(_('Open _Folder'))
|
||
b1.connect('clicked', self._on_open_ft_folder, file_props)
|
||
b2 = Gtk.Button.new_with_mnemonic(_('_Close'))
|
||
b2.connect('clicked', self._on_ok, file_props, 'file-completed')
|
||
self._add_info_bar_message(
|
||
markup,
|
||
[b1, b2],
|
||
file_props)
|
||
|
||
def _got_file_error(self, file_props, type_, pri_txt, sec_txt):
|
||
markup = '<b>%s</b>\n%s' % (pri_txt, sec_txt)
|
||
button = Gtk.Button.new_with_mnemonic(_('_Close'))
|
||
button.connect('clicked', self._on_ok, file_props, type_)
|
||
self._add_info_bar_message(
|
||
markup,
|
||
[button],
|
||
file_props,
|
||
Gtk.MessageType.ERROR)
|
||
|
||
def _on_accept_gc_invitation(self, _widget, event):
|
||
app.interface.show_or_join_groupchat(self.account,
|
||
str(event.muc),
|
||
password=event.password)
|
||
app.events.remove_events(self.account, self.contact.jid, event=event)
|
||
|
||
def _on_cancel_gc_invitation(self, _widget, event):
|
||
app.events.remove_events(self.account, self.contact.jid, event=event)
|
||
|
||
def _get_gc_invitation(self, event):
|
||
markup = '<b>%s</b>\n%s' % (_('Group Chat Invitation'), event.muc)
|
||
if event.reason:
|
||
markup += '\n(%s)' % event.reason
|
||
button_decline = Gtk.Button.new_with_mnemonic(_('_Decline'))
|
||
button_decline.connect('clicked', self._on_cancel_gc_invitation, event)
|
||
button_accept = Gtk.Button.new_with_mnemonic(_('_Accept'))
|
||
button_accept.connect('clicked', self._on_accept_gc_invitation, event)
|
||
self._add_info_bar_message(
|
||
markup,
|
||
[button_decline, button_accept],
|
||
(event.muc, event.reason),
|
||
Gtk.MessageType.QUESTION)
|
||
|
||
def _on_reject_call(self, _button, event):
|
||
app.events.remove_events(
|
||
self.account, self.contact.jid, types='jingle-incoming')
|
||
|
||
con = app.connections[self.account]
|
||
session = con.get_module('Jingle').get_jingle_session(
|
||
event.peerjid, event.sid)
|
||
if not session:
|
||
return
|
||
|
||
if not session.accepted:
|
||
session.decline_session()
|
||
else:
|
||
for content in event.content_types:
|
||
session.reject_content(content)
|
||
|
||
def _on_accept_call(self, _button, event):
|
||
app.events.remove_events(
|
||
self.account, self.contact.jid, types='jingle-incoming')
|
||
|
||
con = app.connections[self.account]
|
||
session = con.get_module('Jingle').get_jingle_session(
|
||
event.peerjid, event.sid)
|
||
if not session:
|
||
return
|
||
|
||
audio = session.get_content('audio')
|
||
video = session.get_content('video')
|
||
|
||
if audio and not audio.negotiated:
|
||
self.set_jingle_state('audio', JingleState.CONNECTING, event.sid)
|
||
if video and not video.negotiated:
|
||
self.set_jingle_state('video', JingleState.CONNECTING, event.sid)
|
||
|
||
if not session.accepted:
|
||
session.approve_session()
|
||
|
||
for content in event.content_types:
|
||
session.approve_content(content)
|
||
|
||
def add_call_received_message(self, event):
|
||
markup = '<b>%s</b>' % (_('Incoming Call'))
|
||
if 'video' in event.content_types:
|
||
markup += _('\nVideo Call')
|
||
else:
|
||
markup += _('\nVoice Call')
|
||
|
||
button_reject = Gtk.Button.new_with_mnemonic(_('_Reject'))
|
||
button_reject.connect('clicked', self._on_reject_call, event)
|
||
button_accept = Gtk.Button.new_with_mnemonic(_('_Accept'))
|
||
button_accept.connect('clicked', self._on_accept_call, event)
|
||
self._add_info_bar_message(
|
||
markup,
|
||
[button_reject, button_accept],
|
||
event,
|
||
Gtk.MessageType.QUESTION)
|
||
|
||
def on_event_added(self, event):
|
||
if event.account != self.account:
|
||
return
|
||
if event.jid != self.contact.jid:
|
||
return
|
||
if event.type_ == 'file-request':
|
||
self._got_file_request(event.file_props)
|
||
elif event.type_ == 'file-completed':
|
||
self._got_file_completed(event.file_props)
|
||
elif event.type_ in ('file-error', 'file-stopped'):
|
||
msg_err = ''
|
||
if event.file_props.error == -1:
|
||
msg_err = _('Remote contact stopped transfer')
|
||
elif event.file_props.error == -6:
|
||
msg_err = _('Error opening file')
|
||
self._got_file_error(event.file_props, event.type_,
|
||
_('File transfer stopped'), msg_err)
|
||
elif event.type_ in ('file-request-error', 'file-send-error'):
|
||
self._got_file_error(
|
||
event.file_props,
|
||
event.type_,
|
||
_('File transfer cancelled'),
|
||
_('Connection with peer cannot be established.'))
|
||
elif event.type_ == 'gc-invitation':
|
||
self._get_gc_invitation(event)
|
||
|
||
def on_event_removed(self, event_list):
|
||
"""
|
||
Called when one or more events are removed from the event list
|
||
"""
|
||
for ev in event_list:
|
||
if ev.account != self.account:
|
||
continue
|
||
if ev.jid != self.contact.jid:
|
||
continue
|
||
if ev.type_ not in ('file-request',
|
||
'file-completed',
|
||
'file-error',
|
||
'file-stopped',
|
||
'file-request-error',
|
||
'file-send-error',
|
||
'gc-invitation',
|
||
'jingle-incoming'):
|
||
continue
|
||
i = 0
|
||
removed = False
|
||
for ib_msg in self.info_bar_queue:
|
||
if ev.type_ == 'gc-invitation':
|
||
if ev.muc == ib_msg[2][0]:
|
||
self.info_bar_queue.remove(ib_msg)
|
||
removed = True
|
||
elif ev.type_ == 'jingle-incoming':
|
||
# TODO: Need to be more specific here?
|
||
self.info_bar_queue.remove(ib_msg)
|
||
removed = True
|
||
else: # file-*
|
||
if ib_msg[2] == ev.file_props:
|
||
self.info_bar_queue.remove(ib_msg)
|
||
removed = True
|
||
if removed:
|
||
if i == 0:
|
||
# We are removing the one currently displayed
|
||
self.info_bar.set_no_show_all(True)
|
||
self.info_bar.hide()
|
||
# show next one?
|
||
GLib.idle_add(self._info_bar_show_message)
|
||
break
|
||
i += 1
|