From 4aca2eeae2e38a49916e81b65e49951197f7801e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Fri, 4 Jan 2019 14:58:27 +0100 Subject: [PATCH] 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 --- gajim/chat_control_base.py | 4 +- gajim/common/connection.py | 4 ++ gajim/common/modules/chatstates.py | 93 +++++++++++++++++++++++++++--- gajim/groupchat_control.py | 9 +++ 4 files changed, 101 insertions(+), 9 deletions(-) diff --git a/gajim/chat_control_base.py b/gajim/chat_control_base.py index aad86c0f6..6d095cf66 100644 --- a/gajim/chat_control_base.py +++ b/gajim/chat_control_base.py @@ -823,8 +823,8 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): con = app.connections[self.account] con.get_module('Chatstate').set_keyboard_activity(self.contact) if not textview.has_text(): - con.get_module('Chatstate').set_chatstate(self.contact, - Chatstate.ACTIVE) + con.get_module('Chatstate').set_chatstate_delayed(self.contact, + Chatstate.ACTIVE) return con.get_module('Chatstate').set_chatstate(self.contact, Chatstate.COMPOSING) diff --git a/gajim/common/connection.py b/gajim/common/connection.py index cab939ca5..99f8c1de6 100644 --- a/gajim/common/connection.py +++ b/gajim/common/connection.py @@ -622,6 +622,7 @@ class Connection(CommonConnection, ConnectionHandlers): self.get_module('Ping').remove_timeout() if self.connection is None: if not reconnect: + self.get_module('Chatstate').enabled = False self._sm_resume_data = {} self._disconnect() app.nec.push_incoming_event(OurShowEvent( @@ -668,6 +669,7 @@ class Connection(CommonConnection, ConnectionHandlers): self._set_reconnect_timer() else: + self.get_module('Chatstate').enabled = False self._sm_resume_data = {} self._disconnect() app.nec.push_incoming_event(OurShowEvent( @@ -1388,6 +1390,7 @@ class Connection(CommonConnection, ConnectionHandlers): # state of all contacts app.nec.push_incoming_event(OurShowEvent( None, conn=self, show='offline')) + self.get_module('Chatstate').enabled = False def _on_resume_successful(self): # Connection was successful, reset sm resume data @@ -1422,6 +1425,7 @@ class Connection(CommonConnection, ConnectionHandlers): self.retrycount = 0 self._discover_server() self._set_send_timeouts() + self.get_module('Chatstate').enabled = True def _set_send_timeouts(self): if app.config.get_per('accounts', self.name, 'keep_alives_enabled'): diff --git a/gajim/common/modules/chatstates.py b/gajim/common/modules/chatstates.py index c740de663..fe74c9039 100644 --- a/gajim/common/modules/chatstates.py +++ b/gajim/common/modules/chatstates.py @@ -16,11 +16,13 @@ from typing import Any from typing import Dict # pylint: disable=unused-import +from typing import List # pylint: disable=unused-import from typing import Optional from typing import Tuple import time import logging +from functools import wraps import nbxmpp from gi.repository import GLib @@ -41,6 +43,15 @@ INACTIVE_AFTER = 60 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]: if parse_delay(stanza) is not None: return None @@ -60,13 +71,39 @@ class Chatstate: self.handlers = [ ('presence', self._presence_received), ] + + # Our current chatstate with a specific contact 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 = None + self._delay_timeout_ids = {} # type: Dict[str, str] + self._blocked = [] # type: List[str] + self._enabled = False - self._timeout_id = GLib.timeout_add_seconds( - 2, self._check_last_interaction) + @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( + 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, _con: ConnectionT, stanza: nbxmpp.Presence) -> None: @@ -135,6 +172,7 @@ class Chatstate: account=self._account, contact=contact)) + @ensure_enabled def _check_last_interaction(self) -> GLib.SOURCE_CONTINUE: setting = app.config.get('outgoing_chat_state_notifications') if setting in ('composing_only', 'disabled'): @@ -183,6 +221,7 @@ class Chatstate: return GLib.SOURCE_CONTINUE + @ensure_enabled def set_active(self, jid: str) -> None: self._last_mouse_activity[jid] = time.time() setting = app.config.get('outgoing_chat_state_notifications') @@ -207,7 +246,36 @@ class Chatstate: self.set_active(contact.jid) 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: + # 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) setting = app.config.get('outgoing_chat_state_notifications') if setting == 'disabled': @@ -255,10 +323,6 @@ class Chatstate: 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, @@ -275,6 +339,7 @@ class Chatstate: self._chatstates[contact.jid] = state + @ensure_enabled def set_mouse_activity(self, contact: ContactT) -> None: self._last_mouse_activity[contact.jid] = time.time() setting = app.config.get('outgoing_chat_state_notifications') @@ -283,11 +348,25 @@ class Chatstate: if self._chatstates.get(contact.jid) == State.INACTIVE: self.set_chatstate(contact, State.ACTIVE) + @ensure_enabled def set_keyboard_activity(self, contact: ContactT) -> None: 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): - GLib.source_remove(self._timeout_id) + self.remove_all_delay_timeouts() + if self._timeout_id is not None: + GLib.source_remove(self._timeout_id) def get_instance(*args: Any, **kwargs: Any) -> Tuple[Chatstate, str]: diff --git a/gajim/groupchat_control.py b/gajim/groupchat_control.py index ac6d515b1..c4763a0e9 100644 --- a/gajim/groupchat_control.py +++ b/gajim/groupchat_control.py @@ -1620,6 +1620,9 @@ class GroupchatControl(ChatControlBase): self.is_connected = False 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, self.room_jid) if contact is not None: @@ -2550,6 +2553,9 @@ class GroupchatControl(ChatControlBase): else: 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) # get a shell-like completion # if there's more than one nick for this completion, complete @@ -2579,6 +2585,9 @@ class GroupchatControl(ChatControlBase): else: completion = self.nick_hits[0] message_buffer.insert_at_cursor(completion + add) + + con.get_module('Chatstate').block_chatstates(self.contact, False) + self.last_key_tabs = True return True self.last_key_tabs = False