Mercurial > libervia-web
diff src/browser/sat_browser/contact_list.py @ 679:a90cc8fc9605
merged branch frontends_multi_profiles
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 18 Mar 2015 16:15:18 +0100 |
parents | e489218886d7 |
children | e876f493dccc |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/contact_list.py Wed Mar 18 16:15:18 2015 +0100 @@ -0,0 +1,476 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) +from sat_frontends.quick_frontend.quick_contact_list import QuickContactList +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.ScrollPanel import ScrollPanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui.Label import Label +from pyjamas import Window +from pyjamas import DOM + +from constants import Const as C +import libervia_widget +import contact_panel +import blog +import chat + +unicode = str # XXX: pyjama doesn't manage unicode + + +def buildPresenceStyle(presence, base_style=None): + """Return the CSS classname to be used for displaying the given presence information. + + @param presence (unicode): presence is a value in ('', 'chat', 'away', 'dnd', 'xa') + @param base_style (unicode): base classname + @return: unicode + """ + if not base_style: + base_style = "contactLabel" + return '%s-%s' % (base_style, presence or 'connected') + + +def setPresenceStyle(widget, presence, base_style=None): + """ + Set the CSS style of a contact's element according to its presence. + + @param widget (Widget): the UI element of the contact + @param presence (unicode): a value in ("", "chat", "away", "dnd", "xa"). + @param base_style (unicode): the base name of the style to apply + """ + if not hasattr(widget, 'presence_style'): + widget.presence_style = None + style = buildPresenceStyle(presence, base_style) + if style == widget.presence_style: + return + if widget.presence_style is not None: + widget.removeStyleName(widget.presence_style) + widget.addStyleName(style) + widget.presence_style = style + + +class GroupLabel(libervia_widget.DragLabel, Label, ClickHandler): + def __init__(self, host, group): + """ + + @param host (SatWebFrontend) + @param group (unicode): group name + """ + self.group = group + Label.__init__(self, group) # , Element=DOM.createElement('div') + self.setStyleName('group') + libervia_widget.DragLabel.__init__(self, group, "GROUP", host) + ClickHandler.__init__(self) + self.addClickListener(self) + + def onClick(self, sender): + self.host.displayWidget(blog.MicroblogPanel, (self.group,)) + + +class GroupPanel(VerticalPanel): + + def __init__(self, parent): + VerticalPanel.__init__(self) + self.setStyleName('groupPanel') + self._parent = parent + self._groups = set() + + def add(self, group): + if group in self._groups: + log.warning("trying to add an already existing group") + return + _item = GroupLabel(self._parent.host, group) + _item.addMouseListener(self._parent) + DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer") + index = 0 + for group_ in [child.group for child in self.getChildren()]: + if group_ > group: + break + index += 1 + VerticalPanel.insert(self, _item, index) + self._groups.add(group) + + def remove(self, group): + for wid in self: + if isinstance(wid, GroupLabel) and wid.group == group: + VerticalPanel.remove(self, wid) + self._groups.remove(group) + return + log.warning("Trying to remove a non existent group") + + def getGroupBox(self, group): + """get the widget of a group + + @param group (unicode): the group + @return: GroupLabel instance if present, else None""" + for wid in self: + if isinstance(wid, GroupLabel) and wid.group == group: + return wid + return None + + def getGroups(self): + return self._groups + + +class ContactsPanel(contact_panel.ContactsPanel): + """The contact list that is displayed on the left side.""" + + def __init__(self, host): + + def on_click(contact_jid): + self.host.displayWidget(chat.Chat, contact_jid, type_=C.CHAT_ONE2ONE) + + contact_panel.ContactsPanel.__init__(self, host, contacts_click=on_click, + contacts_menus=(C.MENU_JID_CONTEXT, C.MENU_ROSTER_JID_CONTEXT)) + + def setState(self, jid_, type_, state): + """Change the appearance of the contact, according to the state + + @param jid_ (jid.JID): jid.JID which need to change state + @param type_ (unicode): one of "availability", "messageWaiting" + @param state: + - for messageWaiting type: + True if message are waiting + - for availability type: + C.PRESENCE_UNAVAILABLE or None if not connected, else presence like RFC6121 #4.7.2.1""" + assert type_ in ('availability', 'messageWaiting') + contact_box = self.getContactBox(jid_) + if type_ == 'availability': + if state is None: + state = C.PRESENCE_UNAVAILABLE + setPresenceStyle(contact_box.label, state) + elif type_ == 'messageWaiting': + contact_box.setAlert(state) + + +class ContactTitleLabel(libervia_widget.DragLabel, Label, ClickHandler): + + def __init__(self, host, text): + Label.__init__(self, text) # , Element=DOM.createElement('div') + self.setStyleName('contactTitle') + libervia_widget.DragLabel.__init__(self, text, "CONTACT_TITLE", host) + ClickHandler.__init__(self) + self.addClickListener(self) + + def onClick(self, sender): + self.host.displayWidget(blog.MicroblogPanel, ()) + + +class ContactList(SimplePanel, QuickContactList): + """Manage the contacts and groups""" + + def __init__(self, host): + QuickContactList.__init__(self, host, C.PROF_KEY_NONE) + SimplePanel.__init__(self) + self.host = host + self.scroll_panel = ScrollPanel() + self.vPanel = VerticalPanel() + _title = ContactTitleLabel(host, 'Contacts') + DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer") + self._contacts_panel = ContactsPanel(host) + self._contacts_panel.setStyleName('contactPanel') # FIXME: style doesn't exists ! + self._group_panel = GroupPanel(self) + + self.vPanel.add(_title) + self.vPanel.add(self._group_panel) + self.vPanel.add(self._contacts_panel) + self.scroll_panel.add(self.vPanel) + self.add(self.scroll_panel) + self.setStyleName('contactList') + Window.addWindowResizeListener(self) + + # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) + self.avatarListener = self.onAvatarUpdate + host.addListener('avatar', self.avatarListener, [C.PROF_KEY_NONE]) + + @property + def profile(self): + return C.PROF_KEY_NONE + + def onDelete(self): + QuickContactList.onDelete(self) + self.host.removeListener('avatar', self.avatarListener) + + def update(self): + ### GROUPS ### + _keys = self._groups.keys() + try: + # XXX: Pyjamas doesn't do the set casting if None is present + _keys.remove(None) + except (KeyError, ValueError): # XXX: error raised depend on pyjama's compilation options + pass + current_groups = set(_keys) + shown_groups = self._group_panel.getGroups() + new_groups = current_groups.difference(shown_groups) + removed_groups = shown_groups.difference(current_groups) + for group in new_groups: + self._group_panel.add(group) + for group in removed_groups: + self._group_panel.remove(group) + + ### JIDS ### + to_show = [jid_ for jid_ in self.roster_entities if self.entityToShow(jid_) and jid_ != self.whoami.bare] + to_show.sort() + + self._contacts_panel.setList(to_show) + + for jid_ in self._alerts: + self._contacts_panel.setState(jid_, "messageWaiting", True) + + def remove(self, entity): + # FIXME: SimplePanel and QuickContactList both have a 'remove' method + QuickContactList.remove(self, entity) + + def onWindowResized(self, width, height): + ideal_height = height - DOM.getAbsoluteTop(self.getElement()) - 5 + tab_panel = self.host.panel.tab_panel + if tab_panel.getWidgetCount() > 1: + ideal_height -= tab_panel.getTabBar().getOffsetHeight() + self.scroll_panel.setHeight("%s%s" % (ideal_height, "px")) + + # def updateContact(self, jid_s, attributes, groups): + # """Add a contact to the panel if it doesn't exist, update it else + + # @param jid_s: jid userhost as unicode + # @param attributes: cf SàT Bridge API's newContact + # @param groups: list of groups""" + # _current_groups = self.getContactGroups(jid_s) + # _new_groups = set(groups) + # _key = "@%s: " + + # for group in _current_groups.difference(_new_groups): + # # We remove the contact from the groups where he isn't anymore + # self.groups[group].remove(jid_s) + # if not self.groups[group]: + # # The group is now empty, we must remove it + # del self.groups[group] + # self._group_panel.remove(group) + # if self.host.uni_box: + # self.host.uni_box.removeKey(_key % group) + + # for group in _new_groups.difference(_current_groups): + # # We add the contact to the groups he joined + # if group not in self.groups.keys(): + # self.groups[group] = set() + # self._group_panel.add(group) + # if self.host.uni_box: + # self.host.uni_box.addKey(_key % group) + # self.groups[group].add(jid_s) + + # # We add the contact to contact list, it will check if contact already exists + # self._contacts_panel.add(jid_s) + # self.updateVisibility([jid_s], self.getContactGroups(jid_s)) + + # def removeContact(self, jid): + # """Remove contacts from groups where he is and contact list""" + # self.updateContact(jid, {}, []) # we remove contact from every group + # self._contacts_panel.remove(jid) + + # def setConnected(self, jid_s, resource, availability, priority, statuses): + # """Set connection status + # @param jid_s (unicode): JID userhost as unicode + # """ + # if availability == 'unavailable': + # if jid_s in self.connected: + # if resource in self.connected[jid_s]: + # del self.connected[jid_s][resource] + # if not self.connected[jid_s]: + # del self.connected[jid_s] + # else: + # if jid_s not in self.connected: + # self.connected[jid_s] = {} + # self.connected[jid_s][resource] = (availability, priority, statuses) + + # # check if the contact is connected with another resource, use the one with highest priority + # if jid_s in self.connected: + # max_resource = max_priority = None + # for tmp_resource in self.connected[jid_s]: + # if max_priority is None or self.connected[jid_s][tmp_resource][1] >= max_priority: + # max_resource = tmp_resource + # max_priority = self.connected[jid_s][tmp_resource][1] + # if availability == "unavailable": # do not check the priority here, because 'unavailable' has a dummy one + # priority = max_priority + # availability = self.connected[jid_s][max_resource][0] + # if jid_s not in self.connected or priority >= max_priority: + # # case 1: jid not in self.connected means all resources are disconnected, update with 'unavailable' + # # case 2: update (or confirm) with the values of the resource which takes precedence + # self._contacts_panel.setState(jid_s, "availability", availability) + + # self.updateVisibility([jid_s], self.getContactGroups(jid_s)) + + def setContactMessageWaiting(self, jid, waiting): + """Show a visual indicator that contact has send a message + + @param jid: jid of the contact + @param waiting: True if message are waiting""" + raise Exception("Should not be there") + # self._contacts_panel.setState(jid, "messageWaiting", waiting) + + # def getConnected(self, filter_muc=False): + # """return a list of all jid (bare jid) connected + # @param filter_muc: if True, remove the groups from the list + # """ + # contacts = self.connected.keys() + # contacts.sort() + # return contacts if not filter_muc else list(set(contacts).intersection(set(self.getContacts()))) + + # def getContactGroups(self, contact_jid_s): + # """Get groups where contact is + # @param group: string of single group, or list of string + # @param contact_jid_s: jid to test, as unicode + # """ + # result = set() + # for group in self.groups: + # if self.isContactInGroup(group, contact_jid_s): + # result.add(group) + # return result + + # def isContactInGroup(self, group, contact_jid): + # """Test if the contact_jid is in the group + # @param group: string of single group, or list of string + # @param contact_jid: jid to test + # @return: True if contact_jid is in on of the groups""" + # if group in self.groups and contact_jid in self.groups[group]: + # return True + # return False + + def isContactInRoster(self, contact_jid): + """Test if the contact is in our roster list""" + for contact_box in self._contacts_panel: + if contact_jid == contact_box.jid: + return True + return False + + # def getContacts(self): + # return self._contacts_panel.getContacts() + + def getGroups(self): + return self.groups.keys() + + def onMouseMove(self, sender, x, y): + pass + + def onMouseDown(self, sender, x, y): + pass + + def onMouseUp(self, sender, x, y): + pass + + def onMouseEnter(self, sender): + if isinstance(sender, GroupLabel): + jids = self.getGroupData(sender.group, "jids") + for contact in self._contacts_panel: + if contact.jid in jids: + contact.label.addStyleName("selected") + + def onMouseLeave(self, sender): + if isinstance(sender, GroupLabel): + jids = self.getGroupData(sender.group, "jids") + for contact in self._contacts_panel: + if contact.jid in jids: + contact.label.removeStyleName("selected") + + def onAvatarUpdate(self, jid_, hash_, profile): + """Called on avatar update events + + @param jid_: jid of the entity with updated avatar + @param hash_: hash of the avatar + @param profile: %(doc_profile)s + """ + self._contacts_panel.updateAvatar(jid_, self.host.getAvatarURL(jid_)) + + def onNickUpdate(self, jid_, new_nick, profile): + self._contacts_panel.updateNick(jid_, new_nick) + + def hasVisibleMembers(self, group): + """Tell if the given group actually has visible members + + @param group (unicode): the group to check + @return: boolean + """ + raise Exception # FIXME: remove this method + for jid_ in self.groups[group]: + if self._contacts_panel.getContactBox(jid_).isVisible(): + return True + return False + + def offlineContactsToShow(self): + """Tell if offline contacts should be visible according to the user settings + + @return: boolean + """ + return C.bool(self.host.getCachedParam('General', C.SHOW_OFFLINE_CONTACTS)) + + def emtyGroupsToShow(self): + """Tell if empty groups should be visible according to the user settings + + @return: boolean + """ + return C.bool(self.host.getCachedParam('General', C.SHOW_EMPTY_GROUPS)) + + def onPresenceUpdate(self, entity, show, priority, statuses, profile): + QuickContactList.onPresenceUpdate(self, entity, show, priority, statuses, profile) + entity_bare = entity.bare + show = self.getCache(entity_bare, C.PRESENCE_SHOW) # we use cache to have the show nformation of main resource only + self._contacts_panel.setState(entity_bare, "availability", show) + self.update() # FIXME: should update the list without rebuilding it all + + # def updateVisibility(self, jids, groups): + # """Set the widgets visibility for the given contacts and groups + + # @param jids (list[unicode]): list of JID + # @param groups (list[unicode]): list of groups + # """ + # for jid_s in jids: + # try: + # self._contacts_panel.getContactBox(jid_s).setVisible(jid_s in self.connected or self.offlineContactsToShow()) + # except TypeError: + # log.warning('No box for contact %s: this code line should not be reached' % jid_s) + # for group in groups: + # try: + # self._group_panel.getGroupBox(group).setVisible(self.hasVisibleMembers(group) or self.emtyGroupsToShow()) + # except TypeError: + # log.warning('No box for group %s: this code line should not be reached' % group) + + # def refresh(self): + # """Show or hide disconnected contacts and empty groups""" + # self.updateVisibility(self._contacts_panel.contacts, self.groups.keys()) + + +class JIDList(list): + """JID-friendly list implementation for Pyjamas""" + + def __contains__(self, item): + """Tells if the list contains the given item. + + @param item (object): element to check + @return: bool + """ + # Since our JID doesn't inherit from str/unicode, without this method + # the test would return True only when the objects references are the + # same. Tests have shown that the other iterable "set" and "dict" don't + # need this hack to reproduce the Twisted's behavior. + for other in self: + if other == item: + return True + return False