From 50c670e61ba3922b68cf10602371648b126e725b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philipp=20H=C3=B6rist?= <philipp@hoerist.com>
Date: Sat, 29 Sep 2018 21:48:21 +0200
Subject: [PATCH] Refactor Chat State Notifications

- Move code into chatstate module
- Refactor most of the code, make it much simpler
---
 gajim/chat_control.py                         | 124 ++--------
 gajim/chat_control_base.py                    | 128 +++-------
 gajim/common/connection.py                    |   6 +-
 gajim/common/connection_handlers.py           |   3 -
 gajim/common/connection_handlers_events.py    |  28 ---
 gajim/common/const.py                         |  12 +
 gajim/common/contacts.py                      |  61 +++--
 gajim/common/modules/chatstates.py            | 223 +++++++++++++++++-
 gajim/common/modules/message.py               |   3 +-
 gajim/common/modules/ping.py                  |   6 +-
 gajim/common/types.py                         |   3 +-
 .../zeroconf/connection_handlers_zeroconf.py  |   6 +-
 gajim/groupchat_control.py                    |  69 +-----
 gajim/gui_interface.py                        |   1 -
 gajim/session.py                              |  27 +--
 test/unit/test_contacts.py                    |  14 +-
 16 files changed, 359 insertions(+), 355 deletions(-)

diff --git a/gajim/chat_control.py b/gajim/chat_control.py
index e2dbc030db..d78b0b317a 100644
--- a/gajim/chat_control.py
+++ b/gajim/chat_control.py
@@ -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_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_CHATSTATES
 
 from gajim import gtkgui_helpers
 from gajim import gui_menu_builder
@@ -50,8 +49,9 @@ from gajim.common import helpers
 from gajim.common import ged
 from gajim.common import i18n
 from gajim.common.contacts import GC_Contact
-from gajim.common.connection_handlers_events import MessageOutgoingEvent
-from gajim.common.const import AvatarSize, KindConstant
+from gajim.common.const import AvatarSize
+from gajim.common.const import KindConstant
+from gajim.common.const import Chatstate
 
 from gajim.command_system.implementation.hosts import ChatCommands
 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)
 
         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 contact.show == 'offline':
                 chatstate = ''
@@ -882,8 +882,8 @@ class ChatControl(ChatControlBase):
             correct_id=obj.correct_id,
             additional_data=obj.additional_data)
 
-    def send_message(self, message, keyID='', chatstate=None, xhtml=None,
-    process_commands=True, attention=False):
+    def send_message(self, message, keyID='', xhtml=None,
+                     process_commands=True, attention=False):
         """
         Send a message to contact
         """
@@ -902,18 +902,13 @@ class ChatControl(ChatControlBase):
         contact = self.contact
         keyID = contact.keyID
 
-        chatstate_to_send = None
-        if contact is not None:
-            if contact.supports(NS_CHATSTATES):
-                # send active chatstate on every message (as XEP says)
-                chatstate_to_send = 'active'
-                contact.our_chatstate = 'active'
-
-                self._schedule_activity_timers()
-
-        ChatControlBase.send_message(self, message, keyID, type_='chat',
-            chatstate=chatstate_to_send, xhtml=xhtml,
-            process_commands=process_commands, attention=attention)
+        ChatControlBase.send_message(self,
+                                     message,
+                                     keyID,
+                                     type_='chat',
+                                     xhtml=xhtml,
+                                     process_commands=process_commands,
+                                     attention=attention)
 
     def get_our_nick(self):
         return app.nicks[self.account]
@@ -1059,79 +1054,6 @@ class ChatControl(ChatControlBase):
                 show_buttonbar_items=not hide_buttonbar_items)
         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):
         # PluginSystem: removing GUI extension points connected with ChatControl
         # instance object
@@ -1161,9 +1083,8 @@ class ChatControl(ChatControlBase):
         self.unsubscribe_events()
 
         # Send 'gone' chatstate
-        self.send_chatstate('gone', self.contact)
-        self.contact.chatstate = None
-        self.contact.our_chatstate = None
+        con = app.connections[self.account]
+        con.get_module('Chatstate').set_chatstate(self.contact, Chatstate.GONE)
 
         for jingle_type in ('audio', 'video'):
             self.close_jingle_content(jingle_type)
