Mercurial > libervia-backend
changeset 1972:02d21a589be2
quick_frontend, primitivus: notifications refactoring
replaced old "alerts" system by a more generic one which use listeners and can activate callbacks on notification click.
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 27 Jun 2016 22:36:22 +0200 |
parents | 9421e721d5e2 |
children | a9908e751c42 |
files | frontends/src/primitivus/constants.py frontends/src/primitivus/contact_list.py frontends/src/primitivus/game_tarot.py frontends/src/primitivus/primitivus frontends/src/quick_frontend/constants.py frontends/src/quick_frontend/quick_app.py frontends/src/quick_frontend/quick_chat.py frontends/src/quick_frontend/quick_contact_list.py |
diffstat | 8 files changed, 131 insertions(+), 139 deletions(-) [+] |
line wrap: on
line diff
--- a/frontends/src/primitivus/constants.py Mon Jun 27 21:45:13 2016 +0200 +++ b/frontends/src/primitivus/constants.py Mon Jun 27 22:36:22 2016 +0200 @@ -31,8 +31,8 @@ ('selected_focus', 'default,bold', 'dark red'), ('default', 'default', 'default'), ('default_focus', 'default,bold', 'default'), - ('alert', 'default,underline', 'default'), - ('alert_focus', 'default,bold,underline', 'default'), + ('cl_notifs', 'default,underline', 'yellow'), + ('cl_notifs_focus', 'default,bold,underline', 'yellow'), # Messages ('date', 'light gray', 'default'), ('my_nick', 'dark red,bold', 'default'), @@ -100,6 +100,3 @@ MODE_COMMAND = 'COMMAND' GROUP_DATA_FOLDED = 'folded' - - # contacts and contact list - ALERT_HEADER='(%i) '
--- a/frontends/src/primitivus/contact_list.py Mon Jun 27 21:45:13 2016 +0200 +++ b/frontends/src/primitivus/contact_list.py Mon Jun 27 22:36:22 2016 +0200 @@ -144,18 +144,16 @@ """Method called when a contact is clicked @param use_bare_jid: True if use_bare_jid is set in self._buildEntityWidget. - If True, all jids in self._alerts with the same bare jid has contact_wid.data will be removed @param contact_wid: widget of the contact, must have the entity set in data attribute @param selected: boolean returned by the widget, telling if it is selected """ entity = contact_wid.data - self.contact_list.removeAlerts(entity, use_bare_jid) self.host.modeHint(C.MODE_INSERTION) self._emit('click', entity) # Methods to build the widget - def _buildEntityWidget(self, entity, keys=None, use_bare_jid=False, with_alert=True, with_show_attr=True, markup_prepend=None, markup_append = None): + def _buildEntityWidget(self, entity, keys=None, use_bare_jid=False, with_notifs=True, with_show_attr=True, markup_prepend=None, markup_append = None): """Build one contact markup data @param entity (jid.JID): entity to build @@ -164,8 +162,8 @@ If key starts with "cache_", it will be checked in cache, else, getattr will be done on entity with the key (e.g. getattr(entity, 'node')). If nothing full or keys is None, full entity is used. - @param use_bare_jid (bool): if True, use bare jid for alerts and selected comparisons - @param with_alert (bool): if True, show alert if entity is in self._alerts + @param use_bare_jid (bool): if True, use bare jid for selected comparisons + @param with_notifs (bool): if True, show notification count @param with_show_attr (bool): if True, show color corresponding to presence status @param markup_prepend (list): markup to prepend to the generated one before building the widget @param markup_append (list): markup to append to the generated one before building the widget @@ -199,12 +197,12 @@ else: entity_attr = 'default' - alerts_count = len(self.contact_list.getAlerts(entity, use_bare_jid=use_bare_jid)) - if with_alert and alerts_count: - entity_attr = 'alert' - header = C.ALERT_HEADER % alerts_count + notifs = self.host.getNotifs(entity.bare, profile=self.profile) + if notifs: + entity_attr = 'cl_notifs' + header = u'({}) '.format(len(notifs)) else: - header = '' + header = u'' markup.append((entity_attr, entity_txt)) if markup_prepend:
--- a/frontends/src/primitivus/game_tarot.py Mon Jun 27 21:45:13 2016 +0200 +++ b/frontends/src/primitivus/game_tarot.py Mon Jun 27 22:36:22 2016 +0200 @@ -294,7 +294,7 @@ QuickTarotGame.tarotGameInvalidCardsHandler(self, phase, played_cards, invalid_cards) self.hand_wid.update(self.hand) if self._autoplay == None: # No dialog if there is autoplay - self.parent.host.notify(_('Cards played are invalid !')) + self.parent.host.barNotify(_('Cards played are invalid !')) self.parent.host.redraw() def tarotGameCardsPlayedHandler(self, player, cards):
--- a/frontends/src/primitivus/primitivus Mon Jun 27 21:45:13 2016 +0200 +++ b/frontends/src/primitivus/primitivus Mon Jun 27 22:36:22 2016 +0200 @@ -586,11 +586,24 @@ else: self.notif_bar.addPopUp(pop_up_widget) - def notify(self, message): + def barNotify(self, message): """"Notify message to user via notification bar""" self.notif_bar.addMessage(message) self.redraw() + def notify(self, type_, entity=None, message=None, subject=None, callback=None, cb_args=None, widget=None, profile=C.PROF_KEY_NONE): + if widget is None or widget is not None and widget != self.selected_widget: + # we ignore notification if the widget is selected but we can + # still do a desktop notification is the X window has not the focus + super(PrimitivusApp, self).notify(type_, entity, message, subject, callback, cb_args, widget, profile) + if not self.x_notify.hasFocus(): + if message is None: + message = _("{app}: a new event has just happened{entity}").format( + app=C.APP_NAME, + entity=u' ({})'.format(entity) if entity else '') + self.x_notify.sendNotification(message) + + def newWidget(self, widget): if self.selected_widget is None: self.selectWidget(widget) @@ -651,6 +664,7 @@ # we have clicked on a private MUC conversation chat_widget = self.widgets.getOrCreateWidget(Chat, entity, on_new_widget=None, force_hash = Chat.getPrivateHash(contact_list.profile, entity), profile=contact_list.profile) else: + self.clearNotifs(entity, profile=contact_list.profile) chat_widget = self.widgets.getOrCreateWidget(Chat, entity, on_new_widget=None, profile=contact_list.profile) self.selectWidget(chat_widget) self.menu_roller.addMenu(_('Chat menu'), chat_widget.getMenu(), C.MENU_ID_WIDGET)
--- a/frontends/src/quick_frontend/constants.py Mon Jun 27 21:45:13 2016 +0200 +++ b/frontends/src/quick_frontend/constants.py Mon Jun 27 22:36:22 2016 +0200 @@ -63,10 +63,6 @@ "paused": u"⦷" } - # Alerts - ALERT_MESSAGE = "MESSAGE" # New message received - ALERT_NICK = "NICK" # our nickname was mentionned - # Blogs ENTRY_MODE_TEXT = "text" ENTRY_MODE_RICH = "rich" @@ -86,4 +82,10 @@ UPDATE_SELECTION = 'SELECTION' UPDATE_STRUCTURE = 'STRUCTURE' # high level update (i.e. not item level but organisation of items) - LISTENERS = {'avatar', 'nick', 'presence', 'profilePlugged', 'disconnect', 'gotMenus', 'menu'} + LISTENERS = {'avatar', 'nick', 'presence', 'profilePlugged', 'disconnect', 'gotMenus', 'menu', 'notification', 'notificationsClear'} + + # Notifications + NOTIFY_MESSAGE = 'MESSAGE' # a message was received + NOTIFY_MENTION = 'MENTION' # user was mentionned + NOTIFY_PROGRESS_END = 'PROGRESS_END' # a progression has finised + NOTIFY_GENERIC = 'GENERIC' # a notification which has not its own type
--- a/frontends/src/quick_frontend/quick_app.py Mon Jun 27 21:45:13 2016 +0200 +++ b/frontends/src/quick_frontend/quick_app.py Mon Jun 27 22:36:22 2016 +0200 @@ -34,6 +34,7 @@ import sys from collections import OrderedDict +import time try: # FIXME: to be removed when an acceptable solution is here @@ -51,13 +52,7 @@ def __init__(self, profile): self.profile = profile self.whoami = None - self.data = {} - - def __getitem__(self, key): - return self.data[key] - - def __setitem__(self, key, value): - self.data[key] = value + self.notifications = {} # key: bare jid or '' for general, value: notif data def plug(self): """Plug the profile to the host""" @@ -268,6 +263,8 @@ self.current_action_ids = set() # FIXME: to be removed self.current_action_ids_cb = {} # FIXME: to be removed + self._notif_id = 0 + self._notifications = OrderedDict() self.media_dir = self.bridge.getConfig('', 'media_dir') self.features = None @@ -284,12 +281,6 @@ """widgets currently visible (must be implemented by frontend)""" raise NotImplementedError - @property - def alerts_count(self): - """Count the over whole alerts for all contact lists""" - # FIXME - # return sum([sum(clist._alerts.values()) for clist in self.contact_lists.values()]) - def registerSignal(self, function_name, handler=None, iface="core", with_profile=True): """Register a handler for a signal @@ -330,6 +321,10 @@ args: (entity, new_nick, profile) - presence: called when a presence is received args: (entity, show, priority, statuses, profile) + - notification: called when a new notification is emited + args: (entity, notification_data, profile) + - notification_clear: called when notifications are cleared + args: (entity, type_, profile) - menu: called when a menu item is added or removed args: (type_, path, path_i18n, item) were values are: type_: same as in [sat.core.sat_main.SAT.importMenu] @@ -496,19 +491,6 @@ chat_widget.messageNew(uid, timestamp, from_jid, target, msg, subject, type_, extra, profile) - # ContactList alert - if not from_me: - visible = False - for widget in self.visible_widgets: - if isinstance(widget, quick_chat.QuickChat) and widget.manageMessage(from_jid, type_): - visible = True - break - if visible: # FIXME: à virer gof: - if self.isHidden(): # the window is hidden - self.updateAlertsCounter(extra_inc=1) - else: - contact_list.addAlert(from_jid.bare if type_ == C.MESS_TYPE_GROUPCHAT else from_jid) - def messageSend(self, to_jid, message, subject=None, mess_type="auto", extra=None, callback=None, errback=None, profile_key=C.PROF_KEY_NONE): if subject is None: subject = {} @@ -602,6 +584,76 @@ # contact_list.setCache(from_jid, 'chat_state', to_display) # widget.update(from_jid) + def notify(self, type_, entity=None, message=None, subject=None, callback=None, cb_args=None, widget=None, profile=C.PROF_KEY_NONE): + """Trigger an event notification + + @param type_(unicode): notifation kind, + one of C.NOTIFY_* constant or any custom type specific to frontend + @param entity(jid.JID, None): entity involved in the notification + if entity is in contact list, a indicator may be added in front of it + @param message(unicode, None): message of the notification + @param subject(unicode, None): subject of the notification + @param callback(callable, None): method to call when notification is selected + @param cb_args(list, None): list of args for callback + @param widget(object, None): widget where the notification happened + """ + notif_dict = self.profiles[profile].notifications + key = '' if entity is None else entity.bare + type_notifs = notif_dict.setdefault(key, {}).setdefault(type_, []) + notif_data = { + 'id': self._notif_id, + 'time': time.time(), + 'entity': entity, + 'callback': callback, + 'cb_args': cb_args, + 'message': message, + 'subject': subject, + } + if widget is not None: + notif_data[widget] = widget + type_notifs.append(notif_data) + self._notifications[self._notif_id] = notif_data + self.callListeners('notification', entity, notif_data, profile=profile) + + def getNotifs(self, entity, type_=None, profile=C.PROF_KEY_NONE): + """return notifications for given entity + + @param entity(jid.JID, None): bare jid of the entity to check + None to get general notifications + @param type_(unicode, None): notification type to filter + None to get all notifications + @return (list[dict]): list of notifications + """ + notif_dict = self.profiles[profile].notifications + key = '' if entity is None else entity.bare + key_notifs = notif_dict.setdefault(key, {}) + if type_ is not None: + return key_notifs.get(type_, []) + ret = [] + for notifs_list in key_notifs.itervalues(): + ret.extend(notifs_list) + return ret + + def clearNotifs(self, entity, type_=None, profile=C.PROF_KEY_NONE): + """return notifications for given entity + + @param entity(jid.JID, None): bare jid of the entity to check + None to clear general notifications (but keep entities ones) + @param type_(unicode, None): notification type to filter + None to clear all notifications + @return (list[dict]): list of notifications + """ + notif_dict = self.profiles[profile].notifications + key = '' if entity is None else entity.bare + try: + if type_ is None: + del notif_dict[key] + else: + del notif_dict[key][type_] + except KeyError: + return + self.callListeners('notificationsClear', entity, type_, profile=profile) + def psEventHandler(self, category, service_s, node, event_type, data, profile): """Called when a PubSub event is received. @@ -688,13 +740,6 @@ """ raise NotImplementedError - def updateAlertsCounter(self, extra_inc=0): - """Update the over whole alerts counter. - - @param extra_inc (int): extra counter - """ - pass - def paramUpdateHandler(self, name, value, namespace, profile): log.debug(_(u"param update: [%(namespace)s] %(name)s = %(value)s") % {'namespace': namespace, 'name': name, 'value': value}) if (namespace, name) == ("Connection", "JabberID"):
--- a/frontends/src/quick_frontend/quick_chat.py Mon Jun 27 21:45:13 2016 +0200 +++ b/frontends/src/quick_frontend/quick_chat.py Mon Jun 27 22:36:22 2016 +0200 @@ -340,15 +340,6 @@ """ raise NotImplementedError - def notify(self, contact="somebody", msg=""): - """Notify the user of a new message if the frontend doesn't have the focus. - - @param contact (unicode): contact who wrote to the users - @param msg (unicode): the message that has been received - """ - # FIXME: not called anymore after refactoring - raise NotImplemented - def printDayChange(self, day): """Display the day on a new line.
--- a/frontends/src/quick_frontend/quick_contact_list.py Mon Jun 27 21:45:13 2016 +0200 +++ b/frontends/src/quick_frontend/quick_contact_list.py Mon Jun 27 22:36:22 2016 +0200 @@ -68,9 +68,6 @@ # contacts in roster (bare jids) self._roster = set() - # alerts per entity (key: full jid, value: list of alerts) - self._alerts = {} - # selected entities, full jid self._selected = set() @@ -91,6 +88,10 @@ self.host.addListener('presence', self.presenceListener, [self.profile]) self.nickListener = self.onNickUpdate self.host.addListener('nick', self.nickListener, [self.profile]) + self.notifListener = self.onNotification + self.host.addListener('notification', self.notifListener, [self.profile]) + # notifListener only update the entity, so we can re-use it + self.host.addListener('notificationsClear', self.notifListener, [self.profile]) def _showEmptyGroups(self, show_str): # Called only by __init__ @@ -321,8 +322,6 @@ self._specials.clear() self._special_extras.clear() self._roster.clear() - self._alerts.clear() - self.host.updateAlertsCounter() self.update() def setContact(self, entity, groups=None, attributes=None, in_roster=False): @@ -392,15 +391,14 @@ show = self.getCache(entity, C.PRESENCE_SHOW) if check_resource: - alerts = self._alerts.keys() selected = self._selected else: - alerts = {alert.bare for alert in self._alerts} selected = {selected.bare for selected in self._selected} return ((show is not None and show != C.PRESENCE_UNAVAILABLE) or self.show_disconnected - or entity in alerts - or entity in selected) + or entity in selected + or self.host.getNotifs(entity.bare, profile=self.profile) + ) def anyEntityToShow(self, entities, check_resources=False): """Tell if in a list of entities, at least one should be shown @@ -443,16 +441,12 @@ self._groups[group]['jids'].remove(entity_bare) if not self._groups[group]['jids']: self._groups.pop(group) # FIXME: we use pop because of pyjamas: http://wiki.goffi.org/wiki/Issues_with_Pyjamas/en - for iterable in (self._selected, self._alerts, self._specials, self._special_extras): + for iterable in (self._selected, self._specials, self._special_extras): to_remove = set() for set_entity in iterable: if set_entity.bare == entity.bare: to_remove.add(set_entity) - if isinstance(iterable, set): - iterable.difference_update(to_remove) - else: # XXX: self._alerts is a dict - for item in to_remove: - del iterable[item] + iterable.difference_update(to_remove) self.update([entity], C.UPDATE_DELETE, self.profile) def onPresenceUpdate(self, entity, show, priority, statuses, profile): @@ -500,6 +494,17 @@ self.setCache(entity, 'nick', new_nick) self.update([entity], C.UPDATE_MODIFY, profile) + def onNotification(self, entity, notif, profile): + """Update entity with notification + + @param entity(jid.JID): entity updated + @param notif(dict): notification data + @param profile: %(doc_profile)s + """ + assert profile == self.profile + if entity is not None: + self.update([entity], C.UPDATE_MODIFY, profile) + def unselect(self, entity): """Unselect an entity @@ -540,66 +545,6 @@ self._selected.add(entity) self.update([entity], C.UPDATE_SELECTION, profile=self.profile) - def getAlerts(self, entity, use_bare_jid=False, filter_=None): - """Return alerts set to this entity. - - @param entity (jid.JID): entity - @param use_bare_jid (bool): if True, cumulate the alerts of all the resources sharing the same bare JID - @param filter_(iterable, None): alert to take into account, - None to count all of them - @return (list[unicode,None]): list of C.ALERT_* or None for undefined ones - """ - return [] # FIXME: temporarily disabled - if not use_bare_jid: - alerts = self._alerts.get(entity, []) - else: - alerts = [] - for contact, contact_alerts in self._alerts: - if contact.bare == entity: - alerts.extend(contact_alerts) - if filter_ is None: - return alerts - else: - return [alert for alert in alerts if alert in filter_] - - def addAlert(self, entity, type_=None): - """Add an alert for this enity - - @param entity(jid.JID): entity who received an alert (resource is significant) - @param type_(unicode, None): type of alert (C.ALERT_*) - None for generic alert - """ - self._alerts.setdefault(entity, []) - self._alerts[entity].append(type_) - self.update([entity], C.UPDATE_MODIFY, self.profile) - self.host.updateAlertsCounter() # FIXME: ? - - def removeAlerts(self, entity, use_bare_jid=True): - """Eventually remove an alert on the entity (usually for a waiting message). - - @param entity(jid.JID): entity (resource is significant) - @param use_bare_jid (bool): if True, ignore the resource - """ - if use_bare_jid: - to_remove = set() - for alert_entity in self._alerts: - if alert_entity.bare == entity.bare: - to_remove.add(alert_entity) - if not to_remove: - return # nothing changed - for entity in to_remove: - del self._alerts[entity] - self.update([to_remove], C.UPDATE_MODIFY, self.profile) - self.host.updateAlertsCounter() # FIXME: ? - else: - try: - del self._alerts[entity] - except KeyError: - return # nothing changed - else: - self.update([entity], C.UPDATE_MODIFY, self.profile) - self.host.updateAlertsCounter() # FIXME: ? - def showOfflineContacts(self, show): """Tell if offline contacts should shown