Mercurial > libervia-web
diff src/browser/sat_browser/contact_list.py @ 589:a5019e62c3e9 frontends_multi_profiles
browser side: big refactoring to base Libervia on QuickFrontend, first draft:
/!\ not finished, partially working and highly instable
- add collections module with an OrderedDict like class
- SatWebFrontend inherit from QuickApp
- general sat_frontends tools.jid module is used
- bridge/json methods have moved to json module
- UniBox is partially removed (should be totally removed before merge to trunk)
- Signals are now register with the generic registerSignal method (which is called mainly in QuickFrontend)
- the generic getOrCreateWidget method from QuickWidgetsManager is used instead of Libervia's specific methods
- all Widget are now based more or less directly on QuickWidget
- with the new QuickWidgetsManager.getWidgets method, it's no more necessary to check all widgets which are instance of a particular class
- ChatPanel and related moved to chat module
- MicroblogPanel and related moved to blog module
- global and overcomplicated send method has been disabled: each class should manage its own sending
- for consistency with other frontends, former ContactPanel has been renamed to ContactList and vice versa
- for the same reason, ChatPanel has been renamed to Chat
- for compatibility with QuickFrontend, a fake profile is used in several places, it is set to C.PROF_KEY_NONE (real profile is managed server side for obvious security reasons)
- changed default url for web panel to SàT website, and contact address to generic SàT contact address
- ContactList is based on QuickContactList, UI changes are done in update method
- bride call (now json module) have been greatly improved, in particular call can be done in the same way as for other frontends (bridge.method_name(arg1, arg2, ..., callback=cb, errback=eb). Blocking method must be called like async methods due to javascript architecture
- in bridge calls, a callback can now exists without errback
- hard reload on BridgeSignals remote error has been disabled, a better option should be implemented
- use of constants where that make sens, some style improvments
- avatars are temporarily disabled
- lot of code disabled, will be fixed or removed before merge
- various other changes, check diff for more details
server side: manage remote exception on getEntityData, removed getProfileJid call, added getWaitingConf, added getRoomsSubjects
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 24 Jan 2015 01:45:39 +0100 |
parents | src/browser/sat_browser/contact.py@668bb04e9708 |
children | c66f7227848e |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/contact_list.py Sat Jan 24 01:45:39 2015 +0100 @@ -0,0 +1,590 @@ +#!/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.ui.HTML import HTML +from pyjamas.ui.Image import Image +from pyjamas import Window +from pyjamas import DOM +from __pyjamas__ import doc + +from sat_frontends.tools import jid +from constants import Const as C +import base_widget +import panels +import html_tools +import chat + + +def buildPresenceStyle(presence, base_style=None): + """Return the CSS classname to be used for displaying the given presence information. + @param presence (str): presence is a value in ('', 'chat', 'away', 'dnd', 'xa') + @param base_style (str): base classname + @return: str + """ + 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 (str): a value in ("", "chat", "away", "dnd", "xa"). + @param base_style (str): 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(base_widget.DragLabel, Label, ClickHandler): + def __init__(self, host, group): + self.group = group + self.host = host + Label.__init__(self, group) # , Element=DOM.createElement('div') + self.setStyleName('group') + base_widget.DragLabel.__init__(self, group, "GROUP") + ClickHandler.__init__(self) + self.addClickListener(self) + + def onClick(self, sender): + self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, {'item': self.group}) + + +class ContactLabel(HTML): + def __init__(self, jid, name=None): + HTML.__init__(self) + self.name = name or str(jid) + self.waiting = False + self.refresh() + self.setStyleName('contactLabel') + + def refresh(self): + if self.waiting: + wait_html = "<b>(*)</b> " + self.setHTML("%(wait)s%(name)s" % {'wait': wait_html, + 'name': html_tools.html_sanitize(self.name)}) + + def setMessageWaiting(self, waiting): + """Show a visual indicator if message are waiting + + @param waiting: True if message are waiting""" + self.waiting = waiting + self.refresh() + + +class ContactMenuBar(base_widget.WidgetMenuBar): + + def onBrowserEvent(self, event): + base_widget.WidgetMenuBar.onBrowserEvent(self, event) + event.stopPropagation() # prevent opening the chat dialog + + @classmethod + def getCategoryHTML(cls, menu_name_i18n, type_): + return '<img src="%s"/>' % C.DEFAULT_AVATAR + + def setUrl(self, url): + """Set the URL of the contact avatar.""" + self.items[0].setHTML('<img src="%s" />' % url) + + +class ContactBox(VerticalPanel, ClickHandler, base_widget.DragLabel): + + def __init__(self, host, jid_, name=None, click_listener=None, handle_menu=None): + VerticalPanel.__init__(self, StyleName='contactBox', VerticalAlignment='middle') + base_widget.DragLabel.__init__(self, jid_, "CONTACT") + self.host = host + self.jid = jid_ + self.label = ContactLabel(jid_, name) + self.avatar = ContactMenuBar(self, host) if handle_menu else Image() + # self.updateAvatar(host.getAvatar(jid_)) # FIXME + self.add(self.avatar) + self.add(self.label) + if click_listener: + ClickHandler.__init__(self) + self.addClickListener(self) + self.click_listener = click_listener + + def addMenus(self, menu_bar): + menu_bar.addCachedMenus(C.MENU_ROSTER_JID_CONTEXT, {'jid': self.jid}) + menu_bar.addCachedMenus(C.MENU_JID_CONTEXT, {'jid': self.jid}) + + def setMessageWaiting(self, waiting): + """Show a visual indicator if message are waiting + + @param waiting: True if message are waiting""" + self.label.setMessageWaiting(waiting) + + def updateAvatar(self, url): + """Update the avatar. + + @param url (str): image url + """ + self.avatar.setUrl(url) + + def onClick(self, sender): + self.click_listener(self.jid) + + +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 (str): 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 BaseContactsPanel(VerticalPanel): + """Class that can be used to represent a contact list, but not necessarily + the one that is displayed on the left side. Special features like popup menu + panel or changing the contact states must be done in a sub-class.""" + + def __init__(self, host, handle_click=False, handle_menu=False): + VerticalPanel.__init__(self) + self.host = host + self.contacts = [] + self.click_listener = None + self.handle_menu = handle_menu + + if handle_click: + def cb(contact_jid): + host.widgets.getOrCreateWidget(chat.Chat, contact_jid, type_=C.CHAT_ONE2ONE, profile=C.PROF_KEY_NONE) + self.click_listener = cb + + def add(self, jid_, name=None): + """Add a contact to the list. + + @param jid_ (jid.JID): jid_ of the contact + @param name (str): optional name of the contact + """ + assert isinstance(jid_, jid.JID) + if jid_ in self.contacts: + return + index = 0 + for contact_ in self.contacts: + if contact_ > jid_: + break + index += 1 + self.contacts.insert(index, jid_) + box = ContactBox(self.host, jid_, name, self.click_listener, self.handle_menu) + VerticalPanel.insert(self, box, index) + + def remove(self, jid_): + box = self.getContactBox(jid_) + if not box: + return + VerticalPanel.remove(self, box) + self.contacts.remove(jid_) + + def isContactPresent(self, contact_jid): + """Return True if a contact is present in the panel""" + return contact_jid in self.contacts + + def getContacts(self): + return self.contacts + + def getContactBox(self, contact_jid): + """get the widget of a contact + + @param contact_jid (jid.JID): the contact + @return: ContactBox instance if present, else None""" + for wid in self: + if isinstance(wid, ContactBox) and wid.jid == contact_jid: + return wid + return None + + def updateAvatar(self, jid_, url): + """Update the avatar of the given contact + + @param jid_ (jid.JID): contact jid + @param url (str): image url + """ + try: + self.getContactBox(jid_).updateAvatar(url) + except TypeError: + pass + + +class ContactsPanel(BaseContactsPanel): + """The contact list that is displayed on the left side.""" + + def __init__(self, host): + BaseContactsPanel.__init__(self, host, handle_click=True, handle_menu=True) + + 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_ (str): 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 not contact_box: + log.warning("No contact box found for {}".format(jid_)) + else: + if type_ == 'availability': + if state is None: + state = C.PRESENCE_UNAVAILABLE + setPresenceStyle(contact_box.label, state) + elif type_ == 'messageWaiting': + contact_box.setMessageWaiting(state) + + +class ContactTitleLabel(base_widget.DragLabel, Label, ClickHandler): + def __init__(self, host, text): + Label.__init__(self, text) # , Element=DOM.createElement('div') + self.host = host + self.setStyleName('contactTitle') + base_widget.DragLabel.__init__(self, text, "CONTACT_TITLE") + ClickHandler.__init__(self) + self.addClickListener(self) + + def onClick(self, sender): + self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, {'item': None}) + + +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.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) + + @property + def profile(self): + return C.PROF_KEY_NONE + + 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: + 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 ### + current_contacts = set(self._cache.keys()) + shown_contacts = set(self._contacts_panel.getContacts()) + new_contacts = current_contacts.difference(shown_contacts) + removed_contacts = shown_contacts.difference(current_contacts) + + for contact in new_contacts: + self._contacts_panel.add(contact) + for contact in removed_contacts: + self._contacts_panel.remove(contact) + + def onWindowResized(self, width, height): + contact_panel_elt = self.getElement() + # FIXME: still needed ? + # classname = 'widgetsPanel' if isinstance(self.getParent().getParent(), panels.UniBoxPanel) else 'gwt-TabBar' + classname = 'gwt-TabBar' + _elts = doc().getElementsByClassName(classname) + if not _elts.length: + log.error("no element of class %s found, it should exist !" % classname) + tab_bar_h = height + else: + tab_bar_h = DOM.getAbsoluteTop(_elts.item(0)) or height # getAbsoluteTop can be 0 if tabBar is hidden + + ideal_height = tab_bar_h - DOM.getAbsoluteTop(contact_panel_elt) - 5 + 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 (str): 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) + + # # update the connected contacts chooser live + # if hasattr(self.host, "room_contacts_chooser") and self.host.room_contacts_chooser is not None: + # self.host.room_contacts_chooser.resetContacts() + + # self.updateVisibility([jid_s], self.getContactGroups(jid_s)) + + def setContactMessageWaiting(self, jid, waiting): + """Show an visual indicator that contact has send a message + @param jid: jid of the contact + @param waiting: True if message are waiting""" + 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 updateAvatar(self, jid_s, url): + """Update the avatar of the given contact + + @param jid_s (str): contact jid + @param url (str): image url + """ + self._contacts_panel.updateAvatar(jid_s, url) + + def hasVisibleMembers(self, group): + """Tell if the given group actually has visible members + + @param group (str): the group to check + @return: boolean + """ + 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 self.host.getCachedParam('General', C.SHOW_OFFLINE_CONTACTS) == 'true' + + def emtyGroupsToShow(self): + """Tell if empty groups should be visible according to the user settings + + @return: boolean + """ + return self.host.getCachedParam('General', C.SHOW_EMPTY_GROUPS) == 'true' + + def updatePresence(self, entity, show, priority, statuses): + QuickContactList.updatePresence(self, entity, show, priority, statuses) + 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) + + # def updateVisibility(self, jids, groups): + # """Set the widgets visibility for the given contacts and groups + + # @param jids (list[str]): list of JID + # @param groups (list[str]): 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())