Refactor Chat State Notifications

- Move code into chatstate module
- Refactor most of the code, make it much simpler
This commit is contained in:
Philipp Hörist 2018-09-29 21:48:21 +02:00
parent c88932fc14
commit 50c670e61b
16 changed files with 359 additions and 355 deletions

View File

@ -36,7 +36,6 @@ from gi.repository import GLib
from nbxmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC from nbxmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC
from nbxmpp.protocol import NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO from nbxmpp.protocol import NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO
from nbxmpp.protocol import NS_JINGLE_ICE_UDP, NS_JINGLE_FILE_TRANSFER_5 from nbxmpp.protocol import NS_JINGLE_ICE_UDP, NS_JINGLE_FILE_TRANSFER_5
from nbxmpp.protocol import NS_CHATSTATES
from gajim import gtkgui_helpers from gajim import gtkgui_helpers
from gajim import gui_menu_builder from gajim import gui_menu_builder
@ -50,8 +49,9 @@ from gajim.common import helpers
from gajim.common import ged from gajim.common import ged
from gajim.common import i18n from gajim.common import i18n
from gajim.common.contacts import GC_Contact from gajim.common.contacts import GC_Contact
from gajim.common.connection_handlers_events import MessageOutgoingEvent from gajim.common.const import AvatarSize
from gajim.common.const import AvatarSize, KindConstant from gajim.common.const import KindConstant
from gajim.common.const import Chatstate
from gajim.command_system.implementation.hosts import ChatCommands from gajim.command_system.implementation.hosts import ChatCommands
from gajim.command_system.framework import CommandHost # pylint: disable=unused-import from gajim.command_system.framework import CommandHost # pylint: disable=unused-import
@ -678,7 +678,7 @@ class ChatControl(ChatControlBase):
status_escaped = GLib.markup_escape_text(status_reduced) status_escaped = GLib.markup_escape_text(status_reduced)
st = app.config.get('displayed_chat_state_notifications') st = app.config.get('displayed_chat_state_notifications')
cs = contact.chatstate cs = app.contacts.get_combined_chatstate(self.account, self.contact.jid)
if cs and st in ('composing_only', 'all'): if cs and st in ('composing_only', 'all'):
if contact.show == 'offline': if contact.show == 'offline':
chatstate = '' chatstate = ''
@ -882,8 +882,8 @@ class ChatControl(ChatControlBase):
correct_id=obj.correct_id, correct_id=obj.correct_id,
additional_data=obj.additional_data) additional_data=obj.additional_data)
def send_message(self, message, keyID='', chatstate=None, xhtml=None, def send_message(self, message, keyID='', xhtml=None,
process_commands=True, attention=False): process_commands=True, attention=False):
""" """
Send a message to contact Send a message to contact
""" """
@ -902,18 +902,13 @@ class ChatControl(ChatControlBase):
contact = self.contact contact = self.contact
keyID = contact.keyID keyID = contact.keyID
chatstate_to_send = None ChatControlBase.send_message(self,
if contact is not None: message,
if contact.supports(NS_CHATSTATES): keyID,
# send active chatstate on every message (as XEP says) type_='chat',
chatstate_to_send = 'active' xhtml=xhtml,
contact.our_chatstate = 'active' process_commands=process_commands,
attention=attention)
self._schedule_activity_timers()
ChatControlBase.send_message(self, message, keyID, type_='chat',
chatstate=chatstate_to_send, xhtml=xhtml,
process_commands=process_commands, attention=attention)
def get_our_nick(self): def get_our_nick(self):
return app.nicks[self.account] return app.nicks[self.account]
@ -1059,79 +1054,6 @@ class ChatControl(ChatControlBase):
show_buttonbar_items=not hide_buttonbar_items) show_buttonbar_items=not hide_buttonbar_items)
return menu return menu
def send_chatstate(self, state, contact=None):
"""
Send OUR chatstate as STANDLONE chat state message (eg. no body)
to contact only if new chatstate is different from the previous one
if jid is not specified, send to active tab
"""
# JEP 85 does not allow resending the same chatstate
# this function checks for that and just returns so it's safe to call it
# with same state.
# This functions also checks for violation in state transitions
# and raises RuntimeException with appropriate message
# more on that http://xmpp.org/extensions/xep-0085.html#statechart
# do not send if we have chat state notifications disabled
# that means we won't reply to the <active/> from other peer
# so we do not broadcast jep85 capabalities
chatstate_setting = app.config.get('outgoing_chat_state_notifications')
if chatstate_setting == 'disabled':
return
# Dont leak presence to contacts
# which are not allowed to see our status
if contact and contact.sub in ('to', 'none'):
return
if self.contact.jid == app.get_jid_from_account(self.account):
return
if chatstate_setting == 'composing_only' and state != 'active' and\
state != 'composing':
return
if contact is None:
contact = self.parent_win.get_active_contact()
if contact is None:
# contact was from pm in MUC, and left the room so contact is None
# so we cannot send chatstate anymore
return
# Don't send chatstates to offline contacts
if contact.show == 'offline':
return
if not contact.supports(NS_CHATSTATES):
return
if contact.our_chatstate is False:
return
# if the new state we wanna send (state) equals
# the current state (contact.our_chatstate) then return
if contact.our_chatstate == state:
return
# if wel're inactive prevent composing (XEP violation)
if contact.our_chatstate == 'inactive' and state == 'composing':
# go active before
app.log('chatstates').info('%-10s - %s', 'active', self.contact.jid)
app.nec.push_outgoing_event(MessageOutgoingEvent(None,
account=self.account, jid=self.contact.jid, chatstate='active',
control=self))
contact.our_chatstate = 'active'
self.reset_kbd_mouse_timeout_vars()
app.log('chatstates').info('%-10s - %s', state, self.contact.jid)
app.nec.push_outgoing_event(MessageOutgoingEvent(None,
account=self.account, jid=self.contact.jid, chatstate=state,
control=self))
contact.our_chatstate = state
if state == 'active':
self.reset_kbd_mouse_timeout_vars()
def shutdown(self): def shutdown(self):
# PluginSystem: removing GUI extension points connected with ChatControl # PluginSystem: removing GUI extension points connected with ChatControl
# instance object # instance object
@ -1161,9 +1083,8 @@ class ChatControl(ChatControlBase):
self.unsubscribe_events() self.unsubscribe_events()
# Send 'gone' chatstate # Send 'gone' chatstate
self.send_chatstate('gone', self.contact) con = app.connections[self.account]
self.contact.chatstate = None con.get_module('Chatstate').set_chatstate(self.contact, Chatstate.GONE)
self.contact.our_chatstate = None
for jingle_type in ('audio', 'video'): for jingle_type in ('audio', 'video'):
self.close_jingle_content(jingle_type) self.close_jingle_content(jingle_type)
@ -1225,13 +1146,18 @@ class ChatControl(ChatControlBase):
return return
on_yes(self) on_yes(self)
def _nec_chatstate_received(self, obj): def _nec_chatstate_received(self, event):
""" if event.account != self.account:
Handle incoming chatstate that jid SENT TO us return
"""
if event.jid != self.contact.jid:
return
self.draw_banner_text() self.draw_banner_text()
# update chatstate in tab for this chat # update chatstate in tab for this chat
self.parent_win.redraw_tab(self, self.contact.chatstate) chatstate = app.contacts.get_combined_chatstate(
self.account, self.contact.jid)
self.parent_win.redraw_tab(self, chatstate)
def _nec_caps_received(self, obj): def _nec_caps_received(self, obj):
if obj.conn.name != self.account: if obj.conn.name != self.account:

