Daniel Brötzmann pushed to branch chat_markers at gajim / gajim
Commits: 8be9e7f4 by lovetox at 2020-10-11T16:56:33+02:00 ServerInfo: Adapt to nbxmpp changes
- - - - - 4e5c3820 by Yann Leboulanger at 2020-10-12T18:49:03+02:00 Add Chat Markers (XEP-0333)
- - - - - 9850a0cc by wurstsalat at 2020-10-12T20:28:51+02:00 Improvements
- - - - -
15 changed files:
- gajim/chat_control.py - gajim/chat_control_base.py - gajim/common/const.py - gajim/common/events.py - gajim/common/modules/__init__.py - + gajim/common/modules/chat_markers.py - gajim/common/modules/message.py - gajim/common/setting_values.py - gajim/conversation_textview.py - gajim/data/style/gajim.css - gajim/groupchat_control.py - gajim/gtk/groupchat_settings.py - gajim/gtk/server_info.py - gajim/gui_interface.py - gajim/gui_menu_builder.py
Changes:
===================================== gajim/chat_control.py ===================================== @@ -123,7 +123,7 @@ class ChatControl(ChatControlBase):
# Menu for the HeaderBar self.control_menu = gui_menu_builder.get_singlechat_menu( - self.control_id, self.account, self.contact.jid) + self.control_id, self.account, self.contact.jid, self._type)
# Settings menu self.xml.settings_menu.set_menu_model(self.control_menu) @@ -228,6 +228,7 @@ class ChatControl(ChatControlBase): ('mam-decrypted-message-received', ged.GUI1, self._on_mam_decrypted_message_received), ('decrypted-message-received', ged.GUI1, self._on_decrypted_message_received), ('receipt-received', ged.GUI1, self._receipt_received), + ('displayed-received', ged.GUI1, self._displayed_received), ('message-error', ged.GUI1, self._on_message_error), ('zeroconf-error', ged.GUI1, self._on_zeroconf_error), ]) @@ -271,6 +272,15 @@ class ChatControl(ChatControlBase): act.connect('change-state', self._on_send_chatstate) self.parent_win.window.add_action(act)
+ marker = self.contact.settings.get('send_marker') + + act = Gio.SimpleAction.new_stateful( + f'send-marker-{self.control_id}', + None, + GLib.Variant.new_boolean(marker)) + act.connect('change-state', self._on_send_marker) + self.parent_win.window.add_action(act) + def update_actions(self): win = self.parent_win.window online = app.account_is_connected(self.account) @@ -363,6 +373,7 @@ class ChatControl(ChatControlBase): 'information-', 'start-call-', 'send-chatstate-', + 'send-marker', ] for action in actions: self.parent_win.window.remove_action(f'{action}{self.control_id}') @@ -435,6 +446,10 @@ class ChatControl(ChatControlBase): action.set_state(param) self.contact.settings.set('send_chatstate', param.get_string())
+ def _on_send_marker(self, action, param): + action.set_state(param) + self.contact.settings.set('send_marker', param.get_boolean()) + def subscribe_events(self): """ Register listeners to the events class @@ -643,6 +658,10 @@ class ChatControl(ChatControlBase): def _receipt_received(self, event): self.conv_textview.show_receipt(event.receipt_id)
+ @event_filter(['account', 'jid']) + def _displayed_received(self, event): + self.conv_textview.show_displayed(event.marker_id) + @event_filter(['account', 'jid']) def _on_zeroconf_error(self, event): self.add_status_message(event.message) @@ -1384,6 +1403,14 @@ class ChatControl(ChatControlBase): self.set_session(event.session) if message_ids: app.storage.archive.set_read_messages(message_ids) + + # XEP-0333 Send <displayed> marker + con = app.connections[self.account] + con.get_module('ChatMarkers').send_displayed_marker( + self.contact, + self.last_msg_id, + self._type) + self.last_msg_id = None app.events.remove_events(self.account, jid_with_resource, types=[str(self._type)])
===================================== gajim/chat_control_base.py ===================================== @@ -214,6 +214,9 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper): self.received_history_pos = 0 self.orig_msg = None
+ # For XEP-0333 + self.last_msg_id = None + self.correcting = False self.last_sent_msg = None
@@ -1111,6 +1114,7 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper): displaymarking=None, msg_log_id=None, message_id=None, + stanza_id=None, correct_id=None, additional_data=None, marker=None, @@ -1154,6 +1158,12 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper): if restored: return
+ if message_id: + if self._type.is_groupchat: + self.last_msg_id = stanza_id + else: + self.last_msg_id = message_id + if kind == 'incoming': if (not self._type.is_groupchat or self.contact.can_notify() or @@ -1198,7 +1208,10 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
event = event_type(text, subject, - self, msg_log_id, + self, + msg_log_id, + message_id=message_id, + stanza_id=stanza_id, show_in_roster=show_in_roster, show_in_systray=show_in_systray) app.events.add_event(self.account, full_jid, event) @@ -1350,7 +1363,7 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper): jid = self.contact.jid if self.conv_textview.autoscroll: # we are at the end - type_ = ['printed_%s' % self._type] + type_ = [f'printed_{self._type}'] if self._type.is_groupchat: type_ = ['printed_gc_msg', 'printed_marked_gc_msg'] if not app.events.remove_events(self.account, @@ -1358,6 +1371,12 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper): types=type_): # There were events to remove self.redraw_after_event_removed(jid) + # XEP-0333 Send <displayed> marker + con.get_module('ChatMarkers').send_displayed_marker( + self.contact, + self.last_msg_id, + self._type) + self.last_msg_id = None # send chatstate inactive to the one we're leaving # and active to the one we visit if self.msg_textview.has_text(): @@ -1386,20 +1405,27 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper): types_list = [] if self._type.is_groupchat: types_list = ['printed_gc_msg', 'gc_msg', 'printed_marked_gc_msg'] - else: # Not a GC - types_list = ['printed_%s' % self._type, str(self._type)] + else: + types_list = [f'printed_{self._type}', str(self._type)]
if not app.events.get_events(self.account, jid, types_list): return if not self.parent_win: return - if self.parent_win.get_active_control() == self and \ - self.parent_win.window.is_active(): + if (self.parent_win.get_active_control() == self and + self.parent_win.window.is_active()): # we are at the end if not app.events.remove_events( self.account, jid, types=types_list): # There were events to remove self.redraw_after_event_removed(jid) + # XEP-0333 Send <displayed> tag + con = app.connections[self.account] + con.get_module('ChatMarkers').send_displayed_marker( + self.contact, + self.last_msg_id, + self._type) + self.last_msg_id = None
def _on_scrollbar_button_release(self, scrollbar, event): if event.get_button()[1] != 1: @@ -1464,6 +1490,8 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper): We just removed a 'printed_*' event, redraw contact in roster or gc_roster and titles in roster and msg_win """ + if not self.parent_win: # minimized groupchat + return self.parent_win.redraw_tab(self) self.parent_win.show_title() # TODO : get the contact and check get_show_in_roster()
===================================== gajim/common/const.py ===================================== @@ -970,6 +970,7 @@ COMMON_FEATURES = [ Namespace.SECLABEL, Namespace.CONFERENCE, Namespace.CORRECT, + Namespace.CHATMARKERS, Namespace.EME, Namespace.XHTML_IM, Namespace.HASHES_2,
===================================== gajim/common/events.py ===================================== @@ -91,13 +91,16 @@ class PmEvent(ChatEvent): class PrintedChatEvent(Event): type_ = 'printed_chat' def __init__(self, message, subject, control, msg_log_id, time_=None, - show_in_roster=False, show_in_systray=True): + message_id=None, stanza_id=None, show_in_roster=False, + show_in_systray=True): Event.__init__(self, time_, show_in_roster=show_in_roster, - show_in_systray=show_in_systray) + show_in_systray=show_in_systray) self.message = message self.subject = subject self.control = control self.msg_log_id = msg_log_id + self.message_id = message_id + self.stanza_id = stanza_id
class PrintedGcMsgEvent(PrintedChatEvent): type_ = 'printed_gc_msg'
===================================== gajim/common/modules/__init__.py ===================================== @@ -40,6 +40,7 @@ MODULES = [ 'bookmarks', 'caps', 'carbons', + 'chat_markers', 'chatstates', 'delimiter', 'discovery',
===================================== gajim/common/modules/chat_markers.py ===================================== @@ -0,0 +1,113 @@ +# This file is part of Gajim. +# +# Gajim is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# Gajim is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Gajim. If not, see http://www.gnu.org/licenses/. + +# Chat Markers (XEP-0333) + +import nbxmpp +from nbxmpp.namespaces import Namespace +from nbxmpp.structs import StanzaHandler + +from gajim.common import app +from gajim.common.nec import NetworkEvent +from gajim.common.modules.base import BaseModule + + +class ChatMarkers(BaseModule): + + _nbxmpp_extends = 'ChatMarkers' + + def __init__(self, con): + BaseModule.__init__(self, con) + + self.handlers = [ + StanzaHandler(name='message', + callback=self._process_chat_marker, + ns=Namespace.CHATMARKERS, + priority=47), + ] + + def _process_chat_marker(self, _con, _stanza, properties): + if not properties.is_marker or not properties.marker.is_displayed: + return + + if properties.type.is_error: + return + + if properties.type.is_groupchat: + manager = self._con.get_module('MUC').get_manager() + muc_data = manager.get(properties.muc_jid) + if muc_data is None: + return + + if properties.muc_nickname != muc_data.nick: + return + + self._raise_event('read-state-sync', properties) + return + + if properties.is_carbon_message and properties.carbon.is_sent: + self._raise_event('read-state-sync', properties) + return + + if properties.is_mam_message: + if properties.from_.bareMatch(self._con.get_own_jid()): + return + + self._raise_event('displayed-received', properties) + + def _raise_event(self, name, properties): + self._log.info('%s: %s %s', + name, + properties.jid, + properties.marker.id) + + jid = properties.jid + if not properties.is_muc_pm and not properties.type.is_groupchat: + jid = properties.jid.bare + + app.nec.push_outgoing_event( + NetworkEvent(name, + account=self._account, + jid=jid, + properties=properties, + type=properties.type, + is_muc_pm=properties.is_muc_pm, + marker_id=properties.marker.id)) + + def _send_marker(self, contact, marker, id_, type_): + jid = contact.jid + if contact.is_pm_contact: + jid = app.get_jid_without_resource(contact.jid) + + if type_ in ('gc', 'pm'): + if not app.settings.get_group_chat_setting( + self._account, jid, 'send_marker'): + return + else: + if not app.settings.get_contact_setting( + self._account, jid, 'send_marker'): + return + + typ = 'groupchat' if type_ == 'gc' else 'chat' + message = nbxmpp.Message(to=contact.jid, typ=typ) + message.setMarker(marker, id_) + self._log.info('Send %s: %s', marker, contact.jid) + self._nbxmpp().send(message) + + def send_displayed_marker(self, contact, id_, type_): + self._send_marker(contact, 'displayed', id_, str(type_)) + + +def get_instance(*args, **kwargs): + return ChatMarkers(*args, **kwargs), 'ChatMarkers'
===================================== gajim/common/modules/message.py ===================================== @@ -305,6 +305,8 @@ class Message(BaseModule): stanza.setID(message.message_id) stanza.setOriginID(message.message_id)
+ stanza.setMarkable() + if message.label: stanza.addChild(node=message.label)
@@ -347,6 +349,10 @@ class Message(BaseModule): stanza.setTag('no-store', namespace=Namespace.MSG_HINTS)
+ # XEP-0333 + if message.message: + stanza.setMarkable() + # Add other nodes if message.nodes is not None: for node in message.nodes:
===================================== gajim/common/setting_values.py ===================================== @@ -255,12 +255,15 @@ ACCOUNT_SETTINGS = { 'filetransfer_preference': 'httpupload', 'send_chatstate_default': 'composing_only', 'gc_send_chatstate_default': 'composing_only', + 'send_marker_default': True, + 'gc_send_marker_default': True, 'chat_history_max_age': -1, },
'contact': { 'speller_language': '', 'send_chatstate': HAS_ACCOUNT_DEFAULT, + 'send_marker': HAS_ACCOUNT_DEFAULT, 'encryption': '', },
@@ -272,6 +275,7 @@ ACCOUNT_SETTINGS = { 'minimize_on_autojoin': True, 'minimize_on_close': True, 'send_chatstate': HAS_ACCOUNT_DEFAULT, + 'send_marker': HAS_ACCOUNT_DEFAULT, 'encryption': '', 'sync_threshold': HAS_APP_DEFAULT, },
===================================== gajim/conversation_textview.py ===================================== @@ -38,6 +38,7 @@ from gajim.common import app from gajim.common import helpers from gajim.common import i18n from gajim.common.i18n import _ +from gajim.common.i18n import Q_ from gajim.common.helpers import AdditionalDataDict from gajim.common.const import StyleAttr from gajim.common.const import Trust @@ -311,6 +312,12 @@ class ConversationTextview(GObject.GObject): return line.set_receipt()
+ def show_displayed(self, id_): + line = self._get_message_line(id_) + if line is None: + return + line.set_displayed() + def show_error(self, id_, error): line = self._get_message_line(id_) if line is None: @@ -1194,16 +1201,27 @@ class MessageLine: self.timestamp = timestamp self.start_mark = start_mark self._has_receipt = False + self._has_displayed = False self._message_icons = message_icons
@property def has_receipt(self): return self._has_receipt
+ @property + def has_displayed(self): + return self._has_displayed + def set_receipt(self): self._has_receipt = True + if self._has_displayed: + return self._message_icons.set_receipt_icon_visible(True)
+ def set_displayed(self): + self._has_displayed = True + self._message_icons.set_displayed_icon_visible(True) + def set_correction(self, tooltip): self._message_icons.set_correction_icon_visible(True) self._message_icons.set_correction_tooltip(tooltip) @@ -1222,12 +1240,12 @@ class MessageIcons(Gtk.Box): 'document-edit-symbolic', Gtk.IconSize.MENU) self._correction_image.set_no_show_all(True)
- self._receipt_image = Gtk.Image.new_from_icon_name( + self._marker_image = Gtk.Image.new_from_icon_name( 'emblem-ok-symbolic', Gtk.IconSize.MENU) - self._receipt_image.get_style_context().add_class( + self._marker_image.get_style_context().add_class( 'receipt-received-color') - self._receipt_image.set_tooltip_text(_('Received')) - self._receipt_image.set_no_show_all(True) + self._marker_image.set_tooltip_text(_('Received')) + self._marker_image.set_no_show_all(True)
self._error_image = Gtk.Image.new_from_icon_name( 'dialog-warning-symbolic', Gtk.IconSize.MENU) @@ -1235,14 +1253,22 @@ class MessageIcons(Gtk.Box): self._error_image.set_no_show_all(True)
self.add(self._correction_image) - self.add(self._receipt_image) + self.add(self._marker_image) self.add(self._error_image) self.show_all()
def set_receipt_icon_visible(self, visible): if not app.settings.get('positive_184_ack'): return - self._receipt_image.set_visible(visible) + self._marker_image.set_visible(visible) + + def set_displayed_icon_visible(self, visible): + self._marker_image.set_visible(visible) + self._marker_image.get_style_context().remove_class( + 'receipt-received-color') + self._marker_image.get_style_context().add_class( + 'displayed-received-color') + self._marker_image.set_tooltip_text(Q_('?Message state:Read'))
def set_correction_icon_visible(self, visible): self._correction_image.set_visible(visible)
===================================== gajim/data/style/gajim.css ===================================== @@ -295,6 +295,7 @@ button.flat.link { padding: 0; border: 0; }
/*Receipts*/ .receipt-received-color { color: rgb(75, 181, 67); } +.displayed-received-color { color: rgb(0, 141, 242); }
/*Dataforms*/ .field-fixed { font-size: 16px; font-weight: bold; padding-top:5px;}
===================================== gajim/groupchat_control.py ===================================== @@ -656,6 +656,17 @@ class GroupchatControl(ChatControlBase): self.update_actions() self.set_lock_image() self.draw_banner_text() + type_ = ['printed_gc_msg', 'printed_marked_gc_msg'] + if not app.events.remove_events(self.account, + self.get_full_jid(), + types=type_): + # XEP-0333 Send <displayed> marker + con = app.connections[self.account] + con.get_module('ChatMarkers').send_displayed_marker( + self.contact, + self.last_msg_id, + self._type) + self.last_msg_id = None
def _on_roster_row_activated(self, _roster, nick): self._start_private_message(nick) @@ -800,11 +811,12 @@ class GroupchatControl(ChatControlBase): displaymarking=event.displaymarking, correct_id=event.correct_id, message_id=event.properties.id, + stanza_id=event.properties.stanza_id.id, additional_data=event.additional_data) event.needs_highlight = self.needs_visual_notification(event.msgtxt)
def on_private_message(self, nick, sent, msg, tim, session, additional_data, - msg_log_id=None, displaymarking=None): + message_id, msg_log_id=None, displaymarking=None): # Do we have a queue? fjid = self.room_jid + '/' + nick
@@ -817,7 +829,9 @@ class GroupchatControl(ChatControlBase): session=session, displaymarking=displaymarking, sent_forwarded=sent, - additional_data=additional_data) + additional_data=additional_data, + message_id=message_id) + app.events.add_event(self.account, fjid, event)
if allow_popup_window(self.account): @@ -835,7 +849,7 @@ class GroupchatControl(ChatControlBase):
def add_message(self, text, contact='', tim=None, displaymarking=None, correct_id=None, message_id=None, - additional_data=None): + stanza_id=None, additional_data=None): """ Add message to the ConversationsTextview
@@ -885,6 +899,7 @@ class GroupchatControl(ChatControlBase): displaymarking=displaymarking, correct_id=correct_id, message_id=message_id, + stanza_id=stanza_id, additional_data=additional_data)
def get_nb_unread(self): @@ -1055,6 +1070,7 @@ class GroupchatControl(ChatControlBase): event.properties.timestamp, self.session, event.additional_data, + event.properties.id, msg_log_id=event.msg_log_id, displaymarking=event.displaymarking)
===================================== gajim/gtk/groupchat_settings.py ===================================== @@ -71,6 +71,12 @@ class GroupChatSettings(SettingsBox): 'send_chatstate', props={'entries': chat_state}),
+ Setting(SettingKind.SWITCH, + _('Send Chat Markers'), + SettingType.GROUP_CHAT, + 'send_marker', + desc=_('Let others know if you read up to this point')), + Setting(SettingKind.POPOVER, _('Sync Threshold'), SettingType.GROUP_CHAT,
===================================== gajim/gtk/server_info.py ===================================== @@ -17,7 +17,7 @@ from collections import namedtuple from datetime import timedelta
import nbxmpp -from nbxmpp.util import is_error_result +from nbxmpp.errors import StanzaError from nbxmpp.namespaces import Namespace from gi.repository import Gtk from gi.repository import Gdk @@ -28,7 +28,6 @@ from gajim.common import ged from gajim.common.helpers import open_uri from gajim.common.i18n import _
-from gajim.gtk.util import ensure_not_destroyed from gajim.gtk.util import get_builder from gajim.gtk.util import EventHelper from gajim.gtk.util import open_window @@ -233,12 +232,14 @@ class ServerInfo(Gtk.ApplicationWindow, EventHelper): 'days': delta.days, 'hours': hours} self._ui.server_uptime.set_text(uptime)
- @ensure_not_destroyed - def _software_version_received(self, result): - if is_error_result(result): + def _software_version_received(self, task): + try: + result = task.finish() + except StanzaError: self.version = _('Unknown') else: self.version = '%s %s' % (result.name, result.version) + self._ui.server_software.set_text(self.version)
@staticmethod
===================================== gajim/gui_interface.py ===================================== @@ -242,6 +242,43 @@ class Interface: if ctrl and ctrl.session and len(obj.contact_list) > 1: ctrl.remove_session(ctrl.session)
+ @staticmethod + def handle_event_read_state_sync(event): + if event.type.is_groupchat: + control = app.get_groupchat_control( + event.account, event.jid.bare) + if control is None: + log.warning('Groupchat control not found') + return + + jid = event.jid.bare + types = ['printed_gc_msg', 'printed_marked_gc_msg'] + + else: + types = ['chat', 'pm', 'printed_chat', 'printed_pm'] + jid = event.jid + + control = app.interface.msg_win_mgr.get_control(jid, event.account) + + # Compare with control.last_msg_id. + events_ = app.events.get_events(event.account, jid, types) + if not events_: + log.warning('No Events') + return + + if event.type.is_groupchat: + id_ = events_[-1].stanza_id + else: + id_ = events_[-1].message_id + + if id_ != event.marker_id: + return + + if not app.events.remove_events(event.account, jid, types=types): + # There were events to remove + if control is not None: + control.redraw_after_event_removed(event.jid) + @staticmethod def handle_event_msgsent(obj): if not obj.play_sound: @@ -1060,6 +1097,7 @@ class Interface: 'unsubscribed-presence-received': [ self.handle_event_unsubscribed_presence], 'zeroconf-name-conflict': [self.handle_event_zc_name_conflict], + 'read-state-sync': [self.handle_event_read_state_sync], }
def register_core_handlers(self):
===================================== gajim/gui_menu_builder.py ===================================== @@ -511,12 +511,13 @@ def get_transport_menu(contact, account): return menu
-def get_singlechat_menu(control_id, account, jid): +def get_singlechat_menu(control_id, account, jid, type_): singlechat_menu = [ (_('Send File'), [ ('win.send-file-httpupload-', _('Upload File…')), ('win.send-file-jingle-', _('Send File Directly…')), ]), + ('win.send-marker-', _('Send Read Markers')), (_('Send Chatstate'), ['chatstate']), ('win.invite-contacts-', _('Invite Contacts…')), ('win.add-to-roster-', _('Add to Contact List…')), @@ -545,6 +546,9 @@ def get_singlechat_menu(control_id, account, jid): for item in preset: if isinstance(item[1], str): action_name, label = item + if action_name == 'win.send-marker-' and type_ == 'pm': + continue + if action_name == 'app.browse-history': menuitem = Gio.MenuItem.new(label, action_name) dict_ = {'account': GLib.Variant('s', account),
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/01e6065007e4468169c55397da9d691b...