...
  View open merge request
Commits (2)
......@@ -114,7 +114,7 @@ class ChatControl(ChatControlBase):
# Menu for the HeaderBar
self.control_menu = gui_menu_builder.get_singlechat_menu(
self.control_id, self.account, self.contact.jid)
self.control_id, self.account, self.contact.jid, self.TYPE_ID)
# Settings menu
self.xml.settings_menu.set_menu_model(self.control_menu)
......@@ -233,6 +233,8 @@ class ChatControl(ChatControlBase):
app.ged.register_event_handler(
'receipt-received',
ged.GUI1, self._receipt_received)
app.ged.register_event_handler('displayed-received',
ged.GUI1, self._displayed_received)
app.ged.register_event_handler('message-error',
ged.GUI1, self._on_message_error)
app.ged.register_event_handler('zeroconf-error', ged.GUI1,
......@@ -284,6 +286,16 @@ class ChatControl(ChatControlBase):
act.connect('change-state', self._on_send_chatstate)
self.parent_win.window.add_action(act)
value = app.config.get_per(
'contacts', self.contact.jid, 'send_marker', False)
act = Gio.SimpleAction.new_stateful(
'send-marker-' + self.control_id,
None,
GLib.Variant.new_boolean(value))
act.connect('change-state', self._on_send_marker)
self.parent_win.window.add_action(act)
def update_actions(self):
win = self.parent_win.window
online = app.account_is_connected(self.account)
......@@ -398,6 +410,11 @@ class ChatControl(ChatControlBase):
app.config.set_per('contacts', self.contact.jid,
'send_chatstate', param.get_string())
def _on_send_marker(self, action, param):
action.set_state(param)
app.config.set_per('contacts', self.contact.jid,
'send_marker', param.get_boolean())
def subscribe_events(self):
"""
Register listeners to the events class
......@@ -956,6 +973,10 @@ class ChatControl(ChatControlBase):
def _receipt_received(self, event):
self.conv_textview.show_receipt(event.receipt_id)
@event_filter(['account', 'jid'])
def _displayed_received(self, event):
self.conv_textview.show_displayed(event.marker_id)
def get_tab_label(self):
unread = ''
if self.resource:
......
......@@ -322,6 +322,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
self.received_history_pos = 0
self.orig_msg = None
# For XEP-0333
self.last_msg_id = None
self.set_emoticon_popover()
# Attach speller
......@@ -1032,6 +1035,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
if restored:
return
if message_id:
self.last_msg_id = message_id
if kind == 'incoming':
if not self.type_id == message_control.TYPE_GC or \
app.config.notify_for_muc(jid) or \
......@@ -1075,6 +1081,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
event_type.type_, self.contact.jid)
event = event_type(text, subject, self, msg_log_id,
message_id=message_id,
show_in_roster=show_in_roster,
show_in_systray=show_in_systray)
app.events.add_event(self.account, full_jid, event)
......@@ -1230,6 +1237,12 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
types=type_):
# There were events to remove
self.redraw_after_event_removed(jid)
# XEP-0333 Send <displayed> tag.
con.get_module('ChatMarkers').send_displayed_marker(
self.contact,
self.last_msg_id,
self.type_id)
self.last_msg_id = None
# send chatstate inactive to the one we're leaving
# and active to the one we visit
if self.msg_textview.has_text():
......@@ -1272,8 +1285,15 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
# we are at the end
if not app.events.remove_events(
self.account, jid, types=types_list):
con = app.connections[self.account]
# There were events to remove
self.redraw_after_event_removed(jid)
# XEP-0333 Send <displayed> tag.
con.get_module('ChatMarkers').send_displayed_marker(
self.contact,
self.last_msg_id,
self.type_id)
self.last_msg_id = None
def _on_scrollbar_button_release(self, scrollbar, event):
if event.get_button()[1] != 1:
......@@ -1338,6 +1358,8 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
We just removed a 'printed_*' event, redraw contact in roster or
gc_roster and titles in roster and msg_win
"""
if not self.parent_win: # minimized groupchat
return
self.parent_win.redraw_tab(self)
self.parent_win.show_title()
# TODO : get the contact and check get_show_in_roster()
......
......@@ -150,6 +150,7 @@ gajim_common_features = [
nbxmpp.NS_BYTESTREAM,
nbxmpp.NS_FILE,
nbxmpp.NS_MUC,
nbxmpp.NS_CHATMARKERS,
nbxmpp.NS_COMMANDS,
nbxmpp.NS_DISCO_INFO,
nbxmpp.NS_LAST,
......
......@@ -399,6 +399,7 @@ class Config:
'contacts': ({
'speller_language': [opt_str, '', _('Language used for spell checking.')],
'send_chatstate': [opt_str, 'composing_only', _('Chat state notifications that are sent to contacts. Possible values: \'all\', \'composing_only\', \'disabled\'')],
'send_marker': [opt_bool, False, _('If enabled, contacts are informed when you read a message.')],
}, {}),
'encryption': ({
'encryption': [opt_str, '', _('The currently active encryption for that contact.')],
......@@ -411,6 +412,7 @@ class Config:
'minimize_on_autojoin': [opt_bool, True, _('If enabled, the group chat is minimized into the contact list when joining automatically.')],
'minimize_on_close': [opt_bool, True, _('If enabled, the group chat is minimized into the contact list when closing it.')],
'send_chatstate': [opt_str, 'composing_only', _('Chat state notifications that are sent to the group chat. Possible values: \'all\', \'composing_only\' or \'disabled\'.')],
'send_marker': [opt_bool, False, _('If enabled, contacts are informed when you read a message.')],
}, {}),
'plugins': ({
'active': [opt_bool, False, _('If enabled, plugins will be activated on startup (this is saved when exiting Gajim). This option SHOULD NOT be used to (de)activate plugins. Use the plugin window instead.')],
......
......@@ -306,6 +306,9 @@ class CommonConnection:
obj.session.last_send = time.time()
msg_iq.setThread(obj.session.thread_id)
if obj.message:
msg_iq.setMarkable()
self._push_stanza_message_outgoing(obj, msg_iq)
def _push_stanza_message_outgoing(self, obj, msg_iq):
......@@ -1635,6 +1638,7 @@ class Connection(CommonConnection, ConnectionHandlers):
msg_iq.setID(obj.stanza_id)
if obj.message:
msg_iq.setOriginID(obj.stanza_id)
msg_iq.setMarkable()
if obj.correct_id:
msg_iq.setTag('replace', attrs={'id': obj.correct_id},
......
......@@ -89,13 +89,14 @@ class PmEvent(ChatEvent):
class PrintedChatEvent(Event):
type_ = 'printed_chat'
def __init__(self, message, subject, control, msg_log_id, time_=None,
show_in_roster=False, show_in_systray=True):
message_id=None, show_in_roster=False, show_in_systray=True):
Event.__init__(self, time_, show_in_roster=show_in_roster,
show_in_systray=show_in_systray)
self.message = message
self.subject = subject
self.control = control
self.msg_log_id = msg_log_id
self.message_id = message_id
class PrintedGcMsgEvent(PrintedChatEvent):
type_ = 'printed_gc_msg'
......
......@@ -40,6 +40,7 @@ MODULES = [
'bookmarks',
'caps',
'carbons',
'chat_markers',
'chatstates',
'delimiter',
'discovery',
......
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# Chat Markers (XEP-0333)
import nbxmpp
from nbxmpp.structs import StanzaHandler
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
class ChatMarkers(BaseModule):
_nbxmpp_extends = 'ChatMarkers'
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='message',
callback=self._process_chat_marker,
ns=nbxmpp.NS_CHATMARKERS,
priority=47),
]
def _process_chat_marker(self, _con, _stanza, properties):
if not properties.is_marker or not properties.marker.is_displayed:
return
if properties.type.is_error:
return
if properties.type.is_groupchat:
manager = self._con.get_module('MUC').get_manager()
muc_data = manager.get(properties.muc_jid)
if muc_data is None:
return
if properties.muc_nickname != muc_data.nick:
return
self._raise_event('read-state-sync', properties)
return
if properties.is_carbon_message and properties.carbon.is_sent:
self._raise_event('read-state-sync', properties)
return
if properties.is_mam_message:
if properties.from_.bareMatch(self._con.get_own_jid()):
return
self._raise_event('displayed-received', properties)
def _raise_event(self, name, properties):
self._log.info('%s: %s %s',
name,
properties.jid,
properties.marker.id)
jid = properties.jid
if not properties.is_muc_pm and not properties.type.is_groupchat:
jid = properties.jid.getBare()
app.nec.push_outgoing_event(
NetworkEvent(name,
account=self._account,
jid=jid,
properties=properties,
type=properties.type,
is_muc_pm=properties.is_muc_pm,
marker_id=properties.marker.id))
def _send_marker(self, contact, marker, id_, type_):
jid = contact.jid
if contact.is_pm_contact:
jid = app.get_jid_without_resource(contact.jid)
config = 'rooms' if type_ in ('gc', 'pm') else 'contacts'
if not app.config.get_per(config, jid, 'send_marker', False):
return
typ = 'groupchat' if type_ == 'gc' else 'chat'
message = nbxmpp.Message(to=contact.jid, typ=typ)
message.setMarker(marker, id_)
self._log.info('Send %s: %s', marker, contact.jid)
self._nbxmpp().send(message)
def send_displayed_marker(self, contact, id_, type_):
self._send_marker(contact, 'displayed', id_, type_)
def get_instance(*args, **kwargs):
return ChatMarkers(*args, **kwargs), 'ChatMarkers'
......@@ -407,6 +407,12 @@ class ConversationTextview(GObject.GObject):
return
line.set_receipt()
def show_displayed(self, id_):
line = self._get_message_line(id_)
if line is None:
return
line.set_displayed()
def show_error(self, id_, error):
line = self._get_message_line(id_)
if line is None:
......@@ -1313,16 +1319,27 @@ class MessageLine:
self.timestamp = timestamp
self.start_mark = start_mark
self._has_receipt = False
self._has_displayed = False
self._message_icons = message_icons
@property
def has_receipt(self):
return self._has_receipt
@property
def has_displayed(self):
return self._has_displayed
def set_receipt(self):
self._has_receipt = True
if self._has_displayed:
return
self._message_icons.set_receipt_icon_visible(True)
def set_displayed(self):
self._has_displayed = True
self._message_icons.set_displayed_icon_visible(True)
def set_correction(self, tooltip):
self._message_icons.set_correction_icon_visible(True)
self._message_icons.set_correction_tooltip(tooltip)
......@@ -1341,12 +1358,12 @@ class MessageIcons(Gtk.Box):
'document-edit-symbolic', Gtk.IconSize.MENU)
self._correction_image.set_no_show_all(True)
self._receipt_image = Gtk.Image.new_from_icon_name(
self._marker_image = Gtk.Image.new_from_icon_name(
'emblem-ok-symbolic', Gtk.IconSize.MENU)
self._receipt_image.get_style_context().add_class(
self._marker_image.get_style_context().add_class(
'receipt-received-color')
self._receipt_image.set_tooltip_text(_('Received'))
self._receipt_image.set_no_show_all(True)
self._marker_image.set_tooltip_text(_('Received'))
self._marker_image.set_no_show_all(True)
self._error_image = Gtk.Image.new_from_icon_name(
'dialog-warning-symbolic', Gtk.IconSize.MENU)
......@@ -1354,14 +1371,22 @@ class MessageIcons(Gtk.Box):
self._error_image.set_no_show_all(True)
self.add(self._correction_image)
self.add(self._receipt_image)
self.add(self._marker_image)
self.add(self._error_image)
self.show_all()
def set_receipt_icon_visible(self, visible):
if not app.config.get('positive_184_ack'):
return
self._receipt_image.set_visible(visible)
self._marker_image.set_visible(visible)
def set_displayed_icon_visible(self, visible):
self._marker_image.set_visible(visible)
self._marker_image.get_style_context().remove_class(
'receipt-received-color')
self._marker_image.get_style_context().add_class(
'displayed-received-color')
self._marker_image.set_tooltip_text(_('Displayed'))
def set_correction_icon_visible(self, visible):
self._correction_image.set_visible(visible)
......
......@@ -248,6 +248,7 @@ button.flat.link { padding: 0; border: 0; }
/*Receipts*/
.receipt-received-color { color: rgb(75, 181, 67) }
.displayed-received-color { color: rgb(106, 0, 242) }
/*Dataforms*/
.field-fixed { font-size: 16px; font-weight: bold; padding-top:5px;}
......
......@@ -354,6 +354,16 @@ class GroupchatControl(ChatControlBase):
act.connect('change-state', self._on_sync_threshold)
self.parent_win.window.add_action(act)
value = app.config.get_per(
'rooms', self.contact.jid, 'send_marker', False)
act = Gio.SimpleAction.new_stateful(
'send-marker-' + self.control_id,
None,
GLib.Variant.new_boolean(value))
act.connect('change-state', self._on_send_marker)
self.parent_win.window.add_action(act)
def update_actions(self):
if self.parent_win is None:
return
......@@ -587,6 +597,11 @@ class GroupchatControl(ChatControlBase):
app.config.set_per('rooms', self.contact.jid,
'send_chatstate', param.get_string())
def _on_send_marker(self, action, param):
action.set_state(param)
app.config.set_per('rooms', self.contact.jid,
'send_marker', param.get_boolean())
def _on_notify_on_all_messages(self, action, param):
action.set_state(param)
app.config.set_per('rooms', self.contact.jid,
......@@ -847,7 +862,7 @@ class GroupchatControl(ChatControlBase):
obj.needs_highlight = self.needs_visual_notification(obj.msgtxt)
def on_private_message(self, nick, sent, msg, tim, session, additional_data,
msg_log_id=None, displaymarking=None):
message_id, msg_log_id=None, displaymarking=None):
# Do we have a queue?
fjid = self.room_jid + '/' + nick
......@@ -860,7 +875,9 @@ class GroupchatControl(ChatControlBase):
session=session,
displaymarking=displaymarking,
sent_forwarded=sent,
additional_data=additional_data)
additional_data=additional_data,
message_id=message_id)
app.events.add_event(self.account, fjid, event)
autopopup = app.config.get('autopopup')
......@@ -1104,6 +1121,7 @@ class GroupchatControl(ChatControlBase):
obj.properties.timestamp,
self.session,
obj.additional_data,
obj.properties.id,
msg_log_id=obj.msg_log_id,
displaymarking=obj.displaymarking)
......
......@@ -285,6 +285,39 @@ class Interface:
if ctrl and ctrl.session and len(obj.contact_list) > 1:
ctrl.remove_session(ctrl.session)
def handle_event_read_state_sync(self, event):
if event.type.is_groupchat:
control = self.get_groupchat_control(event.account,
event.jid.getBare())
if control is None:
log.warning('Groupchat control not found')
return
jid = event.jid.getBare()
types = ['printed_gc_msg', 'printed_marked_gc_msg']
else:
types = ['chat', 'pm', 'printed_chat', 'printed_pm']
jid = event.jid.getBare()
if event.is_muc_pm:
jid = event.jid
control = app.interface.msg_win_mgr.get_control(jid, event.account)
# Compare with control.last_msg_id.
events_ = app.events.get_events(event.account, jid, types)
if not events_:
log.warning('No Events')
return
if events_[-1].message_id != event.marker_id:
return
if not app.events.remove_events(event.account, jid, types=types):
# There were events to remove
if control is not None:
control.redraw_after_event_removed(event.jid)
@staticmethod
def handle_event_msgsent(obj):
# ('MSGSENT', account, (jid, msg))
......@@ -1117,6 +1150,7 @@ class Interface:
'unsubscribed-presence-received': [
self.handle_event_unsubscribed_presence],
'zeroconf-name-conflict': [self.handle_event_zc_name_conflict],
'read-state-sync': [self.handle_event_read_state_sync],
}
def register_core_handlers(self):
......@@ -1393,6 +1427,12 @@ class Interface:
### Methods for opening new messages controls
################################################################################
def get_groupchat_control(self, account, room_jid):
control = self.minimized_controls[account].get(room_jid)
if control is None:
control = self.msg_win_mgr.get_gc_control(room_jid, account)
return control
def show_groupchat(self, account, room_jid):
minimized_control = self.minimized_controls[account].get(room_jid)
if minimized_control is not None:
......
......@@ -511,12 +511,13 @@ def get_transport_menu(contact, account):
return menu
def get_singlechat_menu(control_id, account, jid):
def get_singlechat_menu(control_id, account, jid, type_):
singlechat_menu = [
(_('Send File…'), [
('win.send-file-httpupload-', _('Upload File…')),
('win.send-file-jingle-', _('Send File Directly…')),
]),
('win.send-marker-', _('Send read marker')),
(_('Send Chatstate'), ['chatstate']),
('win.invite-contacts-', _('Invite Contacts')),
('win.add-to-roster-', _('Add to Contact List')),
......@@ -545,6 +546,9 @@ def get_singlechat_menu(control_id, account, jid):
for item in preset:
if isinstance(item[1], str):
action_name, label = item
if action_name == 'win.send-marker-' and type_ == 'pm':
continue
if action_name == 'app.browse-history':
menuitem = Gio.MenuItem.new(label, action_name)
dict_ = {'account': GLib.Variant('s', account),
......@@ -577,6 +581,7 @@ def get_groupchat_menu(control_id, account, jid):
('win.destroy-', _('Destroy Group Chat')),
]),
(_('Chat Settings'), [
('win.send-marker-', _('Send read marker')),
('win.print-join-left-', _('Show join/leave')),
('win.print-status-', _('Show status changes')),
('win.notify-on-message-', _('Notify on all messages')),
......