View File

@ -49,6 +49,7 @@ from gajim.message_textview import MessageTextView
from gajim.common.contacts import GC_Contact from gajim.common.contacts import GC_Contact
from gajim.common.connection_handlers_events import MessageOutgoingEvent from gajim.common.connection_handlers_events import MessageOutgoingEvent
from gajim.common.const import StyleAttr from gajim.common.const import StyleAttr
from gajim.common.const import Chatstate
from gajim.command_system.implementation.middleware import ChatCommandProcessor from gajim.command_system.implementation.middleware import ChatCommandProcessor
from gajim.command_system.implementation.middleware import CommandTools from gajim.command_system.implementation.middleware import CommandTools
@ -325,10 +326,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
# Security Labels # Security Labels
self.seclabel_combo = self.xml.get_object('label_selector') self.seclabel_combo = self.xml.get_object('label_selector')
# chatstate timers and state con = app.connections[self.account]
self.reset_kbd_mouse_timeout_vars() con.get_module('Chatstate').set_active(self.contact.jid)
self.possible_paused_timeout_id = None
self.possible_inactive_timeout_id = None
message_tv_buffer = self.msg_textview.get_buffer() message_tv_buffer = self.msg_textview.get_buffer()
id_ = message_tv_buffer.connect('changed', id_ = message_tv_buffer.connect('changed',
self._on_message_tv_buffer_changed) self._on_message_tv_buffer_changed)
@ -337,7 +337,6 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
id_ = parent_win.window.connect('motion-notify-event', id_ = parent_win.window.connect('motion-notify-event',
self._on_window_motion_notify) self._on_window_motion_notify)
self.handlers[id_] = parent_win.window self.handlers[id_] = parent_win.window
self._schedule_activity_timers()
self.encryption = self.get_encryption_state() self.encryption = self.get_encryption_state()
self.conv_textview.encryption_enabled = self.encryption is not None self.conv_textview.encryption_enabled = self.encryption is not None
@ -520,11 +519,6 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
def shutdown(self): def shutdown(self):
super(ChatControlBase, self).shutdown() super(ChatControlBase, self).shutdown()
# Disconnect timer callbacks
if self.possible_paused_timeout_id:
GLib.source_remove(self.possible_paused_timeout_id)
if self.possible_inactive_timeout_id:
GLib.source_remove(self.possible_inactive_timeout_id)
# PluginSystem: removing GUI extension points connected with ChatControlBase # PluginSystem: removing GUI extension points connected with ChatControlBase
# instance object # instance object
app.plugin_manager.remove_gui_extension_point('chat_control_base', app.plugin_manager.remove_gui_extension_point('chat_control_base',
@ -777,7 +771,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
label = labels[lname] label = labels[lname]
return label return label
def send_message(self, message, keyID='', type_='chat', chatstate=None, def send_message(self, message, keyID='', type_='chat',
resource=None, xhtml=None, process_commands=True, attention=False): resource=None, xhtml=None, process_commands=True, attention=False):
""" """
Send the given message to the active tab. Doesn't return None if error Send the given message to the active tab. Doesn't return None if error
@ -788,14 +782,6 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
if process_commands and self.process_as_command(message): if process_commands and self.process_as_command(message):
return return
# refresh timers
self.reset_kbd_mouse_timeout_vars()
notifications = app.config.get('outgoing_chat_state_notifications')
if (self.contact.jid == app.get_jid_from_account(self.account) or
notifications == 'disabled'):
chatstate = None
label = self.get_seclabel() label = self.get_seclabel()
if self.correcting and self.last_sent_msg: if self.correcting and self.last_sent_msg:
@ -803,6 +789,10 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
else: else:
correct_id = None correct_id = None
con = app.connections[self.account]
chatstate = con.get_module('Chatstate').get_active_chatstate(
self.contact)
app.nec.push_outgoing_event(MessageOutgoingEvent(None, app.nec.push_outgoing_event(MessageOutgoingEvent(None,
account=self.account, jid=self.contact.jid, message=message, account=self.account, jid=self.contact.jid, message=message,
keyID=keyID, type_=type_, chatstate=chatstate, keyID=keyID, type_=type_, chatstate=chatstate,
@ -820,76 +810,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
message_buffer = self.msg_textview.get_buffer() message_buffer = self.msg_textview.get_buffer()
message_buffer.set_text('') # clear message buffer (and tv of course) message_buffer.set_text('') # clear message buffer (and tv of course)
def check_for_possible_paused_chatstate(self, arg): def _on_window_motion_notify(self, *args):
"""
Did we move mouse of that window or write something in message textview
in the last 5 seconds? If yes - we go active for mouse, composing for
kbd. If not - we go paused if we were previously composing
"""
contact = self.contact
jid = contact.jid
current_state = contact.our_chatstate
if current_state is False: # jid doesn't support chatstates
self.possible_paused_timeout_id = None
return False # stop looping
if current_state == 'composing':
if not self.kbd_activity_in_last_5_secs:
if self.msg_textview.has_text():
self.send_chatstate('paused', self.contact)
else:
self.send_chatstate('active', self.contact)
elif current_state == 'inactive':
if (self.mouse_over_in_last_5_secs and
jid == self.parent_win.get_active_jid()):
self.send_chatstate('active', self.contact)
# assume no activity and let the motion-notify or 'insert-text' make them
# True refresh 30 seconds vars too or else it's 30 - 5 = 25 seconds!
self.reset_kbd_mouse_timeout_vars()
return True # loop forever
def check_for_possible_inactive_chatstate(self, arg):
"""
Did we move mouse over that window or wrote something in message textview
in the last 30 seconds? if yes - we go active. If no - we go inactive
"""
contact = self.contact
current_state = contact.our_chatstate
if current_state is False: # jid doesn't support chatstates
self.possible_inactive_timeout_id = None
return False # stop looping
if self.mouse_over_in_last_5_secs or self.kbd_activity_in_last_5_secs:
return True # loop forever
if not self.mouse_over_in_last_30_secs or \
self.kbd_activity_in_last_30_secs:
self.send_chatstate('inactive', contact)
# assume no activity and let the motion-notify or 'insert-text' make them
# True refresh 30 seconds too or else it's 30 - 5 = 25 seconds!
self.reset_kbd_mouse_timeout_vars()
return True # loop forever
def _schedule_activity_timers(self):
if self.possible_paused_timeout_id:
GLib.source_remove(self.possible_paused_timeout_id)
if self.possible_inactive_timeout_id:
GLib.source_remove(self.possible_inactive_timeout_id)
self.possible_paused_timeout_id = GLib.timeout_add_seconds(5,
self.check_for_possible_paused_chatstate, None)
self.possible_inactive_timeout_id = GLib.timeout_add_seconds(30,
self.check_for_possible_inactive_chatstate, None)
def reset_kbd_mouse_timeout_vars(self):
self.kbd_activity_in_last_5_secs = False
self.mouse_over_in_last_5_secs = False
self.mouse_over_in_last_30_secs = False
self.kbd_activity_in_last_30_secs = False
def _on_window_motion_notify(self, widget, event):
""" """
It gets called no matter if it is the active window or not It gets called no matter if it is the active window or not
""" """
@ -897,16 +818,19 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
# when a groupchat is minimized there is no parent window # when a groupchat is minimized there is no parent window
return return
if self.parent_win.get_active_jid() == self.contact.jid: if self.parent_win.get_active_jid() == self.contact.jid:
# if window is the active one, change vars assisting chatstate # if window is the active one, set last interaction
self.mouse_over_in_last_5_secs = True con = app.connections[self.account]
self.mouse_over_in_last_30_secs = True con.get_module('Chatstate').set_mouse_activity(self.contact)
def _on_message_tv_buffer_changed(self, textbuffer): def _on_message_tv_buffer_changed(self, *args):
self.kbd_activity_in_last_5_secs = True con = app.connections[self.account]
self.kbd_activity_in_last_30_secs = True con.get_module('Chatstate').set_keyboard_activity(self.contact)
if not self.msg_textview.has_text(): if not self.msg_textview.has_text():
con.get_module('Chatstate').set_chatstate(self.contact,
Chatstate.ACTIVE)
return return
self.send_chatstate('composing', self.contact) con.get_module('Chatstate').set_chatstate(self.contact,
Chatstate.COMPOSING)
def save_message(self, message, msg_type): def save_message(self, message, msg_type):
# save the message, so user can scroll though the list with key up/down # save the message, so user can scroll though the list with key up/down
@ -1183,6 +1107,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
widget.get_active()) widget.get_active())
def set_control_active(self, state): def set_control_active(self, state):
con = app.connections[self.account]
if state: if state:
self.set_emoticon_popover() self.set_emoticon_popover()
jid = self.contact.jid jid = self.contact.jid
@ -1198,13 +1123,14 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
# send chatstate inactive to the one we're leaving # send chatstate inactive to the one we're leaving
# and active to the one we visit # and active to the one we visit
if self.msg_textview.has_text(): if self.msg_textview.has_text():
self.send_chatstate('paused', self.contact) con.get_module('Chatstate').set_chatstate(self.contact,
Chatstate.PAUSED)
else: else:
self.send_chatstate('active', self.contact) con.get_module('Chatstate').set_chatstate(self.contact,
self.reset_kbd_mouse_timeout_vars() Chatstate.ACTIVE)
self._schedule_activity_timers()
else: else:
self.send_chatstate('inactive', self.contact) con.get_module('Chatstate').set_chatstate(self.contact,
Chatstate.INACTIVE)
def scroll_to_end(self, force=False): def scroll_to_end(self, force=False):
self.conv_textview.scroll_to_end(force) self.conv_textview.scroll_to_end(force)

View File

@ -320,7 +320,7 @@ class CommonConnection:
# chatstates - if peer supports xep85, send chatstates # chatstates - if peer supports xep85, send chatstates
# please note that the only valid tag inside a message containing a # please note that the only valid tag inside a message containing a
# <body> tag is the active event # <body> tag is the active event
if obj.chatstate and contact and contact.supports(nbxmpp.NS_CHATSTATES): if obj.chatstate is not None:
msg_iq.setTag(obj.chatstate, namespace=nbxmpp.NS_CHATSTATES) msg_iq.setTag(obj.chatstate, namespace=nbxmpp.NS_CHATSTATES)
if not obj.message: if not obj.message:
msg_iq.setTag('no-store', msg_iq.setTag('no-store',
@ -1727,7 +1727,7 @@ class Connection(CommonConnection, ConnectionHandlers):
msg_iq.setTag('replace', attrs={'id': obj.correct_id}, msg_iq.setTag('replace', attrs={'id': obj.correct_id},
namespace=nbxmpp.NS_CORRECT) namespace=nbxmpp.NS_CORRECT)
if obj.chatstate: if obj.chatstate is not None:
msg_iq.setTag(obj.chatstate, namespace=nbxmpp.NS_CHATSTATES) msg_iq.setTag(obj.chatstate, namespace=nbxmpp.NS_CHATSTATES)
if not obj.message: if not obj.message:
msg_iq.setTag('no-store', namespace=nbxmpp.NS_MSG_HINTS) msg_iq.setTag('no-store', namespace=nbxmpp.NS_MSG_HINTS)
@ -1754,7 +1754,7 @@ class Connection(CommonConnection, ConnectionHandlers):
obj.stanza_id = self.connection.send(obj.msg_iq) obj.stanza_id = self.connection.send(obj.msg_iq)
app.nec.push_incoming_event(MessageSentEvent( app.nec.push_incoming_event(MessageSentEvent(
None, conn=self, jid=obj.jid, message=obj.message, keyID=None, None, conn=self, jid=obj.jid, message=obj.message, keyID=None,
chatstate=None, automatic_message=obj.automatic_message, automatic_message=obj.automatic_message,
stanza_id=obj.stanza_id, additional_data=obj.additional_data)) stanza_id=obj.stanza_id, additional_data=obj.additional_data))
def send_gc_status(self, nick, jid, show, status, auto=False): def send_gc_status(self, nick, jid, show, status, auto=False):

View File

@ -180,11 +180,8 @@ class ConnectionHandlersBase:
return return
# It isn't an agent # It isn't an agent
# reset chatstate if needed:
# (when contact signs out or has errors) # (when contact signs out or has errors)
if obj.show in ('offline', 'error'): if obj.show in ('offline', 'error'):
obj.contact.our_chatstate = obj.contact.chatstate = None
# TODO: This causes problems when another # TODO: This causes problems when another
# resource signs off! # resource signs off!
self.stop_all_active_file_transfers(obj.contact) self.stop_all_active_file_transfers(obj.contact)

View File

@ -23,7 +23,6 @@ from time import time as time_time
import OpenSSL.crypto import OpenSSL.crypto
import nbxmpp import nbxmpp
from nbxmpp.protocol import NS_CHATSTATES
from gajim.common import nec from gajim.common import nec
from gajim.common import helpers from gajim.common import helpers
@ -99,22 +98,6 @@ class HelperEvent:
log.error('wrong timestamp, ignoring it: %s', tag) log.error('wrong timestamp, ignoring it: %s', tag)
self.timestamp = time_time() self.timestamp = time_time()
def get_chatstate(self):
"""
Extract chatstate from a <message/> stanza
Requires self.stanza and self.msgtxt
"""
self.chatstate = None
# chatstates - look for chatstate tags in a message if not delayed
delayed = self.stanza.getTag('x', namespace=nbxmpp.NS_DELAY) is not None
if not delayed:
children = self.stanza.getChildren()
for child in children:
if child.getNamespace() == NS_CHATSTATES:
self.chatstate = child.getName()
break
def get_oob_data(self, stanza): def get_oob_data(self, stanza):
oob_node = stanza.getTag('x', namespace=nbxmpp.NS_X_OOB) oob_node = stanza.getTag('x', namespace=nbxmpp.NS_X_OOB)
if oob_node is not None: if oob_node is not None:
@ -434,17 +417,6 @@ class OurShowEvent(nec.NetworkIncomingEvent):
class BeforeChangeShowEvent(nec.NetworkIncomingEvent): class BeforeChangeShowEvent(nec.NetworkIncomingEvent):
name = 'before-change-show' name = 'before-change-show'
class ChatstateReceivedEvent(nec.NetworkIncomingEvent):
name = 'chatstate-received'
def generate(self):
self.stanza = self.msg_obj.stanza
self.jid = self.msg_obj.jid
self.fjid = self.msg_obj.fjid
self.resource = self.msg_obj.resource
self.chatstate = self.msg_obj.chatstate
return True
class GcMessageReceivedEvent(nec.NetworkIncomingEvent): class GcMessageReceivedEvent(nec.NetworkIncomingEvent):
name = 'gc-message-received' name = 'gc-message-received'

View File

@ -174,6 +174,18 @@ class PEPEventType(IntEnum):
ATOM = 7 ATOM = 7
@unique
class Chatstate(IntEnum):
COMPOSING = 0
PAUSED = 1
ACTIVE = 2
INACTIVE = 3
GONE = 4
def __str__(self):
return self.name.lower()
ACTIVITIES = { ACTIVITIES = {
'doing_chores': { 'doing_chores': {
'category': _('Doing Chores'), 'category': _('Doing Chores'),

View File

@ -28,6 +28,7 @@ try:
from gajim.common import caps_cache from gajim.common import caps_cache
from gajim.common.account import Account from gajim.common.account import Account
from gajim import common from gajim import common
from gajim.common.const import Chatstate
except ImportError as e: except ImportError as e:
if __name__ != "__main__": if __name__ != "__main__":
raise ImportError(str(e)) raise ImportError(str(e))
@ -45,7 +46,7 @@ class XMPPEntity:
class CommonContact(XMPPEntity): class CommonContact(XMPPEntity):
def __init__(self, jid, account, resource, show, status, name, def __init__(self, jid, account, resource, show, status, name,
our_chatstate, chatstate, client_caps=None): chatstate, client_caps=None):
XMPPEntity.__init__(self, jid, account, resource) XMPPEntity.__init__(self, jid, account, resource)
@ -55,11 +56,8 @@ class CommonContact(XMPPEntity):
self.client_caps = client_caps or caps_cache.NullClientCaps() self.client_caps = client_caps or caps_cache.NullClientCaps()
# please read xep-85 http://www.xmpp.org/extensions/xep-0085.html
# this holds what WE SEND to contact (our current chatstate)
self.our_chatstate = our_chatstate
# this is contact's chatstate # this is contact's chatstate
self.chatstate = chatstate self._chatstate = chatstate
@property @property
def show(self): def show(self):
@ -71,6 +69,27 @@ class CommonContact(XMPPEntity):
raise TypeError('show must be a string') raise TypeError('show must be a string')
self._show = value self._show = value
@property
def chatstate_enum(self):
return self._chatstate
@property
def chatstate(self):
if self._chatstate is None:
return
return str(self._chatstate)
@chatstate.setter
def chatstate(self, value):
if value is None:
self._chatstate = value
else:
self._chatstate = Chatstate[value.upper()]
@property
def is_gc_contact(self):
return isinstance(self, GC_Contact)
def get_full_jid(self): def get_full_jid(self):
raise NotImplementedError raise NotImplementedError
@ -97,14 +116,14 @@ class Contact(CommonContact):
""" """
def __init__(self, jid, account, name='', groups=None, show='', status='', def __init__(self, jid, account, name='', groups=None, show='', status='',
sub='', ask='', resource='', priority=0, keyID='', client_caps=None, sub='', ask='', resource='', priority=0, keyID='', client_caps=None,
our_chatstate=None, chatstate=None, idle_time=None, avatar_sha=None, groupchat=False): chatstate=None, idle_time=None, avatar_sha=None, groupchat=False):
if not isinstance(jid, str): if not isinstance(jid, str):
print('no str') print('no str')
if groups is None: if groups is None:
groups = [] groups = []
CommonContact.__init__(self, jid, account, resource, show, status, name, CommonContact.__init__(self, jid, account, resource, show, status, name,
our_chatstate, chatstate, client_caps=client_caps) chatstate, client_caps=client_caps)
self.contact_name = '' # nick choosen by contact self.contact_name = '' # nick choosen by contact
self.groups = [i if i else _('General') for i in set(groups)] # filter duplicate values self.groups = [i if i else _('General') for i in set(groups)] # filter duplicate values
@ -182,11 +201,10 @@ class GC_Contact(CommonContact):
""" """
def __init__(self, room_jid, account, name='', show='', status='', role='', def __init__(self, room_jid, account, name='', show='', status='', role='',
affiliation='', jid='', resource='', our_chatstate=None, affiliation='', jid='', resource='', chatstate=None, avatar_sha=None):
chatstate=None, avatar_sha=None):
CommonContact.__init__(self, jid, account, resource, show, status, name, CommonContact.__init__(self, jid, account, resource, show, status, name,
our_chatstate, chatstate) chatstate)
self.room_jid = room_jid self.room_jid = room_jid
self.role = role self.role = role
@ -254,7 +272,7 @@ class LegacyContactsAPI:
def create_contact(self, jid, account, name='', groups=None, show='', def create_contact(self, jid, account, name='', groups=None, show='',
status='', sub='', ask='', resource='', priority=0, keyID='', status='', sub='', ask='', resource='', priority=0, keyID='',
client_caps=None, our_chatstate=None, chatstate=None, idle_time=None, client_caps=None, chatstate=None, idle_time=None,
avatar_sha=None, groupchat=False): avatar_sha=None, groupchat=False):
if groups is None: if groups is None:
groups = [] groups = []
@ -263,8 +281,8 @@ class LegacyContactsAPI:
return Contact(jid=jid, account=account, name=name, groups=groups, return Contact(jid=jid, account=account, name=name, groups=groups,
show=show, status=status, sub=sub, ask=ask, resource=resource, show=show, status=status, sub=sub, ask=ask, resource=resource,
priority=priority, keyID=keyID, client_caps=client_caps, priority=priority, keyID=keyID, client_caps=client_caps,
our_chatstate=our_chatstate, chatstate=chatstate, chatstate=chatstate, idle_time=idle_time, avatar_sha=avatar_sha,
idle_time=idle_time, avatar_sha=avatar_sha, groupchat=groupchat) groupchat=groupchat)
def create_self_contact(self, jid, account, resource, show, status, priority, def create_self_contact(self, jid, account, resource, show, status, priority,
name='', keyID=''): name='', keyID=''):
@ -292,7 +310,7 @@ class LegacyContactsAPI:
status=contact.status, sub=contact.sub, ask=contact.ask, status=contact.status, sub=contact.sub, ask=contact.ask,
resource=contact.resource, priority=contact.priority, resource=contact.resource, priority=contact.priority,
keyID=contact.keyID, client_caps=contact.client_caps, keyID=contact.keyID, client_caps=contact.client_caps,
our_chatstate=contact.our_chatstate, chatstate=contact.chatstate, chatstate=contact.chatstate,
idle_time=contact.idle_time, avatar_sha=contact.avatar_sha) idle_time=contact.idle_time, avatar_sha=contact.avatar_sha)
def add_contact(self, account, contact): def add_contact(self, account, contact):
@ -451,6 +469,9 @@ class LegacyContactsAPI:
return return
contact.avatar_sha = sha contact.avatar_sha = sha
def get_combined_chatstate(self, account, jid):
return self._accounts[account].contacts.get_combined_chatstate(jid)
class Contacts(): class Contacts():
""" """
@ -603,6 +624,18 @@ class Contacts():
self._contacts[new_jid].append(_contact) self._contacts[new_jid].append(_contact)
del self._contacts[old_jid] del self._contacts[old_jid]
def get_combined_chatstate(self, jid):
if jid not in self._contacts:
return
contacts = self._contacts[jid]
states = []
for contact in contacts:
if contact.chatstate_enum is None:
continue
states.append(contact.chatstate_enum)
return str(min(states)) if states else None
class GC_Contacts(): class GC_Contacts():

View File

@ -14,20 +14,239 @@
# XEP-0085: Chat State Notifications # XEP-0085: Chat State Notifications
from typing import Any
from typing import Dict # pylint: disable=unused-import
from typing import Optional
from typing import Tuple
import time
import logging import logging
import nbxmpp import nbxmpp
from gi.repository import GLib
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.const import Chatstate as State
from gajim.common.modules.misc import parse_delay from gajim.common.modules.misc import parse_delay
from gajim.common.connection_handlers_events import MessageOutgoingEvent
from gajim.common.connection_handlers_events import GcMessageOutgoingEvent
from gajim.common.types import ContactT
from gajim.common.types import ConnectionT
log = logging.getLogger('gajim.c.m.chatstates') log = logging.getLogger('gajim.c.m.chatstates')
INACTIVE_AFTER = 60
PAUSED_AFTER = 5
def parse_chatstate(stanza):
def parse_chatstate(stanza: nbxmpp.Message) -> Optional[str]:
if parse_delay(stanza) is not None: if parse_delay(stanza) is not None:
return return None
children = stanza.getChildren() children = stanza.getChildren()
for child in children: for child in children:
if child.getNamespace() == nbxmpp.NS_CHATSTATES: if child.getNamespace() == nbxmpp.NS_CHATSTATES:
return child.getName() return child.getName()
return None
class Chatstate:
def __init__(self, con: ConnectionT) -> None:
self._con = con
self._account = con.name
self.handlers = [
('presence', self._presence_received),
]
self._chatstates = {} # type: Dict[str, State]
self._last_keyboard_activity = {} # type: Dict[str, float]
self._last_mouse_activity = {} # type: Dict[str, float]
self._timeout_id = GLib.timeout_add_seconds(
2, self._check_last_interaction)
def _presence_received(self,
_con: ConnectionT,
stanza: nbxmpp.Presence) -> None:
if stanza.getType() not in ('unavailable', 'error'):
return
full_jid = stanza.getFrom()
jid = full_jid.getStripped()
if self._con.get_own_jid().bareMatch(full_jid):
return
contact = app.contacts.get_contact_from_full_jid(
self._account, str(full_jid))
if contact is None or contact.is_gc_contact:
return
if contact.chatstate is None:
return
contact.chatstate = None
self._chatstates.pop(contact.jid, None)
self._last_mouse_activity.pop(contact.jid, None)
log.info('Reset chatstate for %s', jid)
app.nec.push_outgoing_event(
NetworkEvent('chatstate-received',
account=self._account,
jid=jid))
def delegate(self, event: Any) -> None:
if self._con.get_own_jid().bareMatch(event.jid) or event.sent:
# Dont show chatstates from our own resources
return
chatstate = parse_chatstate(event.stanza)
if chatstate is None:
return
contact = app.contacts.get_contact_from_full_jid(
self._account, event.fjid)
if contact is None or contact.is_gc_contact:
return
contact.chatstate = chatstate
log.info('Recv: %-10s - %s', chatstate, event.fjid)
app.nec.push_outgoing_event(
NetworkEvent('chatstate-received',
account=self._account,
jid=event.jid))
def _check_last_interaction(self) -> GLib.SOURCE_CONTINUE:
setting = app.config.get('outgoing_chat_state_notifications')
if setting in ('composing_only', 'disabled'):
return GLib.SOURCE_CONTINUE
now = time.time()
for jid, time_ in self._last_mouse_activity.items():
current_state = self._chatstates.get(jid)
if current_state is None:
self._last_mouse_activity.pop(jid, None)
return GLib.SOURCE_CONTINUE
if current_state in (State.GONE, State.INACTIVE):
return GLib.SOURCE_CONTINUE
new_chatstate = None
if now - time_ > INACTIVE_AFTER:
new_chatstate = State.INACTIVE
elif current_state == State.COMPOSING:
key_time = self._last_keyboard_activity[jid]
if now - key_time > PAUSED_AFTER:
new_chatstate = State.PAUSED
if new_chatstate is not None:
if self._chatstates.get(jid) != new_chatstate:
contact = app.contacts.get_contact(self._account, jid)
if contact is None:
self._last_mouse_activity.pop(jid, None)
return GLib.SOURCE_CONTINUE
self.set_chatstate(contact, new_chatstate)
return GLib.SOURCE_CONTINUE
def set_active(self, jid: str) -> None:
self._last_mouse_activity[jid] = time.time()
setting = app.config.get('outgoing_chat_state_notifications')
if setting == 'disabled':
return
self._chatstates[jid] = State.ACTIVE
def get_active_chatstate(self, contact: ContactT) -> Optional[str]:
# determines if we add 'active' on outgoing messages
setting = app.config.get('outgoing_chat_state_notifications')
if setting == 'disabled':
return None
# Dont send chatstates to ourself
if self._con.get_own_jid().bareMatch(contact.jid):
return None
if not contact.supports(nbxmpp.NS_CHATSTATES):
return None
self.set_active(contact.jid)
return 'active'
def set_chatstate(self, contact: ContactT, state: State) -> None:
current_state = self._chatstates.get(contact.jid)
setting = app.config.get('outgoing_chat_state_notifications')
if setting == 'disabled':
# Send a last 'gone' state after user disabled chatstates
if current_state is not None:
log.info('Send: %-10s - %s', State.GONE, contact.jid)
app.nec.push_outgoing_event(
MessageOutgoingEvent(None,
account=self._account,
jid=contact.jid,
chatstate=str(State.GONE)))
self._chatstates.pop(contact.jid, None)
self._last_mouse_activity.pop(contact.jid, None)
return
if not contact.is_groupchat():
# Dont leak presence to contacts
# which are not allowed to see our status
if contact and contact.sub in ('to', 'none'):
return
if contact.show == 'offline':
return
if not contact.supports(nbxmpp.NS_CHATSTATES):
return
if state in (State.ACTIVE, State.COMPOSING):
self._last_mouse_activity[contact.jid] = time.time()
if setting == 'composing_only':
if state in (State.INACTIVE, State.GONE, State.PAUSED):
state = State.ACTIVE
if current_state == state:
return
# Dont send chatstates to ourself
if self._con.get_own_jid().bareMatch(contact.jid):
return
log.info('Send: %-10s - %s', state, contact.jid)
event_attrs = {'account': self._account,
'jid': contact.jid,
'chatstate': str(state)}
if contact.is_groupchat():
app.nec.push_outgoing_event(
GcMessageOutgoingEvent(None, **event_attrs))
else:
app.nec.push_outgoing_event(
MessageOutgoingEvent(None, **event_attrs))
self._chatstates[contact.jid] = state
def set_mouse_activity(self, contact: ContactT) -> None:
self._last_mouse_activity[contact.jid] = time.time()
setting = app.config.get('outgoing_chat_state_notifications')
if setting == 'disabled':
return
if self._chatstates.get(contact.jid) == State.INACTIVE:
self.set_chatstate(contact, State.ACTIVE)
def set_keyboard_activity(self, contact: ContactT) -> None:
self._last_keyboard_activity[contact.jid] = time.time()
def cleanup(self):
GLib.source_remove(self._timeout_id)
def get_instance(*args: Any, **kwargs: Any) -> Tuple[Chatstate, str]:
return Chatstate(*args, **kwargs), 'Chatstate'

View File

@ -24,7 +24,6 @@ from gajim.common import helpers
from gajim.common.nec import NetworkIncomingEvent, NetworkEvent from gajim.common.nec import NetworkIncomingEvent, NetworkEvent
from gajim.common.modules.security_labels import parse_securitylabel from gajim.common.modules.security_labels import parse_securitylabel
from gajim.common.modules.user_nickname import parse_nickname from gajim.common.modules.user_nickname import parse_nickname
from gajim.common.modules.chatstates import parse_chatstate
from gajim.common.modules.carbons import parse_carbon from gajim.common.modules.carbons import parse_carbon
from gajim.common.modules.misc import parse_delay from gajim.common.modules.misc import parse_delay
from gajim.common.modules.misc import parse_eme from gajim.common.modules.misc import parse_eme
@ -218,6 +217,7 @@ class Message:
def _on_message_decrypted(self, event): def _on_message_decrypted(self, event):
try: try:
self._con.get_module('Receipts').delegate(event) self._con.get_module('Receipts').delegate(event)
self._con.get_module('Chatstate').delegate(event)
except nbxmpp.NodeProcessed: except nbxmpp.NodeProcessed:
return return
@ -236,7 +236,6 @@ class Message:
'user_nick': '' if event.sent else parse_nickname(event.stanza), 'user_nick': '' if event.sent else parse_nickname(event.stanza),
'form_node': parse_form(event.stanza), 'form_node': parse_form(event.stanza),
'xhtml': parse_xhtml(event.stanza), 'xhtml': parse_xhtml(event.stanza),
'chatstate': parse_chatstate(event.stanza),
'timestamp': timestamp, 'timestamp': timestamp,
'delayed': delayed, 'delayed': delayed,
} }

View File

@ -25,7 +25,7 @@ import nbxmpp
from gajim.common import app from gajim.common import app
from gajim.common.nec import NetworkIncomingEvent from gajim.common.nec import NetworkIncomingEvent
from gajim.common.types import ConnectionT from gajim.common.types import ConnectionT
from gajim.common.types import ContactT from gajim.common.types import ContactsT
log = logging.getLogger('gajim.c.m.ping') log = logging.getLogger('gajim.c.m.ping')
@ -73,7 +73,7 @@ class Ping:
log.warning('No reply received for keepalive ping. Reconnecting...') log.warning('No reply received for keepalive ping. Reconnecting...')
self._con.disconnectedReconnCB() self._con.disconnectedReconnCB()
def send_ping(self, contact: ContactT) -> None: def send_ping(self, contact: ContactsT) -> None:
if not app.account_is_connected(self._account): if not app.account_is_connected(self._account):
return return
@ -93,7 +93,7 @@ class Ping:
_con: ConnectionT, _con: ConnectionT,
stanza: nbxmpp.Iq, stanza: nbxmpp.Iq,
ping_time: int, ping_time: int,
contact: ContactT) -> None: contact: ContactsT) -> None:
if not nbxmpp.isResultNode(stanza): if not nbxmpp.isResultNode(stanza):
log.info('Error: %s', stanza.getError()) log.info('Error: %s', stanza.getError())
app.nec.push_incoming_event( app.nec.push_incoming_event(

View File

@ -44,7 +44,8 @@ InterfaceT = Union['Interface']
LoggerT = Union['Logger'] LoggerT = Union['Logger']
ConnectionT = Union['Connection', 'ConnectionZeroconf'] ConnectionT = Union['Connection', 'ConnectionZeroconf']
ContactT = Union['Contact', 'GC_Contact'] ContactsT = Union['Contact', 'GC_Contact']
ContactT = Union['Contact']
UserTuneDataT = Optional[Tuple[str, str, str, str, str]] UserTuneDataT = Optional[Tuple[str, str, str, str, str]]

View File

@ -20,6 +20,7 @@
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import time import time
import logging
import nbxmpp import nbxmpp
@ -30,14 +31,13 @@ from gajim.common.zeroconf.zeroconf import Constant
from gajim.common import connection_handlers from gajim.common import connection_handlers
from gajim.common.nec import NetworkIncomingEvent, NetworkEvent from gajim.common.nec import NetworkIncomingEvent, NetworkEvent
from gajim.common.modules.user_nickname import parse_nickname from gajim.common.modules.user_nickname import parse_nickname
from gajim.common.modules.chatstates import parse_chatstate
from gajim.common.modules.misc import parse_eme from gajim.common.modules.misc import parse_eme
from gajim.common.modules.misc import parse_correction from gajim.common.modules.misc import parse_correction
from gajim.common.modules.misc import parse_attention from gajim.common.modules.misc import parse_attention
from gajim.common.modules.misc import parse_oob from gajim.common.modules.misc import parse_oob
from gajim.common.modules.misc import parse_xhtml from gajim.common.modules.misc import parse_xhtml
import logging
log = logging.getLogger('gajim.c.z.connection_handlers_zeroconf') log = logging.getLogger('gajim.c.z.connection_handlers_zeroconf')
STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd',
@ -147,6 +147,7 @@ connection_handlers.ConnectionJingle):
def _on_message_decrypted(self, event): def _on_message_decrypted(self, event):
try: try:
self.get_module('Receipts').delegate(event) self.get_module('Receipts').delegate(event)
self.get_module('Chatstate').delegate(event)
except nbxmpp.NodeProcessed: except nbxmpp.NodeProcessed:
return return
@ -160,7 +161,6 @@ connection_handlers.ConnectionJingle):
'correct_id': parse_correction(event.stanza), 'correct_id': parse_correction(event.stanza),
'user_nick': parse_nickname(event.stanza), 'user_nick': parse_nickname(event.stanza),
'xhtml': parse_xhtml(event.stanza), 'xhtml': parse_xhtml(event.stanza),
'chatstate': parse_chatstate(event.stanza),
'stanza_id': event.unique_id 'stanza_id': event.unique_id
} }

View File

@ -59,6 +59,7 @@ from gajim.common import ged
from gajim.common import i18n from gajim.common import i18n
from gajim.common import contacts from gajim.common import contacts
from gajim.common.const import StyleAttr from gajim.common.const import StyleAttr
from gajim.common.const import Chatstate
from gajim.chat_control import ChatControl from gajim.chat_control import ChatControl
from gajim.chat_control_base import ChatControlBase from gajim.chat_control_base import ChatControlBase
@ -794,7 +795,6 @@ class GroupchatControl(ChatControlBase):
self.add_actions() self.add_actions()
self.update_actions() self.update_actions()
self.set_lock_image() self.set_lock_image()
self._schedule_activity_timers()
self._connect_window_state_change(self.parent_win) self._connect_window_state_change(self.parent_win)
def set_tooltip(self): def set_tooltip(self):
@ -2187,13 +2187,9 @@ class GroupchatControl(ChatControlBase):
correct_id = self.last_sent_msg correct_id = self.last_sent_msg
else: else:
correct_id = None correct_id = None
con = app.connections[self.account]
# Set chatstate chatstate = con.get_module('Chatstate').get_active_chatstate(
chatstate = None self.contact.jid)
if app.config.get('outgoing_chat_state_notifications') != 'disabled':
chatstate = 'active'
self.reset_kbd_mouse_timeout_vars()
self.contact.our_chatstate = chatstate
# Send the message # Send the message
app.nec.push_outgoing_event(GcMessageOutgoingEvent( app.nec.push_outgoing_event(GcMessageOutgoingEvent(
@ -2228,69 +2224,16 @@ class GroupchatControl(ChatControlBase):
control = win.notebook.get_nth_page(ctrl_page) control = win.notebook.get_nth_page(ctrl_page)
win.notebook.remove_page(ctrl_page) win.notebook.remove_page(ctrl_page)
if self.possible_paused_timeout_id:
GLib.source_remove(self.possible_paused_timeout_id)
self.possible_paused_timeout_id = None
if self.possible_inactive_timeout_id:
GLib.source_remove(self.possible_inactive_timeout_id)
self.possible_inactive_timeout_id = None
control.unparent() control.unparent()
ctrl.parent_win = None ctrl.parent_win = None
self.send_chatstate('inactive', self.contact) con = app.connections[self.account]
con.get_module('Chatstate').set_chatstate(self.contact, Chatstate.INACTIVE)
app.interface.roster.minimize_groupchat( app.interface.roster.minimize_groupchat(
self.account, self.contact.jid, status=self.subject) self.account, self.contact.jid, status=self.subject)
del win._controls[self.account][self.contact.jid] del win._controls[self.account][self.contact.jid]
def send_chatstate(self, state, contact):
"""
Send OUR chatstate as STANDLONE chat state message (eg. no body)
to contact only if new chatstate is different from the previous one
if jid is not specified, send to active tab
"""
# JEP 85 does not allow resending the same chatstate
# this function checks for that and just returns so it's safe to call it
# with same state.
# This functions also checks for violation in state transitions
# and raises RuntimeException with appropriate message
# more on that http://xmpp.org/extensions/xep-0085.html#statechart
# do not send if we have chat state notifications disabled
# that means we won't reply to the <active/> from other peer
# so we do not broadcast jep85 capabalities
chatstate_setting = app.config.get('outgoing_chat_state_notifications')
if chatstate_setting == 'disabled':
return
if (chatstate_setting == 'composing_only' and
state != 'active' and
state != 'composing'):
return
# if the new state we wanna send (state) equals
# the current state (contact.our_chatstate) then return
if contact.our_chatstate == state:
return
# if we're inactive prevent composing (XEP violation)
if contact.our_chatstate == 'inactive' and state == 'composing':
# go active before
app.nec.push_outgoing_event(GcMessageOutgoingEvent(None,
account=self.account, jid=self.contact.jid, chatstate='active',
control=self))
contact.our_chatstate = 'active'
self.reset_kbd_mouse_timeout_vars()
app.nec.push_outgoing_event(GcMessageOutgoingEvent(None,
account=self.account, jid=self.contact.jid, chatstate=state,
control=self))
contact.our_chatstate = state
if state == 'active':
self.reset_kbd_mouse_timeout_vars()
def shutdown(self, status='offline'): def shutdown(self, status='offline'):
# PluginSystem: calling shutdown of super class (ChatControlBase) # PluginSystem: calling shutdown of super class (ChatControlBase)
# to let it remove it's GUI extension points # to let it remove it's GUI extension points

View File

@ -444,7 +444,6 @@ class Interface:
account=account, name=nick, show=show) account=account, name=nick, show=show)
ctrl = self.new_private_chat(gc_c, account, session) ctrl = self.new_private_chat(gc_c, account, session)
ctrl.contact.our_chatstate = False
ctrl.print_conversation(_('Error %(code)s: %(msg)s') % { ctrl.print_conversation(_('Error %(code)s: %(msg)s') % {
'code': obj.error_code, 'msg': obj.error_msg}, 'status') 'code': obj.error_code, 'msg': obj.error_msg}, 'status')
return return

View File

@ -21,14 +21,12 @@ import string
import random import random
import itertools import itertools
from gajim import message_control
from gajim import notify from gajim import notify
from gajim.common import helpers from gajim.common import helpers
from gajim.common import events from gajim.common import events
from gajim.common import app from gajim.common import app
from gajim.common import contacts from gajim.common import contacts
from gajim.common import ged from gajim.common import ged
from gajim.common.connection_handlers_events import ChatstateReceivedEvent
from gajim.common.const import KindConstant from gajim.common.const import KindConstant
from gajim.gtk.single_message import SingleMessageWindow from gajim.gtk.single_message import SingleMessageWindow
@ -97,7 +95,7 @@ class ChatControlSession:
self.control.change_resource(self.resource) self.control.change_resource(self.resource)
if obj.mtype == 'chat': if obj.mtype == 'chat':
if not obj.msgtxt and obj.chatstate is None: if not obj.msgtxt:
return return
log_type = KindConstant.CHAT_MSG_RECV log_type = KindConstant.CHAT_MSG_RECV
@ -142,27 +140,6 @@ class ChatControlSession:
# joined. We log it silently without notification. # joined. We log it silently without notification.
return True return True
# Handle chat states
if contact and (not obj.forwarded or not obj.sent):
if self.control and self.control.type_id == \
message_control.TYPE_CHAT:
if obj.chatstate is not None:
# other peer sent us reply, so he supports jep85 or jep22
contact.chatstate = obj.chatstate
if contact.our_chatstate == 'ask': # we were jep85 disco?
contact.our_chatstate = 'active' # no more
app.nec.push_incoming_event(ChatstateReceivedEvent(None,
conn=obj.conn, msg_obj=obj))
elif contact.chatstate != 'active':
# got no valid jep85 answer, peer does not support it
contact.chatstate = False
elif obj.chatstate == 'active':
# Brand new message, incoming.
contact.our_chatstate = obj.chatstate
contact.chatstate = obj.chatstate
# THIS MUST BE AFTER chatstates handling
# AND BEFORE playsound (else we hear sounding on chatstates!)
if not obj.msgtxt: # empty message text if not obj.msgtxt: # empty message text
return True return True
@ -189,7 +166,7 @@ class ChatControlSession:
if app.interface.remote_ctrl: if app.interface.remote_ctrl:
app.interface.remote_ctrl.raise_signal('NewMessage', ( app.interface.remote_ctrl.raise_signal('NewMessage', (
self.conn.name, [obj.fjid, obj.msgtxt, obj.timestamp, self.conn.name, [obj.fjid, obj.msgtxt, obj.timestamp,
obj.encrypted, obj.mtype, obj.subject, obj.chatstate, obj.encrypted, obj.mtype, obj.subject,
obj.msg_log_id, obj.user_nick, obj.xhtml, obj.form_node])) obj.msg_log_id, obj.user_nick, obj.xhtml, obj.form_node]))
def roster_message2(self, obj): def roster_message2(self, obj):

View File

@ -14,9 +14,9 @@ from gajim.common import caps_cache
class TestCommonContact(unittest.TestCase): class TestCommonContact(unittest.TestCase):
def setUp(self): def setUp(self):
self.contact = CommonContact(jid='', account="", resource='', show='', self.contact = CommonContact(
status='', name='', our_chatstate=None, chatstate=None, jid='', account="", resource='', show='',
client_caps=None) status='', name='', chatstate=None, client_caps=None)
def test_default_client_supports(self): def test_default_client_supports(self):
''' '''
@ -43,8 +43,8 @@ class TestContact(TestCommonContact):
'''This test supports the migration from the old to the new contact '''This test supports the migration from the old to the new contact
domain model by smoke testing that no attribute values are lost''' domain model by smoke testing that no attribute values are lost'''
attributes = ["jid", "resource", "show", "status", "name", "our_chatstate", attributes = ["jid", "resource", "show", "status", "name",
"chatstate", "client_caps", "priority", "sub"] "chatstate", "client_caps", "priority", "sub"]
for attr in attributes: for attr in attributes:
self.assertTrue(hasattr(self.contact, attr), msg="expected: " + attr) self.assertTrue(hasattr(self.contact, attr), msg="expected: " + attr)
@ -59,8 +59,8 @@ class TestGC_Contact(TestCommonContact):
'''This test supports the migration from the old to the new contact '''This test supports the migration from the old to the new contact
domain model by asserting no attributes have been lost''' domain model by asserting no attributes have been lost'''
attributes = ["jid", "resource", "show", "status", "name", "our_chatstate", attributes = ["jid", "resource", "show", "status", "name",
"chatstate", "client_caps", "role", "room_jid"] "chatstate", "client_caps", "role", "room_jid"]
for attr in attributes: for attr in attributes:
self.assertTrue(hasattr(self.contact, attr), msg="expected: " + attr) self.assertTrue(hasattr(self.contact, attr), msg="expected: " + attr)