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

Commits:

16 changed files:

Changes:

  • data/gajim.doap
    ... ... @@ -660,6 +660,13 @@
    660 660
             <xmpp:version>0.2</xmpp:version>
    
    661 661
           </xmpp:SupportedXep>
    
    662 662
         </implements>
    
    663
    +    <implements>
    
    664
    +      <xmpp:SupportedXep>
    
    665
    +        <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0425.html"/>
    
    666
    +        <xmpp:status>complete</xmpp:status>
    
    667
    +        <xmpp:version>0.2.1</xmpp:version>
    
    668
    +      </xmpp:SupportedXep>
    
    669
    +    </implements>
    
    663 670
         <implements>
    
    664 671
           <xmpp:SupportedXep>
    
    665 672
             <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0441.html"/>
    

  • gajim/common/const.py
    ... ... @@ -1025,6 +1025,7 @@ def is_active(self):
    1025 1025
         Namespace.JINGLE_BYTESTREAM,
    
    1026 1026
         Namespace.JINGLE_IBB,
    
    1027 1027
         Namespace.AVATAR_METADATA + '+notify',
    
    1028
    +    Namespace.MESSAGE_MODERATE
    
    1028 1029
     ]
    
    1029 1030
     
    
    1030 1031
     
    

  • gajim/common/helpers.py
    ... ... @@ -752,6 +752,16 @@ def get_subscription_request_msg(account: Optional[str] = None) -> str:
    752 752
         message = _('Hello, I am $name. %s') % message
    
    753 753
         return Template(message).safe_substitute({'name': app.nicks[account]})
    
    754 754
     
    
    755
    +def get_retraction_text(account: str, moderator_jid: str,
    
    756
    +                        reason: Optional[str]) -> str:
    
    757
    +    client = app.get_client(account)
    
    758
    +    contact = client.get_module('Contacts').get_contact(
    
    759
    +        moderator_jid, groupchat=True)
    
    760
    +    text = _('This message has been retracted by %s.') % contact.name
    
    761
    +    if reason is not None:
    
    762
    +        text += ' ' + _('Reason: %s') % reason
    
    763
    +    return text
    
    764
    +
    
    755 765
     def get_user_proxy(account: str) -> Optional[ProxyData]:
    
    756 766
         proxy_name = app.settings.get_account_setting(account, 'proxy')
    
    757 767
         if not proxy_name:
    
    ... ... @@ -1197,6 +1207,12 @@ def is_role_change_allowed(self_contact, contact):
    1197 1207
         return self_contact.affiliation >= contact.affiliation
    
    1198 1208
     
    
    1199 1209
     
    
    1210
    +def is_retraction_allowed(self_contact, contact):
    
    1211
    +    if self_contact.role < Role.MODERATOR:
    
    1212
    +        return False
    
    1213
    +    return self_contact.affiliation >= contact.affiliation
    
    1214
    +
    
    1215
    +
    
    1200 1216
     def get_tls_error_phrase(tls_error: Gio.TlsCertificateFlags) -> str:
    
    1201 1217
         phrase = GIO_TLS_ERRORS.get(tls_error)
    
    1202 1218
         if phrase is None:
    

  • gajim/common/modules/mam.py
    ... ... @@ -34,6 +34,7 @@
    34 34
     from gajim.common.const import KindConstant
    
    35 35
     from gajim.common.const import SyncThreshold
    
    36 36
     from gajim.common.helpers import AdditionalDataDict
    
    37
    +from gajim.common.helpers import get_retraction_text
    
    37 38
     from gajim.common.modules.misc import parse_oob
    
    38 39
     from gajim.common.modules.misc import parse_correction
    
    39 40
     from gajim.common.modules.util import get_eme_message
    
    ... ... @@ -226,6 +227,19 @@ def _mam_message_received(self, _con, stanza, properties):
    226 227
                 if properties.eme is not None:
    
    227 228
                     msgtxt = get_eme_message(properties.eme)
    
    228 229
     
    
    230
    +        if properties.is_moderation:
    
    231
    +            additional_data.set_value(
    
    232
    +                'retracted', 'by', properties.moderation.moderator_jid)
    
    233
    +            additional_data.set_value(
    
    234
    +                'retracted', 'timestamp', properties.moderation.timestamp)
    
    235
    +            additional_data.set_value(
    
    236
    +                'retracted', 'reason', properties.moderation.reason)
    
    237
    +
    
    238
    +            msgtxt = get_retraction_text(
    
    239
    +                self._account,
    
    240
    +                properties.moderation.moderator_jid,
    
    241
    +                properties.moderation.reason)
    
    242
    +
    
    229 243
             if not msgtxt:
    
    230 244
                 # For example Chatstates, Receipts, Chatmarkers
    
    231 245
                 self._log.debug(stanza.getProperties())
    
    ... ... @@ -259,6 +273,7 @@ def _mam_message_received(self, _con, stanza, properties):
    259 273
                              account=self._account,
    
    260 274
                              additional_data=additional_data,
    
    261 275
                              correct_id=parse_correction(properties),
    
    276
    +                         stanza_id=stanza_id,
    
    262 277
                              archive_jid=properties.mam.archive,
    
    263 278
                              msgtxt=properties.body,
    
    264 279
                              properties=properties,
    

  • gajim/common/modules/muc.py
    ... ... @@ -63,7 +63,8 @@ class MUC(BaseModule):
    63 63
             'request_voice',
    
    64 64
             'approve_voice_request',
    
    65 65
             'destroy',
    
    66
    -        'request_disco_info'
    
    66
    +        'request_disco_info',
    
    67
    +        'retract_message'
    
    67 68
         ]
    
    68 69
     
    
    69 70
         def __init__(self, con):
    
    ... ... @@ -82,6 +83,10 @@ def __init__(self, con):
    82 83
                               callback=self._on_subject_change,
    
    83 84
                               typ='groupchat',
    
    84 85
                               priority=49),
    
    86
    +            StanzaHandler(name='message',
    
    87
    +                          callback=self._on_update_message,
    
    88
    +                          typ='groupchat',
    
    89
    +                          priority=49),
    
    85 90
                 StanzaHandler(name='message',
    
    86 91
                               callback=self._on_config_change,
    
    87 92
                               ns=Namespace.MUC_USER,
    
    ... ... @@ -668,6 +673,19 @@ def _on_subject_change(self, _con, _stanza, properties):
    668 673
     
    
    669 674
             raise nbxmpp.NodeProcessed
    
    670 675
     
    
    676
    +    def _on_update_message(self, _con, _stanza, properties):
    
    677
    +        if properties.is_moderation:
    
    678
    +            app.storage.archive.update_additional_data(
    
    679
    +                self._account, properties.moderation.stanza_id, properties)
    
    680
    +
    
    681
    +            app.nec.push_incoming_event(
    
    682
    +                NetworkEvent('message-updated',
    
    683
    +                            account=self._account,
    
    684
    +                            jid=properties.jid,
    
    685
    +                            properties=properties))
    
    686
    +
    
    687
    +            raise nbxmpp.NodeProcessed
    
    688
    +
    
    671 689
         def _fake_subject_change(self, room_jid):
    
    672 690
             # This is for servers which don’t send empty subjects as part of the
    
    673 691
             # event order on joining a MUC. For example jabber.ru
    

  • gajim/common/storage/archive.py
    ... ... @@ -29,6 +29,8 @@
    29 29
     import sqlite3 as sqlite
    
    30 30
     from collections import namedtuple
    
    31 31
     
    
    32
    +from nbxmpp.structs import MessageProperties
    
    33
    +
    
    32 34
     from gajim.common import app
    
    33 35
     from gajim.common import configpaths
    
    34 36
     from gajim.common.helpers import AdditionalDataDict
    
    ... ... @@ -352,7 +354,7 @@ def get_conversation_before_after(self, account, jid, before, timestamp,
    352 354
     
    
    353 355
             sql = '''
    
    354 356
                 SELECT contact_name, time, kind, show, message, subject,
    
    355
    -                   additional_data, log_line_id, message_id,
    
    357
    +                   additional_data, log_line_id, message_id, stanza_id,
    
    356 358
                        error as "error [common_error]",
    
    357 359
                        marker as "marker [marker]"
    
    358 360
                 FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
    
    ... ... @@ -394,7 +396,7 @@ def get_conversation_muc_before_after(self, account, jid, before,
    394 396
     
    
    395 397
             sql = '''
    
    396 398
                 SELECT contact_name, time, kind, show, message, subject,
    
    397
    -                   additional_data, log_line_id, message_id,
    
    399
    +                   additional_data, log_line_id, message_id, stanza_id,
    
    398 400
                        error as "error [common_error]",
    
    399 401
                        marker as "marker [marker]"
    
    400 402
                 FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
    
    ... ... @@ -429,7 +431,8 @@ def get_last_conversation_line(self, account, jid):
    429 431
                               KindConstant.ERROR])
    
    430 432
     
    
    431 433
             sql = '''
    
    432
    -            SELECT contact_name, time, kind, message, additional_data
    
    434
    +            SELECT contact_name, time, kind, message, stanza_id,
    
    435
    +            additional_data
    
    433 436
                 FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
    
    434 437
                 AND account_id = {account_id}
    
    435 438
                 AND kind NOT IN ({kinds})
    
    ... ... @@ -920,6 +923,84 @@ def find_stanza_id(self, account, archive_jid, stanza_id, origin_id=None,
    920 923
                 return True
    
    921 924
             return False
    
    922 925
     
    
    926
    +    @timeit
    
    927
    +    def update_additional_data(self,
    
    928
    +                               account: str,
    
    929
    +                               stanza_id: str,
    
    930
    +                               properties: MessageProperties) -> None:
    
    931
    +        is_groupchat = properties.type.is_groupchat
    
    932
    +        type_ = JIDConstant.NORMAL_TYPE
    
    933
    +        if is_groupchat:
    
    934
    +            type_ = JIDConstant.ROOM_TYPE
    
    935
    +
    
    936
    +        archive_id = self.get_jid_id(str(properties.jid.bare), type_=type_)
    
    937
    +        account_id = self.get_account_id(account)
    
    938
    +
    
    939
    +        if is_groupchat:
    
    940
    +            # Stanza ID is only unique within a specific archive.
    
    941
    +            # So a Stanza ID could be repeated in different MUCs, so we
    
    942
    +            # filter also for the archive JID which is the bare MUC jid.
    
    943
    +
    
    944
    +            # Use Unary-"+" operator for "jid_id", otherwise the
    
    945
    +            # idx_logs_jid_id_time index is used instead of the much better
    
    946
    +            # idx_logs_stanza_id index
    
    947
    +            sql = '''
    
    948
    +                SELECT additional_data FROM logs
    
    949
    +                WHERE stanza_id = ?
    
    950
    +                AND +jid_id = ?
    
    951
    +                AND account_id = ?
    
    952
    +                LIMIT 1
    
    953
    +                '''
    
    954
    +            result = self._con.execute(
    
    955
    +                sql, (stanza_id, archive_id, account_id)).fetchone()
    
    956
    +        else:
    
    957
    +            sql = '''
    
    958
    +                SELECT additional_data FROM logs
    
    959
    +                WHERE stanza_id = ?
    
    960
    +                AND account_id = ?
    
    961
    +                AND kind != ?
    
    962
    +                LIMIT 1
    
    963
    +                '''
    
    964
    +            result = self._con.execute(
    
    965
    +                sql, (stanza_id, account_id, KindConstant.GC_MSG)).fetchone()
    
    966
    +
    
    967
    +        if result is None:
    
    968
    +            return
    
    969
    +
    
    970
    +        if result.additional_data is None:
    
    971
    +            additional_data = AdditionalDataDict()
    
    972
    +        else:
    
    973
    +            additional_data = result.additional_data
    
    974
    +
    
    975
    +        if properties.is_moderation:
    
    976
    +            additional_data.set_value(
    
    977
    +                'retracted', 'by', properties.moderation.moderator_jid)
    
    978
    +            additional_data.set_value(
    
    979
    +                'retracted', 'timestamp', properties.moderation.timestamp)
    
    980
    +            additional_data.set_value(
    
    981
    +                'retracted', 'reason', properties.moderation.reason)
    
    982
    +        serialized_dict = json.dumps(additional_data.data)
    
    983
    +
    
    984
    +        if is_groupchat:
    
    985
    +            sql = '''
    
    986
    +                UPDATE logs SET additional_data = ?
    
    987
    +                WHERE stanza_id = ?
    
    988
    +                AND account_id = ?
    
    989
    +                AND +jid_id = ?
    
    990
    +                '''
    
    991
    +            self._con.execute(
    
    992
    +                sql, (serialized_dict, stanza_id, account_id, archive_id))
    
    993
    +        else:
    
    994
    +            sql = '''
    
    995
    +                UPDATE logs SET additional_data = ?
    
    996
    +                WHERE stanza_id = ?
    
    997
    +                AND account_id = ?
    
    998
    +                AND kind != ?
    
    999
    +                '''
    
    1000
    +            self._con.execute(
    
    1001
    +                sql, (serialized_dict, stanza_id, account_id,
    
    1002
    +                      KindConstant.GC_MSG))
    
    1003
    +
    
    923 1004
         def insert_jid(self, jid, kind=None, type_=JIDConstant.NORMAL_TYPE):
    
    924 1005
             """
    
    925 1006
             Insert a new jid into the `jids` table.
    

  • gajim/data/style/gajim.css
    ... ... @@ -59,7 +59,9 @@ .conversation-row grid textview:not(.sourceview) {
    59 59
     .conversation-row grid textview:not(.sourceview) text {
    
    60 60
     	background: transparent;
    
    61 61
     }
    
    62
    -
    
    62
    +.retracted-message {
    
    63
    +    opacity: 0.4;
    
    64
    +}
    
    63 65
     @keyframes highlight {
    
    64 66
         from { background: rgb(140, 160, 180); }
    
    65 67
         to { background: transparent; }
    

  • gajim/gtk/chat_list.py
    ... ... @@ -29,6 +29,7 @@
    29 29
     from gajim.common.i18n import _
    
    30 30
     from gajim.common.helpers import get_groupchat_name
    
    31 31
     from gajim.common.helpers import get_group_chat_nick
    
    32
    +from gajim.common.helpers import get_retraction_text
    
    32 33
     from gajim.common.helpers import get_uf_relative_time
    
    33 34
     from gajim.common.helpers import AdditionalDataDict
    
    34 35
     from gajim.common.preview_helpers import filename_from_uri
    
    ... ... @@ -318,6 +319,8 @@ def process_event(self, event):
    318 319
                               'mam-message-received',
    
    319 320
                               'gc-message-received'):
    
    320 321
                 self._on_message_received(event)
    
    322
    +        elif event.name == 'message-updated':
    
    323
    +            self._on_message_updated(event)
    
    321 324
             elif event.name == 'presence-received':
    
    322 325
                 self._on_presence_received(event)
    
    323 326
             elif event.name == 'message-sent':
    
    ... ... @@ -341,9 +344,23 @@ def _on_message_received(self, event):
    341 344
                 row.set_timestamp(event.properties.timestamp)
    
    342 345
             row.set_message_text(
    
    343 346
                 event.msgtxt, additional_data=event.additional_data)
    
    347
    +        row.set_stanza_id(event.properties.stanza_id.id)
    
    344 348
             self._add_unread(row, event.properties)
    
    345 349
             self.invalidate_sort()
    
    346 350
     
    
    351
    +    def _on_message_updated(self, event):
    
    352
    +        row = self._chats.get((event.account, event.jid))
    
    353
    +        if row is None:
    
    354
    +            return
    
    355
    +
    
    356
    +        if event.properties.is_moderation:
    
    357
    +            if event.properties.moderation.stanza_id == row.stanza_id:
    
    358
    +                text = get_retraction_text(
    
    359
    +                    event.account,
    
    360
    +                    event.properties.moderation.moderator_jid,
    
    361
    +                    event.properties.moderation.reason)
    
    362
    +                row.set_message_text(text)
    
    363
    +
    
    347 364
         def _on_message_sent(self, event):
    
    348 365
             msgtext = event.message
    
    349 366
             if not msgtext:
    
    ... ... @@ -448,6 +465,7 @@ def __init__(self, workspace_id, account, jid, type_, pinned):
    448 465
     
    
    449 466
             self.contact_name = self.contact.name
    
    450 467
             self.timestamp = 0
    
    468
    +        self.stanza_id: Optional[str] = None
    
    451 469
             self._unread_count = 0
    
    452 470
             self._pinned = pinned
    
    453 471
     
    
    ... ... @@ -475,8 +493,19 @@ def __init__(self, workspace_id, account, jid, type_, pinned):
    475 493
                 return
    
    476 494
     
    
    477 495
             if line.message is not None:
    
    496
    +            message_text = line.message
    
    497
    +
    
    498
    +            if line.additional_data is not None:
    
    499
    +                retracted_by = line.additional_data.get_value(
    
    500
    +                    'retracted', 'by')
    
    501
    +                if retracted_by is not None:
    
    502
    +                    reason = line.additional_data.get_value(
    
    503
    +                        'retracted', 'reason')
    
    504
    +                    message_text = get_retraction_text(
    
    505
    +                        self.account, retracted_by, reason)
    
    506
    +
    
    478 507
                 self.set_message_text(
    
    479
    -                line.message, additional_data=line.additional_data)
    
    508
    +                message_text, additional_data=line.additional_data)
    
    480 509
                 if line.kind in (KindConstant.CHAT_MSG_SENT,
    
    481 510
                                  KindConstant.SINGLE_MSG_SENT):
    
    482 511
                     self._ui.nick_label.set_text(_('Me:'))
    
    ... ... @@ -495,6 +524,8 @@ def __init__(self, workspace_id, account, jid, type_, pinned):
    495 524
                 uf_timestamp = get_uf_relative_time(line.time)
    
    496 525
                 self._ui.timestamp_label.set_text(uf_timestamp)
    
    497 526
     
    
    527
    +            self.stanza_id = line.stanza_id
    
    528
    +
    
    498 529
             if line.kind in (KindConstant.FILE_TRANSFER_INCOMING,
    
    499 530
                              KindConstant.FILE_TRANSFER_OUTGOING):
    
    500 531
                 self.set_message_text(
    
    ... ... @@ -634,6 +665,9 @@ def set_timestamp(self, timestamp):
    634 665
             self.timestamp = timestamp
    
    635 666
             self.update_time()
    
    636 667
     
    
    668
    +    def set_stanza_id(self, stanza_id: str) -> None:
    
    669
    +        self.stanza_id = stanza_id
    
    670
    +
    
    637 671
         def update_time(self):
    
    638 672
             if self.timestamp == 0:
    
    639 673
                 return
    

  • gajim/gtk/chat_list_stack.py
    ... ... @@ -28,6 +28,7 @@
    28 28
         'message-received',
    
    29 29
         'mam-message-received',
    
    30 30
         'gc-message-received',
    
    31
    +    'message-updated',
    
    31 32
         'presence-received',
    
    32 33
         'message-sent',
    
    33 34
         'chatstate-received',
    

  • gajim/gtk/controls/base.py
    ... ... @@ -44,6 +44,7 @@
    44 44
     from gajim.common.nec import EventHelper
    
    45 45
     from gajim.common.nec import NetworkEvent
    
    46 46
     from gajim.common.helpers import AdditionalDataDict
    
    47
    +from gajim.common.helpers import get_retraction_text
    
    47 48
     from gajim.common.const import KindConstant
    
    48 49
     from gajim.common.structs import OutgoingMessage
    
    49 50
     
    
    ... ... @@ -290,6 +291,15 @@ def process_event(self, event):
    290 291
             method_name = f'_on_{method_name}'
    
    291 292
             getattr(self, method_name)(event)
    
    292 293
     
    
    294
    +    def _on_message_updated(self, event):
    
    295
    +        if event.properties.is_moderation:
    
    296
    +            text = get_retraction_text(
    
    297
    +                self.account,
    
    298
    +                event.properties.moderation.moderator_jid,
    
    299
    +                event.properties.moderation.reason)
    
    300
    +            self.conversation_view.show_message_retraction(
    
    301
    +                event.properties.moderation.stanza_id, text)
    
    302
    +
    
    293 303
         def _on_conversation_view_key_press(self, _listbox, event):
    
    294 304
             if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
    
    295 305
                 if event.keyval in (Gdk.KEY_Page_Down, Gdk.KEY_Page_Up):
    
    ... ... @@ -1120,6 +1130,7 @@ def add_message(self,
    1120 1130
                     tim,
    
    1121 1131
                     display_marking=displaymarking,
    
    1122 1132
                     message_id=message_id,
    
    1133
    +                stanza_id=stanza_id,
    
    1123 1134
                     correct_id=correct_id,
    
    1124 1135
                     log_line_id=msg_log_id,
    
    1125 1136
                     additional_data=additional_data)
    
    ... ... @@ -1366,6 +1377,8 @@ def add_messages(self, messages):
    1366 1377
                 if not msg.message:
    
    1367 1378
                     continue
    
    1368 1379
     
    
    1380
    +            message_text = msg.message
    
    1381
    +
    
    1369 1382
                 contact_name = msg.contact_name
    
    1370 1383
                 if msg.kind in (
    
    1371 1384
                         KindConstant.SINGLE_MSG_RECV, KindConstant.CHAT_MSG_RECV):
    
    ... ... @@ -1380,13 +1393,22 @@ def add_messages(self, messages):
    1380 1393
                 else:
    
    1381 1394
                     raise ValueError('no kind attribute')
    
    1382 1395
     
    
    1396
    +            if msg.additional_data is not None:
    
    1397
    +                retracted_by = msg.additional_data.get_value('retracted', 'by')
    
    1398
    +                if retracted_by is not None:
    
    1399
    +                    reason = msg.additional_data.get_value(
    
    1400
    +                        'retracted', 'reason')
    
    1401
    +                    message_text = get_retraction_text(
    
    1402
    +                        self.account, retracted_by, reason)
    
    1403
    +
    
    1383 1404
                 self.conversation_view.add_message(
    
    1384
    -                msg.message,
    
    1405
    +                message_text,
    
    1385 1406
                     kind,
    
    1386 1407
                     contact_name,
    
    1387 1408
                     msg.time,
    
    1388 1409
                     additional_data=msg.additional_data,
    
    1389 1410
                     message_id=msg.message_id,
    
    1411
    +                stanza_id=msg.stanza_id,
    
    1390 1412
                     log_line_id=msg.log_line_id,
    
    1391 1413
                     marker=msg.marker,
    
    1392 1414
                     error=msg.error)
    

  • gajim/gtk/controls/chat.py
    ... ... @@ -414,6 +414,7 @@ def _on_mam_message_received(self, event):
    414 414
                              tim=event.properties.mam.timestamp,
    
    415 415
                              correct_id=event.correct_id,
    
    416 416
                              message_id=event.properties.id,
    
    417
    +                         stanza_id=event.stanza_id,
    
    417 418
                              additional_data=event.additional_data)
    
    418 419
     
    
    419 420
         def _on_message_received(self, event):
    
    ... ... @@ -431,6 +432,7 @@ def _on_message_received(self, event):
    431 432
                              displaymarking=event.displaymarking,
    
    432 433
                              msg_log_id=event.msg_log_id,
    
    433 434
                              message_id=event.properties.id,
    
    435
    +                         stanza_id=event.stanza_id,
    
    434 436
                              correct_id=event.correct_id,
    
    435 437
                              additional_data=event.additional_data)
    
    436 438
     
    
    ... ... @@ -602,6 +604,7 @@ def add_message(self,
    602 604
                         subject=None,
    
    603 605
                         displaymarking=None,
    
    604 606
                         msg_log_id=None,
    
    607
    +                    stanza_id=None,
    
    605 608
                         correct_id=None,
    
    606 609
                         message_id=None,
    
    607 610
                         additional_data=None):
    
    ... ... @@ -622,6 +625,7 @@ def add_message(self,
    622 625
                                     displaymarking=displaymarking,
    
    623 626
                                     msg_log_id=msg_log_id,
    
    624 627
                                     message_id=message_id,
    
    628
    +                                stanza_id=stanza_id,
    
    625 629
                                     correct_id=correct_id,
    
    626 630
                                     additional_data=additional_data)
    
    627 631
     
    

  • gajim/gtk/controls/groupchat.py
    ... ... @@ -673,6 +673,7 @@ def _on_mam_message_received(self, event):
    673 673
                              tim=event.properties.mam.timestamp,
    
    674 674
                              correct_id=event.correct_id,
    
    675 675
                              message_id=event.properties.id,
    
    676
    +                         stanza_id=event.stanza_id,
    
    676 677
                              additional_data=event.additional_data)
    
    677 678
     
    
    678 679
         def _on_gc_message_received(self, event):
    

  • gajim/gtk/conversation/rows/base.py
    ... ... @@ -21,6 +21,7 @@
    21 21
     from gajim.common import app
    
    22 22
     from gajim.common.i18n import _
    
    23 23
     from gajim.common.helpers import from_one_line
    
    24
    +from gajim.common.helpers import is_retraction_allowed
    
    24 25
     
    
    25 26
     from ...util import wrap_with_event_box
    
    26 27
     
    
    ... ... @@ -87,27 +88,44 @@ def create_name_widget(name: str, is_self: bool) -> Gtk.Label:
    87 88
     
    
    88 89
     @wrap_with_event_box
    
    89 90
     class MoreMenuButton(Gtk.MenuButton):
    
    90
    -    def __init__(self, row):
    
    91
    +    def __init__(self, row, contact, name):
    
    91 92
             Gtk.MenuButton.__init__(self)
    
    92
    -
    
    93 93
             self.set_valign(Gtk.Align.START)
    
    94 94
             self.set_halign(Gtk.Align.END)
    
    95 95
             self.set_relief(Gtk.ReliefStyle.NONE)
    
    96
    -        image = Gtk.Image.new_from_icon_name(
    
    97
    -            'feather-more-horizontal-symbolic', Gtk.IconSize.BUTTON)
    
    98
    -        self.add(image)
    
    99
    -
    
    100
    -        self._create_popover(row)
    
    101 96
     
    
    102 97
             self.get_style_context().add_class('conversation-more-button')
    
    103 98
     
    
    104
    -    def _create_popover(self, row):
    
    99
    +        self._row = row
    
    100
    +        self._contact = contact
    
    101
    +        self._name = name
    
    102
    +
    
    103
    +        image = Gtk.Image.new_from_icon_name(
    
    104
    +            'feather-more-horizontal-symbolic', Gtk.IconSize.BUTTON)
    
    105
    +        self.add(image)
    
    106
    +        self._create_popover()
    
    107
    +
    
    108
    +        self.connect('clicked', self._on_click)
    
    109
    +
    
    110
    +    def _on_click(self, _button):
    
    111
    +        show_retract = False
    
    112
    +        if self._contact.is_groupchat:
    
    113
    +            disco_info = app.storage.cache.get_last_disco_info(
    
    114
    +                self._contact.jid)
    
    115
    +            contact = self._contact.get_resource(self._name)
    
    116
    +            self_contact = self._contact.get_self()
    
    117
    +            is_allowed = is_retraction_allowed(self_contact, contact)
    
    118
    +            if disco_info.has_message_moderation and is_allowed:
    
    119
    +                show_retract = True
    
    120
    +        self._retract_button.set_visible(show_retract)
    
    121
    +
    
    122
    +    def _create_popover(self):
    
    105 123
             menu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
    
    106 124
             menu_box.get_style_context().add_class('padding-6')
    
    107 125
     
    
    108 126
             quote_button = Gtk.ModelButton()
    
    109 127
             quote_button.set_halign(Gtk.Align.START)
    
    110
    -        quote_button.connect('clicked', row.on_quote_message)
    
    128
    +        quote_button.connect('clicked', self._row.on_quote_message)
    
    111 129
             quote_button.set_label(_('Quote…'))
    
    112 130
             quote_button.set_image(Gtk.Image.new_from_icon_name(
    
    113 131
                 'mail-reply-sender-symbolic', Gtk.IconSize.MENU))
    
    ... ... @@ -115,12 +133,21 @@ def _create_popover(self, row):
    115 133
     
    
    116 134
             copy_button = Gtk.ModelButton()
    
    117 135
             copy_button.set_halign(Gtk.Align.START)
    
    118
    -        copy_button.connect('clicked', row.on_copy_message)
    
    136
    +        copy_button.connect('clicked', self._row.on_copy_message)
    
    119 137
             copy_button.set_label(_('Copy'))
    
    120 138
             copy_button.set_image(Gtk.Image.new_from_icon_name(
    
    121 139
                 'edit-copy-symbolic', Gtk.IconSize.MENU))
    
    122 140
             menu_box.add(copy_button)
    
    123 141
     
    
    142
    +        self._retract_button = Gtk.ModelButton()
    
    143
    +        self._retract_button.set_halign(Gtk.Align.START)
    
    144
    +        self._retract_button.connect(
    
    145
    +            'clicked', self._row.on_retract_message)
    
    146
    +        self._retract_button.set_label(_('Retract'))
    
    147
    +        self._retract_button.set_image(Gtk.Image.new_from_icon_name(
    
    148
    +            'edit-undo-symbolic', Gtk.IconSize.MENU))
    
    149
    +        menu_box.add(self._retract_button)
    
    150
    +
    
    124 151
             menu_box.show_all()
    
    125 152
     
    
    126 153
             popover = Gtk.PopoverMenu()
    

  • gajim/gtk/conversation/rows/message.py
    ... ... @@ -34,6 +34,8 @@
    34 34
     from .base import BaseRow
    
    35 35
     from .base import MoreMenuButton
    
    36 36
     from ..message_widget import MessageWidget
    
    37
    +from ...dialogs import InputDialog
    
    38
    +from ...dialogs import DialogButton
    
    37 39
     from ...preview import PreviewWidget
    
    38 40
     from ...util import format_fingerprint
    
    39 41
     from ...util import get_cursor
    
    ... ... @@ -46,6 +48,7 @@ def __init__(self,
    46 48
                      account,
    
    47 49
                      contact,
    
    48 50
                      message_id,
    
    51
    +                 stanza_id,
    
    49 52
                      timestamp,
    
    50 53
                      kind,
    
    51 54
                      name,
    
    ... ... @@ -62,6 +65,7 @@ def __init__(self,
    62 65
             self.timestamp = datetime.fromtimestamp(timestamp)
    
    63 66
             self.db_timestamp = timestamp
    
    64 67
             self.message_id = message_id
    
    68
    +        self.stanza_id = stanza_id
    
    65 69
             self.log_line_id = log_line_id
    
    66 70
             self.kind = kind
    
    67 71
             self.name = name or ''
    
    ... ... @@ -78,6 +82,10 @@ def __init__(self,
    78 82
             self._has_receipt = marker == 'received'
    
    79 83
             self._has_displayed = marker == 'displayed'
    
    80 84
     
    
    85
    +        if additional_data is not None:
    
    86
    +            if additional_data.get_value('retracted', 'by') is not None:
    
    87
    +                self.get_style_context().add_class('retracted-message')
    
    88
    +
    
    81 89
             is_previewable = app.interface.preview_manager.get_previewable(
    
    82 90
                 text, additional_data)
    
    83 91
             if is_previewable:
    
    ... ... @@ -160,7 +168,8 @@ def __init__(self,
    160 168
     
    
    161 169
             bottom_box = Gtk.Box(spacing=6)
    
    162 170
             bottom_box.add(self._message_widget)
    
    163
    -        more_menu_button = MoreMenuButton(self)
    
    171
    +
    
    172
    +        more_menu_button = MoreMenuButton(self, self._contact, name)
    
    164 173
             more_menu_button.set_hexpand(True)
    
    165 174
             more_menu_button.set_halign(Gtk.Align.END)
    
    166 175
             bottom_box.pack_end(more_menu_button, False, True, 0)
    
    ... ... @@ -242,6 +251,22 @@ def on_copy_message(self, _widget):
    242 251
         def on_quote_message(self, _widget):
    
    243 252
             self.get_parent().on_quote(self._message_widget.get_text())
    
    244 253
     
    
    254
    +    def on_retract_message(self, _widget):
    
    255
    +        def _on_retract(reason):
    
    256
    +            self._client.get_module('MUC').retract_message(
    
    257
    +                self._contact.jid, self.stanza_id, reason or None)
    
    258
    +
    
    259
    +        InputDialog(
    
    260
    +            _('Retract Message'),
    
    261
    +            _('Retract message?'),
    
    262
    +            _('Why do you want to retract this message?'),
    
    263
    +            [DialogButton.make('Cancel'),
    
    264
    +             DialogButton.make('Remove',
    
    265
    +                               text=_('_Retract'),
    
    266
    +                               callback=_on_retract)],
    
    267
    +            input_str=_('Spam'),
    
    268
    +            transient_for=app.window).show()
    
    269
    +
    
    245 270
         def _get_encryption_image(self, additional_data, encryption_enabled=None):
    
    246 271
             details = self._get_encryption_details(additional_data)
    
    247 272
             if details is None:
    
    ... ... @@ -296,6 +321,10 @@ def set_receipt(self):
    296 321
         def set_displayed(self):
    
    297 322
             self._has_displayed = True
    
    298 323
     
    
    324
    +    def set_retracted(self, text: str) -> None:
    
    325
    +        self._message_widget.add_content(process(text))
    
    326
    +        self.get_style_context().add_class('retracted-message')
    
    327
    +
    
    299 328
         def set_correction(self, text, message_id):
    
    300 329
             self._corrections.append(self._message_widget.get_text())
    
    301 330
             result = process(text)
    

  • gajim/gtk/conversation/view.py
    ... ... @@ -12,6 +12,8 @@
    12 12
     # You should have received a copy of the GNU General Public License
    
    13 13
     # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
    
    14 14
     
    
    15
    +from typing import Optional
    
    16
    +
    
    15 17
     import logging
    
    16 18
     import time
    
    17 19
     
    
    ... ... @@ -209,6 +211,7 @@ def add_message(self,
    209 211
                         timestamp,
    
    210 212
                         log_line_id=None,
    
    211 213
                         message_id=None,
    
    214
    +                    stanza_id=None,
    
    212 215
                         correct_id=None,
    
    213 216
                         display_marking=None,
    
    214 217
                         additional_data=None,
    
    ... ... @@ -227,6 +230,7 @@ def add_message(self,
    227 230
                 self._account,
    
    228 231
                 self._contact,
    
    229 232
                 message_id,
    
    233
    +            stanza_id,
    
    230 234
                 timestamp,
    
    231 235
                 kind,
    
    232 236
                 name,
    
    ... ... @@ -404,6 +408,14 @@ def get_row_by_log_line_id(self, log_line_id):
    404 408
                     return row
    
    405 409
             return None
    
    406 410
     
    
    411
    +    def get_row_by_stanza_id(self, stanza_id: str) -> Optional[MessageRow]:
    
    412
    +        for row in self.get_children():
    
    413
    +            if row.type != 'chat':
    
    414
    +                continue
    
    415
    +            if row.stanza_id == stanza_id:
    
    416
    +                return row
    
    417
    +        return None
    
    418
    +
    
    407 419
         def iter_rows(self):
    
    408 420
             for row in self.get_children():
    
    409 421
                 yield row
    
    ... ... @@ -448,6 +460,11 @@ def correct_message(self, correct_id, message_id, text):
    448 460
                 message_row.set_correction(text, message_id)
    
    449 461
                 message_row.set_merged(False)
    
    450 462
     
    
    463
    +    def show_message_retraction(self, stanza_id: str, text: str) -> None:
    
    464
    +        message_row = self.get_row_by_stanza_id(stanza_id)
    
    465
    +        if message_row is not None:
    
    466
    +            message_row.set_retracted(text)
    
    467
    +
    
    451 468
         def show_receipt(self, id_):
    
    452 469
             message_row = self._get_row_by_message_id(id_)
    
    453 470
             if message_row is not None:
    

  • gajim/gtk/main.py
    ... ... @@ -72,6 +72,7 @@ def __init__(self):
    72 72
                 ('message-received', ged.CORE, self._on_event),
    
    73 73
                 ('mam-message-received', ged.CORE, self._on_event),
    
    74 74
                 ('gc-message-received', ged.CORE, self._on_event),
    
    75
    +            ('message-updated', ged.CORE, self._on_event),
    
    75 76
                 ('receipt-received', ged.GUI1, self._on_event),
    
    76 77
                 ('displayed-received', ged.GUI1, self._on_event),
    
    77 78
                 ('message-error', ged.GUI1, self._on_event),