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),