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

Commits:

27 changed files:

Changes:

  • gajim/chat_control.py
    ... ... @@ -123,7 +123,7 @@ class ChatControl(ChatControlBase):
    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 @@ class ChatControl(ChatControlBase):
    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 @@ class ChatControl(ChatControlBase):
    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)
    
    ... ... @@ -363,6 +373,7 @@ class ChatControl(ChatControlBase):
    363 373
                 'information-',
    
    364 374
                 'start-call-',
    
    365 375
                 'send-chatstate-',
    
    376
    +            'send-marker',
    
    366 377
             ]
    
    367 378
             for action in actions:
    
    368 379
                 self.parent_win.window.remove_action(f'{action}{self.control_id}')
    
    ... ... @@ -435,6 +446,10 @@ class ChatControl(ChatControlBase):
    435 446
             action.set_state(param)
    
    436 447
             self.contact.settings.set('send_chatstate', param.get_string())
    
    437 448
     
    
    449
    +    def _on_send_marker(self, action, param):
    
    450
    +        action.set_state(param)
    
    451
    +        self.contact.settings.set('send_marker', param.get_boolean())
    
    452
    +
    
    438 453
         def subscribe_events(self):
    
    439 454
             """
    
    440 455
             Register listeners to the events class
    
    ... ... @@ -643,6 +658,10 @@ class ChatControl(ChatControlBase):
    643 658
         def _receipt_received(self, event):
    
    644 659
             self.conv_textview.show_receipt(event.receipt_id)
    
    645 660
     
    
    661
    +    @event_filter(['account', 'jid'])
    
    662
    +    def _displayed_received(self, event):
    
    663
    +        self.conv_textview.show_displayed(event.marker_id)
    
    664
    +
    
    646 665
         @event_filter(['account', 'jid'])
    
    647 666
         def _on_zeroconf_error(self, event):
    
    648 667
             self.add_status_message(event.message)
    
    ... ... @@ -1384,6 +1403,14 @@ class ChatControl(ChatControlBase):
    1384 1403
                     self.set_session(event.session)
    
    1385 1404
             if message_ids:
    
    1386 1405
                 app.storage.archive.set_read_messages(message_ids)
    
    1406
    +
    
    1407
    +        # XEP-0333 Send <displayed> marker
    
    1408
    +        con = app.connections[self.account]
    
    1409
    +        con.get_module('ChatMarkers').send_displayed_marker(
    
    1410
    +            self.contact,
    
    1411
    +            self.last_msg_id,
    
    1412
    +            self._type)
    
    1413
    +        self.last_msg_id = None
    
    1387 1414
             app.events.remove_events(self.account,
    
    1388 1415
                                      jid_with_resource,
    
    1389 1416
                                      types=[str(self._type)])
    

  • gajim/chat_control_base.py
    ... ... @@ -214,6 +214,9 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
    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
     
    
    ... ... @@ -1154,6 +1157,9 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
    1154 1157
             if restored:
    
    1155 1158
                 return
    
    1156 1159
     
    
    1160
    +        if message_id:
    
    1161
    +            self.last_msg_id = message_id
    
    1162
    +
    
    1157 1163
             if kind == 'incoming':
    
    1158 1164
                 if (not self._type.is_groupchat or
    
    1159 1165
                         self.contact.can_notify() or
    
    ... ... @@ -1198,7 +1204,9 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
    1198 1204
     
    
    1199 1205
                     event = event_type(text,
    
    1200 1206
                                        subject,
    
    1201
    -                                   self, msg_log_id,
    
    1207
    +                                   self,
    
    1208
    +                                   msg_log_id,
    
    1209
    +                                   message_id=message_id,
    
    1202 1210
                                        show_in_roster=show_in_roster,
    
    1203 1211
                                        show_in_systray=show_in_systray)
    
    1204 1212
                     app.events.add_event(self.account, full_jid, event)
    
    ... ... @@ -1334,7 +1342,7 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
    1334 1342
                         _('Privacy'),
    
    1335 1343
                         _('Warning'),
    
    1336 1344
                         _('If you send a file to <b>%s</b>, your real XMPP '
    
    1337
    -                      'address will be revealed.' % gc_contact.name),
    
    1345
    +                      'address will be revealed.') % gc_contact.name,
    
    1338 1346
                         [DialogButton.make('Cancel'),
    
    1339 1347
                          DialogButton.make(
    
    1340 1348
                              'OK',
    
    ... ... @@ -1350,7 +1358,7 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
    1350 1358
                 jid = self.contact.jid
    
    1351 1359
                 if self.conv_textview.autoscroll:
    
    1352 1360
                     # we are at the end
    
    1353
    -                type_ = ['printed_%s' % self._type]
    
    1361
    +                type_ = [f'printed_{self._type}']
    
    1354 1362
                     if self._type.is_groupchat:
    
    1355 1363
                         type_ = ['printed_gc_msg', 'printed_marked_gc_msg']
    
    1356 1364
                     if not app.events.remove_events(self.account,
    
    ... ... @@ -1358,6 +1366,12 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
    1358 1366
                                                     types=type_):
    
    1359 1367
                         # There were events to remove
    
    1360 1368
                         self.redraw_after_event_removed(jid)
    
    1369
    +                    # XEP-0333 Send <displayed> marker
    
    1370
    +                    con.get_module('ChatMarkers').send_displayed_marker(
    
    1371
    +                        self.contact,
    
    1372
    +                        self.last_msg_id,
    
    1373
    +                        self._type)
    
    1374
    +                    self.last_msg_id = None
    
    1361 1375
                 # send chatstate inactive to the one we're leaving
    
    1362 1376
                 # and active to the one we visit
    
    1363 1377
                 if self.msg_textview.has_text():
    
    ... ... @@ -1386,20 +1400,27 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
    1386 1400
             types_list = []
    
    1387 1401
             if self._type.is_groupchat:
    
    1388 1402
                 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)]
    
    1403
    +        else:
    
    1404
    +            types_list = [f'printed_{self._type}', str(self._type)]
    
    1391 1405
     
    
    1392 1406
             if not app.events.get_events(self.account, jid, types_list):
    
    1393 1407
                 return
    
    1394 1408
             if not self.parent_win:
    
    1395 1409
                 return
    
    1396
    -        if self.parent_win.get_active_control() == self and \
    
    1397
    -        self.parent_win.window.is_active():
    
    1410
    +        if (self.parent_win.get_active_control() == self and
    
    1411
    +                self.parent_win.window.is_active()):
    
    1398 1412
                 # we are at the end
    
    1399 1413
                 if not app.events.remove_events(
    
    1400 1414
                         self.account, jid, types=types_list):
    
    1401 1415
                     # There were events to remove
    
    1402 1416
                     self.redraw_after_event_removed(jid)
    
    1417
    +                # XEP-0333 Send <displayed> tag
    
    1418
    +                con = app.connections[self.account]
    
    1419
    +                con.get_module('ChatMarkers').send_displayed_marker(
    
    1420
    +                    self.contact,
    
    1421
    +                    self.last_msg_id,
    
    1422
    +                    self._type)
    
    1423
    +                self.last_msg_id = None
    
    1403 1424
     
    
    1404 1425
         def _on_scrollbar_button_release(self, scrollbar, event):
    
    1405 1426
             if event.get_button()[1] != 1:
    
    ... ... @@ -1464,6 +1485,8 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
    1464 1485
             We just removed a 'printed_*' event, redraw contact in roster or
    
    1465 1486
             gc_roster and titles in roster and msg_win
    
    1466 1487
             """
    
    1488
    +        if not self.parent_win:  # minimized groupchat
    
    1489
    +            return
    
    1467 1490
             self.parent_win.redraw_tab(self)
    
    1468 1491
             self.parent_win.show_title()
    
    1469 1492
             # TODO : get the contact and check get_show_in_roster()
    

  • gajim/common/const.py
    ... ... @@ -970,6 +970,7 @@ COMMON_FEATURES = [
    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,14 @@ 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, show_in_roster=False, show_in_systray=True):
    
    95 95
             Event.__init__(self, time_, show_in_roster=show_in_roster,
    
    96 96
                 show_in_systray=show_in_systray)
    
    97 97
             self.message = message
    
    98 98
             self.subject = subject
    
    99 99
             self.control = control
    
    100 100
             self.msg_log_id = msg_log_id
    
    101
    +        self.message_id = message_id
    
    101 102
     
    
    102 103
     class PrintedGcMsgEvent(PrintedChatEvent):
    
    103 104
         type_ = 'printed_gc_msg'
    

  • gajim/common/jingle_rtp.py
    ... ... @@ -130,7 +130,7 @@ class JingleRTPContent(JingleContent):
    130 130
                         None,
    
    131 131
                         conn=self.session.connection,
    
    132 132
                         level='error',
    
    133
    -                    pri_txt=_('%s configuration error' % text),
    
    133
    +                    pri_txt=_('%s configuration error') % text,
    
    134 134
                         sec_txt=_('Couldn’t set up %(text)s. Check your '
    
    135 135
                                   'configuration.\nPipeline:\n%(pipeline)s\n'
    
    136 136
                                   'Error:\n%(error)s') % {
    

  • gajim/common/modules/__init__.py
    ... ... @@ -40,6 +40,7 @@ MODULES = [
    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
    +            if not app.settings.get_group_chat_setting(
    
    95
    +                    self._account, jid, 'send_marker'):
    
    96
    +                return
    
    97
    +        else:
    
    98
    +            if not app.settings.get_contact_setting(
    
    99
    +                    self._account, jid, 'send_marker'):
    
    100
    +                return
    
    101
    +
    
    102
    +        typ = 'groupchat' if type_ == 'gc' else 'chat'
    
    103
    +        message = nbxmpp.Message(to=contact.jid, typ=typ)
    
    104
    +        message.setMarker(marker, id_)
    
    105
    +        self._log.info('Send %s: %s', marker, contact.jid)
    
    106
    +        self._nbxmpp().send(message)
    
    107
    +
    
    108
    +    def send_displayed_marker(self, contact, id_, type_):
    
    109
    +        self._send_marker(contact, 'displayed', id_, str(type_))
    
    110
    +
    
    111
    +
    
    112
    +def get_instance(*args, **kwargs):
    
    113
    +    return ChatMarkers(*args, **kwargs), 'ChatMarkers'

  • gajim/common/modules/message.py
    ... ... @@ -305,6 +305,8 @@ class Message(BaseModule):
    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 @@ class Message(BaseModule):
    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/modules/metacontacts.py
    ... ... @@ -30,6 +30,10 @@ class MetaContacts(BaseModule):
    30 30
             self.available = False
    
    31 31
     
    
    32 32
         def get_metacontacts(self):
    
    33
    +        if not app.settings.get('metacontacts_enabled'):
    
    34
    +            self._con.connect_machine()
    
    35
    +            return
    
    36
    +
    
    33 37
             self._log.info('Request')
    
    34 38
             node = nbxmpp.Node('storage', attrs={'xmlns': 'storage:metacontacts'})
    
    35 39
             iq = nbxmpp.Iq('get', Namespace.PRIVATE, payload=node)
    

  • gajim/common/setting_values.py
    ... ... @@ -139,6 +139,7 @@ APP_SETTINGS = {
    139 139
         'chat_merge_consecutive_nickname': False,
    
    140 140
         'chat_merge_consecutive_nickname_indent': '  ',
    
    141 141
         'ctrl_tab_go_to_next_composing': True,
    
    142
    +    'metacontacts_enabled': True,
    
    142 143
         'confirm_metacontacts': '',
    
    143 144
         'confirm_block': '',
    
    144 145
         'enable_negative_priority': False,
    
    ... ... @@ -254,12 +255,15 @@ ACCOUNT_SETTINGS = {
    254 255
             'filetransfer_preference': 'httpupload',
    
    255 256
             'send_chatstate_default': 'composing_only',
    
    256 257
             'gc_send_chatstate_default': 'composing_only',
    
    258
    +        'send_marker_default': True,
    
    259
    +        'gc_send_marker_default': True,
    
    257 260
             'chat_history_max_age': -1,
    
    258 261
         },
    
    259 262
     
    
    260 263
         'contact': {
    
    261 264
             'speller_language': '',
    
    262 265
             'send_chatstate': HAS_ACCOUNT_DEFAULT,
    
    266
    +        'send_marker': HAS_ACCOUNT_DEFAULT,
    
    263 267
             'encryption': '',
    
    264 268
         },
    
    265 269
     
    
    ... ... @@ -271,6 +275,7 @@ ACCOUNT_SETTINGS = {
    271 275
             'minimize_on_autojoin': True,
    
    272 276
             'minimize_on_close': True,
    
    273 277
             'send_chatstate': HAS_ACCOUNT_DEFAULT,
    
    278
    +        'send_marker': HAS_ACCOUNT_DEFAULT,
    
    274 279
             'encryption': '',
    
    275 280
             'sync_threshold': HAS_APP_DEFAULT,
    
    276 281
         },
    

  • gajim/common/zeroconf/connection_zeroconf.py
    ... ... @@ -261,7 +261,7 @@ class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf):
    261 261
                     if result is False:
    
    262 262
                         app.nec.push_incoming_event(ConnectionLostEvent(None,
    
    263 263
                             conn=self, title=_('Could not start local service'),
    
    264
    -                        msg=_('Unable to bind to port %d.' % self.port)))
    
    264
    +                        msg=_('Unable to bind to port %d.') % self.port))
    
    265 265
                     else: # result is None
    
    266 266
                         app.nec.push_incoming_event(ConnectionLostEvent(None,
    
    267 267
                             conn=self, title=_('Could not start local service'),
    

  • gajim/conversation_textview.py
    ... ... @@ -38,6 +38,7 @@ from gajim.common import app
    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 @@ class ConversationTextview(GObject.GObject):
    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 @@ class MessageLine:
    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 @@ class MessageIcons(Gtk.Box):
    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 @@ class MessageIcons(Gtk.Box):
    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
    ... ... @@ -495,7 +495,7 @@ class GroupchatControl(ChatControlBase):
    495 495
             con = app.connections[self.account]
    
    496 496
             message_id = con.get_module('MUC').invite(self.room_jid, contact_jid)
    
    497 497
             self.add_info_message(
    
    498
    -            _('%s has been invited to this group chat' % contact_jid),
    
    498
    +            _('%s has been invited to this group chat') % contact_jid,
    
    499 499
                 message_id=message_id)
    
    500 500
     
    
    501 501
         def _on_destroy_room(self, _action, _param):
    
    ... ... @@ -591,7 +591,7 @@ class GroupchatControl(ChatControlBase):
    591 591
             try:
    
    592 592
                 task.finish()
    
    593 593
             except Exception as error:
    
    594
    -            self.add_info_message(_('Avatar upload failed: %s' % error))
    
    594
    +            self.add_info_message(_('Avatar upload failed: %s') % error)
    
    595 595
     
    
    596 596
             else:
    
    597 597
                 self.add_info_message(_('Avatar upload successful'))
    
    ... ... @@ -612,7 +612,7 @@ class GroupchatControl(ChatControlBase):
    612 612
         def _on_kick(self, _action, param):
    
    613 613
             nick = param.get_string()
    
    614 614
             self._kick_nick = nick
    
    615
    -        self.xml.kick_label.set_text(_('Kick %s' % nick))
    
    615
    +        self.xml.kick_label.set_text(_('Kick %s') % nick)
    
    616 616
             self.xml.kick_reason_entry.grab_focus()
    
    617 617
             self.xml.kick_participant_button.grab_default()
    
    618 618
             self._show_page('kick')
    
    ... ... @@ -621,7 +621,7 @@ class GroupchatControl(ChatControlBase):
    621 621
             jid = param.get_string()
    
    622 622
             self._ban_jid = jid
    
    623 623
             nick = app.get_nick_from_jid(jid)
    
    624
    -        self.xml.ban_label.set_text(_('Ban %s' % nick))
    
    624
    +        self.xml.ban_label.set_text(_('Ban %s') % nick)
    
    625 625
             self.xml.ban_reason_entry.grab_focus()
    
    626 626
             self.xml.ban_participant_button.grab_default()
    
    627 627
             self._show_page('ban')
    
    ... ... @@ -656,6 +656,17 @@ class GroupchatControl(ChatControlBase):
    656 656
             self.update_actions()
    
    657 657
             self.set_lock_image()
    
    658 658
             self.draw_banner_text()
    
    659
    +        type_ = ['printed_gc_msg', 'printed_marked_gc_msg']
    
    660
    +        if not app.events.remove_events(self.account,
    
    661
    +                                        self.get_full_jid(),
    
    662
    +                                        types=type_):
    
    663
    +            # XEP-0333 Send <displayed> marker
    
    664
    +            con = app.connections[self.account]
    
    665
    +            con.get_module('ChatMarkers').send_displayed_marker(
    
    666
    +                self.contact,
    
    667
    +                self.last_msg_id,
    
    668
    +                self._type)
    
    669
    +            self.last_msg_id = None
    
    659 670
     
    
    660 671
         def _on_roster_row_activated(self, _roster, nick):
    
    661 672
             self._start_private_message(nick)
    
    ... ... @@ -799,12 +810,12 @@ class GroupchatControl(ChatControlBase):
    799 810
                                  tim=event.properties.timestamp,
    
    800 811
                                  displaymarking=event.displaymarking,
    
    801 812
                                  correct_id=event.correct_id,
    
    802
    -                             message_id=event.properties.id,
    
    813
    +                             message_id=event.properties.stanza_id.id,
    
    803 814
                                  additional_data=event.additional_data)
    
    804 815
             event.needs_highlight = self.needs_visual_notification(event.msgtxt)
    
    805 816
     
    
    806 817
         def on_private_message(self, nick, sent, msg, tim, session, additional_data,
    
    807
    -                           msg_log_id=None, displaymarking=None):
    
    818
    +                           message_id, msg_log_id=None, displaymarking=None):
    
    808 819
             # Do we have a queue?
    
    809 820
             fjid = self.room_jid + '/' + nick
    
    810 821
     
    
    ... ... @@ -817,7 +828,9 @@ class GroupchatControl(ChatControlBase):
    817 828
                                    session=session,
    
    818 829
                                    displaymarking=displaymarking,
    
    819 830
                                    sent_forwarded=sent,
    
    820
    -                               additional_data=additional_data)
    
    831
    +                               additional_data=additional_data,
    
    832
    +                               message_id=message_id)
    
    833
    +
    
    821 834
             app.events.add_event(self.account, fjid, event)
    
    822 835
     
    
    823 836
             if allow_popup_window(self.account):
    
    ... ... @@ -1055,6 +1068,7 @@ class GroupchatControl(ChatControlBase):
    1055 1068
                                         event.properties.timestamp,
    
    1056 1069
                                         self.session,
    
    1057 1070
                                         event.additional_data,
    
    1071
    +                                    event.properties.id,
    
    1058 1072
                                         msg_log_id=event.msg_log_id,
    
    1059 1073
                                         displaymarking=event.displaymarking)
    
    1060 1074
     
    

  • gajim/gtk/about.py
    ... ... @@ -49,8 +49,8 @@ class AboutDialog(Gtk.AboutDialog):
    49 49
     
    
    50 50
             comments = []
    
    51 51
             comments.append(_('A GTK XMPP client'))
    
    52
    -        comments.append(_('GTK Version: %s' % gtk_ver))
    
    53
    -        comments.append(_('GLib Version: %s' % glib_ver))
    
    52
    +        comments.append(_('GTK Version: %s') % gtk_ver)
    
    53
    +        comments.append(_('GLib Version: %s') % glib_ver)
    
    54 54
             comments.append(_('PyGObject Version: %s') % gobject_ver)
    
    55 55
             comments.append(_('python-nbxmpp Version: %s') % nbxmpp.__version__)
    
    56 56
     
    

  • gajim/gtk/add_contact.py
    ... ... @@ -258,7 +258,7 @@ class AddNewContactWindow(Gtk.ApplicationWindow, EventHelper):
    258 258
             if not jid:
    
    259 259
                 ErrorDialog(
    
    260 260
                     _('%s Missing') % self.uid_label.get_text(),
    
    261
    -                _('You must supply the %s of the new contact.' %
    
    261
    +                (_('You must supply the %s of the new contact.') %
    
    262 262
                         self.uid_label.get_text())
    
    263 263
                 )
    
    264 264
                 return
    

  • gajim/gtk/bookmarks.py
    ... ... @@ -45,7 +45,7 @@ class Bookmarks(Gtk.ApplicationWindow):
    45 45
             self.set_application(app.app)
    
    46 46
             self.set_position(Gtk.WindowPosition.CENTER)
    
    47 47
             self.set_show_menubar(False)
    
    48
    -        self.set_title(_('Bookmarks for %s' % app.get_account_label(account)))
    
    48
    +        self.set_title(_('Bookmarks for %s') % app.get_account_label(account))
    
    49 49
             self.set_default_size(700, 500)
    
    50 50
     
    
    51 51
             self.account = account
    

  • gajim/gtk/filetransfer.py
    ... ... @@ -396,7 +396,7 @@ class FileTransfersWindow:
    396 396
                         file_name = GLib.markup_escape_text(
    
    397 397
                             os.path.basename(file_path))
    
    398 398
                         ErrorDialog(
    
    399
    -                        _('Cannot overwrite existing file \'%s\'' % file_name),
    
    399
    +                        _('Cannot overwrite existing file \'%s\'') % file_name,
    
    400 400
                             _('A file with this name already exists and you do '
    
    401 401
                               'not have permission to overwrite it.'))
    
    402 402
                         return
    

  • gajim/gtk/groupchat_settings.py
    ... ... @@ -71,6 +71,12 @@ class GroupChatSettings(SettingsBox):
    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
    +                    desc=_('Let others know if you read up to this point')),
    
    79
    +
    
    74 80
                 Setting(SettingKind.POPOVER,
    
    75 81
                         _('Sync Threshold'),
    
    76 82
                         SettingType.GROUP_CHAT,
    

  • gajim/gtk/history_sync.py
    ... ... @@ -248,11 +248,11 @@ class DownloadHistoryPage(Gtk.Box):
    248 248
             self.received += 1
    
    249 249
             if self.count:
    
    250 250
                 self.progress.set_fraction(self.received / self.count)
    
    251
    -            self.progress.set_text(_('%(received)s of %(max)s' % {
    
    252
    -                'received': self.received, 'max': self.count}))
    
    251
    +            self.progress.set_text(_('%(received)s of %(max)s') % {
    
    252
    +                'received': self.received, 'max': self.count})
    
    253 253
             else:
    
    254 254
                 self.progress.pulse()
    
    255
    -            self.progress.set_text(_('Downloaded %s messages' % self.received))
    
    255
    +            self.progress.set_text(_('Downloaded %s messages') % self.received)
    
    256 256
     
    
    257 257
         def finished(self):
    
    258 258
             self.progress.set_fraction(1)
    
    ... ... @@ -273,7 +273,7 @@ class SummaryPage(Gtk.Box):
    273 273
         def finished(self):
    
    274 274
             received = self.assistant.download_history.received
    
    275 275
             self.label.set_text(_('Finished synchronising chat history:\n'
    
    276
    -                              '%s messages downloaded' % received))
    
    276
    +                              '%s messages downloaded') % received)
    
    277 277
     
    
    278 278
         def nothing_to_do(self):
    
    279 279
             self.label.set_text(_('Gajim is fully synchronised with the archive.'))
    

  • gajim/gtk/pep_config.py
    ... ... @@ -14,7 +14,7 @@
    14 14
     
    
    15 15
     from gi.repository import Gdk
    
    16 16
     from gi.repository import Gtk
    
    17
    -from nbxmpp.util import is_error_result
    
    17
    +from nbxmpp.errors import StanzaError
    
    18 18
     
    
    19 19
     from gajim.common import app
    
    20 20
     from gajim.common import ged
    
    ... ... @@ -85,9 +85,11 @@ class PEPConfig(Gtk.ApplicationWindow, EventHelper):
    85 85
             self._con.get_module('Discovery').disco_items(
    
    86 86
                 jid, callback=self._items_received)
    
    87 87
     
    
    88
    -    def _items_received(self, result):
    
    89
    -        if is_error_result(result):
    
    90
    -            ErrorDialog('Error', to_user_string(result))
    
    88
    +    def _items_received(self, task):
    
    89
    +        try:
    
    90
    +            result = task.finish()
    
    91
    +        except StanzaError as error:
    
    92
    +            ErrorDialog('Error', to_user_string(error))
    
    91 93
                 return
    
    92 94
     
    
    93 95
             jid = result.jid.bare
    

  • gajim/gtk/preferences.py
    ... ... @@ -249,6 +249,11 @@ class ContactList(PreferenceBox):
    249 249
                         'mergeaccounts',
    
    250 250
                         callback=self._on_merge_accounts),
    
    251 251
     
    
    252
    +            Setting(SettingKind.SWITCH,
    
    253
    +                    _('Enable Metacontacts'),
    
    254
    +                    SettingType.CONFIG,
    
    255
    +                    'metacontacts_enabled'),
    
    256
    +
    
    252 257
                 Setting(SettingKind.SWITCH,
    
    253 258
                         _('Show Avatars'),
    
    254 259
                         SettingType.CONFIG,
    

  • gajim/gtk/roster_item_exchange.py
    ... ... @@ -194,7 +194,7 @@ class RosterItemExchangeWindow(Gtk.ApplicationWindow):
    194 194
                         count += 1
    
    195 195
                         # It is selected
    
    196 196
                         message = _('%s suggested me to add you to my '
    
    197
    -                                'contact list.' % self.jid_from)
    
    197
    +                                'contact list.') % self.jid_from
    
    198 198
                         # Keep same groups and same nickname
    
    199 199
                         groups = model[iter_][3].split(', ')
    
    200 200
                         if groups == ['']:
    

  • gajim/gtk/start_chat.py
    ... ... @@ -652,7 +652,7 @@ class ContactRow(Gtk.ListBoxRow):
    652 652
             if show_account:
    
    653 653
                 account_badge = generate_account_badge(account)
    
    654 654
                 account_badge.set_tooltip_text(
    
    655
    -                _('Account: %s' % self.account_label))
    
    655
    +                _('Account: %s') % self.account_label)
    
    656 656
                 account_badge.set_halign(Gtk.Align.END)
    
    657 657
                 account_badge.set_valign(Gtk.Align.START)
    
    658 658
                 account_badge.set_hexpand(True)
    

  • gajim/gui_interface.py
    ... ... @@ -242,6 +242,37 @@ class Interface:
    242 242
             if ctrl and ctrl.session and len(obj.contact_list) > 1:
    
    243 243
                 ctrl.remove_session(ctrl.session)
    
    244 244
     
    
    245
    +    def handle_event_read_state_sync(self, event):
    
    246
    +        if event.type.is_groupchat:
    
    247
    +            control = app.get_groupchat_control(
    
    248
    +                event.account, event.jid.bare)
    
    249
    +            if control is None:
    
    250
    +                log.warning('Groupchat control not found')
    
    251
    +                return
    
    252
    +
    
    253
    +            jid = event.jid.bare
    
    254
    +            types = ['printed_gc_msg', 'printed_marked_gc_msg']
    
    255
    +
    
    256
    +        else:
    
    257
    +            types = ['chat', 'pm', 'printed_chat', 'printed_pm']
    
    258
    +            jid = event.jid
    
    259
    +
    
    260
    +            control = app.interface.msg_win_mgr.get_control(jid, event.account)
    
    261
    +
    
    262
    +        # Compare with control.last_msg_id.
    
    263
    +        events_ = app.events.get_events(event.account, jid, types)
    
    264
    +        if not events_:
    
    265
    +            log.warning('No Events')
    
    266
    +            return
    
    267
    +
    
    268
    +        if events_[-1].message_id != event.marker_id:
    
    269
    +            return
    
    270
    +
    
    271
    +        if not app.events.remove_events(event.account, jid, types=types):
    
    272
    +            # There were events to remove
    
    273
    +            if control is not None:
    
    274
    +                control.redraw_after_event_removed(event.jid)
    
    275
    +
    
    245 276
         @staticmethod
    
    246 277
         def handle_event_msgsent(obj):
    
    247 278
             if not obj.play_sound:
    
    ... ... @@ -926,7 +957,7 @@ class Interface:
    926 957
             if helpers.allow_showing_notification(account):
    
    927 958
                 heading = _('Incoming Call')
    
    928 959
                 contact = app.get_name_from_jid(account, event.jid)
    
    929
    -            text = _('%s is calling' % contact)
    
    960
    +            text = _('%s is calling') % contact
    
    930 961
                 app.notification.popup(
    
    931 962
                     heading,
    
    932 963
                     event.fjid,
    
    ... ... @@ -1060,6 +1091,7 @@ class Interface:
    1060 1091
                 'unsubscribed-presence-received': [
    
    1061 1092
                     self.handle_event_unsubscribed_presence],
    
    1062 1093
                 'zeroconf-name-conflict': [self.handle_event_zc_name_conflict],
    
    1094
    +            'read-state-sync': [self.handle_event_read_state_sync],
    
    1063 1095
             }
    
    1064 1096
     
    
    1065 1097
         def register_core_handlers(self):
    
    ... ... @@ -1983,7 +2015,7 @@ class Interface:
    1983 2015
                     _('Update Available'),
    
    1984 2016
                     _('Gajim Update Available'),
    
    1985 2017
                     _('There is an update available for Gajim '
    
    1986
    -                  '(latest version: %s)' % str(latest_version)),
    
    2018
    +                  '(latest version: %s)') % str(latest_version),
    
    1987 2019
                     _('_Do not show again'),
    
    1988 2020
                     [DialogButton.make('Cancel',
    
    1989 2021
                                         text=_('_Later'),
    

  • 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 get_singlechat_menu(control_id, account, jid):
    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),
    

  • gajim/roster_window.py
    ... ... @@ -66,7 +66,6 @@ from gajim.gtk.dialogs import ConfirmationDialog
    66 66
     from gajim.gtk.dialogs import ConfirmationCheckDialog
    
    67 67
     from gajim.gtk.dialogs import ErrorDialog
    
    68 68
     from gajim.gtk.dialogs import InputDialog
    
    69
    -from gajim.gtk.dialogs import WarningDialog
    
    70 69
     from gajim.gtk.dialogs import InformationDialog
    
    71 70
     from gajim.gtk.single_message import SingleMessageWindow
    
    72 71
     from gajim.gtk.add_contact import AddNewContactWindow
    
    ... ... @@ -3664,11 +3663,7 @@ class RosterWindow:
    3664 3663
             con_dest = app.connections[account_dest]
    
    3665 3664
             if (not con_source.get_module('MetaContacts').available or
    
    3666 3665
                     not con_dest.get_module('MetaContacts').available):
    
    3667
    -            WarningDialog(_('Metacontacts storage not supported by '
    
    3668
    -                'your server'),
    
    3669
    -                _('Your server does not support storing metacontacts '
    
    3670
    -                'information. So this information will not be saved on next '
    
    3671
    -                'reconnection.'))
    
    3666
    +            return
    
    3672 3667
     
    
    3673 3668
             def merge_contacts(is_checked=None):
    
    3674 3669
                 contacts = 0