@@ -1225,13 +1146,18 @@ class ChatControl(ChatControlBase):
             return
         on_yes(self)
 
-    def _nec_chatstate_received(self, obj):
-        """
-        Handle incoming chatstate that jid SENT TO us
-        """
+    def _nec_chatstate_received(self, event):
+        if event.account != self.account:
+            return
+
+        if event.jid != self.contact.jid:
+            return
+
         self.draw_banner_text()
         # 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):
         if obj.conn.name != self.account:
diff --git a/gajim/chat_control_base.py b/gajim/chat_control_base.py
index 49eab673e1..5a2c4793e3 100644
--- a/gajim/chat_control_base.py
+++ b/gajim/chat_control_base.py
@@ -49,6 +49,7 @@ from gajim.message_textview import MessageTextView
 from gajim.common.contacts import GC_Contact
 from gajim.common.connection_handlers_events import MessageOutgoingEvent
 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 CommandTools
@@ -325,10 +326,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
         # Security Labels
         self.seclabel_combo = self.xml.get_object('label_selector')
 
-        # chatstate timers and state
-        self.reset_kbd_mouse_timeout_vars()
-        self.possible_paused_timeout_id = None
-        self.possible_inactive_timeout_id = None
+        con = app.connections[self.account]
+        con.get_module('Chatstate').set_active(self.contact.jid)
+
         message_tv_buffer = self.msg_textview.get_buffer()
         id_ = message_tv_buffer.connect('changed',
             self._on_message_tv_buffer_changed)
@@ -337,7 +337,6 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
             id_ = parent_win.window.connect('motion-notify-event',
                 self._on_window_motion_notify)
             self.handlers[id_] = parent_win.window
-            self._schedule_activity_timers()
 
         self.encryption = self.get_encryption_state()
         self.conv_textview.encryption_enabled = self.encryption is not None
