diff --git a/data/sounds/attention.wav b/data/sounds/attention.wav new file mode 100644 index 0000000000000000000000000000000000000000..16c221182bed1c517af8e7e61c720008582cba77 Binary files /dev/null and b/data/sounds/attention.wav differ diff --git a/src/chat_control.py b/src/chat_control.py index 8f36e225ce40f8ab5571a34b5af4f88dea0c01d5..3661d74ba92f85e4e452d8629235e86ec186217c 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 34a0e3feaa329b015572b6df4a47e38734a91ef0..af3ef675c9212c2efe2dd6abbb4183791efcb2e9 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 a069e4b753f009ca45c057162e83dec76224ca69..75d10e89b27ca06c6110bdef5cf1cd00c31b2231 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 418c1694e9e57bbe79f1f09f080f4d4a5413ffa0..edd53ee4ab443028743987f7e6612b30c5dfe21f 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 41f989d69a48f6d6ca95a4d204f657cf9be55d41..bddac1143d220727e5ef43d9f58eec034285cf36 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 ea9fec9700195f14975365e9d6aa4155223b3ca9..1c92d1f477204f789e9c22c9bb7c263331c5df9f 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 413ea100379b409f24f503716c0907c84f42b0b1..7e8a372a9b242b5f55a49d99122886056938bb69 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 7851b228c13d15f4f9283edd305e0740f4bc5b41..dc597d5663db639178a9e304f30afda85030866d 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 1f5e15946b9d3b93eb2327b9714d16e5307f0e6b..42a33d8308c93baaedf13895cb2de5938297b538 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( - '<span foreground="black" weight="bold">%s</span>' % - gobject.markup_escape_text(title)) + '<span foreground="black" weight="bold">%s</span>' % + 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 541f9bb40eb2a752c8cda3cc0a03010c8016437d..d718ec67de2c3d74e8884c76d045d660285ebbc9 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 3f945d54658d18f6cbf3c530d6afdbc0fb67b3ae..b214caf8b0382e38b6e49252ccd32e7f238f1b2a 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 = ('<html><img src="%(image)s" align=left />' \ @@ -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):