diff --git a/gajim/chat_control.py b/gajim/chat_control.py index 2a142ef02b9943f7868d56f2dcf4aed1a52d0a61..b932135571a7eab5f7672abcdb711622e6ac92f1 100644 --- a/gajim/chat_control.py +++ b/gajim/chat_control.py @@ -123,7 +123,7 @@ def __init__(self, parent_win, contact, acct, session, resource=None): # 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 @@ def __init__(self, parent_win, contact, acct, session, resource=None): ('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 @@ def add_actions(self): 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) @@ -338,6 +348,12 @@ def update_actions(self): tooltip_text = _('No File Transfer available') self.xml.sendfile_button.set_tooltip_text(tooltip_text) + # Chat markers + state = GLib.Variant.new_boolean( + self.contact.settings.get('send_marker')) + win.lookup_action( + f'send-marker-{self.control_id}').change_state(state) + # Convert to GC if app.settings.get_account_setting(self.account, 'is_zeroconf'): win.lookup_action( @@ -363,6 +379,7 @@ def remove_actions(self): 'information-', 'start-call-', 'send-chatstate-', + 'send-marker-', ] for action in actions: self.parent_win.window.remove_action(f'{action}{self.control_id}') @@ -435,6 +452,10 @@ def _on_send_chatstate(self, action, param): 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 +664,10 @@ def _on_message_sent(self, event): 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) @@ -1385,6 +1410,14 @@ def read_queue(self): 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)]) diff --git a/gajim/chat_control_base.py b/gajim/chat_control_base.py index bdb98846ca68de96f1befc9d34db0c4b99bbcce3..41f5516b49115513ff43fc3750d58cd2c7c8db1b 100644 --- a/gajim/chat_control_base.py +++ b/gajim/chat_control_base.py @@ -218,6 +218,9 @@ def __init__(self, parent_win, widget_name, contact, acct, 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 @@ -1116,6 +1119,7 @@ def add_message(self, displaymarking=None, msg_log_id=None, message_id=None, + stanza_id=None, correct_id=None, additional_data=None, marker=None, @@ -1159,6 +1163,12 @@ def add_message(self, if restored: return + if message_id: + if self._type.is_groupchat: + self.last_msg_id = stanza_id or message_id + else: + self.last_msg_id = message_id + if kind == 'incoming': if (not self._type.is_groupchat or self.contact.can_notify() or @@ -1203,7 +1213,10 @@ def add_message(self, 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) @@ -1355,7 +1368,7 @@ def set_control_active(self, state): 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, @@ -1363,6 +1376,12 @@ def set_control_active(self, state): 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(): @@ -1391,20 +1410,27 @@ def _on_edge_reached(self, _scrolledwindow, pos): 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: @@ -1469,6 +1495,8 @@ def redraw_after_event_removed(self, jid): 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() diff --git a/gajim/common/const.py b/gajim/common/const.py index 70a7dd4bbe706098aae2f00b1f008250ff85afa9..665fba11e277c6ccaef7bfe180b40306121891af 100644 --- a/gajim/common/const.py +++ b/gajim/common/const.py @@ -970,6 +970,7 @@ def is_error(self): Namespace.SECLABEL, Namespace.CONFERENCE, Namespace.CORRECT, + Namespace.CHATMARKERS, Namespace.EME, Namespace.XHTML_IM, Namespace.HASHES_2, diff --git a/gajim/common/events.py b/gajim/common/events.py index 647cdf5a63b029658608f762e29d34050edadef6..800fc6612e313fb36da0305fae479742095f5fef 100644 --- a/gajim/common/events.py +++ b/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' diff --git a/gajim/common/helpers.py b/gajim/common/helpers.py index 327330224aaedf47a9bb982f94396f13497ec4d6..bca56eb01b3a970c63883ac8f5e3a5b5a4563ad7 100644 --- a/gajim/common/helpers.py +++ b/gajim/common/helpers.py @@ -1406,3 +1406,13 @@ def get_group_chat_nick(account, room_jid): nick = bookmark.nick return nick + + +def get_muc_context(jid): + disco_info = app.storage.cache.get_last_disco_info(jid) + if disco_info is None: + return None + + if (disco_info.muc_is_members_only and disco_info.muc_is_nonanonymous): + return 'private' + return 'public' diff --git a/gajim/common/modules/__init__.py b/gajim/common/modules/__init__.py index 643981ca49717202bc916c5735ac13bbc0c771e3..c1d9e92fa071ecab26fb9fd2e5ce130e8dacda08 100644 --- a/gajim/common/modules/__init__.py +++ b/gajim/common/modules/__init__.py @@ -40,6 +40,7 @@ 'bookmarks', 'caps', 'carbons', + 'chat_markers', 'chatstates', 'delimiter', 'discovery', diff --git a/gajim/common/modules/chat_markers.py b/gajim/common/modules/chat_markers.py new file mode 100644 index 0000000000000000000000000000000000000000..930110cc0054549626509a911047a5dcd9e248e1 --- /dev/null +++ b/gajim/common/modules/chat_markers.py @@ -0,0 +1,123 @@ +# 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) + +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 +from gajim.common.structs import OutgoingMessage + + +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'): + disco_info = app.storage.cache.get_last_disco_info(jid) + + context = 'public' + if disco_info is not None and disco_info.muc_is_members_only: + context = 'private' + + if not app.settings.get_group_chat_setting( + self._account, jid, 'send_marker', context=context): + return + else: + if not app.settings.get_contact_setting( + self._account, jid, 'send_marker'): + return + + typ = 'groupchat' if type_ == 'gc' else 'chat' + message = OutgoingMessage(account=self._account, + contact=contact, + message=None, + type_=typ, + marker=(marker, id_), + play_sound=False) + self._con.send_message(message) + self._log.info('Send %s: %s', marker, contact.jid) + + 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' diff --git a/gajim/common/modules/message.py b/gajim/common/modules/message.py index 82ab7a97fbfe66df8691e866a928aa8ea168c4c2..92b47008a297186f411ec81b3d2cdfbbd11d918e 100644 --- a/gajim/common/modules/message.py +++ b/gajim/common/modules/message.py @@ -350,6 +350,13 @@ def build_message_stanza(self, message): stanza.setTag('no-store', namespace=Namespace.MSG_HINTS) + # XEP-0333 + if message.message: + stanza.setMarkable() + if message.marker: + marker, id_ = message.marker + stanza.setMarker(marker, id_) + # Add other nodes if message.nodes is not None: for node in message.nodes: diff --git a/gajim/common/setting_values.py b/gajim/common/setting_values.py index e13d05a8ea8482a807f090d3c9c5852e5ad1af36..94f328ffc94ff217a2427db077f116c51124367c 100644 --- a/gajim/common/setting_values.py +++ b/gajim/common/setting_values.py @@ -255,12 +255,16 @@ class _ACCOUNT_DEFAULT: 'filetransfer_preference': 'httpupload', 'send_chatstate_default': 'composing_only', 'gc_send_chatstate_default': 'composing_only', + 'send_marker_default': True, + 'gc_send_marker_private_default': True, + 'gc_send_marker_public_default': False, 'chat_history_max_age': -1, }, 'contact': { 'speller_language': '', 'send_chatstate': HAS_ACCOUNT_DEFAULT, + 'send_marker': HAS_ACCOUNT_DEFAULT, 'encryption': '', }, @@ -272,6 +276,7 @@ class _ACCOUNT_DEFAULT: 'minimize_on_autojoin': True, 'minimize_on_close': True, 'send_chatstate': HAS_ACCOUNT_DEFAULT, + 'send_marker': HAS_ACCOUNT_DEFAULT, 'encryption': '', 'sync_threshold': HAS_APP_DEFAULT, }, diff --git a/gajim/common/settings.py b/gajim/common/settings.py index 2f46f51c608c6abd1a9a0489f1556b8b56e0a8ba..d11ba1fdcce0fba9ea93adca2a299b69c666cc07 100644 --- a/gajim/common/settings.py +++ b/gajim/common/settings.py @@ -33,6 +33,7 @@ from gajim.common import app from gajim.common import configpaths from gajim.common import optparser +from gajim.common.helpers import get_muc_context from gajim.common.setting_values import APP_SETTINGS from gajim.common.setting_values import ACCOUNT_SETTINGS from gajim.common.setting_values import PROXY_SETTINGS @@ -678,11 +679,16 @@ def set_group_chat_setting(self, def set_group_chat_settings(self, setting: str, - value: SETTING_TYPE) -> None: + value: SETTING_TYPE, + context: str = None) -> None: for account in self._account_settings: for jid in self._account_settings[account]['group_chat']: - self.set_group_chat_setting(account, jid, setting, value) + if context is not None: + if get_muc_context(jid) != context: + continue + self.set_group_chat_setting( + account, jid, setting, value, context) def get_contact_setting(self, account: str, diff --git a/gajim/common/structs.py b/gajim/common/structs.py index 04cba87dbb14e7a9b8054dc2282aad1ffdbc587d..8d8d0347613c73c1928d0410d8d61156c4b165b9 100644 --- a/gajim/common/structs.py +++ b/gajim/common/structs.py @@ -55,6 +55,7 @@ def __init__(self, type_, subject=None, chatstate=None, + marker=None, resource=None, user_nick=None, label=None, @@ -69,7 +70,7 @@ def __init__(self, if type_ not in ('chat', 'groupchat', 'normal', 'headline'): raise ValueError('Unknown message type: %s' % type_) - if not message and chatstate is None: + if not message and chatstate is None and marker is None: raise ValueError('Trying to send message without content') self.account = account @@ -91,6 +92,7 @@ def __init__(self, self.subject = subject self.chatstate = chatstate + self.marker = marker self.resource = resource self.user_nick = user_nick self.label = label diff --git a/gajim/conversation_textview.py b/gajim/conversation_textview.py index 96b2794a9515265fb83f955d53e15e8f5d9398e5..ffb37a56d6edcd48d68974fb6567741f69396f27 100644 --- a/gajim/conversation_textview.py +++ b/gajim/conversation_textview.py @@ -38,6 +38,7 @@ 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 @@ def show_receipt(self, id_): 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 @@ def __init__(self, id_, timestamp, message_icons, start_mark): 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 @@ def __init__(self): '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 @@ def __init__(self): 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) diff --git a/gajim/data/style/gajim.css b/gajim/data/style/gajim.css index b9ccfcd639f2aca5329439005c8032a32e4df81d..8e4e3d60501d538c517bdc9d0a7ff2b094d50263 100644 --- a/gajim/data/style/gajim.css +++ b/gajim/data/style/gajim.css @@ -295,6 +295,7 @@ .unencrypted-color { color: @error_color; } /*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;} diff --git a/gajim/groupchat_control.py b/gajim/groupchat_control.py index 77954c930d52d3a636990d2be9b1062cc3ec6119..f2e047f9bb9ce04e3a09aa857e53e1e2df9074ce 100644 --- a/gajim/groupchat_control.py +++ b/gajim/groupchat_control.py @@ -658,6 +658,17 @@ def on_groupchat_maximize(self): 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) @@ -796,17 +807,21 @@ def _on_gc_message_received(self, event): else: if event.properties.muc_nickname == self.nick: self.last_sent_txt = event.msgtxt + stanza_id = None + if event.properties.stanza_id: + stanza_id = event.properties.stanza_id.id self.add_message(event.msgtxt, contact=event.properties.muc_nickname, tim=event.properties.timestamp, displaymarking=event.displaymarking, correct_id=event.correct_id, message_id=event.properties.id, + stanza_id=stanza_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 @@ -819,7 +834,9 @@ def on_private_message(self, nick, sent, msg, tim, session, additional_data, 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): @@ -837,7 +854,7 @@ def on_private_message(self, nick, sent, msg, tim, session, additional_data, 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 @@ -887,6 +904,7 @@ def add_message(self, text, contact='', tim=None, displaymarking=displaymarking, correct_id=correct_id, message_id=message_id, + stanza_id=stanza_id, additional_data=additional_data) def get_nb_unread(self): @@ -1057,6 +1075,7 @@ def _on_decrypted_message_received(self, event): event.properties.timestamp, self.session, event.additional_data, + event.properties.id, msg_log_id=event.msg_log_id, displaymarking=event.displaymarking) diff --git a/gajim/gtk/accounts.py b/gajim/gtk/accounts.py index cbd6c00e4f16481cf7b75810528b126790ffe19c..b7dd832bef7e3a7505fa2c850970554dbb552d9f 100644 --- a/gajim/gtk/accounts.py +++ b/gajim/gtk/accounts.py @@ -730,6 +730,19 @@ def __init__(self, account): 'button-style': 'destructive-action', 'button-callback': self._reset_gc_send_chatstate}), + Setting(SettingKind.SWITCH, + _('Send Read Markers'), + SettingType.VALUE, + app.settings.get_account_setting( + account, 'send_marker_default'), + callback=self._send_read_marker, + desc=_('Default for chats and private group chats'), + props={'button-text': _('Reset'), + 'button-tooltip': _('Reset all chats to the ' + 'current default value'), + 'button-style': 'destructive-action', + 'button-callback': self._reset_send_read_marker}), + Setting(SettingKind.POPOVER, _('Keep Chat History'), SettingType.ACCOUNT_CONFIG, @@ -749,6 +762,20 @@ def _reset_gc_send_chatstate(button): button.set_sensitive(False) app.settings.set_group_chat_settings('send_chatstate', None) + def _send_read_marker(self, state, _data): + app.settings.set_account_setting( + self._account, 'send_marker_default', state) + app.settings.set_account_setting( + self._account, 'gc_send_marker_private_default', state) + + def _reset_send_read_marker(self, button): + button.set_sensitive(False) + app.settings.set_contact_settings('send_marker', None) + app.settings.set_group_chat_settings( + 'send_marker', None, context='private') + for ctrl in app.interface.msg_win_mgr.get_controls(acct=self._account): + ctrl.update_actions() + class ConnectionPage(GenericSettingPage): diff --git a/gajim/gtk/groupchat_settings.py b/gajim/gtk/groupchat_settings.py index e8a39839f47531bdbc5853f227ac9473447ff8e4..20ff01496c7c86277185aad0e8fe603730bbc3bf 100644 --- a/gajim/gtk/groupchat_settings.py +++ b/gajim/gtk/groupchat_settings.py @@ -71,6 +71,13 @@ def __init__(self, account, jid, context): 'send_chatstate', props={'entries': chat_state}), + Setting(SettingKind.SWITCH, + _('Send Chat Markers'), + SettingType.GROUP_CHAT, + 'send_marker', + context=context, + desc=_('Let others know if you read up to this point')), + Setting(SettingKind.POPOVER, _('Sync Threshold'), SettingType.GROUP_CHAT, diff --git a/gajim/gui_interface.py b/gajim/gui_interface.py index 12973739e4990d4e5f96d859a0ad2b5aef4932ee..1c0801fbdc897ba1242036f86f3e6f94e2d08b93 100644 --- a/gajim/gui_interface.py +++ b/gajim/gui_interface.py @@ -242,6 +242,43 @@ def handle_event_presence(self, obj): 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 or events_[-1].message_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 @@ def create_core_handlers_list(self): '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): diff --git a/gajim/gui_menu_builder.py b/gajim/gui_menu_builder.py index d9b6c25ecc50f777c9102c52b334cc242d14c49b..407b612578ed35c7bb92c03c54765258c4ab9368 100644 --- a/gajim/gui_menu_builder.py +++ b/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 build_menu(preset): 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),