gajim3/gajim/chat_control.py

1747 lines
67 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
# Dont 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