Dont send chatstates when cycling MUC nicks

- Add ability to enable/disable the whole module so it doesnt try to send chatstates when we are offline
This commit is contained in:
Philipp Hörist 2019-01-04 14:58:27 +01:00
parent 4bd14bc51d
commit 4aca2eeae2
4 changed files with 101 additions and 9 deletions

View File

@ -823,7 +823,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
con = app.connections[self.account] con = app.connections[self.account]
con.get_module('Chatstate').set_keyboard_activity(self.contact) con.get_module('Chatstate').set_keyboard_activity(self.contact)
if not textview.has_text(): if not textview.has_text():
con.get_module('Chatstate').set_chatstate(self.contact, con.get_module('Chatstate').set_chatstate_delayed(self.contact,
Chatstate.ACTIVE) Chatstate.ACTIVE)
return return
con.get_module('Chatstate').set_chatstate(self.contact, con.get_module('Chatstate').set_chatstate(self.contact,

View File

@ -622,6 +622,7 @@ class Connection(CommonConnection, ConnectionHandlers):
self.get_module('Ping').remove_timeout() self.get_module('Ping').remove_timeout()
if self.connection is None: if self.connection is None:
if not reconnect: if not reconnect:
self.get_module('Chatstate').enabled = False
self._sm_resume_data = {} self._sm_resume_data = {}
self._disconnect() self._disconnect()
app.nec.push_incoming_event(OurShowEvent( app.nec.push_incoming_event(OurShowEvent(
@ -668,6 +669,7 @@ class Connection(CommonConnection, ConnectionHandlers):
self._set_reconnect_timer() self._set_reconnect_timer()
else: else:
self.get_module('Chatstate').enabled = False
self._sm_resume_data = {} self._sm_resume_data = {}
self._disconnect() self._disconnect()
app.nec.push_incoming_event(OurShowEvent( app.nec.push_incoming_event(OurShowEvent(
@ -1388,6 +1390,7 @@ class Connection(CommonConnection, ConnectionHandlers):
# state of all contacts # state of all contacts
app.nec.push_incoming_event(OurShowEvent( app.nec.push_incoming_event(OurShowEvent(
None, conn=self, show='offline')) None, conn=self, show='offline'))
self.get_module('Chatstate').enabled = False
def _on_resume_successful(self): def _on_resume_successful(self):
# Connection was successful, reset sm resume data # Connection was successful, reset sm resume data
@ -1422,6 +1425,7 @@ class Connection(CommonConnection, ConnectionHandlers):
self.retrycount = 0 self.retrycount = 0
self._discover_server() self._discover_server()
self._set_send_timeouts() self._set_send_timeouts()
self.get_module('Chatstate').enabled = True
def _set_send_timeouts(self): def _set_send_timeouts(self):
if app.config.get_per('accounts', self.name, 'keep_alives_enabled'): if app.config.get_per('accounts', self.name, 'keep_alives_enabled'):

View File

@ -16,11 +16,13 @@
from typing import Any from typing import Any
from typing import Dict # pylint: disable=unused-import from typing import Dict # pylint: disable=unused-import
from typing import List # pylint: disable=unused-import
from typing import Optional from typing import Optional
from typing import Tuple from typing import Tuple
import time import time
import logging import logging
from functools import wraps
import nbxmpp import nbxmpp
from gi.repository import GLib from gi.repository import GLib
@ -41,6 +43,15 @@ INACTIVE_AFTER = 60
PAUSED_AFTER = 5 PAUSED_AFTER = 5
def ensure_enabled(func):
@wraps(func)
def func_wrapper(self, *args, **kwargs):
if not self.enabled:
return
return func(self, *args, **kwargs)
return func_wrapper
def parse_chatstate(stanza: nbxmpp.Message) -> Optional[str]: def parse_chatstate(stanza: nbxmpp.Message) -> Optional[str]:
if parse_delay(stanza) is not None: if parse_delay(stanza) is not None:
return None return None
@ -60,13 +71,39 @@ class Chatstate:
self.handlers = [ self.handlers = [
('presence', self._presence_received), ('presence', self._presence_received),
] ]
# Our current chatstate with a specific contact
self._chatstates = {} # type: Dict[str, State] self._chatstates = {} # type: Dict[str, State]
self._last_keyboard_activity = {} # type: Dict[str, float] self._last_keyboard_activity = {} # type: Dict[str, float]
self._last_mouse_activity = {} # type: Dict[str, float] self._last_mouse_activity = {} # type: Dict[str, float]
self._timeout_id = None
self._delay_timeout_ids = {} # type: Dict[str, str]
self._blocked = [] # type: List[str]
self._enabled = False
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, value):
if self._enabled == value:
return
log.info('Chatstate module %s', 'enabled' if value else 'disabled')
self._enabled = value
if value:
self._timeout_id = GLib.timeout_add_seconds( self._timeout_id = GLib.timeout_add_seconds(
2, self._check_last_interaction) 2, self._check_last_interaction)
else:
self.cleanup()
self._chatstates = {}
self._last_keyboard_activity = {}
self._last_mouse_activity = {}
self._blocked = []
@ensure_enabled
def _presence_received(self, def _presence_received(self,
_con: ConnectionT, _con: ConnectionT,
stanza: nbxmpp.Presence) -> None: stanza: nbxmpp.Presence) -> None:
@ -135,6 +172,7 @@ class Chatstate:
account=self._account, account=self._account,
contact=contact)) contact=contact))
@ensure_enabled
def _check_last_interaction(self) -> GLib.SOURCE_CONTINUE: def _check_last_interaction(self) -> GLib.SOURCE_CONTINUE:
setting = app.config.get('outgoing_chat_state_notifications') setting = app.config.get('outgoing_chat_state_notifications')
if setting in ('composing_only', 'disabled'): if setting in ('composing_only', 'disabled'):
@ -183,6 +221,7 @@ class Chatstate:
return GLib.SOURCE_CONTINUE return GLib.SOURCE_CONTINUE
@ensure_enabled
def set_active(self, jid: str) -> None: def set_active(self, jid: str) -> None:
self._last_mouse_activity[jid] = time.time() self._last_mouse_activity[jid] = time.time()
setting = app.config.get('outgoing_chat_state_notifications') setting = app.config.get('outgoing_chat_state_notifications')
@ -207,7 +246,36 @@ class Chatstate:
self.set_active(contact.jid) self.set_active(contact.jid)
return 'active' return 'active'
@ensure_enabled
def block_chatstates(self, contact: ContactT, block: bool) -> None:
# Block sending chatstates to a contact
# Used for example if we cycle through the MUC nick list, which
# produces a lot of text-changed signals from the textview. This
# Would lead to sending ACTIVE -> COMPOSING -> ACTIVE ...
if block:
self._blocked.append(contact.jid)
else:
self._blocked.remove(contact.jid)
@ensure_enabled
def set_chatstate_delayed(self, contact: ContactT, state: State) -> None:
# Used when we go from Composing -> Active after deleting all text
# from the Textview. We delay the Active state because maybe the
# User starts writing again.
self.remove_delay_timeout(contact)
self._delay_timeout_ids[contact.jid] = GLib.timeout_add_seconds(
2, self.set_chatstate, contact, state)
@ensure_enabled
def set_chatstate(self, contact: ContactT, state: State) -> None: def set_chatstate(self, contact: ContactT, state: State) -> None:
# Dont send chatstates to ourself
if self._con.get_own_jid().bareMatch(contact.jid):
return
if contact.jid in self._blocked:
return
self.remove_delay_timeout(contact)
current_state = self._chatstates.get(contact.jid) current_state = self._chatstates.get(contact.jid)
setting = app.config.get('outgoing_chat_state_notifications') setting = app.config.get('outgoing_chat_state_notifications')
if setting == 'disabled': if setting == 'disabled':
@ -255,10 +323,6 @@ class Chatstate:
if current_state == state: if current_state == state:
return return
# Dont send chatstates to ourself
if self._con.get_own_jid().bareMatch(contact.jid):
return
log.info('Send: %-10s - %s', state, contact.jid) log.info('Send: %-10s - %s', state, contact.jid)
event_attrs = {'account': self._account, event_attrs = {'account': self._account,
@ -275,6 +339,7 @@ class Chatstate:
self._chatstates[contact.jid] = state self._chatstates[contact.jid] = state
@ensure_enabled
def set_mouse_activity(self, contact: ContactT) -> None: def set_mouse_activity(self, contact: ContactT) -> None:
self._last_mouse_activity[contact.jid] = time.time() self._last_mouse_activity[contact.jid] = time.time()
setting = app.config.get('outgoing_chat_state_notifications') setting = app.config.get('outgoing_chat_state_notifications')
@ -283,10 +348,24 @@ class Chatstate:
if self._chatstates.get(contact.jid) == State.INACTIVE: if self._chatstates.get(contact.jid) == State.INACTIVE:
self.set_chatstate(contact, State.ACTIVE) self.set_chatstate(contact, State.ACTIVE)
@ensure_enabled
def set_keyboard_activity(self, contact: ContactT) -> None: def set_keyboard_activity(self, contact: ContactT) -> None:
self._last_keyboard_activity[contact.jid] = time.time() self._last_keyboard_activity[contact.jid] = time.time()
def remove_delay_timeout(self, contact):
timeout = self._delay_timeout_ids.get(contact.jid)
if timeout is not None:
GLib.source_remove(timeout)
del self._delay_timeout_ids[contact.jid]
def remove_all_delay_timeouts(self):
for timeout in self._delay_timeout_ids.values():
GLib.source_remove(timeout)
self._delay_timeout_ids = {}
def cleanup(self): def cleanup(self):
self.remove_all_delay_timeouts()
if self._timeout_id is not None:
GLib.source_remove(self._timeout_id) GLib.source_remove(self._timeout_id)

View File

@ -1620,6 +1620,9 @@ class GroupchatControl(ChatControlBase):
self.is_connected = False self.is_connected = False
ChatControlBase.got_disconnected(self) ChatControlBase.got_disconnected(self)
con = app.connections[self.account]
con.get_module('Chatstate').remove_delay_timeout(self.contact)
contact = app.contacts.get_groupchat_contact(self.account, contact = app.contacts.get_groupchat_contact(self.account,
self.room_jid) self.room_jid)
if contact is not None: if contact is not None:
@ -2550,6 +2553,9 @@ class GroupchatControl(ChatControlBase):
else: else:
start_iter.backward_chars(len(begin)) start_iter.backward_chars(len(begin))
con = app.connections[self.account]
con.get_module('Chatstate').block_chatstates(self.contact, True)
message_buffer.delete(start_iter, end_iter) message_buffer.delete(start_iter, end_iter)
# get a shell-like completion # get a shell-like completion
# if there's more than one nick for this completion, complete # if there's more than one nick for this completion, complete
@ -2579,6 +2585,9 @@ class GroupchatControl(ChatControlBase):
else: else:
completion = self.nick_hits[0] completion = self.nick_hits[0]
message_buffer.insert_at_cursor(completion + add) message_buffer.insert_at_cursor(completion + add)
con.get_module('Chatstate').block_chatstates(self.contact, False)
self.last_key_tabs = True self.last_key_tabs = True
return True return True
self.last_key_tabs = False self.last_key_tabs = False