Daniel Brötzmann pushed to branch mainwindow at gajim / gajim
Commits:
-
365d7b17
by wurstsalat at 2021-11-10T22:11:54+01:00
16 changed files:
- data/gajim.doap
- gajim/common/const.py
- gajim/common/helpers.py
- gajim/common/modules/mam.py
- gajim/common/modules/muc.py
- gajim/common/storage/archive.py
- gajim/data/style/gajim.css
- gajim/gtk/chat_list.py
- gajim/gtk/chat_list_stack.py
- gajim/gtk/controls/base.py
- gajim/gtk/controls/chat.py
- gajim/gtk/controls/groupchat.py
- gajim/gtk/conversation/rows/base.py
- gajim/gtk/conversation/rows/message.py
- gajim/gtk/conversation/view.py
- gajim/gtk/main.py
Changes:
| ... | ... | @@ -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"/>
|
| ... | ... | @@ -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 |
|
| ... | ... | @@ -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:
|
| ... | ... | @@ -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,
|
| ... | ... | @@ -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
|
| ... | ... | @@ -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.
|
| ... | ... | @@ -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; }
|
| ... | ... | @@ -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
|
| ... | ... | @@ -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',
|
| ... | ... | @@ -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)
|
| ... | ... | @@ -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 |
|
| ... | ... | @@ -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):
|
| ... | ... | @@ -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()
|
| ... | ... | @@ -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)
|
| ... | ... | @@ -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:
|
| ... | ... | @@ -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),
|