Daniel Brötzmann pushed to branch chat_markers at gajim / gajim
Commits:
-
8be9e7f4
by lovetox at 2020-10-11T16:56:33+02:00
-
4e5c3820
by Yann Leboulanger at 2020-10-12T18:49:03+02:00
-
9850a0cc
by wurstsalat at 2020-10-12T20:28:51+02:00
15 changed files:
- gajim/chat_control.py
- gajim/chat_control_base.py
- gajim/common/const.py
- gajim/common/events.py
- gajim/common/modules/__init__.py
- + gajim/common/modules/chat_markers.py
- gajim/common/modules/message.py
- gajim/common/setting_values.py
- gajim/conversation_textview.py
- gajim/data/style/gajim.css
- gajim/groupchat_control.py
- gajim/gtk/groupchat_settings.py
- gajim/gtk/server_info.py
- gajim/gui_interface.py
- gajim/gui_menu_builder.py
Changes:
| ... | ... | @@ -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)])
|
| ... | ... | @@ -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()
|
| ... | ... | @@ -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,
|
| ... | ... | @@ -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'
|
| ... | ... | @@ -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',
|
| 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'
|
| ... | ... | @@ -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:
|
| ... | ... | @@ -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 |
},
|
| ... | ... | @@ -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)
|
| ... | ... | @@ -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;}
|
| ... | ... | @@ -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 |
|
| ... | ... | @@ -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,
|
| ... | ... | @@ -17,7 +17,7 @@ from collections import namedtuple |
| 17 | 17 |
from datetime import timedelta
|
| 18 | 18 |
|
| 19 | 19 |
import nbxmpp
|
| 20 |
-from nbxmpp.util import is_error_result
|
|
| 20 |
+from nbxmpp.errors import StanzaError
|
|
| 21 | 21 |
from nbxmpp.namespaces import Namespace
|
| 22 | 22 |
from gi.repository import Gtk
|
| 23 | 23 |
from gi.repository import Gdk
|
| ... | ... | @@ -28,7 +28,6 @@ from gajim.common import ged |
| 28 | 28 |
from gajim.common.helpers import open_uri
|
| 29 | 29 |
from gajim.common.i18n import _
|
| 30 | 30 |
|
| 31 |
-from gajim.gtk.util import ensure_not_destroyed
|
|
| 32 | 31 |
from gajim.gtk.util import get_builder
|
| 33 | 32 |
from gajim.gtk.util import EventHelper
|
| 34 | 33 |
from gajim.gtk.util import open_window
|
| ... | ... | @@ -233,12 +232,14 @@ class ServerInfo(Gtk.ApplicationWindow, EventHelper): |
| 233 | 232 |
'days': delta.days, 'hours': hours}
|
| 234 | 233 |
self._ui.server_uptime.set_text(uptime)
|
| 235 | 234 |
|
| 236 |
- @ensure_not_destroyed
|
|
| 237 |
- def _software_version_received(self, result):
|
|
| 238 |
- if is_error_result(result):
|
|
| 235 |
+ def _software_version_received(self, task):
|
|
| 236 |
+ try:
|
|
| 237 |
+ result = task.finish()
|
|
| 238 |
+ except StanzaError:
|
|
| 239 | 239 |
self.version = _('Unknown')
|
| 240 | 240 |
else:
|
| 241 | 241 |
self.version = '%s %s' % (result.name, result.version)
|
| 242 |
+ |
|
| 242 | 243 |
self._ui.server_software.set_text(self.version)
|
| 243 | 244 |
|
| 244 | 245 |
@staticmethod
|
| ... | ... | @@ -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):
|
| ... | ... | @@ -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),
|