diff --git a/data/sounds/attention.wav b/data/sounds/attention.wav
new file mode 100644
index 000000000..16c221182
Binary files /dev/null and b/data/sounds/attention.wav differ
diff --git a/src/chat_control.py b/src/chat_control.py
index 8f36e225c..3661d74ba 100644
--- a/src/chat_control.py
+++ b/src/chat_control.py
@@ -863,7 +863,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
def send_message(self, message, keyID='', type_='chat', chatstate=None,
msg_id=None, resource=None, xhtml=None, callback=None, callback_args=[],
- process_commands=True):
+ process_commands=True, attention=False):
"""
Send the given message to the active tab. Doesn't return None if error
"""
@@ -880,7 +880,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
keyID=keyID, type_=type_, chatstate=chatstate, msg_id=msg_id,
resource=resource, user_nick=self.user_nick, xhtml=xhtml,
label=label, callback=callback, callback_args=callback_args,
- control=self))
+ control=self, attention=attention))
# Record the history of sent messages
self.save_message(message, 'sent')
@@ -2235,7 +2235,7 @@ class ChatControl(ChatControlBase):
dialogs.ESessionInfoWindow(self.session)
def send_message(self, message, keyID='', chatstate=None, xhtml=None,
- process_commands=True):
+ process_commands=True, attention=False):
"""
Send a message to contact
"""
@@ -2286,7 +2286,8 @@ class ChatControl(ChatControlBase):
ChatControlBase.send_message(self, message, keyID, type_='chat',
chatstate=chatstate_to_send, xhtml=xhtml, callback=_on_sent,
callback_args=[contact, message, encrypted, xhtml,
- self.get_seclabel()], process_commands=process_commands)
+ self.get_seclabel()], process_commands=process_commands,
+ attention=attention)
def check_for_possible_paused_chatstate(self, arg):
"""
diff --git a/src/command_system/implementation/standard.py b/src/command_system/implementation/standard.py
index 34a0e3fea..af3ef675c 100644
--- a/src/command_system/implementation/standard.py
+++ b/src/command_system/implementation/standard.py
@@ -230,6 +230,11 @@ class StandardCommonChatCommands(CommandContainer):
state = self._video_button.get_active()
self._video_button.set_active(not state)
+ @command(raw=True)
+ @doc(_("Send a message to the contact that will attract his (her) attention"))
+ def attention(self, message):
+ self.send_message(message, process_commands=False, attention=True)
+
class StandardChatCommands(CommandContainer):
"""
This command container contains standard commands which are unique
diff --git a/src/common/config.py b/src/common/config.py
index a069e4b75..75d10e89b 100644
--- a/src/common/config.py
+++ b/src/common/config.py
@@ -300,6 +300,7 @@ class Config:
'stun_server': [opt_str, '', _('STUN server to use when using jingle')],
'show_affiliation_in_groupchat': [opt_bool, True, _('If True, Gajim will show affiliation of groupchat occupants by adding a colored square to the status icon')],
'global_proxy': [opt_str, '', _('Proxy used for all outgoing connections if the account does not have a specific proxy configured')],
+ 'ignore_incoming_attention': [opt_bool, False, _('If True, Gajim will ignore incoming attention requestd ("wizz").')],
}
__options_per_key = {
@@ -497,6 +498,7 @@ class Config:
}
soundevents_default = {
+ 'attention_received': [True, 'attention.wav'],
'first_message_received': [ True, 'message1.wav' ],
'next_message_received_focused': [ True, 'message2.wav' ],
'next_message_received_unfocused': [ True, 'message2.wav' ],
diff --git a/src/common/connection.py b/src/common/connection.py
index 418c1694e..edd53ee4a 100644
--- a/src/common/connection.py
+++ b/src/common/connection.py
@@ -253,7 +253,7 @@ class CommonConnection:
def _prepare_message(self, jid, msg, keyID, type_='chat', subject='',
chatstate=None, msg_id=None, resource=None, user_nick=None, xhtml=None,
session=None, forward_from=None, form_node=None, label=None,
- original_message=None, delayed=None, callback=None):
+ original_message=None, delayed=None, attention=False, callback=None):
if not self.connection or self.connected < 2:
return 1
try:
@@ -304,7 +304,8 @@ class CommonConnection:
msgtxt, original_message, fjid, resource,
jid, xhtml, subject, chatstate, msg_id,
label, forward_from, delayed, session,
- form_node, user_nick, keyID, callback)
+ form_node, user_nick, keyID, attention,
+ callback)
gajim.nec.push_incoming_event(GPGTrustKeyEvent(None,
conn=self, callback=_on_always_trust))
else:
@@ -312,7 +313,7 @@ class CommonConnection:
original_message, fjid, resource, jid, xhtml,
subject, chatstate, msg_id, label, forward_from,
delayed, session, form_node, user_nick, keyID,
- callback)
+ attention, callback)
gajim.thread_interface(encrypt_thread, [msg, keyID, False],
_on_encrypted, [])
return
@@ -320,18 +321,18 @@ class CommonConnection:
self._message_encrypted_cb(('', error), type_, msg, msgtxt,
original_message, fjid, resource, jid, xhtml, subject,
chatstate, msg_id, label, forward_from, delayed, session,
- form_node, user_nick, keyID, callback)
+ form_node, user_nick, keyID, attention, callback)
return
self._on_continue_message(type_, msg, msgtxt, original_message, fjid,
resource, jid, xhtml, subject, msgenc, keyID, chatstate, msg_id,
label, forward_from, delayed, session, form_node, user_nick,
- callback)
+ attention, callback)
def _message_encrypted_cb(self, output, type_, msg, msgtxt,
original_message, fjid, resource, jid, xhtml, subject, chatstate, msg_id,
label, forward_from, delayed, session, form_node, user_nick, keyID,
- callback):
+ attention, callback):
msgenc, error = output
if msgenc and not error:
@@ -344,7 +345,7 @@ class CommonConnection:
self._on_continue_message(type_, msg, msgtxt, original_message,
fjid, resource, jid, xhtml, subject, msgenc, keyID,
chatstate, msg_id, label, forward_from, delayed, session,
- form_node, user_nick, callback)
+ form_node, user_nick, attention, callback)
return
# Encryption failed, do not send message
tim = localtime()
@@ -353,7 +354,8 @@ class CommonConnection:
def _on_continue_message(self, type_, msg, msgtxt, original_message, fjid,
resource, jid, xhtml, subject, msgenc, keyID, chatstate, msg_id,
- label, forward_from, delayed, session, form_node, user_nick, callback):
+ label, forward_from, delayed, session, form_node, user_nick, attention,
+ callback):
if type_ == 'chat':
msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ=type_,
xhtml=xhtml)
@@ -424,6 +426,10 @@ class CommonConnection:
if session.enable_encryption:
msg_iq = session.encrypt_stanza(msg_iq)
+ # XEP-0224
+ if attention:
+ msg_iq.setTag('attention', namespace=common.xmpp.NS_ATTENTION)
+
if callback:
callback(jid, msg, keyID, forward_from, session, original_message,
subject, type_, msg_iq, xhtml)
@@ -1794,8 +1800,8 @@ class Connection(CommonConnection, ConnectionHandlers):
def send_message(self, jid, msg, keyID=None, type_='chat', subject='',
chatstate=None, msg_id=None, resource=None, user_nick=None, xhtml=None,
label=None, session=None, forward_from=None, form_node=None,
- original_message=None, delayed=None, callback=None, callback_args=[],
- now=False):
+ original_message=None, delayed=None, attention=False, callback=None,
+ callback_args=[], now=False):
def cb(jid, msg, keyID, forward_from, session, original_message,
subject, type_, msg_iq, xhtml):
@@ -1813,7 +1819,8 @@ class Connection(CommonConnection, ConnectionHandlers):
chatstate=chatstate, msg_id=msg_id, resource=resource,
user_nick=user_nick, xhtml=xhtml, label=label, session=session,
forward_from=forward_from, form_node=form_node,
- original_message=original_message, delayed=delayed, callback=cb)
+ original_message=original_message, delayed=delayed,
+ attention=attention, callback=cb)
def _nec_message_outgoing(self, obj):
if obj.account != self.name:
@@ -1838,7 +1845,7 @@ class Connection(CommonConnection, ConnectionHandlers):
resource=obj.resource, user_nick=obj.user_nick, xhtml=obj.xhtml,
label=obj.label, session=obj.session, forward_from=obj.forward_from,
form_node=obj.form_node, original_message=obj.original_message,
- delayed=obj.delayed, callback=cb)
+ delayed=obj.delayed, attention=obj.attention, callback=cb)
def send_contacts(self, contacts, jid):
"""
diff --git a/src/common/connection_handlers_events.py b/src/common/connection_handlers_events.py
index 41f989d69..bddac1143 100644
--- a/src/common/connection_handlers_events.py
+++ b/src/common/connection_handlers_events.py
@@ -1192,6 +1192,7 @@ class DecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
self.sent = self.msg_obj.sent
self.popup = False
self.msg_id = None # id in log database
+ self.attention = False # XEP-0224
self.receipt_request_tag = self.stanza.getTag('request',
namespace=xmpp.NS_RECEIPTS)
@@ -1206,6 +1207,9 @@ class DecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
if self.seclabel:
self.displaymarking = self.seclabel.getTag('displaymarking')
+ if self.stanza.getTag('attention', namespace=xmpp.NS_ATTENTION):
+ self.attention = True
+
self.form_node = self.stanza.getTag('x', namespace=xmpp.NS_DATA)
if gajim.config.get('ignore_incoming_xhtml'):
@@ -2062,7 +2066,19 @@ class NotificationEvent(nec.NetworkIncomingEvent):
# we're online or chat
self.do_popup = True
- if self.first_unread and helpers.allow_sound_notification(
+ if msg_obj.attention and not gajim.config.get(
+ 'ignore_incoming_attention'):
+ self.popup_timeout = 0
+ self.do_popup = True
+ else:
+ self.popup_timeout = gajim.config.get('notification_timeout')
+
+ if msg_obj.attention and not gajim.config.get(
+ 'ignore_incoming_attention') and gajim.config.get_per('soundevents',
+ 'attention_received', 'enabled'):
+ self.sound_event = 'attention_received'
+ self.do_sound = True
+ elif self.first_unread and helpers.allow_sound_notification(
self.conn.name, 'first_message_received'):
self.do_sound = True
elif not self.first_unread and self.control_focused and \
@@ -2175,6 +2191,8 @@ class NotificationEvent(nec.NetworkIncomingEvent):
self.popup_image = gtkgui_helpers.get_path_to_generic_or_avatar(
img_path, jid=self.jid, suffix=suffix)
+ self.popup_timeout = gajim.config.get('notification_timeout')
+
if event == 'status_change':
self.popup_title = _('%(nick)s Changed Status') % \
{'nick': gajim.get_name_from_jid(account, self.jid)}
@@ -2219,6 +2237,7 @@ class NotificationEvent(nec.NetworkIncomingEvent):
self.popup_event_type = ''
self.popup_msg_type = ''
self.popup_image = ''
+ self.popup_timeout = -1
self.do_command = False
self.command = ''
@@ -2261,6 +2280,7 @@ class MessageOutgoingEvent(nec.NetworkOutgoingEvent):
self.now = False
self.is_loggable = True
self.control = None
+ self.attention = False
def generate(self):
return True
diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py
index ea9fec970..1c92d1f47 100644
--- a/src/common/xmpp/protocol.py
+++ b/src/common/xmpp/protocol.py
@@ -40,6 +40,7 @@ NS_ARCHIVE_MANAGE = NS_ARCHIVE + ':manage' # XEP-0136
NS_ARCHIVE_MANUAL = NS_ARCHIVE + ':manual' # XEP-0136
NS_ARCHIVE_PREF = NS_ARCHIVE + ':pref'
NS_ATOM = 'http://www.w3.org/2005/Atom'
+NS_ATTENTION = 'urn:xmpp:attention:0' # XEP-0224
NS_AUTH = 'jabber:iq:auth'
NS_AVATAR = 'http://www.xmpp.org/extensions/xep-0084.html#ns-metadata'
NS_BIND = 'urn:ietf:params:xml:ns:xmpp-bind'
diff --git a/src/common/zeroconf/connection_zeroconf.py b/src/common/zeroconf/connection_zeroconf.py
index 413ea1003..7e8a372a9 100644
--- a/src/common/zeroconf/connection_zeroconf.py
+++ b/src/common/zeroconf/connection_zeroconf.py
@@ -336,8 +336,8 @@ class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf):
def send_message(self, jid, msg, keyID, type_='chat', subject='',
chatstate=None, msg_id=None, resource=None, user_nick=None, xhtml=None,
label=None, session=None, forward_from=None, form_node=None,
- original_message=None, delayed=None, callback=None, callback_args=[],
- now=True):
+ original_message=None, delayed=None, attention=False, callback=None,
+ callback_args=[], now=True):
def on_send_ok(msg_id):
gajim.nec.push_incoming_event(MessageSentEvent(None, conn=self,
@@ -370,7 +370,8 @@ class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf):
chatstate=chatstate, msg_id=msg_id, resource=resource,
user_nick=user_nick, xhtml=xhtml, session=session,
forward_from=forward_from, form_node=form_node,
- original_message=original_message, delayed=delayed, callback=cb)
+ original_message=original_message, delayed=delayed,
+ attention=attention, callback=cb)
def _nec_message_outgoing(self, obj):
if obj.account != self.name:
@@ -411,7 +412,7 @@ class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf):
resource=obj.resource, user_nick=obj.user_nick, xhtml=obj.xhtml,
label=obj.label, session=obj.session, forward_from=obj.forward_from,
form_node=obj.form_node, original_message=obj.original_message,
- delayed=obj.delayed, callback=cb)
+ delayed=obj.delayed, attention=obj.attention, callback=cb)
def send_stanza(self, stanza):
# send a stanza untouched
diff --git a/src/config.py b/src/config.py
index 7851b228c..dc597d566 100644
--- a/src/config.py
+++ b/src/config.py
@@ -4188,6 +4188,7 @@ class ManageSoundsWindow:
# NOTE: sounds_ui_names MUST have all items of
# sounds = gajim.config.get_per('soundevents') as keys
sounds_dict = {
+ 'attention_received': _('Attention Message Received'),
'first_message_received': _('First Message Received'),
'next_message_received_focused': _('Next Message Received Focused'),
'next_message_received_unfocused':
diff --git a/src/dialogs.py b/src/dialogs.py
index 1f5e15946..42a33d830 100644
--- a/src/dialogs.py
+++ b/src/dialogs.py
@@ -2745,7 +2745,7 @@ class ChangePasswordDialog:
class PopupNotificationWindow:
def __init__(self, event_type, jid, account, msg_type='',
- path_to_image=None, title=None, text=None):
+ path_to_image=None, title=None, text=None, timeout=-1):
self.account = account
self.jid = jid
self.msg_type = msg_type
@@ -2765,8 +2765,8 @@ class PopupNotificationWindow:
title = ''
event_type_label.set_markup(
- '%s' %
- gobject.markup_escape_text(title))
+ '%s' %
+ gobject.markup_escape_text(title))
# set colors [ http://www.pitt.edu/~nisg/cis/web/cgi/rgb.html ]
self.window.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('black'))
@@ -2787,7 +2787,7 @@ class PopupNotificationWindow:
elif event_type == _('File Transfer Error'):
bg_color = gajim.config.get('notif_fterror_color')
elif event_type in (_('File Transfer Completed'),
- _('File Transfer Stopped')):
+ _('File Transfer Stopped')):
bg_color = gajim.config.get('notif_ftcomplete_color')
elif event_type == _('Groupchat Invitation'):
bg_color = gajim.config.get('notif_invite_color')
@@ -2813,13 +2813,13 @@ class PopupNotificationWindow:
pos_y = gajim.config.get('notification_position_y')
if pos_y < 0:
pos_y = gtk.gdk.screen_height() - \
- gajim.interface.roster.popups_notification_height + pos_y + 1
+ gajim.interface.roster.popups_notification_height + pos_y + 1
self.window.move(pos_x, pos_y)
xml.connect_signals(self)
self.window.show_all()
- timeout = gajim.config.get('notification_timeout')
- gobject.timeout_add_seconds(timeout, self.on_timeout)
+ if timeout > 0:
+ gobject.timeout_add_seconds(timeout, self.on_timeout)
def on_close_button_clicked(self, widget):
self.adjust_height_and_move_popup_notification_windows()
diff --git a/src/groupchat_control.py b/src/groupchat_control.py
index 541f9bb40..d718ec67d 100644
--- a/src/groupchat_control.py
+++ b/src/groupchat_control.py
@@ -212,7 +212,8 @@ class PrivateChatControl(ChatControl):
self.parent_win.redraw_tab(self)
self.update_ui()
- def send_message(self, message, xhtml=None, process_commands=True):
+ def send_message(self, message, xhtml=None, process_commands=True,
+ attention=False):
"""
Call this method to send the message
"""
@@ -237,7 +238,7 @@ class PrivateChatControl(ChatControl):
return
ChatControl.send_message(self, message, xhtml=xhtml,
- process_commands=process_commands)
+ process_commands=process_commands, attention=attention)
def update_ui(self):
if self.contact.show == 'offline':
diff --git a/src/notify.py b/src/notify.py
index 3f945d546..b214caf8b 100644
--- a/src/notify.py
+++ b/src/notify.py
@@ -73,7 +73,7 @@ def get_show_in_systray(event, account, contact, type_=None):
return gajim.config.get('trayicon_notification_on_events')
def popup(event_type, jid, account, msg_type='', path_to_image=None, title=None,
-text=None):
+text=None, timeout=-1):
"""
Notify a user of an event. It first tries to a valid implementation of
the Desktop Notification Specification. If that fails, then we fall back to
@@ -83,11 +83,14 @@ text=None):
if not path_to_image:
path_to_image = gtkgui_helpers.get_icon_path('gajim-chat_msg_recv', 48)
+ if timeout < 0:
+ timeout = gajim.config.get('notification_timeout')
+
# Try to show our popup via D-Bus and notification daemon
if gajim.config.get('use_notif_daemon') and dbus_support.supported:
try:
DesktopNotification(event_type, jid, account, msg_type,
- path_to_image, title, gobject.markup_escape_text(text))
+ path_to_image, title, gobject.markup_escape_text(text), timeout)
return # sucessfully did D-Bus Notification procedure!
except dbus.DBusException, e:
# Connection to D-Bus failed
@@ -112,8 +115,7 @@ text=None):
_title = title
notification = pynotify.Notification(_title, _text)
- timeout = gajim.config.get('notification_timeout') * 1000 # make it ms
- notification.set_timeout(timeout)
+ notification.set_timeout(timeout*1000)
notification.set_category(event_type)
notification.set_data('event_type', event_type)
@@ -134,7 +136,7 @@ text=None):
# Either nothing succeeded or the user wants old-style notifications
instance = PopupNotificationWindow(event_type, jid, account, msg_type,
- path_to_image, title, text)
+ path_to_image, title, text, timeout)
gajim.interface.roster.popup_notification_windows.append(instance)
def on_pynotify_notification_clicked(notification, action):
@@ -157,7 +159,8 @@ class Notification:
if obj.do_popup:
popup(obj.popup_event_type, obj.jid, obj.conn.name,
obj.popup_msg_type, path_to_image=obj.popup_image,
- title=obj.popup_title, text=obj.popup_text)
+ title=obj.popup_title, text=obj.popup_text,
+ timeout=obj.popup_timeout)
if obj.do_sound:
if obj.sound_file:
@@ -235,11 +238,12 @@ class DesktopNotification:
"""
def __init__(self, event_type, jid, account, msg_type='',
- path_to_image=None, title=None, text=None):
+ path_to_image=None, title=None, text=None, timeout=-1):
self.path_to_image = os.path.abspath(path_to_image)
self.event_type = event_type
self.title = title
self.text = text
+ self.timeout = timeout
# 0.3.1 is the only version of notification daemon that has no way
# to determine which version it is. If no method exists, it means
# they're using that one.
@@ -302,7 +306,6 @@ class DesktopNotification:
self.get_version()
def attempt_notify(self):
- timeout = gajim.config.get('notification_timeout') # in seconds
ntype = self.ntype
if self.kde_notifications:
notification_text = ('
' \
@@ -320,8 +323,8 @@ class DesktopNotification:
# actions (stringlist)
(dbus.String('default'), dbus.String(self.event_type),
dbus.String('ignore'), dbus.String(_('Ignore'))),
- [], # hints (not used in KDE yet)
- dbus.UInt32(timeout*1000), # timeout (int), in ms
+ [], # hints (not used in KDE yet)
+ dbus.UInt32(self.timeout*1000), # timeout (int), in ms
reply_handler=self.attach_by_id,
error_handler=self.notify_another_way)
return
@@ -345,7 +348,7 @@ class DesktopNotification:
actions,
[''],
True,
- dbus.UInt32(timeout),
+ dbus.UInt32(self.timeout),
reply_handler=self.attach_by_id,
error_handler=self.notify_another_way)
except AttributeError:
@@ -392,7 +395,7 @@ class DesktopNotification:
dbus.String(text),
actions,
hints,
- dbus.UInt32(timeout*1000),
+ dbus.UInt32(self.timeout*1000),
reply_handler=self.attach_by_id,
error_handler=self.notify_another_way)
except Exception, e:
@@ -407,7 +410,7 @@ class DesktopNotification:
dbus.String(self.text),
dbus.String(''),
hints,
- dbus.UInt32(timeout*1000),
+ dbus.UInt32(self.timeout*1000),
reply_handler=self.attach_by_id,
error_handler=self.notify_another_way)
except Exception, e:
@@ -422,7 +425,7 @@ class DesktopNotification:
str(e))
instance = PopupNotificationWindow(self.event_type, self.jid,
self.account, self.msg_type, self.path_to_image, self.title,
- self.text)
+ self.text, self.timeout)
gajim.interface.roster.popup_notification_windows.append(instance)
def on_action_invoked(self, id_, reason):