@@ -520,11 +519,6 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
 
     def shutdown(self):
         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
         # instance object
         app.plugin_manager.remove_gui_extension_point('chat_control_base',
@@ -777,7 +771,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
         label = labels[lname]
         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):
         """
         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):
             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()
 
         if self.correcting and self.last_sent_msg:
@@ -803,6 +789,10 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
         else:
             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,
             account=self.account, jid=self.contact.jid, message=message,
             keyID=keyID, type_=type_, chatstate=chatstate,
@@ -820,76 +810,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
         message_buffer = self.msg_textview.get_buffer()
         message_buffer.set_text('') # clear message buffer (and tv of course)
 
-    def check_for_possible_paused_chatstate(self, arg):
-        """
-        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):
+    def _on_window_motion_notify(self, *args):
         """
         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
             return
         if self.parent_win.get_active_jid() == self.contact.jid:
-            # if window is the active one, change vars assisting chatstate
-            self.mouse_over_in_last_5_secs = True
-            self.mouse_over_in_last_30_secs = True
+            # if window is the active one, set last interaction
+            con = app.connections[self.account]
+            con.get_module('Chatstate').set_mouse_activity(self.contact)
 
-    def _on_message_tv_buffer_changed(self, textbuffer):
-        self.kbd_activity_in_last_5_secs = True
-        self.kbd_activity_in_last_30_secs = True
+    def _on_message_tv_buffer_changed(self, *args):
+        con = app.connections[self.account]
+        con.get_module('Chatstate').set_keyboard_activity(self.contact)
         if not self.msg_textview.has_text():
+            con.get_module('Chatstate').set_chatstate(self.contact,
+                                                      Chatstate.ACTIVE)
             return
-        self.send_chatstate('composing', self.contact)
+        con.get_module('Chatstate').set_chatstate(self.contact,
+                                                  Chatstate.COMPOSING)
 
     def save_message(self, message, msg_type):
         # 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())
 
     def set_control_active(self, state):
+        con = app.connections[self.account]
         if state:
             self.set_emoticon_popover()
             jid = self.contact.jid
@@ -1198,13 +1123,14 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
             # send chatstate inactive to the one we're leaving
             # and active to the one we visit
             if self.msg_textview.has_text():
-                self.send_chatstate('paused', self.contact)
+                con.get_module('Chatstate').set_chatstate(self.contact,
+                                                          Chatstate.PAUSED)
             else:
-                self.send_chatstate('active', self.contact)
-            self.reset_kbd_mouse_timeout_vars()
-            self._schedule_activity_timers()
+                con.get_module('Chatstate').set_chatstate(self.contact,
+                                                          Chatstate.ACTIVE)
         else:
-            self.send_chatstate('inactive', self.contact)
+            con.get_module('Chatstate').set_chatstate(self.contact,
+                                                      Chatstate.INACTIVE)
 
     def scroll_to_end(self, force=False):
         self.conv_textview.scroll_to_end(force)
diff --git a/gajim/common/connection.py b/gajim/common/connection.py
index 0f0cfd9220..8edf36616b 100644
--- a/gajim/common/connection.py
+++ b/gajim/common/connection.py
@@ -320,7 +320,7 @@ class CommonConnection:
             # chatstates - if peer supports xep85, send chatstates
             # please note that the only valid tag inside a message containing a
             # <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)
                 if not obj.message:
                     msg_iq.setTag('no-store',
@@ -1727,7 +1727,7 @@ class Connection(CommonConnection, ConnectionHandlers):
             msg_iq.setTag('replace', attrs={'id': obj.correct_id},
                           namespace=nbxmpp.NS_CORRECT)
 
-        if obj.chatstate:
+        if obj.chatstate is not None:
             msg_iq.setTag(obj.chatstate, namespace=nbxmpp.NS_CHATSTATES)
             if not obj.message:
                 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)
         app.nec.push_incoming_event(MessageSentEvent(
             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))
 
     def send_gc_status(self, nick, jid, show, status, auto=False):
diff --git a/gajim/common/connection_handlers.py b/gajim/common/connection_handlers.py
index a2c58b88be..e188befb4f 100644
--- a/gajim/common/connection_handlers.py
+++ b/gajim/common/connection_handlers.py
@@ -180,11 +180,8 @@ class ConnectionHandlersBase:
             return
 
         # It isn't an agent
-        # reset chatstate if needed:
         # (when contact signs out or has errors)
         if obj.show in ('offline', 'error'):
-            obj.contact.our_chatstate = obj.contact.chatstate = None
-
             # TODO: This causes problems when another
             # resource signs off!
             self.stop_all_active_file_transfers(obj.contact)
diff --git a/gajim/common/connection_handlers_events.py b/gajim/common/connection_handlers_events.py
index 4e842a23a8..fa696b549c 100644
--- a/gajim/common/connection_handlers_events.py
+++ b/gajim/common/connection_handlers_events.py
@@ -23,7 +23,6 @@ from time import time as time_time
 
 import OpenSSL.crypto
 import nbxmpp
-from nbxmpp.protocol import NS_CHATSTATES
 
 from gajim.common import nec
 from gajim.common import helpers
@@ -99,22 +98,6 @@ class HelperEvent:
             log.error('wrong timestamp, ignoring it: %s', tag)
             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):
         oob_node = stanza.getTag('x', namespace=nbxmpp.NS_X_OOB)
         if oob_node is not None:
@@ -434,17 +417,6 @@ class OurShowEvent(nec.NetworkIncomingEvent):
 class BeforeChangeShowEvent(nec.NetworkIncomingEvent):
     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):
     name = 'gc-message-received'
 
diff --git a/gajim/common/const.py b/gajim/common/const.py
index 471a734592..8e92af7014 100644
--- a/gajim/common/const.py
+++ b/gajim/common/const.py
@@ -174,6 +174,18 @@ class PEPEventType(IntEnum):
     ATOM = 7
 
 
+@unique
+class Chatstate(IntEnum):
+    COMPOSING = 0
+    PAUSED = 1
+    ACTIVE = 2
+    INACTIVE = 3
+    GONE = 4
+
+    def __str__(self):
+        return self.name.lower()
+
+
 ACTIVITIES = {
     'doing_chores': {
         'category': _('Doing Chores'),
diff --git a/gajim/common/contacts.py b/gajim/common/contacts.py
index 4d9dd91fff..52903230b9 100644
--- a/gajim/common/contacts.py
+++ b/gajim/common/contacts.py
@@ -28,6 +28,7 @@ try:
     from gajim.common import caps_cache
     from gajim.common.account import Account
     from gajim import common
+    from gajim.common.const import Chatstate
 except ImportError as e:
     if __name__ != "__main__":
         raise ImportError(str(e))
@@ -45,7 +46,7 @@ class XMPPEntity:
 class CommonContact(XMPPEntity):
 
     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)
 
@@ -55,11 +56,8 @@ class CommonContact(XMPPEntity):
 
         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
-        self.chatstate = chatstate
+        self._chatstate = chatstate
 
     @property
     def show(self):
@@ -71,6 +69,27 @@ class CommonContact(XMPPEntity):
             raise TypeError('show must be a string')
         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):
         raise NotImplementedError
 
@@ -97,14 +116,14 @@ class Contact(CommonContact):
     """
     def __init__(self, jid, account, name='', groups=None, show='', status='',
     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):
             print('no str')
         if groups is None:
             groups = []
 
         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.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='',
