Implement chatstate in MUC, as defined in XEP-0085 §5.5.
This commit is contained in:
parent
78b562f7a5
commit
f6739730af
|
@ -188,14 +188,7 @@ class ChatControl(ChatControlBase):
|
|||
self.bigger_avatar_window = None
|
||||
self.show_avatar()
|
||||
|
||||
# chatstate timers and state
|
||||
self.reset_kbd_mouse_timeout_vars()
|
||||
self._schedule_activity_timers()
|
||||
|
||||
# Hook up signals
|
||||
id_ = self.parent_win.window.connect('motion-notify-event',
|
||||
self._on_window_motion_notify)
|
||||
self.handlers[id_] = self.parent_win.window
|
||||
message_tv_buffer = self.msg_textview.get_buffer()
|
||||
id_ = message_tv_buffer.connect('changed',
|
||||
self._on_message_tv_buffer_changed)
|
||||
|
@ -672,21 +665,6 @@ class ChatControl(ChatControlBase):
|
|||
cursor = gtkgui_helpers.get_cursor('HAND2')
|
||||
self.parent_win.window.get_window().set_cursor(cursor)
|
||||
|
||||
def _on_window_motion_notify(self, widget, event):
|
||||
"""
|
||||
It gets called no matter if it is the active window or not
|
||||
"""
|
||||
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
|
||||
|
||||
def _schedule_activity_timers(self):
|
||||
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 update_ui(self):
|
||||
# The name banner is drawn here
|
||||
ChatControlBase.update_ui(self)
|
||||
|
@ -976,9 +954,6 @@ class ChatControl(ChatControlBase):
|
|||
if message in ('', None, '\n'):
|
||||
return None
|
||||
|
||||
# refresh timers
|
||||
self.reset_kbd_mouse_timeout_vars()
|
||||
|
||||
contact = self.contact
|
||||
|
||||
encrypted = bool(self.session) and self.session.enable_encryption
|
||||
|
@ -990,10 +965,8 @@ class ChatControl(ChatControlBase):
|
|||
if not keyID:
|
||||
keyID = 'UNKNOWN'
|
||||
|
||||
chatstates_on = gajim.config.get('outgoing_chat_state_notifications') != \
|
||||
'disabled'
|
||||
chatstate_to_send = None
|
||||
if chatstates_on and contact is not None:
|
||||
if contact is not None:
|
||||
if contact.supports(NS_CHATSTATES):
|
||||
# send active chatstate on every message (as XEP says)
|
||||
chatstate_to_send = 'active'
|
||||
|
@ -1031,65 +1004,6 @@ class ChatControl(ChatControlBase):
|
|||
process_commands=process_commands,
|
||||
attention=attention)
|
||||
|
||||
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
|
||||
return False # stop looping
|
||||
|
||||
message_buffer = self.msg_textview.get_buffer()
|
||||
if (self.kbd_activity_in_last_5_secs and
|
||||
message_buffer.get_char_count()):
|
||||
# Only composing if the keyboard activity was in text entry
|
||||
self.send_chatstate('composing', self.contact)
|
||||
elif (self.mouse_over_in_last_5_secs and
|
||||
current_state == 'inactive' and
|
||||
jid == self.parent_win.get_active_jid()):
|
||||
self.send_chatstate('active', self.contact)
|
||||
else:
|
||||
if current_state == 'composing':
|
||||
self.send_chatstate('paused', self.contact) # pause composing
|
||||
|
||||
# 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
|
||||
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 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_cancel_session_negotiation(self):
|
||||
msg = _('Session negotiation cancelled')
|
||||
ChatControlBase.print_conversation_line(self, msg, 'status', '', None)
|
||||
|
@ -1391,9 +1305,6 @@ class ChatControl(ChatControlBase):
|
|||
if self.session:
|
||||
self.session.control = None
|
||||
|
||||
# Disconnect timer callbacks
|
||||
GLib.source_remove(self.possible_paused_timeout_id)
|
||||
GLib.source_remove(self.possible_inactive_timeout_id)
|
||||
# Remove bigger avatar window
|
||||
if self.bigger_avatar_window:
|
||||
self.bigger_avatar_window.destroy()
|
||||
|
@ -1482,20 +1393,6 @@ class ChatControl(ChatControlBase):
|
|||
|
||||
def set_control_active(self, state):
|
||||
ChatControlBase.set_control_active(self, state)
|
||||
# send chatstate inactive to the one we're leaving
|
||||
# and active to the one we visit
|
||||
if state:
|
||||
message_buffer = self.msg_textview.get_buffer()
|
||||
if message_buffer.get_char_count():
|
||||
self.send_chatstate('paused', self.contact)
|
||||
else:
|
||||
self.send_chatstate('active', self.contact)
|
||||
self.reset_kbd_mouse_timeout_vars()
|
||||
GLib.source_remove(self.possible_paused_timeout_id)
|
||||
GLib.source_remove(self.possible_inactive_timeout_id)
|
||||
self._schedule_activity_timers()
|
||||
else:
|
||||
self.send_chatstate('inactive', self.contact)
|
||||
# Hide bigger avatar window
|
||||
if self.bigger_avatar_window:
|
||||
self.bigger_avatar_window.destroy()
|
||||
|
@ -1580,11 +1477,8 @@ class ChatControl(ChatControlBase):
|
|||
dialogs.TransformChatToMUC(self.account, [c.jid], [dropped_jid])
|
||||
|
||||
def _on_message_tv_buffer_changed(self, textbuffer):
|
||||
self.kbd_activity_in_last_5_secs = True
|
||||
self.kbd_activity_in_last_30_secs = True
|
||||
super()._on_message_tv_buffer_changed(textbuffer)
|
||||
if textbuffer.get_char_count():
|
||||
self.send_chatstate('composing', self.contact)
|
||||
|
||||
e2e_is_active = self.session and \
|
||||
self.session.enable_encryption
|
||||
e2e_pref = gajim.config.get_per('accounts', self.account,
|
||||
|
@ -1600,8 +1494,6 @@ class ChatControl(ChatControlBase):
|
|||
elif (not self.session or not self.session.status) and \
|
||||
gajim.connections[self.account].archiving_136_supported:
|
||||
self.begin_archiving_negotiation()
|
||||
else:
|
||||
self.send_chatstate('active', self.contact)
|
||||
|
||||
def restore_conversation(self):
|
||||
jid = self.contact.jid
|
||||
|
|
|
@ -383,6 +383,18 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
|
|||
self.command_hits = []
|
||||
self.last_key_tabs = False
|
||||
|
||||
# chatstate timers and state
|
||||
self.reset_kbd_mouse_timeout_vars()
|
||||
self._schedule_activity_timers()
|
||||
message_tv_buffer = self.msg_textview.get_buffer()
|
||||
id_ = message_tv_buffer.connect('changed',
|
||||
self._on_message_tv_buffer_changed)
|
||||
self.handlers[id_] = message_tv_buffer
|
||||
if parent_win is not None:
|
||||
id_ = parent_win.window.connect('motion-notify-event',
|
||||
self._on_window_motion_notify)
|
||||
self.handlers[id_] = parent_win.window
|
||||
|
||||
# PluginSystem: adding GUI extension point for ChatControlBase
|
||||
# instance object (also subclasses, eg. ChatControl or GroupchatControl)
|
||||
gajim.plugin_manager.gui_extension_point('chat_control_base', self)
|
||||
|
@ -448,6 +460,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
|
|||
|
||||
def shutdown(self):
|
||||
super(ChatControlBase, self).shutdown()
|
||||
# Disconnect timer callbacks
|
||||
GLib.source_remove(self.possible_paused_timeout_id)
|
||||
GLib.source_remove(self.possible_inactive_timeout_id)
|
||||
# PluginSystem: removing GUI extension points connected with ChatControlBase
|
||||
# instance object
|
||||
gajim.plugin_manager.remove_gui_extension_point('chat_control_base',
|
||||
|
@ -683,6 +698,12 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
|
|||
if process_commands and self.process_as_command(message):
|
||||
return
|
||||
|
||||
# refresh timers
|
||||
self.reset_kbd_mouse_timeout_vars()
|
||||
|
||||
if gajim.config.get('outgoing_chat_state_notifications') == 'disabled':
|
||||
chatstate = None
|
||||
|
||||
label = self.get_seclabel()
|
||||
|
||||
def _cb(obj, msg, cb, *cb_args):
|
||||
|
@ -712,6 +733,88 @@ 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
|
||||
return False # stop looping
|
||||
|
||||
message_buffer = self.msg_textview.get_buffer()
|
||||
if (self.kbd_activity_in_last_5_secs and
|
||||
message_buffer.get_char_count()):
|
||||
# Only composing if the keyboard activity was in text entry
|
||||
self.send_chatstate('composing', self.contact)
|
||||
elif (self.mouse_over_in_last_5_secs and
|
||||
current_state == 'inactive' and
|
||||
jid == self.parent_win.get_active_jid()):
|
||||
self.send_chatstate('active', self.contact)
|
||||
else:
|
||||
if current_state == 'composing':
|
||||
self.send_chatstate('paused', self.contact) # pause composing
|
||||
|
||||
# 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
|
||||
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):
|
||||
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
|
||||
"""
|
||||
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
|
||||
|
||||
def _on_message_tv_buffer_changed(self, textbuffer):
|
||||
self.kbd_activity_in_last_5_secs = True
|
||||
self.kbd_activity_in_last_30_secs = True
|
||||
if textbuffer.get_char_count():
|
||||
self.send_chatstate('composing', self.contact)
|
||||
else:
|
||||
self.send_chatstate('active', self.contact)
|
||||
|
||||
def save_message(self, message, msg_type):
|
||||
# save the message, so user can scroll though the list with key up/down
|
||||
if msg_type == 'sent':
|
||||
|
@ -982,6 +1085,19 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
|
|||
types=type_):
|
||||
# There were events to remove
|
||||
self.redraw_after_event_removed(jid)
|
||||
# send chatstate inactive to the one we're leaving
|
||||
# and active to the one we visit
|
||||
message_buffer = self.msg_textview.get_buffer()
|
||||
if message_buffer.get_char_count():
|
||||
self.send_chatstate('paused', self.contact)
|
||||
else:
|
||||
self.send_chatstate('active', self.contact)
|
||||
self.reset_kbd_mouse_timeout_vars()
|
||||
GLib.source_remove(self.possible_paused_timeout_id)
|
||||
GLib.source_remove(self.possible_inactive_timeout_id)
|
||||
self._schedule_activity_timers()
|
||||
else:
|
||||
self.send_chatstate('inactive', self.contact)
|
||||
|
||||
def scroll_to_end_iter(self):
|
||||
self.conv_textview.scroll_to_end_iter()
|
||||
|
|
|
@ -2719,6 +2719,8 @@ class Connection(CommonConnection, ConnectionHandlers):
|
|||
obj.xhtml = create_xhtml(obj.message)
|
||||
msg_iq = nbxmpp.Message(obj.jid, obj.message, typ='groupchat',
|
||||
xhtml=obj.xhtml)
|
||||
if obj.chatstate:
|
||||
msg_iq.setTag(obj.chatstate, namespace=nbxmpp.NS_CHATSTATES)
|
||||
if obj.label is not None:
|
||||
msg_iq.addChild(node=obj.label)
|
||||
gajim.nec.push_incoming_event(GcStanzaMessageOutgoingEvent(
|
||||
|
|
|
@ -2769,6 +2769,7 @@ class GcMessageOutgoingEvent(nec.NetworkOutgoingEvent):
|
|||
def init(self):
|
||||
self.additional_data = {}
|
||||
self.message = ''
|
||||
self.chatstate = None
|
||||
self.xhtml = None
|
||||
self.label = None
|
||||
self.callback = None
|
||||
|
|
|
@ -343,7 +343,7 @@ class GroupchatControl(ChatControlBase):
|
|||
img.set_from_icon_name('document-open-recent', Gtk.IconSize.MENU)
|
||||
|
||||
self.current_tooltip = None
|
||||
if parent_win:
|
||||
if parent_win is not None:
|
||||
# On AutoJoin with minimize Groupchats are created without parent
|
||||
# Tooltip Window has to be created with parent
|
||||
self.set_tooltip()
|
||||
|
@ -2035,6 +2035,53 @@ class GroupchatControl(ChatControlBase):
|
|||
|
||||
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 = gajim.config.get('outgoing_chat_state_notifications')
|
||||
if chatstate_setting == 'disabled':
|
||||
return
|
||||
|
||||
elif 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 wel're inactive prevent composing (XEP violation)
|
||||
if contact.our_chatstate == 'inactive' and state == 'composing':
|
||||
# go active before
|
||||
gajim.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()
|
||||
|
||||
gajim.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
|
||||
|
|
|
@ -3215,6 +3215,9 @@ class RosterWindow:
|
|||
if not mw:
|
||||
mw = gajim.interface.msg_win_mgr.create_window(ctrl.contact,
|
||||
ctrl.account, ctrl.type_id)
|
||||
id_ = mw.window.connect('motion-notify-event',
|
||||
ctrl._on_window_motion_notify)
|
||||
ctrl.handlers[id_] = mw.window
|
||||
ctrl.parent_win = mw
|
||||
ctrl.set_tooltip()
|
||||
mw.new_tab(ctrl)
|
||||
|
|
Loading…
Reference in New Issue