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

Commits:

18 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
     
    
    ... ... @@ -1111,6 +1114,7 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
    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 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
    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 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
    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 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
    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 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
    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 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
    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 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
    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 @@ 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,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 @@ 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/blocking.py
    ... ... @@ -15,14 +15,15 @@
    15 15
     # XEP-0191: Blocking Command
    
    16 16
     
    
    17 17
     import nbxmpp
    
    18
    +from nbxmpp.protocol import JID
    
    18 19
     from nbxmpp.namespaces import Namespace
    
    19 20
     from nbxmpp.structs import StanzaHandler
    
    20
    -from nbxmpp.util import is_error_result
    
    21
    +from nbxmpp.modules.util import raise_if_error
    
    21 22
     
    
    22 23
     from gajim.common import app
    
    23 24
     from gajim.common.nec import NetworkEvent
    
    24
    -from gajim.common.nec import NetworkIncomingEvent
    
    25 25
     from gajim.common.modules.base import BaseModule
    
    26
    +from gajim.common.modules.util import as_task
    
    26 27
     
    
    27 28
     
    
    28 29
     class Blocking(BaseModule):
    
    ... ... @@ -31,7 +32,7 @@ class Blocking(BaseModule):
    31 32
         _nbxmpp_methods = [
    
    32 33
             'block',
    
    33 34
             'unblock',
    
    34
    -        'get_blocking_list',
    
    35
    +        'request_blocking_list',
    
    35 36
         ]
    
    36 37
     
    
    37 38
         def __init__(self, con):
    
    ... ... @@ -46,9 +47,6 @@ class Blocking(BaseModule):
    46 47
                               ns=Namespace.BLOCKING),
    
    47 48
             ]
    
    48 49
     
    
    49
    -        self._register_callback('get_blocking_list',
    
    50
    -                                self._blocking_list_received)
    
    51
    -
    
    52 50
             self.supported = False
    
    53 51
     
    
    54 52
         def pass_disco(self, info):
    
    ... ... @@ -63,59 +61,65 @@ class Blocking(BaseModule):
    63 61
     
    
    64 62
             self._log.info('Discovered blocking: %s', info.jid)
    
    65 63
     
    
    66
    -    def _blocking_list_received(self, result):
    
    67
    -        if is_error_result(result):
    
    68
    -            self._log.info(result)
    
    69
    -            return
    
    64
    +    @as_task
    
    65
    +    def get_blocking_list(self):
    
    66
    +        _task = yield
    
    70 67
     
    
    71
    -        self.blocked = result.blocking_list
    
    72
    -        app.nec.push_incoming_event(
    
    73
    -            BlockingEvent(None, conn=self._con, changed=self.blocked))
    
    68
    +        blocking_list = yield self._nbxmpp('Blocking').request_blocking_list()
    
    69
    +
    
    70
    +        raise_if_error(blocking_list)
    
    71
    +
    
    72
    +        self.blocked = blocking_list
    
    73
    +        app.nec.push_incoming_event(NetworkEvent('blocking',
    
    74
    +                                                 conn=self._con,
    
    75
    +                                                 changed=self.blocked))
    
    76
    +        yield blocking_list
    
    77
    +
    
    78
    +    @as_task
    
    79
    +    def update_blocking_list(self, block, unblock):
    
    80
    +        _task = yield
    
    81
    +
    
    82
    +        if block:
    
    83
    +            result = yield self.block(block)
    
    84
    +            raise_if_error(result)
    
    74 85
     
    
    75
    -    def _blocking_push_received(self, _con, stanza, _properties):
    
    76
    -        reply = stanza.buildReply('result')
    
    77
    -        children = reply.getChildren()
    
    78
    -        for child in children:
    
    79
    -            reply.delChild(child)
    
    80
    -        self._nbxmpp().send(reply)
    
    86
    +        if unblock:
    
    87
    +            result = yield self.unblock(unblock)
    
    88
    +            raise_if_error(result)
    
    89
    +
    
    90
    +        yield True
    
    91
    +
    
    92
    +    def _blocking_push_received(self, _con, _stanza, properties):
    
    93
    +        if not properties.is_blocking:
    
    94
    +            return
    
    81 95
     
    
    82 96
             changed_list = []
    
    83 97
     
    
    84
    -        unblock = stanza.getTag('unblock', namespace=Namespace.BLOCKING)
    
    85
    -        if unblock is not None:
    
    86
    -            items = unblock.getTags('item')
    
    87
    -            if not items:
    
    88
    -                # Unblock all
    
    89
    -                changed_list = list(self.blocked)
    
    90
    -                self.blocked = []
    
    91
    -                for jid in self.blocked:
    
    92
    -                    self._presence_probe(jid)
    
    93
    -                self._log.info('Unblock all Push')
    
    94
    -                raise nbxmpp.NodeProcessed
    
    95
    -
    
    96
    -            for item in items:
    
    97
    -                # Unblock some contacts
    
    98
    -                jid = item.getAttr('jid')
    
    99
    -                changed_list.append(jid)
    
    100
    -                if jid not in self.blocked:
    
    101
    -                    continue
    
    102
    -                self.blocked.remove(jid)
    
    98
    +        if properties.blocking.unblock_all:
    
    99
    +            self.blocked = []
    
    100
    +            for jid in self.blocked:
    
    103 101
                     self._presence_probe(jid)
    
    104
    -                self._log.info('Unblock Push: %s', jid)
    
    105
    -
    
    106
    -        block = stanza.getTag('block', namespace=Namespace.BLOCKING)
    
    107
    -        if block is not None:
    
    108
    -            for item in block.getTags('item'):
    
    109
    -                jid = item.getAttr('jid')
    
    110
    -                if jid in self.blocked:
    
    111
    -                    continue
    
    112
    -                changed_list.append(jid)
    
    113
    -                self.blocked.append(jid)
    
    114
    -                self._set_contact_offline(jid)
    
    115
    -                self._log.info('Block Push: %s', jid)
    
    116
    -
    
    117
    -        app.nec.push_incoming_event(
    
    118
    -            BlockingEvent(None, conn=self._con, changed=changed_list))
    
    102
    +            self._log.info('Unblock all Push')
    
    103
    +
    
    104
    +        for jid in properties.blocking.unblock:
    
    105
    +            changed_list.append(jid)
    
    106
    +            if jid not in self.blocked:
    
    107
    +                continue
    
    108
    +            self.blocked.remove(jid)
    
    109
    +            self._presence_probe(jid)
    
    110
    +            self._log.info('Unblock Push: %s', jid)
    
    111
    +
    
    112
    +        for jid in properties.blocking.block:
    
    113
    +            if jid in self.blocked:
    
    114
    +                continue
    
    115
    +            changed_list.append(jid)
    
    116
    +            self.blocked.append(jid)
    
    117
    +            self._set_contact_offline(str(jid))
    
    118
    +            self._log.info('Block Push: %s', jid)
    
    119
    +
    
    120
    +        app.nec.push_incoming_event(NetworkEvent('blocking',
    
    121
    +                                                 conn=self._con,
    
    122
    +                                                 changed=changed_list))
    
    119 123
     
    
    120 124
             raise nbxmpp.NodeProcessed
    
    121 125
     
    
    ... ... @@ -124,16 +128,12 @@ class Blocking(BaseModule):
    124 128
             for contact in contact_list:
    
    125 129
                 contact.show = 'offline'
    
    126 130
     
    
    127
    -    def _presence_probe(self, jid: str) -> None:
    
    131
    +    def _presence_probe(self, jid: JID) -> None:
    
    128 132
             self._log.info('Presence probe: %s', jid)
    
    129 133
             # Send a presence Probe to get the current Status
    
    130 134
             probe = nbxmpp.Presence(jid, 'probe', frm=self._con.get_own_jid())
    
    131 135
             self._nbxmpp().send(probe)
    
    132 136
     
    
    133 137
     
    
    134
    -class BlockingEvent(NetworkIncomingEvent):
    
    135
    -    name = 'blocking'
    
    136
    -
    
    137
    -
    
    138 138
     def get_instance(*args, **kwargs):
    
    139 139
         return Blocking(*args, **kwargs), 'Blocking'

  • 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/setting_values.py
    ... ... @@ -255,12 +255,15 @@ ACCOUNT_SETTINGS = {
    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_default': True,
    
    258 260
             'chat_history_max_age': -1,
    
    259 261
         },
    
    260 262
     
    
    261 263
         'contact': {
    
    262 264
             'speller_language': '',
    
    263 265
             'send_chatstate': HAS_ACCOUNT_DEFAULT,
    
    266
    +        'send_marker': HAS_ACCOUNT_DEFAULT,
    
    264 267
             'encryption': '',
    
    265 268
         },
    
    266 269
     
    
    ... ... @@ -272,6 +275,7 @@ ACCOUNT_SETTINGS = {
    272 275
             'minimize_on_autojoin': True,
    
    273 276
             'minimize_on_close': True,
    
    274 277
             'send_chatstate': HAS_ACCOUNT_DEFAULT,
    
    278
    +        'send_marker': HAS_ACCOUNT_DEFAULT,
    
    275 279
             'encryption': '',
    
    276 280
             'sync_threshold': HAS_APP_DEFAULT,
    
    277 281
         },
    

  • 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
    ... ... @@ -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)
    
    ... ... @@ -800,11 +811,12 @@ class GroupchatControl(ChatControlBase):
    800 811
                                  displaymarking=event.displaymarking,
    
    801 812
                                  correct_id=event.correct_id,
    
    802 813
                                  message_id=event.properties.id,
    
    814
    +                             stanza_id=event.properties.stanza_id.id,
    
    803 815
                                  additional_data=event.additional_data)
    
    804 816
             event.needs_highlight = self.needs_visual_notification(event.msgtxt)
    
    805 817
     
    
    806 818
         def on_private_message(self, nick, sent, msg, tim, session, additional_data,
    
    807
    -                           msg_log_id=None, displaymarking=None):
    
    819
    +                           message_id, msg_log_id=None, displaymarking=None):
    
    808 820
             # Do we have a queue?
    
    809 821
             fjid = self.room_jid + '/' + nick
    
    810 822
     
    
    ... ... @@ -817,7 +829,9 @@ class GroupchatControl(ChatControlBase):
    817 829
                                    session=session,
    
    818 830
                                    displaymarking=displaymarking,
    
    819 831
                                    sent_forwarded=sent,
    
    820
    -                               additional_data=additional_data)
    
    832
    +                               additional_data=additional_data,
    
    833
    +                               message_id=message_id)
    
    834
    +
    
    821 835
             app.events.add_event(self.account, fjid, event)
    
    822 836
     
    
    823 837
             if allow_popup_window(self.account):
    
    ... ... @@ -835,7 +849,7 @@ class GroupchatControl(ChatControlBase):
    835 849
     
    
    836 850
         def add_message(self, text, contact='', tim=None,
    
    837 851
                         displaymarking=None, correct_id=None, message_id=None,
    
    838
    -                    additional_data=None):
    
    852
    +                    stanza_id=None, additional_data=None):
    
    839 853
             """
    
    840 854
             Add message to the ConversationsTextview
    
    841 855
     
    
    ... ... @@ -885,6 +899,7 @@ class GroupchatControl(ChatControlBase):
    885 899
                                         displaymarking=displaymarking,
    
    886 900
                                         correct_id=correct_id,
    
    887 901
                                         message_id=message_id,
    
    902
    +                                    stanza_id=stanza_id,
    
    888 903
                                         additional_data=additional_data)
    
    889 904
     
    
    890 905
         def get_nb_unread(self):
    
    ... ... @@ -1055,6 +1070,7 @@ class GroupchatControl(ChatControlBase):
    1055 1070
                                         event.properties.timestamp,
    
    1056 1071
                                         self.session,
    
    1057 1072
                                         event.additional_data,
    
    1073
    +                                    event.properties.id,
    
    1058 1074
                                         msg_log_id=event.msg_log_id,
    
    1059 1075
                                         displaymarking=event.displaymarking)
    
    1060 1076
     
    

  • gajim/gtk/blocking.py
    ... ... @@ -14,7 +14,7 @@
    14 14
     
    
    15 15
     import logging
    
    16 16
     
    
    17
    -from nbxmpp.util import is_error_result
    
    17
    +from nbxmpp.errors import StanzaError
    
    18 18
     from gi.repository import Gtk
    
    19 19
     from gi.repository import Gdk
    
    20 20
     from gi.repository import GLib
    
    ... ... @@ -42,8 +42,6 @@ class BlockingList(Gtk.ApplicationWindow):
    42 42
             self.account = account
    
    43 43
             self._con = app.connections[account]
    
    44 44
             self._prev_blocked_jids = set()
    
    45
    -        self._await_results = 2
    
    46
    -        self._received_errors = False
    
    47 45
     
    
    48 46
             self._ui = get_builder('blocking_list.ui')
    
    49 47
             self.add(self._ui.blocking_grid)
    
    ... ... @@ -57,15 +55,9 @@ class BlockingList(Gtk.ApplicationWindow):
    57 55
     
    
    58 56
             self._activate_spinner()
    
    59 57
     
    
    60
    -        self._con.get_module('Blocking').get_blocking_list(
    
    58
    +        self._con.get_module('Blocking').request_blocking_list(
    
    61 59
                 callback=self._on_blocking_list_received)
    
    62 60
     
    
    63
    -    def _reset_after_error(self):
    
    64
    -        self._received_errors = False
    
    65
    -        self._await_results = 2
    
    66
    -        self._disable_spinner()
    
    67
    -        self._set_grid_state(True)
    
    68
    -
    
    69 61
         def _show_error(self, error):
    
    70 62
             dialog = HigDialog(
    
    71 63
                 self, Gtk.MessageType.INFO, Gtk.ButtonsType.OK,
    
    ... ... @@ -73,31 +65,32 @@ class BlockingList(Gtk.ApplicationWindow):
    73 65
                 GLib.markup_escape_text(error))
    
    74 66
             dialog.popup()
    
    75 67
     
    
    76
    -    def _on_blocking_list_received(self, result):
    
    77
    -        is_error = is_error_result(result)
    
    78
    -        self._disable_spinner()
    
    79
    -        self._set_grid_state(not is_error)
    
    68
    +    def _on_blocking_list_received(self, task):
    
    69
    +        try:
    
    70
    +            blocking_list = task.finish()
    
    71
    +        except StanzaError as error:
    
    72
    +            self._set_grid_state(False)
    
    73
    +            self._show_error(to_user_string(error))
    
    74
    +            return
    
    80 75
     
    
    81
    -        if is_error:
    
    82
    -            self._show_error(to_user_string(result))
    
    76
    +        self._prev_blocked_jids = set(blocking_list)
    
    77
    +        self._ui.blocking_store.clear()
    
    78
    +        for item in blocking_list:
    
    79
    +            self._ui.blocking_store.append((item,))
    
    83 80
     
    
    84
    -        else:
    
    85
    -            self._prev_blocked_jids = set(result.blocking_list)
    
    86
    -            self._ui.blocking_store.clear()
    
    87
    -            for item in result.blocking_list:
    
    88
    -                self._ui.blocking_store.append((item,))
    
    81
    +        self._set_grid_state(True)
    
    82
    +        self._disable_spinner()
    
    89 83
     
    
    90
    -    def _on_save_result(self, result):
    
    91
    -        self._await_results -= 1
    
    92
    -        if is_error_result(result) and not self._received_errors:
    
    93
    -            self._show_error(to_user_string(result))
    
    94
    -            self._received_errors = True
    
    84
    +    def _on_save_result(self, task):
    
    85
    +        try:
    
    86
    +            _successful = task.finish()
    
    87
    +        except StanzaError as error:
    
    88
    +            self._show_error(to_user_string(error))
    
    89
    +            self._disable_spinner()
    
    90
    +            self._set_grid_state(True)
    
    91
    +            return
    
    95 92
     
    
    96
    -        if not self._await_results:
    
    97
    -            if self._received_errors:
    
    98
    -                self._reset_after_error()
    
    99
    -            else:
    
    100
    -                self.destroy()
    
    93
    +        self.destroy()
    
    101 94
     
    
    102 95
         def _set_grid_state(self, state):
    
    103 96
             self._ui.blocking_grid.set_sensitive(state)
    
    ... ... @@ -124,22 +117,10 @@ class BlockingList(Gtk.ApplicationWindow):
    124 117
                 blocked_jids.add(item[0].lower())
    
    125 118
     
    
    126 119
             unblock_jids = self._prev_blocked_jids - blocked_jids
    
    127
    -        if unblock_jids:
    
    128
    -            self._con.get_module('Blocking').unblock(
    
    129
    -                unblock_jids, callback=self._on_save_result)
    
    130
    -        else:
    
    131
    -            self._await_results -= 1
    
    132
    -
    
    133 120
             block_jids = blocked_jids - self._prev_blocked_jids
    
    134
    -        if block_jids:
    
    135
    -            self._con.get_module('Blocking').block(
    
    136
    -                block_jids, callback=self._on_save_result)
    
    137
    -        else:
    
    138
    -            self._await_results -= 1
    
    139
    -
    
    140
    -        if not self._await_results:
    
    141
    -            # No changes
    
    142
    -            self.destroy()
    
    121
    +
    
    122
    +        self._con.get_module('Blocking').update_blocking_list(
    
    123
    +            block_jids, unblock_jids, callback=self._on_save_result)
    
    143 124
     
    
    144 125
         def _activate_spinner(self):
    
    145 126
             self._spinner.show()
    

  • 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/gui_interface.py
    ... ... @@ -242,6 +242,43 @@ 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
    +    @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 @@ class Interface:
    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 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
    ... ... @@ -2614,7 +2614,7 @@ class RosterWindow:
    2614 2614
     
    
    2615 2615
         def _nec_blocking(self, obj):
    
    2616 2616
             for jid in obj.changed:
    
    2617
    -            self.draw_contact(jid, obj.conn.name)
    
    2617
    +            self.draw_contact(str(jid), obj.conn.name)
    
    2618 2618
     
    
    2619 2619
         def on_block(self, widget, list_):
    
    2620 2620
             """
    

  • gajim/vcard.py
    ... ... @@ -32,7 +32,7 @@ from gi.repository import Gtk
    32 32
     from gi.repository import GLib
    
    33 33
     from gi.repository import Gdk
    
    34 34
     from nbxmpp.structs import AnnotationNote
    
    35
    -from nbxmpp.util import is_error_result
    
    35
    +from nbxmpp.errors import StanzaError
    
    36 36
     from nbxmpp.protocol import JID
    
    37 37
     from nbxmpp.errors import is_error
    
    38 38
     
    
    ... ... @@ -261,11 +261,18 @@ class VcardWindow(EventHelper):
    261 261
             self._set_values(vcard.data, str(jid))
    
    262 262
             self._load_avatar(vcard)
    
    263 263
     
    
    264
    -    def set_os_info(self, result, jid):
    
    264
    +    def set_os_info(self, task):
    
    265
    +        error = False
    
    266
    +        try:
    
    267
    +            result = task.finish()
    
    268
    +        except StanzaError:
    
    269
    +            error = True
    
    270
    +
    
    271
    +        jid = task.get_user_data()
    
    272
    +
    
    265 273
             if self.xml.get_object('information_notebook').get_n_pages() < 5:
    
    266 274
                 return
    
    267 275
     
    
    268
    -        error = is_error_result(result)
    
    269 276
             i = 0
    
    270 277
             client = ''
    
    271 278
             os_info = ''