-    affiliation='', jid='', resource='', our_chatstate=None,
-    chatstate=None, avatar_sha=None):
+    affiliation='', jid='', resource='', chatstate=None, avatar_sha=None):
 
         CommonContact.__init__(self, jid, account, resource, show, status, name,
-            our_chatstate, chatstate)
+                               chatstate)
 
         self.room_jid = room_jid
         self.role = role
@@ -254,7 +272,7 @@ class LegacyContactsAPI:
 
     def create_contact(self, jid, account, name='', groups=None, show='',
     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):
         if groups is None:
             groups = []
@@ -263,8 +281,8 @@ class LegacyContactsAPI:
         return Contact(jid=jid, account=account, name=name, groups=groups,
             show=show, status=status, sub=sub, ask=ask, resource=resource,
             priority=priority, keyID=keyID, client_caps=client_caps,
-            our_chatstate=our_chatstate, chatstate=chatstate,
-            idle_time=idle_time, avatar_sha=avatar_sha, groupchat=groupchat)
+            chatstate=chatstate, idle_time=idle_time, avatar_sha=avatar_sha,
+            groupchat=groupchat)
 
     def create_self_contact(self, jid, account, resource, show, status, priority,
     name='', keyID=''):
@@ -292,7 +310,7 @@ class LegacyContactsAPI:
             status=contact.status, sub=contact.sub, ask=contact.ask,
             resource=contact.resource, priority=contact.priority,
             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)
 
     def add_contact(self, account, contact):
@@ -451,6 +469,9 @@ class LegacyContactsAPI:
             return
         contact.avatar_sha = sha
 
