Philipp Hörist pushed to branch master at gajim / gajim

Commits:

18 changed files:

Changes:

  • gajim/chat_control.py
    ... ... @@ -123,7 +123,7 @@ def __init__(self, parent_win, contact, acct, session, resource=None):
    123 123
     
    
    124 124
             # Menu for the HeaderBar
    
    125 125
             self.control_menu = gui_menu_builder.get_singlechat_menu(
    
    126
    -            self.control_id, self.account, self.contact.jid)
    
    126
    +            self.control_id, self.account, self.contact.jid, self._type)
    
    127 127
     
    
    128 128
             # Settings menu
    
    129 129
             self.xml.settings_menu.set_menu_model(self.control_menu)
    
    ... ... @@ -228,6 +228,7 @@ def __init__(self, parent_win, contact, acct, session, resource=None):
    228 228
                 ('mam-decrypted-message-received', ged.GUI1, self._on_mam_decrypted_message_received),
    
    229 229
                 ('decrypted-message-received', ged.GUI1, self._on_decrypted_message_received),
    
    230 230
                 ('receipt-received', ged.GUI1, self._receipt_received),
    
    231
    +            ('displayed-received', ged.GUI1, self._displayed_received),
    
    231 232
                 ('message-error', ged.GUI1, self._on_message_error),
    
    232 233
                 ('zeroconf-error', ged.GUI1, self._on_zeroconf_error),
    
    233 234
             ])
    
    ... ... @@ -271,6 +272,15 @@ def add_actions(self):
    271 272
             act.connect('change-state', self._on_send_chatstate)
    
    272 273
             self.parent_win.window.add_action(act)
    
    273 274
     
    
    275
    +        marker = self.contact.settings.get('send_marker')
    
    276
    +
    
    277
    +        act = Gio.SimpleAction.new_stateful(
    
    278
    +            f'send-marker-{self.control_id}',
    
    279
    +            None,
    
    280
    +            GLib.Variant.new_boolean(marker))
    
    281
    +        act.connect('change-state', self._on_send_marker)
    
    282
    +        self.parent_win.window.add_action(act)
    
    283
    +
    
    274 284
         def update_actions(self):
    
    275 285
             win = self.parent_win.window
    
    276 286
             online = app.account_is_connected(self.account)
    
    ... ... @@ -338,6 +348,12 @@ def update_actions(self):
    338 348
                 tooltip_text = _('No File Transfer available')
    
    339 349
             self.xml.sendfile_button.set_tooltip_text(tooltip_text)
    
    340 350
     
    
    351
    +        # Chat markers
    
    352
    +        state = GLib.Variant.new_boolean(
    
    353
    +            self.contact.settings.get('send_marker'))
    
    354
    +        win.lookup_action(
    
    355
    +            f'send-marker-{self.control_id}').change_state(state)
    
    356
    +
    
    341 357
             # Convert to GC
    
    342 358
             if app.settings.get_account_setting(self.account, 'is_zeroconf'):
    
    343 359
                 win.lookup_action(
    
    ... ... @@ -363,6 +379,7 @@ def remove_actions(self):
    363 379
                 'information-',
    
    364 380
                 'start-call-',
    
    365 381
                 'send-chatstate-',
    
    382
    +            'send-marker-',
    
    366 383
             ]
    
    367 384
             for action in actions:
    
    368 385
                 self.parent_win.window.remove_action(f'{action}{self.control_id}')
    
    ... ... @@ -435,6 +452,10 @@ def _on_send_chatstate(self, action, param):
    435 452
             action.set_state(param)
    
    436 453
             self.contact.settings.set('send_chatstate', param.get_string())
    
    437 454
     
    
    455
    +    def _on_send_marker(self, action, param):
    
    456
    +        action.set_state(param)
    
    457
    +        self.contact.settings.set('send_marker', param.get_boolean())
    
    458
    +
    
    438 459
         def subscribe_events(self):
    
    439 460
             """
    
    440 461
             Register listeners to the events class
    
    ... ... @@ -643,6 +664,10 @@ def _on_message_sent(self, event):
    643 664
         def _receipt_received(self, event):
    
    644 665
             self.conv_textview.show_receipt(event.receipt_id)
    
    645 666
     
    
    667
    +    @event_filter(['account', 'jid'])
    
    668
    +    def _displayed_received(self, event):
    
    669
    +        self.conv_textview.show_displayed(event.marker_id)
    
    670
    +
    
    646 671
         @event_filter(['account', 'jid'])
    
    647 672
         def _on_zeroconf_error(self, event):
    
    648 673
             self.add_status_message(event.message)
    
    ... ... @@ -1385,6 +1410,14 @@ def read_queue(self):
    1385 1410
                     self.set_session(event.session)
    
    1386 1411
             if message_ids:
    
    1387 1412
                 app.storage.archive.set_read_messages(message_ids)
    
    1413
    +
    
    1414
    +        # XEP-0333 Send <displayed> marker
    
    1415
    +        con = app.connections[self.account]
    
    1416
    +        con.get_module('ChatMarkers').send_displayed_marker(
    
    1417
    +            self.contact,
    
    1418
    +            self.last_msg_id,
    
    1419
    +            self._type)
    
    1420
    +        self.last_msg_id = None
    
    1388 1421
             app.events.remove_events(self.account,
    
    1389 1422
                                      jid_with_resource,
    
    1390 1423
                                      types=[str(self._type)])
    

  • gajim/chat_control_base.py
    ... ... @@ -218,6 +218,9 @@ def __init__(self, parent_win, widget_name, contact, acct,
    218 218
             self.received_history_pos = 0
    
    219 219
             self.orig_msg = None
    
    220 220
     
    
    221
    +        # For XEP-0333
    
    222
    +        self.last_msg_id = None
    
    223
    +
    
    221 224
             self.correcting = False
    
    222 225
             self.last_sent_msg = None
    
    223 226
     
    
    ... ... @@ -1116,6 +1119,7 @@ def add_message(self,
    1116 1119
                         displaymarking=None,
    
    1117 1120
                         msg_log_id=None,
    
    1118 1121
                         message_id=None,
    
    1122
    +                    stanza_id=None,
    
    1119 1123
                         correct_id=None,
    
    1120 1124
                         additional_data=None,
    
    1121 1125
                         marker=None,
    
    ... ... @@ -1159,6 +1163,12 @@ def add_message(self,
    1159 1163
             if restored:
    
    1160 1164
                 return
    
    1161 1165
     
    
    1166
    +        if message_id:
    
    1167
    +            if self._type.is_groupchat:
    
    1168
    +                self.last_msg_id = stanza_id or message_id
    
    1169
    +            else:
    
    1170
    +                self.last_msg_id = message_id
    
    1171
    +
    
    1162 1172
             if kind == 'incoming':
    
    1163 1173
                 if (not self._type.is_groupchat or
    
    1164 1174
                         self.contact.can_notify() or
    
    ... ... @@ -1203,7 +1213,10 @@ def add_message(self,
    1203 1213
     
    
    1204 1214
                     event = event_type(text,
    
    1205 1215
                                        subject,
    
    1206
    -                                   self, msg_log_id,
    
    1216
    +                                   self,
    
    1217
    +                                   msg_log_id,
    
    1218
    +                                   message_id=message_id,
    
    1219
    +                                   stanza_id=stanza_id,
    
    1207 1220
                                        show_in_roster=show_in_roster,
    
    1208 1221
                                        show_in_systray=show_in_systray)
    
    1209 1222
                     app.events.add_event(self.account, full_jid, event)
    
    ... ... @@ -1355,7 +1368,7 @@ def set_control_active(self, state):
    1355 1368
                 jid = self.contact.jid
    
    1356 1369
                 if self.conv_textview.autoscroll:
    
    1357 1370
                     # we are at the end
    
    1358
    -                type_ = ['printed_%s' % self._type]
    
    1371
    +                type_ = [f'printed_{self._type}']
    
    1359 1372
                     if self._type.is_groupchat:
    
    1360 1373
                         type_ = ['printed_gc_msg', 'printed_marked_gc_msg']
    
    1361 1374
                     if not app.events.remove_events(self.account,
    
    ... ... @@ -1363,6 +1376,12 @@ def set_control_active(self, state):
    1363 1376
                                                     types=type_):
    
    1364 1377
                         # There were events to remove
    
    1365 1378
                         self.redraw_after_event_removed(jid)
    
    1379
    +                    # XEP-0333 Send <displayed> marker
    
    1380
    +                    con.get_module('ChatMarkers').send_displayed_marker(
    
    1381
    +                        self.contact,
    
    1382
    +                        self.last_msg_id,
    
    1383
    +                        self._type)
    
    1384
    +                    self.last_msg_id = None
    
    1366 1385
                 # send chatstate inactive to the one we're leaving
    
    1367 1386
                 # and active to the one we visit
    
    1368 1387
                 if self.msg_textview.has_text():
    
    ... ... @@ -1391,20 +1410,27 @@ def _on_edge_reached(self, _scrolledwindow, pos):
    1391 1410
             types_list = []
    
    1392 1411
             if self._type.is_groupchat:
    
    1393 1412
                 types_list = ['printed_gc_msg', 'gc_msg', 'printed_marked_gc_msg']
    
    1394
    -        else: # Not a GC
    
    1395
    -            types_list = ['printed_%s' % self._type, str(self._type)]
    
    1413
    +        else:
    
    1414
    +            types_list = [f'printed_{self._type}', str(self._type)]
    
    1396 1415
     
    
    1397 1416
             if not app.events.get_events(self.account, jid, types_list):
    
    1398 1417
                 return
    
    1399 1418
             if not self.parent_win:
    
    1400 1419
                 return
    
    1401
    -        if self.parent_win.get_active_control() == self and \
    
    1402
    -        self.parent_win.window.is_active():
    
    1420
    +        if (self.parent_win.get_active_control() == self and
    
    1421
    +                self.parent_win.window.is_active()):
    
    1403 1422
                 # we are at the end
    
    1404 1423
                 if not app.events.remove_events(
    
    1405 1424
                         self.account, jid, types=types_list):
    
    1406 1425
                     # There were events to remove
    
    1407 1426
                     self.redraw_after_event_removed(jid)
    
    1427
    +                # XEP-0333 Send <displayed> tag
    
    1428
    +                con = app.connections[self.account]
    
    1429
    +                con.get_module('ChatMarkers').send_displayed_marker(
    
    1430
    +                    self.contact,
    
    1431
    +                    self.last_msg_id,
    
    1432
    +                    self._type)
    
    1433
    +                self.last_msg_id = None
    
    1408 1434
     
    
    1409 1435
         def _on_scrollbar_button_release(self, scrollbar, event):
    
    1410 1436
             if event.get_button()[1] != 1:
    
    ... ... @@ -1469,6 +1495,8 @@ def redraw_after_event_removed(self, jid):
    1469 1495
             We just removed a 'printed_*' event, redraw contact in roster or
    
    1470 1496
             gc_roster and titles in roster and msg_win
    
    1471 1497
             """
    
    1498
    +        if not self.parent_win:  # minimized groupchat
    
    1499
    +            return
    
    1472 1500
             self.parent_win.redraw_tab(self)
    
    1473 1501
             self.parent_win.show_title()
    
    1474 1502
             # TODO : get the contact and check get_show_in_roster()
    

  • gajim/common/const.py
    ... ... @@ -970,6 +970,7 @@ def is_error(self):
    970 970
         Namespace.SECLABEL,
    
    971 971
         Namespace.CONFERENCE,
    
    972 972
         Namespace.CORRECT,
    
    973
    +    Namespace.CHATMARKERS,
    
    973 974
         Namespace.EME,
    
    974 975
         Namespace.XHTML_IM,
    
    975 976
         Namespace.HASHES_2,
    

  • gajim/common/events.py
    ... ... @@ -91,13 +91,16 @@ class PmEvent(ChatEvent):
    91 91
     class PrintedChatEvent(Event):
    
    92 92
         type_ = 'printed_chat'
    
    93 93
         def __init__(self, message, subject, control, msg_log_id, time_=None,
    
    94
    -    show_in_roster=False, show_in_systray=True):
    
    94
    +                 message_id=None, stanza_id=None, show_in_roster=False,
    
    95
    +                 show_in_systray=True):
    
    95 96
             Event.__init__(self, time_, show_in_roster=show_in_roster,
    
    96
    -            show_in_systray=show_in_systray)
    
    97
    +                       show_in_systray=show_in_systray)
    
    97 98
             self.message = message
    
    98 99
             self.subject = subject
    
    99 100
             self.control = control
    
    100 101
             self.msg_log_id = msg_log_id
    
    102
    +        self.message_id = message_id
    
    103
    +        self.stanza_id = stanza_id
    
    101 104
     
    
    102 105
     class PrintedGcMsgEvent(PrintedChatEvent):
    
    103 106
         type_ = 'printed_gc_msg'
    

  • gajim/common/helpers.py
    ... ... @@ -1406,3 +1406,13 @@ def get_group_chat_nick(account, room_jid):
    1406 1406
                 nick = bookmark.nick
    
    1407 1407
     
    
    1408 1408
         return nick
    
    1409
    +
    
    1410
    +
    
    1411
    +def get_muc_context(jid):
    
    1412
    +    disco_info = app.storage.cache.get_last_disco_info(jid)
    
    1413
    +    if disco_info is None:
    
    1414
    +        return None
    
    1415
    +
    
    1416
    +    if (disco_info.muc_is_members_only and disco_info.muc_is_nonanonymous):
    
    1417
    +        return 'private'
    
    1418
    +    return 'public'

  • gajim/common/modules/__init__.py
    ... ... @@ -40,6 +40,7 @@
    40 40
         'bookmarks',
    
    41 41
         'caps',
    
    42 42
         'carbons',
    
    43
    +    'chat_markers',
    
    43 44
         'chatstates',
    
    44 45
         'delimiter',
    
    45 46
         'discovery',
    

  • gajim/common/modules/chat_markers.py
    1
    +# This file is part of Gajim.
    
    2
    +#
    
    3
    +# Gajim is free software; you can redistribute it and/or modify
    
    4
    +# it under the terms of the GNU General Public License as published
    
    5
    +# by the Free Software Foundation; version 3 only.
    
    6
    +#
    
    7
    +# Gajim is distributed in the hope that it will be useful,
    
    8
    +# but WITHOUT ANY WARRANTY; without even the implied warranty of
    
    9
    +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    
    10
    +# GNU General Public License for more details.
    
    11
    +#
    
    12
    +# You should have received a copy of the GNU General Public License
    
    13
    +# along with Gajim.  If not, see <http://www.gnu.org/licenses/>.
    
    14
    +
    
    15
    +# Chat Markers (XEP-0333)
    
    16
    +
    
    17
    +from nbxmpp.namespaces import Namespace
    
    18
    +from nbxmpp.structs import StanzaHandler
    
    19
    +
    
    20
    +from gajim.common import app
    
    21
    +from gajim.common.nec import NetworkEvent
    
    22
    +from gajim.common.modules.base import BaseModule
    
    23
    +from gajim.common.structs import OutgoingMessage
    
    24
    +
    
    25
    +
    
    26
    +class ChatMarkers(BaseModule):
    
    27
    +
    
    28
    +    _nbxmpp_extends = 'ChatMarkers'
    
    29
    +
    
    30
    +    def __init__(self, con):
    
    31
    +        BaseModule.__init__(self, con)
    
    32
    +
    
    33
    +        self.handlers = [
    
    34
    +            StanzaHandler(name='message',
    
    35
    +                          callback=self._process_chat_marker,
    
    36
    +                          ns=Namespace.CHATMARKERS,
    
    37
    +                          priority=47),
    
    38
    +        ]
    
    39
    +
    
    40
    +    def _process_chat_marker(self, _con, _stanza, properties):
    
    41
    +        if not properties.is_marker or not properties.marker.is_displayed:
    
    42
    +            return
    
    43
    +
    
    44
    +        if properties.type.is_error:
    
    45
    +            return
    
    46
    +
    
    47
    +        if properties.type.is_groupchat:
    
    48
    +            manager = self._con.get_module('MUC').get_manager()
    
    49
    +            muc_data = manager.get(properties.muc_jid)
    
    50
    +            if muc_data is None:
    
    51
    +                return
    
    52
    +
    
    53
    +            if properties.muc_nickname != muc_data.nick:
    
    54
    +                return
    
    55
    +
    
    56
    +            self._raise_event('read-state-sync', properties)
    
    57
    +            return
    
    58
    +
    
    59
    +        if properties.is_carbon_message and properties.carbon.is_sent:
    
    60
    +            self._raise_event('read-state-sync', properties)
    
    61
    +            return
    
    62
    +
    
    63
    +        if properties.is_mam_message:
    
    64
    +            if properties.from_.bareMatch(self._con.get_own_jid()):
    
    65
    +                return
    
    66
    +
    
    67
    +        self._raise_event('displayed-received', properties)
    
    68
    +
    
    69
    +    def _raise_event(self, name, properties):
    
    70
    +        self._log.info('%s: %s %s',
    
    71
    +                       name,
    
    72
    +                       properties.jid,
    
    73
    +                       properties.marker.id)
    
    74
    +
    
    75
    +        jid = properties.jid
    
    76
    +        if not properties.is_muc_pm and not properties.type.is_groupchat:
    
    77
    +            jid = properties.jid.bare
    
    78
    +
    
    79
    +        app.nec.push_outgoing_event(
    
    80
    +            NetworkEvent(name,
    
    81
    +                         account=self._account,
    
    82
    +                         jid=jid,
    
    83
    +                         properties=properties,
    
    84
    +                         type=properties.type,
    
    85
    +                         is_muc_pm=properties.is_muc_pm,
    
    86
    +                         marker_id=properties.marker.id))
    
    87
    +
    
    88
    +    def _send_marker(self, contact, marker, id_, type_):
    
    89
    +        jid = contact.jid
    
    90
    +        if contact.is_pm_contact:
    
    91
    +            jid = app.get_jid_without_resource(contact.jid)
    
    92
    +
    
    93
    +        if type_ in ('gc', 'pm'):
    
    94
    +            disco_info = app.storage.cache.get_last_disco_info(jid)
    
    95
    +
    
    96
    +            context = 'public'
    
    97
    +            if disco_info is not None and disco_info.muc_is_members_only:
    
    98
    +                context = 'private'
    
    99
    +
    
    100
    +            if not app.settings.get_group_chat_setting(
    
    101
    +                    self._account, jid, 'send_marker', context=context):
    
    102
    +                return
    
    103
    +        else:
    
    104
    +            if not app.settings.get_contact_setting(
    
    105
    +                    self._account, jid, 'send_marker'):
    
    106
    +                return
    
    107
    +
    
    108
    +        typ = 'groupchat' if type_ == 'gc' else 'chat'
    
    109
    +        message = OutgoingMessage(account=self._account,
    
    110
    +                                  contact=contact,
    
    111
    +                                  message=None,
    
    112
    +                                  type_=typ,
    
    113
    +                                  marker=(marker, id_),
    
    114
    +                                  play_sound=False)
    
    115
    +        self._con.send_message(message)
    
    116
    +        self._log.info('Send %s: %s', marker, contact.jid)
    
    117
    +
    
    118
    +    def send_displayed_marker(self, contact, id_, type_):
    
    119
    +        self._send_marker(contact, 'displayed', id_, str(type_))
    
    120
    +
    
    121
    +
    
    122
    +def get_instance(*args, **kwargs):
    
    123
    +    return ChatMarkers(*args, **kwargs), 'ChatMarkers'

  • gajim/common/modules/message.py
    ... ... @@ -350,6 +350,13 @@ def build_message_stanza(self, message):
    350 350
                     stanza.setTag('no-store',
    
    351 351
                                   namespace=Namespace.MSG_HINTS)
    
    352 352
     
    
    353
    +        # XEP-0333
    
    354
    +        if message.message:
    
    355
    +            stanza.setMarkable()
    
    356
    +        if message.marker:
    
    357
    +            marker, id_ = message.marker
    
    358
    +            stanza.setMarker(marker, id_)
    
    359
    +
    
    353 360
             # Add other nodes
    
    354 361
             if message.nodes is not None:
    
    355 362
                 for node in message.nodes:
    

  • gajim/common/setting_values.py
    ... ... @@ -255,12 +255,16 @@ class _ACCOUNT_DEFAULT:
    255 255
             'filetransfer_preference': 'httpupload',
    
    256 256
             'send_chatstate_default': 'composing_only',
    
    257 257
             'gc_send_chatstate_default': 'composing_only',
    
    258
    +        'send_marker_default': True,
    
    259
    +        'gc_send_marker_private_default': True,
    
    260
    +        'gc_send_marker_public_default': False,
    
    258 261
             'chat_history_max_age': -1,
    
    259 262
         },
    
    260 263
     
    
    261 264
         'contact': {
    
    262 265
             'speller_language': '',
    
    263 266
             'send_chatstate': HAS_ACCOUNT_DEFAULT,
    
    267
    +        'send_marker': HAS_ACCOUNT_DEFAULT,
    
    264 268
             'encryption': '',
    
    265 269
         },
    
    266 270
     
    
    ... ... @@ -272,6 +276,7 @@ class _ACCOUNT_DEFAULT:
    272 276
             'minimize_on_autojoin': True,
    
    273 277
             'minimize_on_close': True,
    
    274 278
             'send_chatstate': HAS_ACCOUNT_DEFAULT,
    
    279
    +        'send_marker': HAS_ACCOUNT_DEFAULT,
    
    275 280
             'encryption': '',
    
    276 281
             'sync_threshold': HAS_APP_DEFAULT,
    
    277 282
         },
    

  • gajim/common/settings.py
    ... ... @@ -33,6 +33,7 @@
    33 33
     from gajim.common import app
    
    34 34
     from gajim.common import configpaths
    
    35 35
     from gajim.common import optparser
    
    36
    +from gajim.common.helpers import get_muc_context
    
    36 37
     from gajim.common.setting_values import APP_SETTINGS
    
    37 38
     from gajim.common.setting_values import ACCOUNT_SETTINGS
    
    38 39
     from gajim.common.setting_values import PROXY_SETTINGS
    
    ... ... @@ -678,11 +679,16 @@ def set_group_chat_setting(self,
    678 679
     
    
    679 680
         def set_group_chat_settings(self,
    
    680 681
                                     setting: str,
    
    681
    -                                value: SETTING_TYPE) -> None:
    
    682
    +                                value: SETTING_TYPE,
    
    683
    +                                context: str = None) -> None:
    
    682 684
     
    
    683 685
             for account in self._account_settings:
    
    684 686
                 for jid in self._account_settings[account]['group_chat']:
    
    685
    -                self.set_group_chat_setting(account, jid, setting, value)
    
    687
    +                if context is not None:
    
    688
    +                    if get_muc_context(jid) != context:
    
    689
    +                        continue
    
    690
    +                self.set_group_chat_setting(
    
    691
    +                    account, jid, setting, value, context)
    
    686 692
     
    
    687 693
         def get_contact_setting(self,
    
    688 694
                                 account: str,
    

  • gajim/common/structs.py
    ... ... @@ -55,6 +55,7 @@ def __init__(self,
    55 55
                      type_,
    
    56 56
                      subject=None,
    
    57 57
                      chatstate=None,
    
    58
    +                 marker=None,
    
    58 59
                      resource=None,
    
    59 60
                      user_nick=None,
    
    60 61
                      label=None,
    
    ... ... @@ -69,7 +70,7 @@ def __init__(self,
    69 70
             if type_ not in ('chat', 'groupchat', 'normal', 'headline'):
    
    70 71
                 raise ValueError('Unknown message type: %s' % type_)
    
    71 72
     
    
    72
    -        if not message and chatstate is None:
    
    73
    +        if not message and chatstate is None and marker is None:
    
    73 74
                 raise ValueError('Trying to send message without content')
    
    74 75
     
    
    75 76
             self.account = account
    
    ... ... @@ -91,6 +92,7 @@ def __init__(self,
    91 92
     
    
    92 93
             self.subject = subject
    
    93 94
             self.chatstate = chatstate
    
    95
    +        self.marker = marker
    
    94 96
             self.resource = resource
    
    95 97
             self.user_nick = user_nick
    
    96 98
             self.label = label
    

  • gajim/conversation_textview.py
    ... ... @@ -38,6 +38,7 @@
    38 38
     from gajim.common import helpers
    
    39 39
     from gajim.common import i18n
    
    40 40
     from gajim.common.i18n import _
    
    41
    +from gajim.common.i18n import Q_
    
    41 42
     from gajim.common.helpers import AdditionalDataDict
    
    42 43
     from gajim.common.const import StyleAttr
    
    43 44
     from gajim.common.const import Trust
    
    ... ... @@ -311,6 +312,12 @@ def show_receipt(self, id_):
    311 312
                 return
    
    312 313
             line.set_receipt()
    
    313 314
     
    
    315
    +    def show_displayed(self, id_):
    
    316
    +        line = self._get_message_line(id_)
    
    317
    +        if line is None:
    
    318
    +            return
    
    319
    +        line.set_displayed()
    
    320
    +
    
    314 321
         def show_error(self, id_, error):
    
    315 322
             line = self._get_message_line(id_)
    
    316 323
             if line is None:
    
    ... ... @@ -1194,16 +1201,27 @@ def __init__(self, id_, timestamp, message_icons, start_mark):
    1194 1201
             self.timestamp = timestamp
    
    1195 1202
             self.start_mark = start_mark
    
    1196 1203
             self._has_receipt = False
    
    1204
    +        self._has_displayed = False
    
    1197 1205
             self._message_icons = message_icons
    
    1198 1206
     
    
    1199 1207
         @property
    
    1200 1208
         def has_receipt(self):
    
    1201 1209
             return self._has_receipt
    
    1202 1210
     
    
    1211
    +    @property
    
    1212
    +    def has_displayed(self):
    
    1213
    +        return self._has_displayed
    
    1214
    +
    
    1203 1215
         def set_receipt(self):
    
    1204 1216
             self._has_receipt = True
    
    1217
    +        if self._has_displayed:
    
    1218
    +            return
    
    1205 1219
             self._message_icons.set_receipt_icon_visible(True)
    
    1206 1220
     
    
    1221
    +    def set_displayed(self):
    
    1222
    +        self._has_displayed = True
    
    1223
    +        self._message_icons.set_displayed_icon_visible(True)
    
    1224
    +
    
    1207 1225
         def set_correction(self, tooltip):
    
    1208 1226
             self._message_icons.set_correction_icon_visible(True)
    
    1209 1227
             self._message_icons.set_correction_tooltip(tooltip)
    
    ... ... @@ -1222,12 +1240,12 @@ def __init__(self):
    1222 1240
                 'document-edit-symbolic', Gtk.IconSize.MENU)
    
    1223 1241
             self._correction_image.set_no_show_all(True)
    
    1224 1242
     
    
    1225
    -        self._receipt_image = Gtk.Image.new_from_icon_name(
    
    1243
    +        self._marker_image = Gtk.Image.new_from_icon_name(
    
    1226 1244
                 'emblem-ok-symbolic', Gtk.IconSize.MENU)
    
    1227
    -        self._receipt_image.get_style_context().add_class(
    
    1245
    +        self._marker_image.get_style_context().add_class(
    
    1228 1246
                 'receipt-received-color')
    
    1229
    -        self._receipt_image.set_tooltip_text(_('Received'))
    
    1230
    -        self._receipt_image.set_no_show_all(True)
    
    1247
    +        self._marker_image.set_tooltip_text(_('Received'))
    
    1248
    +        self._marker_image.set_no_show_all(True)
    
    1231 1249
     
    
    1232 1250
             self._error_image = Gtk.Image.new_from_icon_name(
    
    1233 1251
                 'dialog-warning-symbolic', Gtk.IconSize.MENU)
    
    ... ... @@ -1235,14 +1253,22 @@ def __init__(self):
    1235 1253
             self._error_image.set_no_show_all(True)
    
    1236 1254
     
    
    1237 1255
             self.add(self._correction_image)
    
    1238
    -        self.add(self._receipt_image)
    
    1256
    +        self.add(self._marker_image)
    
    1239 1257
             self.add(self._error_image)
    
    1240 1258
             self.show_all()
    
    1241 1259
     
    
    1242 1260
         def set_receipt_icon_visible(self, visible):
    
    1243 1261
             if not app.settings.get('positive_184_ack'):
    
    1244 1262
                 return
    
    1245
    -        self._receipt_image.set_visible(visible)
    
    1263
    +        self._marker_image.set_visible(visible)
    
    1264
    +
    
    1265
    +    def set_displayed_icon_visible(self, visible):
    
    1266
    +        self._marker_image.set_visible(visible)
    
    1267
    +        self._marker_image.get_style_context().remove_class(
    
    1268
    +            'receipt-received-color')
    
    1269
    +        self._marker_image.get_style_context().add_class(
    
    1270
    +            'displayed-received-color')
    
    1271
    +        self._marker_image.set_tooltip_text(Q_('?Message state:Read'))
    
    1246 1272
     
    
    1247 1273
         def set_correction_icon_visible(self, visible):
    
    1248 1274
             self._correction_image.set_visible(visible)
    

  • gajim/data/style/gajim.css
    ... ... @@ -295,6 +295,7 @@ button.flat.link { padding: 0; border: 0; }
    295 295
     
    
    296 296
     /*Receipts*/
    
    297 297
     .receipt-received-color { color: rgb(75, 181, 67); }
    
    298
    +.displayed-received-color { color: rgb(0, 141, 242); }
    
    298 299
     
    
    299 300
     /*Dataforms*/
    
    300 301
     .field-fixed { font-size: 16px; font-weight: bold; padding-top:5px;}
    

  • gajim/groupchat_control.py
    ... ... @@ -658,6 +658,17 @@ def on_groupchat_maximize(self):
    658 658
             self.update_actions()
    
    659 659
             self.set_lock_image()
    
    660 660
             self.draw_banner_text()
    
    661
    +        type_ = ['printed_gc_msg', 'printed_marked_gc_msg']
    
    662
    +        if not app.events.remove_events(self.account,
    
    663
    +                                        self.get_full_jid(),
    
    664
    +                                        types=type_):
    
    665
    +            # XEP-0333 Send <displayed> marker
    
    666
    +            con = app.connections[self.account]
    
    667
    +            con.get_module('ChatMarkers').send_displayed_marker(
    
    668
    +                self.contact,
    
    669
    +                self.last_msg_id,
    
    670
    +                self._type)
    
    671
    +            self.last_msg_id = None
    
    661 672
     
    
    662 673
         def _on_roster_row_activated(self, _roster, nick):
    
    663 674
             self._start_private_message(nick)
    
    ... ... @@ -796,17 +807,21 @@ def _on_gc_message_received(self, event):
    796 807
             else:
    
    797 808
                 if event.properties.muc_nickname == self.nick:
    
    798 809
                     self.last_sent_txt = event.msgtxt
    
    810
    +            stanza_id = None
    
    811
    +            if event.properties.stanza_id:
    
    812
    +                stanza_id = event.properties.stanza_id.id
    
    799 813
                 self.add_message(event.msgtxt,
    
    800 814
                                  contact=event.properties.muc_nickname,
    
    801 815
                                  tim=event.properties.timestamp,
    
    802 816
                                  displaymarking=event.displaymarking,
    
    803 817
                                  correct_id=event.correct_id,
    
    804 818
                                  message_id=event.properties.id,
    
    819
    +                             stanza_id=stanza_id,
    
    805 820
                                  additional_data=event.additional_data)
    
    806 821
             event.needs_highlight = self.needs_visual_notification(event.msgtxt)
    
    807 822
     
    
    808 823
         def on_private_message(self, nick, sent, msg, tim, session, additional_data,
    
    809
    -                           msg_log_id=None, displaymarking=None):
    
    824
    +                           message_id, msg_log_id=None, displaymarking=None):
    
    810 825
             # Do we have a queue?
    
    811 826
             fjid = self.room_jid + '/' + nick
    
    812 827
     
    
    ... ... @@ -819,7 +834,9 @@ def on_private_message(self, nick, sent, msg, tim, session, additional_data,
    819 834
                                    session=session,
    
    820 835
                                    displaymarking=displaymarking,
    
    821 836
                                    sent_forwarded=sent,
    
    822
    -                               additional_data=additional_data)
    
    837
    +                               additional_data=additional_data,
    
    838
    +                               message_id=message_id)
    
    839
    +
    
    823 840
             app.events.add_event(self.account, fjid, event)
    
    824 841
     
    
    825 842
             if allow_popup_window(self.account):
    
    ... ... @@ -837,7 +854,7 @@ def on_private_message(self, nick, sent, msg, tim, session, additional_data,
    837 854
     
    
    838 855
         def add_message(self, text, contact='', tim=None,
    
    839 856
                         displaymarking=None, correct_id=None, message_id=None,
    
    840
    -                    additional_data=None):
    
    857
    +                    stanza_id=None, additional_data=None):
    
    841 858
             """
    
    842 859
             Add message to the ConversationsTextview
    
    843 860
     
    
    ... ... @@ -887,6 +904,7 @@ def add_message(self, text, contact='', tim=None,
    887 904
                                         displaymarking=displaymarking,
    
    888 905
                                         correct_id=correct_id,
    
    889 906
                                         message_id=message_id,
    
    907
    +                                    stanza_id=stanza_id,
    
    890 908
                                         additional_data=additional_data)
    
    891 909
     
    
    892 910
         def get_nb_unread(self):
    
    ... ... @@ -1057,6 +1075,7 @@ def _on_decrypted_message_received(self, event):
    1057 1075
                                         event.properties.timestamp,
    
    1058 1076
                                         self.session,
    
    1059 1077
                                         event.additional_data,
    
    1078
    +                                    event.properties.id,
    
    1060 1079
                                         msg_log_id=event.msg_log_id,
    
    1061 1080
                                         displaymarking=event.displaymarking)
    
    1062 1081
     
    

  • gajim/gtk/accounts.py
    ... ... @@ -730,6 +730,19 @@ def __init__(self, account):
    730 730
                                'button-style': 'destructive-action',
    
    731 731
                                'button-callback': self._reset_gc_send_chatstate}),
    
    732 732
     
    
    733
    +            Setting(SettingKind.SWITCH,
    
    734
    +                    _('Send Read Markers'),
    
    735
    +                    SettingType.VALUE,
    
    736
    +                    app.settings.get_account_setting(
    
    737
    +                        account, 'send_marker_default'),
    
    738
    +                    callback=self._send_read_marker,
    
    739
    +                    desc=_('Default for chats and private group chats'),
    
    740
    +                    props={'button-text': _('Reset'),
    
    741
    +                           'button-tooltip': _('Reset all chats to the '
    
    742
    +                                               'current default value'),
    
    743
    +                           'button-style': 'destructive-action',
    
    744
    +                           'button-callback': self._reset_send_read_marker}),
    
    745
    +
    
    733 746
                 Setting(SettingKind.POPOVER,
    
    734 747
                         _('Keep Chat History'),
    
    735 748
                         SettingType.ACCOUNT_CONFIG,
    
    ... ... @@ -749,6 +762,20 @@ def _reset_gc_send_chatstate(button):
    749 762
             button.set_sensitive(False)
    
    750 763
             app.settings.set_group_chat_settings('send_chatstate', None)
    
    751 764
     
    
    765
    +    def _send_read_marker(self, state, _data):
    
    766
    +        app.settings.set_account_setting(
    
    767
    +            self._account, 'send_marker_default', state)
    
    768
    +        app.settings.set_account_setting(
    
    769
    +            self._account, 'gc_send_marker_private_default', state)
    
    770
    +
    
    771
    +    def _reset_send_read_marker(self, button):
    
    772
    +        button.set_sensitive(False)
    
    773
    +        app.settings.set_contact_settings('send_marker', None)
    
    774
    +        app.settings.set_group_chat_settings(
    
    775
    +            'send_marker', None, context='private')
    
    776
    +        for ctrl in app.interface.msg_win_mgr.get_controls(acct=self._account):
    
    777
    +            ctrl.update_actions()
    
    778
    +
    
    752 779
     
    
    753 780
     class ConnectionPage(GenericSettingPage):
    
    754 781
     
    

  • gajim/gtk/groupchat_settings.py
    ... ... @@ -71,6 +71,13 @@ def __init__(self, account, jid, context):
    71 71
                         'send_chatstate',
    
    72 72
                         props={'entries': chat_state}),
    
    73 73
     
    
    74
    +            Setting(SettingKind.SWITCH,
    
    75
    +                    _('Send Chat Markers'),
    
    76
    +                    SettingType.GROUP_CHAT,
    
    77
    +                    'send_marker',
    
    78
    +                    context=context,
    
    79
    +                    desc=_('Let others know if you read up to this point')),
    
    80
    +
    
    74 81
                 Setting(SettingKind.POPOVER,
    
    75 82
                         _('Sync Threshold'),
    
    76 83
                         SettingType.GROUP_CHAT,
    

  • gajim/gui_interface.py
    ... ... @@ -242,6 +242,43 @@ def handle_event_presence(self, obj):
    242 242
             if ctrl and ctrl.session and len(obj.contact_list) > 1:
    
    243 243
                 ctrl.remove_session(ctrl.session)
    
    244 244
     
    
    245
    +    @staticmethod
    
    246
    +    def handle_event_read_state_sync(event):
    
    247
    +        if event.type.is_groupchat:
    
    248
    +            control = app.get_groupchat_control(
    
    249
    +                event.account, event.jid.bare)
    
    250
    +            if control is None:
    
    251
    +                log.warning('Groupchat control not found')
    
    252
    +                return
    
    253
    +
    
    254
    +            jid = event.jid.bare
    
    255
    +            types = ['printed_gc_msg', 'printed_marked_gc_msg']
    
    256
    +
    
    257
    +        else:
    
    258
    +            types = ['chat', 'pm', 'printed_chat', 'printed_pm']
    
    259
    +            jid = event.jid
    
    260
    +
    
    261
    +            control = app.interface.msg_win_mgr.get_control(jid, event.account)
    
    262
    +
    
    263
    +        # Compare with control.last_msg_id.
    
    264
    +        events_ = app.events.get_events(event.account, jid, types)
    
    265
    +        if not events_:
    
    266
    +            log.warning('No Events')
    
    267
    +            return
    
    268
    +
    
    269
    +        if event.type.is_groupchat:
    
    270
    +            id_ = events_[-1].stanza_id or events_[-1].message_id
    
    271
    +        else:
    
    272
    +            id_ = events_[-1].message_id
    
    273
    +
    
    274
    +        if id_ != event.marker_id:
    
    275
    +            return
    
    276
    +
    
    277
    +        if not app.events.remove_events(event.account, jid, types=types):
    
    278
    +            # There were events to remove
    
    279
    +            if control is not None:
    
    280
    +                control.redraw_after_event_removed(event.jid)
    
    281
    +
    
    245 282
         @staticmethod
    
    246 283
         def handle_event_msgsent(obj):
    
    247 284
             if not obj.play_sound:
    
    ... ... @@ -1060,6 +1097,7 @@ def create_core_handlers_list(self):
    1060 1097
                 'unsubscribed-presence-received': [
    
    1061 1098
                     self.handle_event_unsubscribed_presence],
    
    1062 1099
                 'zeroconf-name-conflict': [self.handle_event_zc_name_conflict],
    
    1100
    +            'read-state-sync': [self.handle_event_read_state_sync],
    
    1063 1101
             }
    
    1064 1102
     
    
    1065 1103
         def register_core_handlers(self):
    

  • gajim/gui_menu_builder.py
    ... ... @@ -511,12 +511,13 @@ def get_transport_menu(contact, account):
    511 511
         return menu
    
    512 512
     
    
    513 513
     
    
    514
    -def get_singlechat_menu(control_id, account, jid):
    
    514
    +def get_singlechat_menu(control_id, account, jid, type_):
    
    515 515
         singlechat_menu = [
    
    516 516
             (_('Send File'), [
    
    517 517
                 ('win.send-file-httpupload-', _('Upload File…')),
    
    518 518
                 ('win.send-file-jingle-', _('Send File Directly…')),
    
    519 519
                 ]),
    
    520
    +        ('win.send-marker-', _('Send Read Markers')),
    
    520 521
             (_('Send Chatstate'), ['chatstate']),
    
    521 522
             ('win.invite-contacts-', _('Invite Contacts…')),
    
    522 523
             ('win.add-to-roster-', _('Add to Contact List…')),
    
    ... ... @@ -545,6 +546,9 @@ def build_menu(preset):
    545 546
             for item in preset:
    
    546 547
                 if isinstance(item[1], str):
    
    547 548
                     action_name, label = item
    
    549
    +                if action_name == 'win.send-marker-' and type_ == 'pm':
    
    550
    +                    continue
    
    551
    +
    
    548 552
                     if action_name == 'app.browse-history':
    
    549 553
                         menuitem = Gio.MenuItem.new(label, action_name)
    
    550 554
                         dict_ = {'account': GLib.Variant('s', account),