Refactor Chat State Notifications
- Move code into chatstate module - Refactor most of the code, make it much simpler
This commit is contained in:
parent
07b175d541
commit
460d390795
|
@ -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,7 +882,7 @@ 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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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():
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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]]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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,7 +43,7 @@ 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,7 +59,7 @@ 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)
|
||||||
|
|
Loading…
Reference in New Issue