+    def get_combined_chatstate(self, account, jid):
+        return self._accounts[account].contacts.get_combined_chatstate(jid)
+
 
 class Contacts():
     """
@@ -603,6 +624,18 @@ class Contacts():
             self._contacts[new_jid].append(_contact)
             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():
 
diff --git a/gajim/common/modules/chatstates.py b/gajim/common/modules/chatstates.py
index 407d05a56f..c80abe0e5b 100644
--- a/gajim/common/modules/chatstates.py
+++ b/gajim/common/modules/chatstates.py
@@ -14,20 +14,239 @@
 
 # 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 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.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')
 
+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:
-        return
+        return None
 
     children = stanza.getChildren()
     for child in children:
         if child.getNamespace() == nbxmpp.NS_CHATSTATES:
             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'
diff --git a/gajim/common/modules/message.py b/gajim/common/modules/message.py
index 946bc70690..328ea32eca 100644
--- a/gajim/common/modules/message.py
+++ b/gajim/common/modules/message.py
@@ -24,7 +24,6 @@ from gajim.common import helpers
 from gajim.common.nec import NetworkIncomingEvent, NetworkEvent
 from gajim.common.modules.security_labels import parse_securitylabel
 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.misc import parse_delay
 from gajim.common.modules.misc import parse_eme
@@ -218,6 +217,7 @@ class Message:
     def _on_message_decrypted(self, event):
         try:
             self._con.get_module('Receipts').delegate(event)
+            self._con.get_module('Chatstate').delegate(event)
         except nbxmpp.NodeProcessed:
             return
 
@@ -236,7 +236,6 @@ class Message:
             'user_nick': '' if event.sent else parse_nickname(event.stanza),
             'form_node': parse_form(event.stanza),
             'xhtml': parse_xhtml(event.stanza),
-            'chatstate': parse_chatstate(event.stanza),
             'timestamp': timestamp,
             'delayed': delayed,
         }
diff --git a/gajim/common/modules/ping.py b/gajim/common/modules/ping.py
index 0b7f7dc77e..fea6d7d5d1 100644
--- a/gajim/common/modules/ping.py
+++ b/gajim/common/modules/ping.py
@@ -25,7 +25,7 @@ import nbxmpp
 from gajim.common import app
 from gajim.common.nec import NetworkIncomingEvent
 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')
 
@@ -73,7 +73,7 @@ class Ping:
         log.warning('No reply received for keepalive ping. Reconnecting...')
         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):
             return
 
@@ -93,7 +93,7 @@ class Ping:
                        _con: ConnectionT,
                        stanza: nbxmpp.Iq,
                        ping_time: int,
-                       contact: ContactT) -> None:
+                       contact: ContactsT) -> None:
         if not nbxmpp.isResultNode(stanza):
             log.info('Error: %s', stanza.getError())
             app.nec.push_incoming_event(
diff --git a/gajim/common/types.py b/gajim/common/types.py
index 5f9b6a6859..ae9cc25987 100644
--- a/gajim/common/types.py
+++ b/gajim/common/types.py
@@ -44,7 +44,8 @@ InterfaceT = Union['Interface']
 LoggerT = Union['Logger']
 
 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]]
 
diff --git a/gajim/common/zeroconf/connection_handlers_zeroconf.py b/gajim/common/zeroconf/connection_handlers_zeroconf.py
index 54b4a8f2a4..bdd5af53de 100644
--- a/gajim/common/zeroconf/connection_handlers_zeroconf.py
+++ b/gajim/common/zeroconf/connection_handlers_zeroconf.py
@@ -20,6 +20,7 @@
 # along with Gajim.  If not, see <http://www.gnu.org/licenses/>.
 
 import time
+import logging
 
 import nbxmpp
 
@@ -30,14 +31,13 @@ from gajim.common.zeroconf.zeroconf import Constant
 from gajim.common import connection_handlers
 from gajim.common.nec import NetworkIncomingEvent, NetworkEvent
 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_correction
 from gajim.common.modules.misc import parse_attention
 from gajim.common.modules.misc import parse_oob
 from gajim.common.modules.misc import parse_xhtml
 
-import logging
+
 log = logging.getLogger('gajim.c.z.connection_handlers_zeroconf')
 
 STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd',
@@ -147,6 +147,7 @@ connection_handlers.ConnectionJingle):
     def _on_message_decrypted(self, event):
         try:
             self.get_module('Receipts').delegate(event)
+            self.get_module('Chatstate').delegate(event)
         except nbxmpp.NodeProcessed:
             return
 
@@ -160,7 +161,6 @@ connection_handlers.ConnectionJingle):
             'correct_id': parse_correction(event.stanza),
             'user_nick': parse_nickname(event.stanza),
             'xhtml': parse_xhtml(event.stanza),
-            'chatstate': parse_chatstate(event.stanza),
             'stanza_id': event.unique_id
         }
 
diff --git a/gajim/groupchat_control.py b/gajim/groupchat_control.py
index 33b1599514..2d60ea18fb 100644
--- a/gajim/groupchat_control.py
+++ b/gajim/groupchat_control.py
@@ -59,6 +59,7 @@ from gajim.common import ged
 from gajim.common import i18n
 from gajim.common import contacts
 from gajim.common.const import StyleAttr
+from gajim.common.const import Chatstate
 from gajim.chat_control import ChatControl
 from gajim.chat_control_base import ChatControlBase
 
@@ -794,7 +795,6 @@ class GroupchatControl(ChatControlBase):
         self.add_actions()
         self.update_actions()
         self.set_lock_image()
-        self._schedule_activity_timers()
         self._connect_window_state_change(self.parent_win)
 
     def set_tooltip(self):
@@ -2187,13 +2187,9 @@ class GroupchatControl(ChatControlBase):
                 correct_id = self.last_sent_msg
             else:
                 correct_id = None
-
-            # Set chatstate
-            chatstate = None
-            if app.config.get('outgoing_chat_state_notifications') != 'disabled':
-                chatstate = 'active'
-                self.reset_kbd_mouse_timeout_vars()
-                self.contact.our_chatstate = chatstate
+            con = app.connections[self.account]
+            chatstate = con.get_module('Chatstate').get_active_chatstate(
+                self.contact.jid)
 
             # Send the message
             app.nec.push_outgoing_event(GcMessageOutgoingEvent(
@@ -2228,69 +2224,16 @@ class GroupchatControl(ChatControlBase):
         control = win.notebook.get_nth_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()
         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(
             self.account, self.contact.jid, status=self.subject)
 
         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'):
         # PluginSystem: calling shutdown of super class (ChatControlBase)
         # to let it remove it's GUI extension points
diff --git a/gajim/gui_interface.py b/gajim/gui_interface.py
index 41d2e0d5de..4e08ede69d 100644
--- a/gajim/gui_interface.py
+++ b/gajim/gui_interface.py
@@ -444,7 +444,6 @@ class Interface:
                         account=account, name=nick, show=show)
                     ctrl = self.new_private_chat(gc_c, account, session)
 
-                ctrl.contact.our_chatstate = False
                 ctrl.print_conversation(_('Error %(code)s: %(msg)s') % {
                     'code': obj.error_code, 'msg': obj.error_msg}, 'status')
                 return
diff --git a/gajim/session.py b/gajim/session.py
index d907bb36b5..f52798f322 100644
--- a/gajim/session.py
+++ b/gajim/session.py
@@ -21,14 +21,12 @@ import string
 import random
 import itertools
 
-from gajim import message_control
 from gajim import notify
 from gajim.common import helpers
 from gajim.common import events
 from gajim.common import app
 from gajim.common import contacts
 from gajim.common import ged
-from gajim.common.connection_handlers_events import ChatstateReceivedEvent
 from gajim.common.const import KindConstant
 from gajim.gtk.single_message import SingleMessageWindow
 
@@ -97,7 +95,7 @@ class ChatControlSession:
                     self.control.change_resource(self.resource)
 
         if obj.mtype == 'chat':
-            if not obj.msgtxt and obj.chatstate is None:
+            if not obj.msgtxt:
                 return
 
             log_type = KindConstant.CHAT_MSG_RECV
@@ -142,27 +140,6 @@ class ChatControlSession:
             # joined. We log it silently without notification.
             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
             return True
 
@@ -189,7 +166,7 @@ class ChatControlSession:
         if app.interface.remote_ctrl:
             app.interface.remote_ctrl.raise_signal('NewMessage', (
                 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]))
 
     def roster_message2(self, obj):
diff --git a/test/unit/test_contacts.py b/test/unit/test_contacts.py
index a722621c5d..40127dabf3 100644
--- a/test/unit/test_contacts.py
+++ b/test/unit/test_contacts.py
@@ -14,9 +14,9 @@ from gajim.common import caps_cache
 class TestCommonContact(unittest.TestCase):
 
     def setUp(self):
-        self.contact = CommonContact(jid='', account="", resource='', show='',
-            status='', name='', our_chatstate=None, chatstate=None,
-            client_caps=None)
+        self.contact = CommonContact(
+            jid='', account="", resource='', show='',
+            status='', name='', chatstate=None, client_caps=None)
 
     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
         domain model by smoke testing that no attribute values are lost'''
 
-        attributes = ["jid", "resource", "show", "status", "name", "our_chatstate",
-            "chatstate", "client_caps", "priority", "sub"]
+        attributes = ["jid", "resource", "show", "status", "name",
+                      "chatstate", "client_caps", "priority", "sub"]
         for attr in attributes:
             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
         domain model by asserting no attributes have been lost'''
 
-        attributes = ["jid", "resource", "show", "status", "name", "our_chatstate",
-            "chatstate", "client_caps", "role", "room_jid"]
+        attributes = ["jid", "resource", "show", "status", "name",
+                      "chatstate", "client_caps", "role", "room_jid"]
         for attr in attributes:
             self.assertTrue(hasattr(self.contact, attr), msg="expected: " + attr)
 
-- 
GitLab