Daniel Brötzmann pushed to branch chat_markers at gajim / gajim

Commits:

17 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)
    
    ... ... @@ -1384,6 +1409,14 @@ def read_queue(self):
    1384 1409
                     self.set_session(event.session)
    
    1385 1410
             if message_ids:
    
    1386 1411
                 app.storage.archive.set_read_messages(message_ids)
    
    1412
    +
    
    1413
    +        # XEP-0333 Send <displayed> marker
    
    1414
    +        con = app.connections[self.account]
    
    1415
    +        con.get_module('ChatMarkers').send_displayed_marker(
    
    1416
    +            self.contact,
    
    1417
    +            self.last_msg_id,
    
    1418
    +            self._type)
    
    1419
    +        self.last_msg_id = None
    
    1387 1420
             app.events.remove_events(self.account,
    
    1388 1421
                                      jid_with_resource,
    
    1389 1422
                                      types=[str(self._type)])
    

  • gajim/chat_control_base.py
    ... ... @@ -214,6 +214,9 @@ def __init__(self, parent_win, widget_name, contact, acct,
    214 214
             self.received_history_pos = 0
    
    215 215
             self.orig_msg = None
    
    216 216
     
    
    217
    +        # For XEP-0333
    
    218
    +        self.last_msg_id = None
    
    219
    +
    
    217 220
             self.correcting = False
    
    218 221
             self.last_sent_msg = None
    
    219 222
     
    
    ... ... @@ -1111,6 +1114,7 @@ def add_message(self,
    1111 1114
                         displaymarking=None,
    
    1112 1115
                         msg_log_id=None,
    
    1113 1116
                         message_id=None,
    
    1117
    +                    stanza_id=None,
    
    1114 1118
                         correct_id=None,
    
    1115 1119
                         additional_data=None,
    
    1116 1120
                         marker=None,
    
    ... ... @@ -1154,6 +1158,12 @@ def add_message(self,
    1154 1158
             if restored:
    
    1155 1159
                 return
    
    1156 1160
     
    
    1161
    +        if message_id:
    
    1162
    +            if self._type.is_groupchat:
    
    1163
    +                self.last_msg_id = stanza_id
    
    1164
    +            else:
    
    1165
    +                self.last_msg_id = message_id
    
    1166
    +
    
    1157 1167
             if kind == 'incoming':
    
    1158 1168
                 if (not self._type.is_groupchat or
    
    1159 1169
                         self.contact.can_notify() or
    
    ... ... @@ -1198,7 +1208,10 @@ def add_message(self,
    1198 1208
     
    
    1199 1209
                     event = event_type(text,
    
    1200 1210
                                        subject,
    
    1201
    -                                   self, msg_log_id,
    
    1211
    +                                   self,
    
    1212
    +                                   msg_log_id,
    
    1213
    +                                   message_id=message_id,
    
    1214
    +                                   stanza_id=stanza_id,
    
    1202 1215
                                        show_in_roster=show_in_roster,
    
    1203 1216
                                        show_in_systray=show_in_systray)
    
    1204 1217
                     app.events.add_event(self.account, full_jid, event)
    
    ... ... @@ -1350,7 +1363,7 @@ def set_control_active(self, state):
    1350 1363
                 jid = self.contact.jid
    
    1351 1364
                 if self.conv_textview.autoscroll:
    
    1352 1365
                     # we are at the end
    
    1353
    -                type_ = ['printed_%s' % self._type]
    
    1366
    +                type_ = [f'printed_{self._type}']
    
    1354 1367
                     if self._type.is_groupchat:
    
    1355 1368
                         type_ = ['printed_gc_msg', 'printed_marked_gc_msg']
    
    1356 1369
                     if not app.events.remove_events(self.account,
    
    ... ... @@ -1358,6 +1371,12 @@ def set_control_active(self, state):
    1358 1371
                                                     types=type_):
    
    1359 1372
                         # There were events to remove
    
    1360 1373
                         self.redraw_after_event_removed(jid)
    
    1374
    +                    # XEP-0333 Send <displayed> marker
    
    1375
    +                    con.get_module('ChatMarkers').send_displayed_marker(
    
    1376
    +                        self.contact,
    
    1377
    +                        self.last_msg_id,
    
    1378
    +                        self._type)
    
    1379
    +                    self.last_msg_id = None
    
    1361 1380
                 # send chatstate inactive to the one we're leaving
    
    1362 1381
                 # and active to the one we visit
    
    1363 1382
                 if self.msg_textview.has_text():
    
    ... ... @@ -1386,20 +1405,27 @@ def _on_edge_reached(self, _scrolledwindow, pos):
    1386 1405
             types_list = []
    
    1387 1406
             if self._type.is_groupchat:
    
    1388 1407
                 types_list = ['printed_gc_msg', 'gc_msg', 'printed_marked_gc_msg']
    
    1389
    -        else: # Not a GC
    
    1390
    -            types_list = ['printed_%s' % self._type, str(self._type)]
    
    1408
    +        else:
    
    1409
    +            types_list = [f'printed_{self._type}', str(self._type)]
    
    1391 1410
     
    
    1392 1411
             if not app.events.get_events(self.account, jid, types_list):
    
    1393 1412
                 return
    
    1394 1413
             if not self.parent_win:
    
    1395 1414
                 return
    
    1396
    -        if self.parent_win.get_active_control() == self and \
    
    1397
    -        self.parent_win.window.is_active():
    
    1415
    +        if (self.parent_win.get_active_control() == self and
    
    1416
    +                self.parent_win.window.is_active()):
    
    1398 1417
                 # we are at the end
    
    1399 1418
                 if not app.events.remove_events(
    
    1400 1419
                         self.account, jid, types=types_list):
    
    1401 1420
                     # There were events to remove
    
    1402 1421
                     self.redraw_after_event_removed(jid)
    
    1422
    +                # XEP-0333 Send <displayed> tag
    
    1423
    +                con = app.connections[self.account]
    
    1424
    +                con.get_module('ChatMarkers').send_displayed_marker(
    
    1425
    +                    self.contact,
    
    1426
    +                    self.last_msg_id,
    
    1427
    +                    self._type)
    
    1428
    +                self.last_msg_id = None
    
    1403 1429
     
    
    1404 1430
         def _on_scrollbar_button_release(self, scrollbar, event):
    
    1405 1431
             if event.get_button()[1] != 1:
    
    ... ... @@ -1464,6 +1490,8 @@ def redraw_after_event_removed(self, jid):
    1464 1490
             We just removed a 'printed_*' event, redraw contact in roster or
    
    1465 1491
             gc_roster and titles in roster and msg_win
    
    1466 1492
             """
    
    1493
    +        if not self.parent_win:  # minimized groupchat
    
    1494
    +            return
    
    1467 1495
             self.parent_win.redraw_tab(self)
    
    1468 1496
             self.parent_win.show_title()
    
    1469 1497
             # 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/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
    +import nbxmpp
    
    18
    +from nbxmpp.namespaces import Namespace
    
    19
    +from nbxmpp.structs import StanzaHandler
    
    20
    +
    
    21
    +from gajim.common import app
    
    22
    +from gajim.common.nec import NetworkEvent
    
    23
    +from gajim.common.modules.base import BaseModule
    
    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
    +            # TODO: Get correct context
    
    95
    +            context = 'private'
    
    96
    +            if not app.settings.get_group_chat_setting(
    
    97
    +                    self._account, jid, 'send_marker', context=context):
    
    98
    +                return
    
    99
    +        else:
    
    100
    +            if not app.settings.get_contact_setting(
    
    101
    +                    self._account, jid, 'send_marker'):
    
    102
    +                return
    
    103
    +
    
    104
    +        typ = 'groupchat' if type_ == 'gc' else 'chat'
    
    105
    +        message = nbxmpp.Message(to=contact.jid, typ=typ)
    
    106
    +        message.setMarker(marker, id_)
    
    107
    +        self._log.info('Send %s: %s', marker, contact.jid)
    
    108
    +        self._nbxmpp().send(message)
    
    109
    +
    
    110
    +    def send_displayed_marker(self, contact, id_, type_):
    
    111
    +        self._send_marker(contact, 'displayed', id_, str(type_))
    
    112
    +
    
    113
    +
    
    114
    +def get_instance(*args, **kwargs):
    
    115
    +    return ChatMarkers(*args, **kwargs), 'ChatMarkers'

  • gajim/common/modules/ibb.py
    ... ... @@ -20,7 +20,7 @@
    20 20
     from nbxmpp.namespaces import Namespace
    
    21 21
     from nbxmpp.protocol import NodeProcessed
    
    22 22
     from nbxmpp.structs import StanzaHandler
    
    23
    -from nbxmpp.util import is_error_result
    
    23
    +from nbxmpp.errors import StanzaError
    
    24 24
     
    
    25 25
     from gajim.common import app
    
    26 26
     from gajim.common.helpers import to_user_string
    
    ... ... @@ -149,11 +149,15 @@ def send_open(self, to, sid, fp):
    149 149
                                           user_data=file_props)
    
    150 150
             return file_props
    
    151 151
     
    
    152
    -    def _on_open_result(self, result, file_props):
    
    153
    -        if is_error_result(result):
    
    154
    -            app.socks5queue.error_cb('Error', to_user_string(result))
    
    155
    -            self._log.warning(result)
    
    152
    +    def _on_open_result(self, task):
    
    153
    +        try:
    
    154
    +            task.finish()
    
    155
    +        except StanzaError as error:
    
    156
    +            app.socks5queue.error_cb('Error', to_user_string(error))
    
    157
    +            self._log.warning(error)
    
    156 158
                 return
    
    159
    +
    
    160
    +        file_props = task.get_user_data()
    
    157 161
             self.send_data(file_props)
    
    158 162
     
    
    159 163
         def send_close(self, file_props):
    
    ... ... @@ -183,10 +187,12 @@ def send_close(self, file_props):
    183 187
                 if session.weinitiate:
    
    184 188
                     session.cancel_session()
    
    185 189
     
    
    186
    -    def _on_close_result(self, result):
    
    187
    -        if is_error_result(result):
    
    188
    -            app.socks5queue.error_cb('Error', to_user_string(result))
    
    189
    -            self._log.warning(result)
    
    190
    +    def _on_close_result(self, task):
    
    191
    +        try:
    
    192
    +            task.finish()
    
    193
    +        except StanzaError as error:
    
    194
    +            app.socks5queue.error_cb('Error', to_user_string(error))
    
    195
    +            self._log.warning(error)
    
    190 196
                 return
    
    191 197
     
    
    192 198
         def send_data(self, file_props):
    
    ... ... @@ -217,11 +223,15 @@ def send_data(self, file_props):
    217 223
                     file_props.completed = True
    
    218 224
                 app.socks5queue.progress_transfer_cb(self._account, file_props)
    
    219 225
     
    
    220
    -    def _on_data_result(self, result, file_props):
    
    221
    -        if is_error_result(result):
    
    222
    -            app.socks5queue.error_cb('Error', to_user_string(result))
    
    223
    -            self._log.warning(result)
    
    226
    +    def _on_data_result(self, task):
    
    227
    +        try:
    
    228
    +            task.finish()
    
    229
    +        except StanzaError as error:
    
    230
    +            app.socks5queue.error_cb('Error', to_user_string(error))
    
    231
    +            self._log.warning(error)
    
    224 232
                 return
    
    233
    +
    
    234
    +        file_props = task.get_user_data()
    
    225 235
             self.send_data(file_props)
    
    226 236
     
    
    227 237
     
    

  • gajim/common/modules/message.py
    ... ... @@ -305,6 +305,8 @@ def build_message_stanza(self, message):
    305 305
             stanza.setID(message.message_id)
    
    306 306
             stanza.setOriginID(message.message_id)
    
    307 307
     
    
    308
    +        stanza.setMarkable()
    
    309
    +
    
    308 310
             if message.label:
    
    309 311
                 stanza.addChild(node=message.label)
    
    310 312
     
    
    ... ... @@ -347,6 +349,10 @@ def build_message_stanza(self, message):
    347 349
                     stanza.setTag('no-store',
    
    348 350
                                   namespace=Namespace.MSG_HINTS)
    
    349 351
     
    
    352
    +        # XEP-0333
    
    353
    +        if message.message:
    
    354
    +            stanza.setMarkable()
    
    355
    +
    
    350 356
             # Add other nodes
    
    351 357
             if message.nodes is not None:
    
    352 358
                 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
    ... ... @@ -678,11 +678,13 @@ def set_group_chat_setting(self,
    678 678
     
    
    679 679
         def set_group_chat_settings(self,
    
    680 680
                                     setting: str,
    
    681
    -                                value: SETTING_TYPE) -> None:
    
    681
    +                                value: SETTING_TYPE,
    
    682
    +                                context: str = None) -> None:
    
    682 683
     
    
    683 684
             for account in self._account_settings:
    
    684 685
                 for jid in self._account_settings[account]['group_chat']:
    
    685
    -                self.set_group_chat_setting(account, jid, setting, value)
    
    686
    +                self.set_group_chat_setting(
    
    687
    +                    account, jid, setting, value, context)
    
    686 688
     
    
    687 689
         def get_contact_setting(self,
    
    688 690
                                 account: str,
    

  • 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)
    
    ... ... @@ -802,11 +813,12 @@ def _on_gc_message_received(self, event):
    802 813
                                  displaymarking=event.displaymarking,
    
    803 814
                                  correct_id=event.correct_id,
    
    804 815
                                  message_id=event.properties.id,
    
    816
    +                             stanza_id=event.properties.stanza_id.id,
    
    805 817
                                  additional_data=event.additional_data)
    
    806 818
             event.needs_highlight = self.needs_visual_notification(event.msgtxt)
    
    807 819
     
    
    808 820
         def on_private_message(self, nick, sent, msg, tim, session, additional_data,
    
    809
    -                           msg_log_id=None, displaymarking=None):
    
    821
    +                           message_id, msg_log_id=None, displaymarking=None):
    
    810 822
             # Do we have a queue?
    
    811 823
             fjid = self.room_jid + '/' + nick
    
    812 824
     
    
    ... ... @@ -819,7 +831,9 @@ def on_private_message(self, nick, sent, msg, tim, session, additional_data,
    819 831
                                    session=session,
    
    820 832
                                    displaymarking=displaymarking,
    
    821 833
                                    sent_forwarded=sent,
    
    822
    -                               additional_data=additional_data)
    
    834
    +                               additional_data=additional_data,
    
    835
    +                               message_id=message_id)
    
    836
    +
    
    823 837
             app.events.add_event(self.account, fjid, event)
    
    824 838
     
    
    825 839
             if allow_popup_window(self.account):
    
    ... ... @@ -837,7 +851,7 @@ def on_private_message(self, nick, sent, msg, tim, session, additional_data,
    837 851
     
    
    838 852
         def add_message(self, text, contact='', tim=None,
    
    839 853
                         displaymarking=None, correct_id=None, message_id=None,
    
    840
    -                    additional_data=None):
    
    854
    +                    stanza_id=None, additional_data=None):
    
    841 855
             """
    
    842 856
             Add message to the ConversationsTextview
    
    843 857
     
    
    ... ... @@ -887,6 +901,7 @@ def add_message(self, text, contact='', tim=None,
    887 901
                                         displaymarking=displaymarking,
    
    888 902
                                         correct_id=correct_id,
    
    889 903
                                         message_id=message_id,
    
    904
    +                                    stanza_id=stanza_id,
    
    890 905
                                         additional_data=additional_data)
    
    891 906
     
    
    892 907
         def get_nb_unread(self):
    
    ... ... @@ -1057,6 +1072,7 @@ def _on_decrypted_message_received(self, event):
    1057 1072
                                         event.properties.timestamp,
    
    1058 1073
                                         self.session,
    
    1059 1074
                                         event.additional_data,
    
    1075
    +                                    event.properties.id,
    
    1060 1076
                                         msg_log_id=event.msg_log_id,
    
    1061 1077
                                         displaymarking=event.displaymarking)
    
    1062 1078
     
    

  • gajim/gtk/accounts.py
    ... ... @@ -720,6 +720,19 @@ def __init__(self, account):
    720 720
                                'button-style': 'destructive-action',
    
    721 721
                                'button-callback': self._reset_gc_send_chatstate}),
    
    722 722
     
    
    723
    +            Setting(SettingKind.SWITCH,
    
    724
    +                    _('Send Read Markers'),
    
    725
    +                    SettingType.VALUE,
    
    726
    +                    app.settings.get_account_setting(
    
    727
    +                        account, 'send_marker_default'),
    
    728
    +                    callback=self._send_read_marker,
    
    729
    +                    desc=_('Default for chats and private group chats'),
    
    730
    +                    props={'button-text': _('Reset'),
    
    731
    +                           'button-tooltip': _('Reset all chats to the '
    
    732
    +                                               'current default value'),
    
    733
    +                           'button-style': 'destructive-action',
    
    734
    +                           'button-callback': self._reset_send_read_marker}),
    
    735
    +
    
    723 736
                 Setting(SettingKind.POPOVER,
    
    724 737
                         _('Keep Chat History'),
    
    725 738
                         SettingType.ACCOUNT_CONFIG,
    
    ... ... @@ -739,6 +752,20 @@ def _reset_gc_send_chatstate(button):
    739 752
             button.set_sensitive(False)
    
    740 753
             app.settings.set_group_chat_settings('send_chatstate', None)
    
    741 754
     
    
    755
    +    def _send_read_marker(self, state, _data):
    
    756
    +        app.settings.set_account_setting(
    
    757
    +            self._account, 'send_marker_default', state)
    
    758
    +        app.settings.set_account_setting(
    
    759
    +            self._account, 'gc_send_marker_private_default', state)
    
    760
    +
    
    761
    +    def _reset_send_read_marker(self, button):
    
    762
    +        button.set_sensitive(False)
    
    763
    +        app.settings.set_contact_settings('send_marker', None)
    
    764
    +        app.settings.set_group_chat_settings(
    
    765
    +            'send_marker', None, context='private')
    
    766
    +        for ctrl in app.interface.msg_win_mgr.get_controls(acct=self._account):
    
    767
    +            ctrl.update_actions()
    
    768
    +
    
    742 769
     
    
    743 770
     class ConnectionPage(GenericSettingPage):
    
    744 771
     
    

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