quick_frontend, primitivus: notifications refactoring

replaced old "alerts" system by a more generic one which use listeners and can activate callbacks on notification click.
This commit is contained in:
Goffi 2016-06-27 22:36:22 +02:00
commit b388d2b172
8 changed files with 131 additions and 139 deletions

View file

@ -31,8 +31,8 @@ class Const(constants.Const):
('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 @@ class Const(constants.Const):
MODE_COMMAND = 'COMMAND'
GROUP_DATA_FOLDED = 'folded'
# contacts and contact list
ALERT_HEADER='(%i) '

View file

@ -144,18 +144,16 @@ class ContactList(PrimitivusWidget, QuickContactList):
"""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 @@ class ContactList(PrimitivusWidget, QuickContactList):
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 @@ class ContactList(PrimitivusWidget, QuickContactList):
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:

View file

@ -294,7 +294,7 @@ class TarotGame(QuickTarotGame, urwid.WidgetWrap):
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):

View file

@ -586,11 +586,24 @@ class PrimitivusApp(QuickApp, InputHistory):
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 @@ class PrimitivusApp(QuickApp, InputHistory):
# 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)

View file

@ -63,10 +63,6 @@ class Const(constants.Const):
"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 @@ class Const(constants.Const):
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

View file

@ -34,6 +34,7 @@ from sat_frontends.quick_frontend.constants import Const as C
import sys
from collections import OrderedDict
import time
try:
# FIXME: to be removed when an acceptable solution is here
@ -51,13 +52,7 @@ class ProfileManager(object):
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 @@ class QuickApp(object):
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 @@ class QuickApp(object):
"""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 @@ class QuickApp(object):
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 @@ class QuickApp(object):
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 @@ class QuickApp(object):
# 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 @@ class QuickApp(object):
"""
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"):

View file

@ -340,15 +340,6 @@ class QuickChat(quick_widgets.QuickWidget):
"""
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.

View file

@ -68,9 +68,6 @@ class ProfileContactList(object):
# 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 @@ class ProfileContactList(object):
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 @@ class ProfileContactList(object):
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 @@ class ProfileContactList(object):
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 @@ class ProfileContactList(object):
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 @@ class ProfileContactList(object):
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 @@ class ProfileContactList(object):
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