changeset 449:981ed669d3b3

/!\ reorganize all the file hierarchy, move the code and launching script to src: - browser_side --> src/browser - public --> src/browser_side/public - libervia.py --> src/browser/libervia_main.py - libervia_server --> src/server - libervia_server/libervia.sh --> src/libervia.sh - twisted --> src/twisted - new module src/common - split constants.py in 3 files: - src/common/constants.py - src/browser/constants.py - src/server/constants.py - output --> html (generated by pyjsbuild during the installation) - new option/parameter "data_dir" (-d) to indicates the directory containing html and server_css - setup.py installs libervia to the following paths: - src/common --> <LIB>/libervia/common - src/server --> <LIB>/libervia/server - src/twisted --> <LIB>/twisted - html --> <SHARE>/libervia/html - server_side --> <SHARE>libervia/server_side - LIBERVIA_INSTALL environment variable takes 2 new options with prompt confirmation: - clean: remove previous installation directories - purge: remove building and previous installation directories You may need to update your sat.conf and/or launching script to update the following options/parameters: - ssl_certificate - data_dir
author souliane <souliane@mailoo.org>
date Tue, 20 May 2014 06:41:16 +0200
parents 14c35f7f1ef5
children 41aae13cab2b
files browser_side/__init__.py browser_side/base_panels.py browser_side/base_widget.py browser_side/card_game.py browser_side/contact.py browser_side/contact_group.py browser_side/dialog.py browser_side/file_tools.py browser_side/html_tools.py browser_side/jid.py browser_side/list_manager.py browser_side/logging.py browser_side/menu.py browser_side/nativedom.py browser_side/notification.py browser_side/panels.py browser_side/plugin_xep_0085.py browser_side/radiocol.py browser_side/register.py browser_side/richtext.py browser_side/xmlui.py constants.py libervia.py libervia_server/__init__.py libervia_server/blog.py libervia_server/html_tools.py libervia_server/libervia.sh public/contrat_social.html public/libervia.css public/libervia.html public/sat_logo_16.png setup.py src/__init__.py src/browser/__init__.py src/browser/base_panels.py src/browser/base_widget.py src/browser/card_game.py src/browser/constants.py src/browser/contact.py src/browser/contact_group.py src/browser/dialog.py src/browser/file_tools.py src/browser/html_tools.py src/browser/jid.py src/browser/libervia_main.py src/browser/list_manager.py src/browser/logging.py src/browser/menu.py src/browser/nativedom.py src/browser/notification.py src/browser/panels.py src/browser/plugin_xep_0085.py src/browser/public/contrat_social.html src/browser/public/libervia.css src/browser/public/libervia.html src/browser/public/sat_logo_16.png src/browser/radiocol.py src/browser/register.py src/browser/richtext.py src/browser/xmlui.py src/common/__init__.py src/common/constants.py src/libervia.sh src/server/__init__.py src/server/blog.py src/server/constants.py src/server/html_tools.py src/server/server.py src/twisted/plugins/libervia_server.py tools/__init__.py twisted/plugins/libervia.py
diffstat 65 files changed, 11796 insertions(+), 11601 deletions(-) [+]
line wrap: on
line diff
--- a/browser_side/base_panels.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,609 +0,0 @@
-#!/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 pyjamas.ui.AbsolutePanel import AbsolutePanel
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.HTMLPanel import HTMLPanel
-from pyjamas.ui.Button import Button
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.SimplePanel import SimplePanel
-from pyjamas.ui.PopupPanel import PopupPanel
-from pyjamas.ui.StackPanel import StackPanel
-from pyjamas.ui.TextArea import TextArea
-from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT
-from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KeyboardHandler
-from pyjamas.ui.FocusListener import FocusHandler
-from pyjamas.ui.ClickListener import ClickHandler
-from pyjamas import DOM
-
-from datetime import datetime
-from time import time
-
-from html_tools import html_sanitize, html_strip, inlineRoot, convertNewLinesToXHTML
-
-from constants import Const as C
-from sat_frontends.tools.strings import addURLToText, addURLToImage
-from sat.core.i18n import _
-
-
-class ChatText(HTMLPanel):
-
-    def __init__(self, timestamp, nick, mymess, msg, xhtml=None):
-        _date = datetime.fromtimestamp(float(timestamp or time()))
-        _msg_class = ["chat_text_msg"]
-        if mymess:
-            _msg_class.append("chat_text_mymess")
-        HTMLPanel.__init__(self, "<span class='chat_text_timestamp'>%(timestamp)s</span> <span class='chat_text_nick'>%(nick)s</span> <span class='%(msg_class)s'>%(msg)s</span>" %
-                           {"timestamp": _date.strftime("%H:%M"),
-                            "nick": "[%s]" % html_sanitize(nick),
-                            "msg_class": ' '.join(_msg_class),
-                            "msg": addURLToText(html_sanitize(msg)) if not xhtml else inlineRoot(xhtml)}  # FIXME: images and external links must be removed according to preferences
-                           )
-        self.setStyleName('chatText')
-
-
-class Occupant(HTML):
-    """Occupant of a MUC room"""
-
-    def __init__(self, nick, state=None, special=""):
-        """
-        @param nick: the user nickname
-        @param state: the user chate state (XEP-0085)
-        @param special: a string of symbols (e.g: for activities)
-        """
-        HTML.__init__(self)
-        self.nick = nick
-        self._state = state
-        self.special = special
-        self._refresh()
-
-    def __str__(self):
-        return self.nick
-
-    def setState(self, state):
-        self._state = state
-        self._refresh()
-
-    def addSpecial(self, special):
-        """@param special: unicode"""
-        if special not in self.special:
-            self.special += special
-            self._refresh()
-
-    def removeSpecials(self, special):
-        """@param special: unicode or list"""
-        if not isinstance(special, list):
-            special = [special]
-        for symbol in special:
-            self.special = self.special.replace(symbol, "")
-            self._refresh()
-
-    def _refresh(self):
-        state = (' %s' % C.MUC_USER_STATES[self._state]) if self._state else ''
-        special = "" if len(self.special) == 0 else " %s" % self.special
-        self.setHTML("<div class='occupant'>%s%s%s</div>" % (html_sanitize(self.nick), special, state))
-
-
-class OccupantsList(AbsolutePanel):
-    """Panel user to show occupants of a room"""
-
-    def __init__(self):
-        AbsolutePanel.__init__(self)
-        self.occupants_list = {}
-        self.setStyleName('occupantsList')
-
-    def addOccupant(self, nick):
-        _occupant = Occupant(nick)
-        self.occupants_list[nick] = _occupant
-        self.add(_occupant)
-
-    def removeOccupant(self, nick):
-        try:
-            self.remove(self.occupants_list[nick])
-        except KeyError:
-            log.error("trying to remove an unexisting nick")
-
-    def clear(self):
-        self.occupants_list.clear()
-        AbsolutePanel.clear(self)
-
-    def updateSpecials(self, occupants=[], html=""):
-        """Set the specified html "symbol" to the listed occupants,
-        and eventually remove it from the others (if they got it).
-        This is used for example to visualize who is playing a game.
-        @param occupants: list of the occupants that need the symbol
-        @param html: unicode symbol (actually one character or more)
-        or a list to assign different symbols of the same family.
-        """
-        index = 0
-        special = html
-        for occupant in self.occupants_list.keys():
-            if occupant in occupants:
-                if isinstance(html, list):
-                    special = html[index]
-                    index = (index + 1) % len(html)
-                self.occupants_list[occupant].addSpecial(special)
-            else:
-                self.occupants_list[occupant].removeSpecials(html)
-
-
-class PopupMenuPanel(PopupPanel):
-    """This implementation of a popup menu (context menu) allow you to assign
-    two special methods which are common to all the items, in order to hide
-    certain items and also easily define their callbacks. The menu can be
-    bound to any of the mouse button (left, middle, right).
-    """
-    def __init__(self, entries, hide=None, callback=None, vertical=True, style=None, **kwargs):
-        """
-        @param entries: a dict of dicts, where each sub-dict is representing
-        one menu item: the sub-dict key can be used as the item text and
-        description, but optional "title" and "desc" entries would be used
-        if they exists. The sub-dicts may be extended later to do
-        more complicated stuff or overwrite the common methods.
-        @param hide: function  with 2 args: widget, key as string and
-        returns True if that item should be hidden from the context menu.
-        @param callback: function with 2 args: sender, key as string
-        @param vertical: True or False, to set the direction
-        @param item_style: alternative CSS class for the menu items
-        @param menu_style: supplementary CSS class for the sender widget
-        """
-        PopupPanel.__init__(self, autoHide=True, **kwargs)
-        self._entries = entries
-        self._hide = hide
-        self._callback = callback
-        self.vertical = vertical
-        self.style = {"selected": None, "menu": "recipientTypeMenu", "item": "popupMenuItem"}
-        if isinstance(style, dict):
-            self.style.update(style)
-        self._senders = {}
-
-    def _show(self, sender):
-        """Popup the menu relative to this sender's position.
-        @param sender: the widget that has been clicked
-        """
-        menu = VerticalPanel() if self.vertical is True else HorizontalPanel()
-        menu.setStyleName(self.style["menu"])
-
-        def button_cb(item):
-            """You can not put that method in the loop and rely
-            on _key, because it is overwritten by each step.
-            You can rely on item.key instead, which is copied
-            from _key after the item creation.
-            @param item: the menu item that has been clicked
-            """
-            if self._callback is not None:
-                self._callback(sender=sender, key=item.key)
-            self.hide(autoClosed=True)
-
-        for _key in self._entries.keys():
-            entry = self._entries[_key]
-            if self._hide is not None and self._hide(sender=sender, key=_key) is True:
-                continue
-            title = entry["title"] if "title" in entry.keys() else _key
-            item = Button(title, button_cb)
-            item.key = _key
-            item.setStyleName(self.style["item"])
-            item.setTitle(entry["desc"] if "desc" in entry.keys() else title)
-            menu.add(item)
-        if len(menu.getChildren()) == 0:
-            return
-        self.add(menu)
-        if self.vertical is True:
-            x = sender.getAbsoluteLeft() + sender.getOffsetWidth()
-            y = sender.getAbsoluteTop()
-        else:
-            x = sender.getAbsoluteLeft()
-            y = sender.getAbsoluteTop() + sender.getOffsetHeight()
-        self.setPopupPosition(x, y)
-        self.show()
-        if self.style["selected"]:
-            sender.addStyleDependentName(self.style["selected"])
-
-        def _onHide(popup):
-            if self.style["selected"]:
-                sender.removeStyleDependentName(self.style["selected"])
-            return PopupPanel.onHideImpl(self, popup)
-
-        self.onHideImpl = _onHide
-
-    def registerClickSender(self, sender, button=BUTTON_LEFT):
-        """Bind the menu to the specified sender.
-        @param sender: the widget to which the menu should be bound
-        @param: BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT
-        """
-        self._senders.setdefault(sender, [])
-        self._senders[sender].append(button)
-
-        if button == BUTTON_RIGHT:
-            # WARNING: to disable the context menu is a bit tricky...
-            # The following seems to work on Firefox 24.0, but:
-            # TODO: find a cleaner way to disable the context menu
-            sender.getElement().setAttribute("oncontextmenu", "return false")
-
-        def _onBrowserEvent(event):
-            button = DOM.eventGetButton(event)
-            if DOM.eventGetType(event) == "mousedown" and button in self._senders[sender]:
-                self._show(sender)
-            return sender.__class__.onBrowserEvent(sender, event)
-
-        sender.onBrowserEvent = _onBrowserEvent
-
-    def registerMiddleClickSender(self, sender):
-        self.registerClickSender(sender, BUTTON_MIDDLE)
-
-    def registerRightClickSender(self, sender):
-        self.registerClickSender(sender, BUTTON_RIGHT)
-
-
-class ToggleStackPanel(StackPanel):
-    """This is a pyjamas.ui.StackPanel with modified behavior. All sub-panels ca be
-    visible at the same time, clicking a sub-panel header will not display it and hide
-    the others but only toggle its own visibility. The argument 'visibleStack' is ignored.
-    Note that the argument 'visible' has been added to listener's 'onStackChanged' method.
-    """
-
-    def __init__(self, **kwargs):
-        StackPanel.__init__(self, **kwargs)
-
-    def onBrowserEvent(self, event):
-        if DOM.eventGetType(event) == "click":
-            index = self.getDividerIndex(DOM.eventGetTarget(event))
-            if index != -1:
-                self.toggleStack(index)
-
-    def add(self, widget, stackText="", asHTML=False, visible=False):
-        StackPanel.add(self, widget, stackText, asHTML)
-        self.setStackVisible(self.getWidgetCount() - 1, visible)
-
-    def toggleStack(self, index):
-        if index >= self.getWidgetCount():
-            return
-        visible = not self.getWidget(index).getVisible()
-        self.setStackVisible(index, visible)
-        for listener in self.stackListeners:
-            listener.onStackChanged(self, index, visible)
-
-
-class TitlePanel(ToggleStackPanel):
-    """A toggle panel to set the message title"""
-    def __init__(self):
-        ToggleStackPanel.__init__(self, Width="100%")
-        self.text_area = TextArea()
-        self.add(self.text_area, _("Title"))
-        self.addStackChangeListener(self)
-
-    def onStackChanged(self, sender, index, visible=None):
-        if visible is None:
-            visible = sender.getWidget(index).getVisible()
-        text = self.text_area.getText()
-        suffix = "" if (visible or not text) else (": %s" % text)
-        sender.setStackText(index, _("Title") + suffix)
-
-    def getText(self):
-        return self.text_area.getText()
-
-    def setText(self, text):
-        self.text_area.setText(text)
-
-
-class BaseTextEditor(object):
-    """Basic definition of a text editor. The method edit gets a boolean parameter which
-    should be set to True when you want to edit the text and False to only display it."""
-
-    def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None):
-        """
-        Remark when inheriting this class: since the setContent method could be
-        overwritten by the child class, you should consider calling this __init__
-        after all the parameters affecting this setContent method have been set.
-        @param content: dict with at least a 'text' key
-        @param strproc: method to be applied on strings to clean the content
-        @param modifiedCb: method to be called when the text has been modified.
-        If this method returns:
-        - True: the modification will be saved and afterEditCb called;
-        - False: the modification won't be saved and afterEditCb called;
-        - None: the modification won't be saved and afterEditCb not called.
-        @param afterEditCb: method to be called when the edition is done
-        """
-        if content is None:
-            content = {'text': ''}
-        assert('text' in content)
-        if strproc is None:
-            def strproc(text):
-                try:
-                    return text.strip()
-                except (TypeError, AttributeError):
-                    return text
-        self.strproc = strproc
-        self.__modifiedCb = modifiedCb
-        self._afterEditCb = afterEditCb
-        self.initialized = False
-        self.edit_listeners = []
-        self.setContent(content)
-
-    def setContent(self, content=None):
-        """Set the editable content. The displayed content, which is set from the child class, could differ.
-        @param content: dict with at least a 'text' key
-        """
-        if content is None:
-            content = {'text': ''}
-        elif not isinstance(content, dict):
-            content = {'text': content}
-        assert('text' in content)
-        self._original_content = {}
-        for key in content:
-            self._original_content[key] = self.strproc(content[key])
-
-    def getContent(self):
-        """Get the current edited or editable content.
-        @return: dict with at least a 'text' key
-        """
-        raise NotImplementedError
-
-    def setOriginalContent(self, content):
-        """Use this method with care! Content initialization should normally be
-        done with self.setContent. This method exists to let you trick the editor,
-        e.g. for self.modified to return True also when nothing has been modified.
-        @param content: dict
-        """
-        self._original_content = content
-
-    def getOriginalContent(self):
-        """
-        @return the original content before modification (dict)
-        """
-        return self._original_content
-
-    def modified(self, content=None):
-        """Check if the content has been modified.
-        Remark: we don't use the direct comparison because we want to ignore empty elements
-        @content: content to be check against the original content or None to use the current content
-        @return: True if the content has been modified.
-        """
-        if content is None:
-            content = self.getContent()
-        # the following method returns True if one non empty element exists in a but not in b
-        diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != []
-        # the following method returns True if the values for the common keys are not equals
-        diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != []
-        # finally the combination of both to return True if a difference is found
-        diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b)
-
-        return diff(content, self._original_content)
-
-    def edit(self, edit, abort=False, sync=False):
-        """
-        Remark: the editor must be visible before you call this method.
-        @param edit: set to True to edit the content or False to only display it
-        @param abort: set to True to cancel the edition and loose the changes.
-        If edit and abort are both True, self.abortEdition can be used to ask for a
-        confirmation. When edit is False and abort is True, abortion is actually done.
-        @param sync: set to True to cancel the edition after the content has been saved somewhere else
-        """
-        if edit:
-            if not self.initialized:
-                self.syncToEditor()  # e.g.: use the selected target and unibox content
-            self.setFocus(True)
-            if abort:
-                content = self.getContent()
-                if not self.modified(content) or self.abortEdition(content):  # e.g: ask for confirmation
-                    self.edit(False, True, sync)
-                    return
-            if sync:
-                self.syncFromEditor(content)  # e.g.: save the content to unibox
-                return
-        else:
-            if not self.initialized:
-                return
-            content = self.getContent()
-            if abort:
-                self._afterEditCb(content)
-                return
-            if self.__modifiedCb and self.modified(content):
-                result = self.__modifiedCb(content)  # e.g.: send a message or update something
-                if result is not None:
-                    if self._afterEditCb:
-                        self._afterEditCb(content)  # e.g.: restore the display mode
-                    if result is True:
-                        self.setContent(content)
-            elif self._afterEditCb:
-                self._afterEditCb(content)
-
-        self.initialized = True
-
-    def setFocus(self, focus):
-        """
-        @param focus: set to True to focus the editor
-        """
-        raise NotImplementedError
-
-    def syncToEditor(self):
-        pass
-
-    def syncFromEditor(self, content):
-        pass
-
-    def abortEdition(self, content):
-        return True
-
-    def addEditListener(self, listener):
-        """Add a method to be called whenever the text is edited.
-        @param listener: method taking two arguments: sender, keycode"""
-        self.edit_listeners.append(listener)
-
-
-class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler):
-    """Base class for manage a simple text editor."""
-
-    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
-        """
-        @param content
-        @param modifiedCb
-        @param afterEditCb
-        @param options: dict with the following value:
-        - no_xhtml: set to True to clean any xhtml content.
-        - enhance_display: if True, the display text will be enhanced with addURLToText
-        - listen_keyboard: set to True to terminate the edition with <enter> or <escape>.
-        - listen_focus: set to True to terminate the edition when the focus is lost.
-        - listen_click: set to True to start the edition when you click on the widget.
-        """
-        self.options = {'no_xhtml': False,
-                        'enhance_display': True,
-                        'listen_keyboard': True,
-                        'listen_focus': False,
-                        'listen_click': False
-                        }
-        if options:
-            self.options.update(options)
-        self.__shift_down = False
-        if self.options['listen_focus']:
-            FocusHandler.__init__(self)
-        if self.options['listen_click']:
-            ClickHandler.__init__(self)
-        KeyboardHandler.__init__(self)
-        strproc = lambda text: html_sanitize(html_strip(text)) if self.options['no_xhtml'] else html_strip(text)
-        BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb)
-        self.textarea = self.display = None
-
-    def setContent(self, content=None):
-        BaseTextEditor.setContent(self, content)
-
-    def getContent(self):
-        raise NotImplementedError
-
-    def edit(self, edit, abort=False, sync=False):
-        BaseTextEditor.edit(self, edit)
-        if edit:
-            if self.options['listen_focus'] and self not in self.textarea._focusListeners:
-                self.textarea.addFocusListener(self)
-            if self.options['listen_click']:
-                self.display.clearClickListener()
-            if self not in self.textarea._keyboardListeners:
-                self.textarea.addKeyboardListener(self)
-        else:
-            self.setDisplayContent()
-            if self.options['listen_focus']:
-                try:
-                    self.textarea.removeFocusListener(self)
-                except ValueError:
-                    pass
-            if self.options['listen_click'] and self not in self.display._clickListeners:
-                self.display.addClickListener(self)
-            try:
-                self.textarea.removeKeyboardListener(self)
-            except ValueError:
-                pass
-
-    def setDisplayContent(self):
-        text = self._original_content['text']
-        if not self.options['no_xhtml']:
-            text = addURLToImage(text)
-        if self.options['enhance_display']:
-            text = addURLToText(text)
-        self.display.setHTML(convertNewLinesToXHTML(text))
-
-    def setFocus(self, focus):
-        raise NotImplementedError
-
-    def onKeyDown(self, sender, keycode, modifiers):
-        for listener in self.edit_listeners:
-            listener(self.textarea, keycode)
-        if not self.options['listen_keyboard']:
-            return
-        if keycode == KEY_SHIFT or self.__shift_down:  # allow input a new line with <shift> + <enter>
-            self.__shift_down = True
-            return
-        if keycode == KEY_ENTER:  # finish the edition
-            self.textarea.setFocus(False)
-            if not self.options['listen_focus']:
-                self.edit(False)
-
-    def onKeyUp(self, sender, keycode, modifiers):
-        if keycode == KEY_SHIFT:
-            self.__shift_down = False
-
-    def onLostFocus(self, sender):
-        """Finish the edition when focus is lost"""
-        if self.options['listen_focus']:
-            self.edit(False)
-
-    def onClick(self, sender=None):
-        """Start the edition when the widget is clicked"""
-        if self.options['listen_click']:
-            self.edit(True)
-
-    def onBrowserEvent(self, event):
-        if self.options['listen_focus']:
-            FocusHandler.onBrowserEvent(self, event)
-        if self.options['listen_click']:
-            ClickHandler.onBrowserEvent(self, event)
-        KeyboardHandler.onBrowserEvent(self, event)
-
-
-class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler):
-    """Manage a simple text editor with the HTML 5 "contenteditable" property."""
-
-    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
-        HTML.__init__(self)
-        SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
-        self.textarea = self.display = self
-
-    def getContent(self):
-        text = DOM.getInnerHTML(self.getElement())
-        return {'text': self.strproc(text) if text else ''}
-
-    def edit(self, edit, abort=False, sync=False):
-        if edit:
-            self.textarea.setHTML(self._original_content['text'])
-        self.getElement().setAttribute('contenteditable', 'true' if edit else 'false')
-        SimpleTextEditor.edit(self, edit, abort, sync)
-
-    def setFocus(self, focus):
-        if focus:
-            self.getElement().focus()
-        else:
-            self.getElement().blur()
-
-
-class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler):
-    """Manage a simple text editor with a TextArea for editing, HTML for display."""
-
-    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
-        SimplePanel.__init__(self)
-        SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
-        self.textarea = TextArea()
-        self.display = HTML()
-
-    def getContent(self):
-        text = self.textarea.getText()
-        return {'text': self.strproc(text) if text else ''}
-
-    def edit(self, edit, abort=False, sync=False):
-        if edit:
-            self.textarea.setText(self._original_content['text'])
-        self.setWidget(self.textarea if edit else self.display)
-        SimpleTextEditor.edit(self, edit, abort, sync)
-
-    def setFocus(self, focus):
-        if focus:
-            self.textarea.setCursorPos(len(self.textarea.getText()))
-        self.textarea.setFocus(focus)
--- a/browser_side/base_widget.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,730 +0,0 @@
-#!/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 pyjamas.ui.SimplePanel import SimplePanel
-from pyjamas.ui.AbsolutePanel import AbsolutePanel
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.ScrollPanel import ScrollPanel
-from pyjamas.ui.FlexTable import FlexTable
-from pyjamas.ui.TabPanel import TabPanel
-from pyjamas.ui.HTMLPanel import HTMLPanel
-from pyjamas.ui.Label import Label
-from pyjamas.ui.Button import Button
-from pyjamas.ui.Image import Image
-from pyjamas.ui.Widget import Widget
-from pyjamas.ui.DragWidget import DragWidget
-from pyjamas.ui.DropWidget import DropWidget
-from pyjamas.ui.ClickListener import ClickHandler
-from pyjamas.ui import HasAlignment
-from pyjamas import DOM
-from pyjamas import Window
-from __pyjamas__ import doc
-
-from browser_side import dialog
-
-
-class DragLabel(DragWidget):
-
-    def __init__(self, text, _type):
-        DragWidget.__init__(self)
-        self._text = text
-        self._type = _type
-
-    def onDragStart(self, event):
-        dt = event.dataTransfer
-        dt.setData('text/plain', "%s\n%s" % (self._text, self._type))
-        dt.setDragImage(self.getElement(), 15, 15)
-
-
-class LiberviaDragWidget(DragLabel):
-    """ A DragLabel which keep the widget being dragged as class value """
-    current = None  # widget currently dragged
-
-    def __init__(self, text, _type, widget):
-        DragLabel.__init__(self, text, _type)
-        self.widget = widget
-
-    def onDragStart(self, event):
-        LiberviaDragWidget.current = self.widget
-        DragLabel.onDragStart(self, event)
-
-    def onDragEnd(self, event):
-        LiberviaDragWidget.current = None
-
-
-class DropCell(DropWidget):
-    """Cell in the middle grid which replace itself with the dropped widget on DnD"""
-    drop_keys = {}
-
-    def __init__(self, host):
-        DropWidget.__init__(self)
-        self.host = host
-        self.setStyleName('dropCell')
-
-    @classmethod
-    def addDropKey(cls, key, callback):
-        DropCell.drop_keys[key] = callback
-
-    def onDragEnter(self, event):
-        if self == LiberviaDragWidget.current:
-            return
-        self.addStyleName('dragover')
-        DOM.eventPreventDefault(event)
-
-    def onDragLeave(self, event):
-        if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\
-            event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1:
-            # We check that we are inside widget's box, and we don't remove the style in this case because
-            # if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we
-            # don't want that
-            self.removeStyleName('dragover')
-
-    def onDragOver(self, event):
-        DOM.eventPreventDefault(event)
-
-    def _getCellAndRow(self, grid, event):
-        """Return cell and row index where the event is occuring"""
-        cell = grid.getEventTargetCell(event)
-        row = DOM.getParent(cell)
-        return (row.rowIndex, cell.cellIndex)
-
-    def onDrop(self, event):
-        self.removeStyleName('dragover')
-        DOM.eventPreventDefault(event)
-        dt = event.dataTransfer
-        # 'text', 'text/plain', and 'Text' are equivalent.
-        try:
-            item, item_type = dt.getData("text/plain").split('\n')  # Workaround for webkit, only text/plain seems to be managed
-            if item_type and item_type[-1] == '\0':  # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
-                item_type = item_type[:-1]           # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
-            # item_type = dt.getData("type")
-            log.debug("message: %s" % item)
-            log.debug("type: %s" % item_type)
-        except:
-            log.debug("no message found")
-            item = '&nbsp;'
-            item_type = None
-        if item_type == "WIDGET":
-            if not LiberviaDragWidget.current:
-                log.error("No widget registered in LiberviaDragWidget !")
-                return
-            _new_panel = LiberviaDragWidget.current
-            if self == _new_panel:  # We can't drop on ourself
-                return
-            # we need to remove the widget from the panel as it will be inserted elsewhere
-            widgets_panel = _new_panel.getWidgetsPanel()
-            wid_row = widgets_panel.getWidgetCoords(_new_panel)[0]
-            row_wids = widgets_panel.getLiberviaRowWidgets(wid_row)
-            if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]:
-                # the dropped widget is the only one in the same row
-                # as the target widget (self), we don't do anything
-                return
-            widgets_panel.removeWidget(_new_panel)
-        elif item_type in self.drop_keys:
-            _new_panel = self.drop_keys[item_type](self.host, item)
-        else:
-            log.warning("unmanaged item type")
-            return
-        if isinstance(self, LiberviaWidget):
-            self.host.unregisterWidget(self)
-            self.onQuit()
-            if not isinstance(_new_panel, LiberviaWidget):
-                log.warning("droping an object which is not a class of LiberviaWidget")
-        _flextable = self.getParent()
-        _widgetspanel = _flextable.getParent().getParent()
-        row_idx, cell_idx = self._getCellAndRow(_flextable, event)
-        if self.host.getSelected == self:
-            self.host.setSelected(None)
-        _widgetspanel.changeWidget(row_idx, cell_idx, _new_panel)
-        """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable))
-        _width = 90/float(len(_unempty_panels) or 1)
-        #now we resize all the cell of the column
-        for panel in _unempty_panels:
-            td_elt = panel.getElement().parentNode
-            DOM.setStyleAttribute(td_elt, "width", "%s%%" % _width)"""
-        #FIXME: delete object ? Check the right way with pyjamas
-
-
-class WidgetHeader(AbsolutePanel, LiberviaDragWidget):
-
-    def __init__(self, parent, title):
-        AbsolutePanel.__init__(self)
-        self.add(title)
-        button_group_wrapper = SimplePanel()
-        button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper')
-        button_group = HorizontalPanel()
-        button_group.setStyleName('widgetHeader_buttonGroup')
-        setting_button = Image("media/icons/misc/settings.png")
-        setting_button.setStyleName('widgetHeader_settingButton')
-        setting_button.addClickListener(parent.onSetting)
-        close_button = Image("media/icons/misc/close.png")
-        close_button.setStyleName('widgetHeader_closeButton')
-        close_button.addClickListener(parent.onClose)
-        button_group.add(setting_button)
-        button_group.add(close_button)
-        button_group_wrapper.setWidget(button_group)
-        self.add(button_group_wrapper)
-        self.addStyleName('widgetHeader')
-        LiberviaDragWidget.__init__(self, "", "WIDGET", parent)
-
-
-class LiberviaWidget(DropCell, VerticalPanel, ClickHandler):
-    """Libervia's widget which can replace itself with a dropped widget on DnD"""
-
-    def __init__(self, host, title='', selectable=False):
-        """Init the widget
-        @param host: SatWebFrontend object
-        @param title: title show in the header of the widget
-        @param selectable: True is widget can be selected by user"""
-        VerticalPanel.__init__(self)
-        DropCell.__init__(self, host)
-        ClickHandler.__init__(self)
-        self.__selectable = selectable
-        self.__title_id = HTMLPanel.createUniqueId()
-        self.__setting_button_id = HTMLPanel.createUniqueId()
-        self.__close_button_id = HTMLPanel.createUniqueId()
-        self.__title = Label(title)
-        self.__title.setStyleName('widgetHeader_title')
-        self._close_listeners = []
-        header = WidgetHeader(self, self.__title)
-        self.add(header)
-        self.setSize('100%', '100%')
-        self.addStyleName('widget')
-        if self.__selectable:
-            self.addClickListener(self)
-
-            def onClose(sender):
-                """Check dynamically if the unibox is enable or not"""
-                if self.host.uni_box:
-                    self.host.uni_box.onWidgetClosed(sender)
-
-            self.addCloseListener(onClose)
-        self.host.registerWidget(self)
-
-    def getDebugName(self):
-        return "%s (%s)" % (self, self.__title.getText())
-
-    def getWidgetsPanel(self, verbose=True):
-        return self.getParent(WidgetsPanel, verbose)
-
-    def getParent(self, class_=None, verbose=True):
-        """
-        Note: this method overrides pyjamas.ui.Widget.getParent
-        @param class_: class of the ancestor to look for or None to return the first parent
-        @param verbose: set to True to log error messages # FIXME: must be removed
-        @return: the parent/ancestor or None if it has not been found
-        """
-        current = Widget.getParent(self)
-        if class_ is None:
-            return current  # this is the default behavior
-        while current is not None and not isinstance(current, class_):
-            current = Widget.getParent(current)
-        if current is None and verbose:
-            log.debug("Can't find parent %s for %s" % (class_, self))
-        return current
-
-    def onClick(self, sender):
-        self.host.setSelected(self)
-
-    def onClose(self, sender):
-        """ Called when the close button is pushed """
-        _widgetspanel = self.getWidgetsPanel()
-        _widgetspanel.removeWidget(self)
-        for callback in self._close_listeners:
-            callback(self)
-        self.onQuit()
-
-    def onQuit(self):
-        """ Called when the widget is actually ending """
-        pass
-
-    def addCloseListener(self, callback):
-        """Add a close listener to this widget
-        @param callback: function to be called from self.onClose"""
-        self._close_listeners.append(callback)
-
-    def refresh(self):
-        """This can be overwritten by a child class to refresh the display when,
-        instead of creating a new one, an existing widget is found and reused.
-        """
-        pass
-
-    def onSetting(self, sender):
-        widpanel = self.getWidgetsPanel()
-        row, col = widpanel.getIndex(self)
-        body = VerticalPanel()
-
-        # colspan & rowspan
-        colspan = widpanel.getColSpan(row, col)
-        rowspan = widpanel.getRowSpan(row, col)
-
-        def onColSpanChange(value):
-            widpanel.setColSpan(row, col, value)
-
-        def onRowSpanChange(value):
-            widpanel.setRowSpan(row, col, value)
-        colspan_setter = dialog.IntSetter("Columns span", colspan)
-        colspan_setter.addValueChangeListener(onColSpanChange)
-        colspan_setter.setWidth('100%')
-        rowspan_setter = dialog.IntSetter("Rows span", rowspan)
-        rowspan_setter.addValueChangeListener(onRowSpanChange)
-        rowspan_setter.setWidth('100%')
-        body.add(colspan_setter)
-        body.add(rowspan_setter)
-
-        # size
-        width_str = self.getWidth()
-        if width_str.endswith('px'):
-            width = int(width_str[:-2])
-        else:
-            width = 0
-        height_str = self.getHeight()
-        if height_str.endswith('px'):
-            height = int(height_str[:-2])
-        else:
-            height = 0
-
-        def onWidthChange(value):
-            if not value:
-                self.setWidth('100%')
-            else:
-                self.setWidth('%dpx' % value)
-
-        def onHeightChange(value):
-            if not value:
-                self.setHeight('100%')
-            else:
-                self.setHeight('%dpx' % value)
-        width_setter = dialog.IntSetter("width (0=auto)", width)
-        width_setter.addValueChangeListener(onWidthChange)
-        width_setter.setWidth('100%')
-        height_setter = dialog.IntSetter("height (0=auto)", height)
-        height_setter.addValueChangeListener(onHeightChange)
-        height_setter.setHeight('100%')
-        body.add(width_setter)
-        body.add(height_setter)
-
-        # reset
-        def onReset(sender):
-            colspan_setter.setValue(1)
-            rowspan_setter.setValue(1)
-            width_setter.setValue(0)
-            height_setter.setValue(0)
-
-        reset_bt = Button("Reset", onReset)
-        body.add(reset_bt)
-        body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER)
-
-        _dialog = dialog.GenericDialog("Widget setting", body)
-        _dialog.show()
-
-    def setTitle(self, text):
-        """change the title in the header of the widget
-        @param text: text of the new title"""
-        self.__title.setText(text)
-
-    def isSelectable(self):
-        return self.__selectable
-
-    def setSelectable(self, selectable):
-        if not self.__selectable:
-            try:
-                self.removeClickListener(self)
-            except ValueError:
-                pass
-        if self.selectable and not self in self._clickListeners:
-            self.addClickListener(self)
-        self.__selectable = selectable
-
-    def getWarningData(self):
-        """ Return exposition warning level when this widget is selected and something is sent to it
-        This method should be overriden by children
-        @return: tuple (warning level type/HTML msg). Type can be one of:
-            - PUBLIC
-            - GROUP
-            - ONE2ONE
-            - MISC
-            - NONE
-        """
-        if not self.__selectable:
-            log.error("getWarningLevel must not be called for an unselectable widget")
-            raise Exception
-        # TODO: cleaner warning types (more general constants)
-        return ("NONE", None)
-
-    def setWidget(self, widget, scrollable=True):
-        """Set the widget that will be in the body of the LiberviaWidget
-        @param widget: widget to put in the body
-        @param scrollable: if true, the widget will be in a ScrollPanelWrapper"""
-        if scrollable:
-            _scrollpanelwrapper = ScrollPanelWrapper()
-            _scrollpanelwrapper.setStyleName('widgetBody')
-            _scrollpanelwrapper.setWidget(widget)
-            body_wid = _scrollpanelwrapper
-        else:
-            body_wid = widget
-        self.add(body_wid)
-        self.setCellHeight(body_wid, '100%')
-
-    def doDetachChildren(self):
-        # We need to force the use of a panel subclass method here,
-        # for the same reason as doAttachChildren
-        VerticalPanel.doDetachChildren(self)
-
-    def doAttachChildren(self):
-        # We need to force the use of a panel subclass method here, else
-        # the event will not propagate to children
-        VerticalPanel.doAttachChildren(self)
-
-    def matchEntity(self, entity):
-        """This method should be overwritten by child classes."""
-        raise NotImplementedError
-
-
-class ScrollPanelWrapper(SimplePanel):
-    """Scroll Panel like component, wich use the full available space
-    to work around percent size issue, it use some of the ideas found
-    here: http://code.google.com/p/google-web-toolkit/issues/detail?id=316
-    specially in code given at comment #46, thanks to Stefan Bachert"""
-
-    def __init__(self, *args, **kwargs):
-        SimplePanel.__init__(self)
-        self.spanel = ScrollPanel(*args, **kwargs)
-        SimplePanel.setWidget(self, self.spanel)
-        DOM.setStyleAttribute(self.getElement(), "position", "relative")
-        DOM.setStyleAttribute(self.getElement(), "top", "0px")
-        DOM.setStyleAttribute(self.getElement(), "left", "0px")
-        DOM.setStyleAttribute(self.getElement(), "width", "100%")
-        DOM.setStyleAttribute(self.getElement(), "height", "100%")
-        DOM.setStyleAttribute(self.spanel.getElement(), "position", "absolute")
-        DOM.setStyleAttribute(self.spanel.getElement(), "width", "100%")
-        DOM.setStyleAttribute(self.spanel.getElement(), "height", "100%")
-
-    def setWidget(self, widget):
-        self.spanel.setWidget(widget)
-
-    def setScrollPosition(self, position):
-        self.spanel.setScrollPosition(position)
-
-    def scrollToBottom(self):
-        self.setScrollPosition(self.spanel.getElement().scrollHeight)
-
-
-class EmptyWidget(DropCell, SimplePanel):
-    """Empty dropable panel"""
-
-    def __init__(self, host):
-        SimplePanel.__init__(self)
-        DropCell.__init__(self, host)
-        #self.setWidget(HTML(''))
-        self.setSize('100%', '100%')
-
-
-class BorderWidget(EmptyWidget):
-    def __init__(self, host):
-        EmptyWidget.__init__(self, host)
-        self.addStyleName('borderPanel')
-
-
-class LeftBorderWidget(BorderWidget):
-    def __init__(self, host):
-        BorderWidget.__init__(self, host)
-        self.addStyleName('leftBorderWidget')
-
-
-class RightBorderWidget(BorderWidget):
-    def __init__(self, host):
-        BorderWidget.__init__(self, host)
-        self.addStyleName('rightBorderWidget')
-
-
-class BottomBorderWidget(BorderWidget):
-    def __init__(self, host):
-        BorderWidget.__init__(self, host)
-        self.addStyleName('bottomBorderWidget')
-
-
-class WidgetsPanel(ScrollPanelWrapper):
-
-    def __init__(self, host, locked=False):
-        ScrollPanelWrapper.__init__(self)
-        self.setSize('100%', '100%')
-        self.host = host
-        self.locked = locked  # if True: tab will not be removed when there are no more widgets inside
-        self.selected = None
-        self.flextable = FlexTable()
-        self.flextable.setSize('100%', '100%')
-        self.setWidget(self.flextable)
-        self.setStyleName('widgetsPanel')
-        _bottom = BottomBorderWidget(self.host)
-        self.flextable.setWidget(0, 0, _bottom)  # There will be always an Empty widget on the last row,
-                                                 # dropping a widget there will add a new row
-        td_elt = _bottom.getElement().parentNode
-        DOM.setStyleAttribute(td_elt, "height", "1px")  # needed so the cell adapt to the size of the border (specially in webkit)
-        self._max_cols = 1  # give the maximum number of columns i a raw
-
-    def isLocked(self):
-        return self.locked
-
-    def changeWidget(self, row, col, wid):
-        """Change the widget in the given location, add row or columns when necessary"""
-        log.debug("changing widget: %s %s %s" % (wid.getDebugName(), row, col))
-        last_row = max(0, self.flextable.getRowCount() - 1)
-        try:
-            prev_wid = self.flextable.getWidget(row, col)
-        except:
-            log.error("Trying to change an unexisting widget !")
-            return
-
-        cellFormatter = self.flextable.getFlexCellFormatter()
-
-        if isinstance(prev_wid, BorderWidget):
-            # We are on a border, we must create a row and/or columns
-            log.debug("BORDER WIDGET")
-            prev_wid.removeStyleName('dragover')
-
-            if isinstance(prev_wid, BottomBorderWidget):
-                # We are on the bottom border, we create a new row
-                self.flextable.insertRow(last_row)
-                self.flextable.setWidget(last_row, 0, LeftBorderWidget(self.host))
-                self.flextable.setWidget(last_row, 1, wid)
-                self.flextable.setWidget(last_row, 2, RightBorderWidget(self.host))
-                cellFormatter.setHorizontalAlignment(last_row, 2, HasAlignment.ALIGN_RIGHT)
-                row = last_row
-
-            elif isinstance(prev_wid, LeftBorderWidget):
-                if col != 0:
-                    log.error("LeftBorderWidget must be on the first column !")
-                    return
-                self.flextable.insertCell(row, col + 1)
-                self.flextable.setWidget(row, 1, wid)
-
-            elif isinstance(prev_wid, RightBorderWidget):
-                if col != self.flextable.getCellCount(row) - 1:
-                    log.error("RightBorderWidget must be on the last column !")
-                    return
-                self.flextable.insertCell(row, col)
-                self.flextable.setWidget(row, col, wid)
-
-        else:
-            prev_wid.removeFromParent()
-            self.flextable.setWidget(row, col, wid)
-
-        _max_cols = max(self._max_cols, self.flextable.getCellCount(row))
-        if _max_cols != self._max_cols:
-            self._max_cols = _max_cols
-            self._sizesAdjust()
-
-    def _sizesAdjust(self):
-        cellFormatter = self.flextable.getFlexCellFormatter()
-        width = 100.0 / max(1, self._max_cols - 2)  # we don't count the borders
-
-        for row_idx in xrange(self.flextable.getRowCount()):
-            for col_idx in xrange(self.flextable.getCellCount(row_idx)):
-                _widget = self.flextable.getWidget(row_idx, col_idx)
-                if not isinstance(_widget, BorderWidget):
-                    td_elt = _widget.getElement().parentNode
-                    DOM.setStyleAttribute(td_elt, "width", "%.2f%%" % width)
-
-        last_row = max(0, self.flextable.getRowCount() - 1)
-        cellFormatter.setColSpan(last_row, 0, self._max_cols)
-
-    def addWidget(self, wid):
-        """Add a widget to a new cell on the next to last row"""
-        last_row = max(0, self.flextable.getRowCount() - 1)
-        log.debug("putting widget %s at %d, %d" % (wid.getDebugName(), last_row, 0))
-        self.changeWidget(last_row, 0, wid)
-
-    def removeWidget(self, wid):
-        """Remove a widget and the cell where it is"""
-        _row, _col = self.flextable.getIndex(wid)
-        self.flextable.remove(wid)
-        self.flextable.removeCell(_row, _col)
-        if not self.getLiberviaRowWidgets(_row):  # we have no more widgets, we remove the row
-            self.flextable.removeRow(_row)
-        _max_cols = 1
-        for row_idx in xrange(self.flextable.getRowCount()):
-            _max_cols = max(_max_cols, self.flextable.getCellCount(row_idx))
-        if _max_cols != self._max_cols:
-            self._max_cols = _max_cols
-            self._sizesAdjust()
-        current = self
-
-        blank_page = self.getLiberviaWidgetsCount() == 0  # do we still have widgets on the page ?
-
-        if blank_page and not self.isLocked():
-            # we now notice the MainTabPanel that the WidgetsPanel is empty and need to be removed
-            while current is not None:
-                if isinstance(current, MainTabPanel):
-                    current.onWidgetPanelRemove(self)
-                    return
-                current = current.getParent()
-            log.error("no MainTabPanel found !")
-
-    def getWidgetCoords(self, wid):
-        return self.flextable.getIndex(wid)
-
-    def getLiberviaRowWidgets(self, row):
-        """ Return all the LiberviaWidget in the row """
-        return [wid for wid in self.getRowWidgets(row) if isinstance(wid, LiberviaWidget)]
-
-    def getRowWidgets(self, row):
-        """ Return all the widgets in the row """
-        widgets = []
-        cols = self.flextable.getCellCount(row)
-        for col in xrange(cols):
-            widgets.append(self.flextable.getWidget(row, col))
-        return widgets
-
-    def getLiberviaWidgetsCount(self):
-        """ Get count of contained widgets """
-        return len([wid for wid in self.flextable if isinstance(wid, LiberviaWidget)])
-
-    def getIndex(self, wid):
-        return self.flextable.getIndex(wid)
-
-    def getColSpan(self, row, col):
-        cellFormatter = self.flextable.getFlexCellFormatter()
-        return cellFormatter.getColSpan(row, col)
-
-    def setColSpan(self, row, col, value):
-        cellFormatter = self.flextable.getFlexCellFormatter()
-        return cellFormatter.setColSpan(row, col, value)
-
-    def getRowSpan(self, row, col):
-        cellFormatter = self.flextable.getFlexCellFormatter()
-        return cellFormatter.getRowSpan(row, col)
-
-    def setRowSpan(self, row, col, value):
-        cellFormatter = self.flextable.getFlexCellFormatter()
-        return cellFormatter.setRowSpan(row, col, value)
-
-
-class DropTab(Label, DropWidget):
-
-    def __init__(self, tab_panel, text):
-        Label.__init__(self, text)
-        DropWidget.__init__(self, tab_panel)
-        self.tab_panel = tab_panel
-        self.setStyleName('dropCell')
-        self.setWordWrap(False)
-        DOM.setStyleAttribute(self.getElement(), "min-width", "30px")
-
-    def _getIndex(self):
-        """ get current index of the DropTab """
-        # XXX: awful hack, but seems the only way to get index
-        return self.tab_panel.tabBar.panel.getWidgetIndex(self.getParent().getParent()) - 1
-
-    def onDragEnter(self, event):
-        #if self == LiberviaDragWidget.current:
-        #    return
-        self.addStyleName('dragover')
-        DOM.eventPreventDefault(event)
-
-    def onDragLeave(self, event):
-        self.removeStyleName('dragover')
-
-    def onDragOver(self, event):
-        DOM.eventPreventDefault(event)
-
-    def onDrop(self, event):
-        DOM.eventPreventDefault(event)
-        self.removeStyleName('dragover')
-        if self._getIndex() == self.tab_panel.tabBar.getSelectedTab():
-            # the widget come from the DragTab, so nothing to do, we let it there
-            return
-
-        # FIXME: quite the same stuff as in DropCell, need some factorisation
-        dt = event.dataTransfer
-        # 'text', 'text/plain', and 'Text' are equivalent.
-        try:
-            item, item_type = dt.getData("text/plain").split('\n')  # Workaround for webkit, only text/plain seems to be managed
-            if item_type and item_type[-1] == '\0':  # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
-                item_type = item_type[:-1]           # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
-            # item_type = dt.getData("type")
-            log.debug("message: %s" % item)
-            log.debug("type: %s" % item_type)
-        except:
-            log.debug("no message found")
-            item = '&nbsp;'
-            item_type = None
-        if item_type == "WIDGET":
-            if not LiberviaDragWidget.current:
-                log.error("No widget registered in LiberviaDragWidget !")
-                return
-            _new_panel = LiberviaDragWidget.current
-            _new_panel.getWidgetsPanel().removeWidget(_new_panel)
-        elif item_type in DropCell.drop_keys:
-            _new_panel = DropCell.drop_keys[item_type](self.tab_panel.host, item)
-        else:
-            log.warning("unmanaged item type")
-            return
-
-        widgets_panel = self.tab_panel.getWidget(self._getIndex())
-        widgets_panel.addWidget(_new_panel)
-
-
-class MainTabPanel(TabPanel):
-
-    def __init__(self, host):
-        TabPanel.__init__(self)
-        self.host = host
-        self.tabBar.setVisible(False)
-        self.setStyleName('liberviaTabPanel')
-        self.addStyleName('mainTabPanel')
-        Window.addWindowResizeListener(self)
-
-    def getCurrentPanel(self):
-        """ Get the panel of the currently selected tab """
-        return self.deck.visibleWidget
-
-    def onWindowResized(self, width, height):
-        tab_panel_elt = self.getElement()
-        _elts = doc().getElementsByClassName('gwt-TabBar')
-        if not _elts.length:
-            log.error("no TabBar found, it should exist !")
-            tab_bar_h = 0
-        else:
-            tab_bar_h = _elts.item(0).offsetHeight
-        ideal_height = height - DOM.getAbsoluteTop(tab_panel_elt) - tab_bar_h - 5
-        ideal_width = width - DOM.getAbsoluteLeft(tab_panel_elt) - 5
-        self.setWidth("%s%s" % (ideal_width, "px"))
-        self.setHeight("%s%s" % (ideal_height, "px"))
-
-    def add(self, widget, text=''):
-        tab = DropTab(self, text)
-        TabPanel.add(self, widget, tab, False)
-        if self.getWidgetCount() > 1:
-            self.tabBar.setVisible(True)
-            self.host.resize()
-
-    def onWidgetPanelRemove(self, panel):
-        """ Called when a child WidgetsPanel is empty and need to be removed """
-        self.remove(panel)
-        widgets_count = self.getWidgetCount()
-        if widgets_count == 1:
-            self.tabBar.setVisible(False)
-            self.host.resize()
-            self.selectTab(0)
-        else:
-            self.selectTab(widgets_count - 1)
--- a/browser_side/card_game.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,386 +0,0 @@
-#!/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 pyjamas.ui.AbsolutePanel import AbsolutePanel
-from pyjamas.ui.DockPanel import DockPanel
-from pyjamas.ui.SimplePanel import SimplePanel
-from pyjamas.ui.Image import Image
-from pyjamas.ui.Label import Label
-from pyjamas.ui.ClickListener import ClickHandler
-from pyjamas.ui.MouseListener import MouseHandler
-from pyjamas.ui import HasAlignment
-from pyjamas import Window
-from pyjamas import DOM
-
-from dialog import ConfirmDialog, GenericDialog
-from xmlui import XMLUI
-from sat_frontends.tools.games import TarotCard
-from sat.core.i18n import _
-
-
-CARD_WIDTH = 74
-CARD_HEIGHT = 136
-CARD_DELTA_Y = 30
-MIN_WIDTH = 950  # Minimum size of the panel
-MIN_HEIGHT = 500
-
-
-class CardWidget(TarotCard, Image, MouseHandler):
-    """This class is used to represent a card, graphically and logically"""
-
-    def __init__(self, parent, file_):
-        """@param file: path of the PNG file"""
-        self._parent = parent
-        Image.__init__(self, file_)
-        root_name = file_[file_.rfind("/") + 1:-4]
-        suit, value = root_name.split('_')
-        TarotCard.__init__(self, (suit, value))
-        MouseHandler.__init__(self)
-        self.addMouseListener(self)
-
-    def onMouseEnter(self, sender):
-        if self._parent.state == "ecart" or self._parent.state == "play":
-            DOM.setStyleAttribute(self.getElement(), "top", "0px")
-
-    def onMouseLeave(self, sender):
-        if not self in self._parent.hand:
-            return
-        if not self in list(self._parent.selected):  # FIXME: Workaround pyjs bug, must report it
-            DOM.setStyleAttribute(self.getElement(), "top", "%dpx" % CARD_DELTA_Y)
-
-    def onMouseUp(self, sender, x, y):
-        if self._parent.state == "ecart":
-            if self not in list(self._parent.selected):
-                self._parent.addToSelection(self)
-            else:
-                self._parent.removeFromSelection(self)
-        elif self._parent.state == "play":
-            self._parent.playCard(self)
-
-
-class CardPanel(DockPanel, ClickHandler):
-
-    def __init__(self, parent, referee, player_nick, players):
-        DockPanel.__init__(self)
-        ClickHandler.__init__(self)
-        self._parent = parent
-        self._autoplay = None  # XXX: use 0 to activate fake play, None else
-        self.referee = referee
-        self.players = players
-        self.player_nick = player_nick
-        self.bottom_nick = self.player_nick
-        idx = self.players.index(self.player_nick)
-        idx = (idx + 1) % len(self.players)
-        self.right_nick = self.players[idx]
-        idx = (idx + 1) % len(self.players)
-        self.top_nick = self.players[idx]
-        idx = (idx + 1) % len(self.players)
-        self.left_nick = self.players[idx]
-        self.bottom_nick = player_nick
-        self.selected = set()  # Card choosed by the player (e.g. during ecart)
-        self.hand_size = 13  # number of cards in a hand
-        self.hand = []
-        self.to_show = []
-        self.state = None
-        self.setSize("%dpx" % MIN_WIDTH, "%dpx" % MIN_HEIGHT)
-        self.setStyleName("cardPanel")
-
-        # Now we set up the layout
-        _label = Label(self.top_nick)
-        _label.setStyleName('cardGamePlayerNick')
-        self.add(_label, DockPanel.NORTH)
-        self.setCellWidth(_label, '100%')
-        self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_CENTER)
-
-        self.hand_panel = AbsolutePanel()
-        self.add(self.hand_panel, DockPanel.SOUTH)
-        self.setCellWidth(self.hand_panel, '100%')
-        self.setCellHorizontalAlignment(self.hand_panel, HasAlignment.ALIGN_CENTER)
-
-        _label = Label(self.left_nick)
-        _label.setStyleName('cardGamePlayerNick')
-        self.add(_label, DockPanel.WEST)
-        self.setCellHeight(_label, '100%')
-        self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE)
-
-        _label = Label(self.right_nick)
-        _label.setStyleName('cardGamePlayerNick')
-        self.add(_label, DockPanel.EAST)
-        self.setCellHeight(_label, '100%')
-        self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_RIGHT)
-        self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE)
-
-        self.center_panel = DockPanel()
-        self.inner_left = SimplePanel()
-        self.inner_left.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
-        self.center_panel.add(self.inner_left, DockPanel.WEST)
-        self.center_panel.setCellHeight(self.inner_left, '100%')
-        self.center_panel.setCellHorizontalAlignment(self.inner_left, HasAlignment.ALIGN_RIGHT)
-        self.center_panel.setCellVerticalAlignment(self.inner_left, HasAlignment.ALIGN_MIDDLE)
-
-        self.inner_right = SimplePanel()
-        self.inner_right.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
-        self.center_panel.add(self.inner_right, DockPanel.EAST)
-        self.center_panel.setCellHeight(self.inner_right, '100%')
-        self.center_panel.setCellVerticalAlignment(self.inner_right, HasAlignment.ALIGN_MIDDLE)
-
-        self.inner_top = SimplePanel()
-        self.inner_top.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
-        self.center_panel.add(self.inner_top, DockPanel.NORTH)
-        self.center_panel.setCellHorizontalAlignment(self.inner_top, HasAlignment.ALIGN_CENTER)
-        self.center_panel.setCellVerticalAlignment(self.inner_top, HasAlignment.ALIGN_BOTTOM)
-
-        self.inner_bottom = SimplePanel()
-        self.inner_bottom.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
-        self.center_panel.add(self.inner_bottom, DockPanel.SOUTH)
-        self.center_panel.setCellHorizontalAlignment(self.inner_bottom, HasAlignment.ALIGN_CENTER)
-        self.center_panel.setCellVerticalAlignment(self.inner_bottom, HasAlignment.ALIGN_TOP)
-
-        self.inner_center = SimplePanel()
-        self.center_panel.add(self.inner_center, DockPanel.CENTER)
-        self.center_panel.setCellHorizontalAlignment(self.inner_center, HasAlignment.ALIGN_CENTER)
-        self.center_panel.setCellVerticalAlignment(self.inner_center, HasAlignment.ALIGN_MIDDLE)
-
-        self.add(self.center_panel, DockPanel.CENTER)
-        self.setCellWidth(self.center_panel, '100%')
-        self.setCellHeight(self.center_panel, '100%')
-        self.setCellVerticalAlignment(self.center_panel, HasAlignment.ALIGN_MIDDLE)
-        self.setCellHorizontalAlignment(self.center_panel, HasAlignment.ALIGN_CENTER)
-
-        self.loadCards()
-        self.mouse_over_card = None  # contain the card to highlight
-        self.visible_size = CARD_WIDTH / 2  # number of pixels visible for cards
-        self.addClickListener(self)
-
-    def loadCards(self):
-        """Load all the cards in memory"""
-        def _getTarotCardsPathsCb(paths):
-            log.debug("_getTarotCardsPathsCb")
-            for file_ in paths:
-                log.debug("path:", file_)
-                card = CardWidget(self, file_)
-                log.debug("card:", card)
-                self.cards[(card.suit, card.value)] = card
-                self.deck.append(card)
-            self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee)
-        self.cards = {}
-        self.deck = []
-        self.cards["atout"] = {}  # As Tarot is a french game, it's more handy & logical to keep french names
-        self.cards["pique"] = {}  # spade
-        self.cards["coeur"] = {}  # heart
-        self.cards["carreau"] = {}  # diamond
-        self.cards["trefle"] = {}  # club
-        self._parent.host.bridge.call('getTarotCardsPaths', _getTarotCardsPathsCb)
-
-    def onClick(self, sender):
-        if self.state == "chien":
-            self.to_show = []
-            self.state = "wait"
-            self.updateToShow()
-        elif self.state == "wait_for_ecart":
-            self.state = "ecart"
-            self.hand.extend(self.to_show)
-            self.hand.sort()
-            self.to_show = []
-            self.updateToShow()
-            self.updateHand()
-
-    def tarotGameNew(self, hand):
-        """Start a new game, with given hand"""
-        if hand is []:  # reset the display after the scores have been showed
-            self.selected.clear()
-            del self.hand[:]
-            del self.to_show[:]
-            self.state = None
-            #empty hand
-            self.updateHand()
-            #nothing on the table
-            self.updateToShow()
-            for pos in ['top', 'left', 'bottom', 'right']:
-                getattr(self, "inner_%s" % pos).setWidget(None)
-            self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee)
-            return
-        for suit, value in hand:
-            self.hand.append(self.cards[(suit, value)])
-        self.hand.sort()
-        self.state = "init"
-        self.updateHand()
-
-    def updateHand(self):
-        """Show the cards in the hand in the hand_panel (SOUTH panel)"""
-        self.hand_panel.clear()
-        self.hand_panel.setSize("%dpx" % (self.visible_size * (len(self.hand) + 1)), "%dpx" % (CARD_HEIGHT + CARD_DELTA_Y + 10))
-        x_pos = 0
-        y_pos = CARD_DELTA_Y
-        for card in self.hand:
-            self.hand_panel.add(card, x_pos, y_pos)
-            x_pos += self.visible_size
-
-    def updateToShow(self):
-        """Show cards in the center panel"""
-        if not self.to_show:
-            _widget = self.inner_center.getWidget()
-            if _widget:
-                self.inner_center.remove(_widget)
-            return
-        panel = AbsolutePanel()
-        panel.setSize("%dpx" % ((CARD_WIDTH + 5) * len(self.to_show) - 5), "%dpx" % (CARD_HEIGHT))
-        x_pos = 0
-        y_pos = 0
-        for card in self.to_show:
-            panel.add(card, x_pos, y_pos)
-            x_pos += CARD_WIDTH + 5
-        self.inner_center.setWidget(panel)
-
-    def _ecartConfirm(self, confirm):
-        if not confirm:
-            return
-        ecart = []
-        for card in self.selected:
-            ecart.append((card.suit, card.value))
-            self.hand.remove(card)
-        self.selected.clear()
-        self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, ecart)
-        self.state = "wait"
-        self.updateHand()
-
-    def addToSelection(self, card):
-        self.selected.add(card)
-        if len(self.selected) == 6:
-            ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show()
-
-    def tarotGameInvalidCards(self, phase, played_cards, invalid_cards):
-        """Invalid cards have been played
-        @param phase: phase of the game
-        @param played_cards: all the cards played
-        @param invalid_cards: cards which are invalid"""
-
-        if phase == "play":
-            self.state = "play"
-        elif phase == "ecart":
-            self.state = "ecart"
-        else:
-            log.error("INTERNAL ERROR: unmanaged game phase") # FIXME: raise an exception here
-
-        for suit, value in played_cards:
-            self.hand.append(self.cards[(suit, value)])
-
-        self.hand.sort()
-        self.updateHand()
-        if self._autoplay == None:  # No dialog if there is autoplay
-            Window.alert('Cards played are invalid !')
-        self.__fakePlay()
-
-    def removeFromSelection(self, card):
-        self.selected.remove(card)
-        if len(self.selected) == 6:
-            ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show()
-
-    def tarotGameChooseContrat(self, xml_data):
-        """Called when the player has to select his contrat
-        @param xml_data: SàT xml representation of the form"""
-        body = XMLUI(self._parent.host, xml_data, flags=['NO_CANCEL'])
-        _dialog = GenericDialog(_('Please choose your contrat'), body, options=['NO_CLOSE'])
-        body.setCloseCb(_dialog.close)
-        _dialog.show()
-
-    def tarotGameShowCards(self, game_stage, cards, data):
-        """Display cards in the middle of the game (to show for e.g. chien ou poignée)"""
-        self.to_show = []
-        for suit, value in cards:
-            self.to_show.append(self.cards[(suit, value)])
-        self.updateToShow()
-        if game_stage == "chien" and data['attaquant'] == self.player_nick:
-            self.state = "wait_for_ecart"
-        else:
-            self.state = "chien"
-
-    def getPlayerLocation(self, nick):
-        """return player location (top,bottom,left or right)"""
-        for location in ['top', 'left', 'bottom', 'right']:
-            if getattr(self, '%s_nick' % location) == nick:
-                return location
-        log.error("This line should not be reached")
-
-    def tarotGameCardsPlayed(self, player, cards):
-        """A card has been played by player"""
-        if not len(cards):
-            log.warning("cards should not be empty")
-            return
-        if len(cards) > 1:
-            log.error("can't manage several cards played")
-        if self.to_show:
-            self.to_show = []
-            self.updateToShow()
-        suit, value = cards[0]
-        player_pos = self.getPlayerLocation(player)
-        player_panel = getattr(self, "inner_%s" % player_pos)
-
-        if player_panel.getWidget() != None:
-            #We have already cards on the table, we remove them
-            for pos in ['top', 'left', 'bottom', 'right']:
-                getattr(self, "inner_%s" % pos).setWidget(None)
-
-        card = self.cards[(suit, value)]
-        DOM.setElemAttribute(card.getElement(), "style", "")
-        player_panel.setWidget(card)
-
-    def tarotGameYourTurn(self):
-        """Called when we have to play :)"""
-        if self.state == "chien":
-            self.to_show = []
-            self.updateToShow()
-        self.state = "play"
-        self.__fakePlay()
-
-    def __fakePlay(self):
-        """Convenience method for stupid autoplay
-        /!\ don't forgot to comment any interactive dialog for invalid card"""
-        if self._autoplay == None:
-            return
-        if self._autoplay >= len(self.hand):
-            self._autoplay = 0
-        card = self.hand[self._autoplay]
-        self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)])
-        del self.hand[self._autoplay]
-        self.state = "wait"
-        self._autoplay += 1
-
-    def playCard(self, card):
-        self.hand.remove(card)
-        self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)])
-        self.state = "wait"
-        self.updateHand()
-
-    def tarotGameScore(self, xml_data, winners, loosers):
-        """Show score at the end of a round"""
-        if not winners and not loosers:
-            title = "Draw game"
-        else:
-            if self.player_nick in winners:
-                title = "You <b>win</b> !"
-            else:
-                title = "You <b>loose</b> :("
-        body = XMLUI(self._parent.host, xml_data, title=title, flags=['NO_CANCEL'])
-        _dialog = GenericDialog(title, body, options=['NO_CLOSE'])
-        body.setCloseCb(_dialog.close)
-        _dialog.show()
--- a/browser_side/contact.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,414 +0,0 @@
-#!/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 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 jid import JID
-from pyjamas import Window
-from pyjamas import DOM
-
-from browser_side.base_panels import PopupMenuPanel
-from browser_side.base_widget import DragLabel
-from browser_side.panels import ChatPanel, MicroblogPanel, WebPanel, UniBoxPanel
-from browser_side.html_tools import html_sanitize
-from __pyjamas__ import doc
-
-
-def setPresenceStyle(element, presence, base_style="contact"):
-    """
-    Set the CSS style of a contact's element according to its presence.
-    @param item: the UI element of the contact
-    @param presence: a value in ("", "chat", "away", "dnd", "xa").
-    @param base_style: the base name of the style to apply
-    """
-    if not hasattr(element, 'presence_style'):
-        element.presence_style = None
-    style = '%s-%s' % (base_style, presence or 'connected')
-    if style == element.presence_style:
-        return
-    if element.presence_style is not None:
-        element.removeStyleName(element.presence_style)
-    element.addStyleName(style)
-    element.presence_style = style
-
-
-class GroupLabel(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')
-        DragLabel.__init__(self, group, "GROUP")
-        ClickHandler.__init__(self)
-        self.addClickListener(self)
-
-    def onClick(self, sender):
-        self.host.getOrCreateLiberviaWidget(MicroblogPanel, self.group)
-
-
-class ContactLabel(DragLabel, HTML, ClickHandler):
-    def __init__(self, host, jid, name=None, handleClick=True):
-        HTML.__init__(self)
-        self.host = host
-        self.name = name or jid
-        self.waiting = False
-        self.jid = jid
-        self._fill()
-        self.setStyleName('contact')
-        DragLabel.__init__(self, jid, "CONTACT")
-        if handleClick:
-            ClickHandler.__init__(self)
-            self.addClickListener(self)
-
-    def _fill(self):
-        if self.waiting:
-            _wait_html = "<b>(*)</b>&nbsp;"
-        self.setHTML("%(wait)s%(name)s" % {'wait': _wait_html,
-                                           'name': 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._fill()
-
-    def onClick(self, sender):
-        self.host.getOrCreateLiberviaWidget(ChatPanel, self.jid)
-
-
-class GroupList(VerticalPanel):
-
-    def __init__(self, parent):
-        VerticalPanel.__init__(self)
-        self.setStyleName('groupList')
-        self._parent = parent
-
-    def add(self, group):
-        _item = GroupLabel(self._parent.host, group)
-        _item.addMouseListener(self._parent)
-        DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer")
-        index = 0
-        for group_ in [group.group for group in self.getChildren()]:
-            if group_ > group:
-                break
-            index += 1
-        VerticalPanel.insert(self, _item, index)
-
-    def remove(self, group):
-        for wid in self:
-            if isinstance(wid, GroupLabel) and wid.group == group:
-                VerticalPanel.remove(self, wid)
-
-
-class GenericContactList(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, handleClick=False):
-        VerticalPanel.__init__(self)
-        self.host = host
-        self.contacts = []
-        self.handleClick = handleClick
-
-    def add(self, jid, name=None, item_cb=None):
-        if jid in self.contacts:
-            return
-        index = 0
-        for contact_ in self.contacts:
-            if contact_ > jid:
-                break
-            index += 1
-        self.contacts.insert(index, jid)
-        _item = ContactLabel(self.host, jid, name, handleClick=self.handleClick)
-        DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer")
-        VerticalPanel.insert(self, _item, index)
-        if item_cb is not None:
-            item_cb(_item)
-
-    def remove(self, jid):
-        wid = self.getContactLabel(jid)
-        if not wid:
-            return
-        VerticalPanel.remove(self, wid)
-        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 getContactLabel(self, contact_jid):
-        """get contactList widget of a contact
-        @return: ContactLabel item if present, else None"""
-        for wid in self:
-            if isinstance(wid, ContactLabel) and wid.jid == contact_jid:
-                return wid
-        return None
-
-
-class ContactList(GenericContactList):
-    """The contact list that is displayed on the left side."""
-
-    def __init__(self, host):
-        GenericContactList.__init__(self, host, handleClick=True)
-        self.menu_entries = {"blog": {"title": "Public blog..."}}
-        self.context_menu = PopupMenuPanel(entries=self.menu_entries,
-                                           hide=self.contextMenuHide,
-                                           callback=self.contextMenuCallback,
-                                           vertical=False, style={"selected": "menu-selected"})
-
-    def contextMenuHide(self, sender, key):
-        """Return True if the item for that sender should be hidden."""
-        # TODO: enable the blogs of users that are on another server
-        return JID(sender.jid).domain != self.host._defaultDomain
-
-    def contextMenuCallback(self, sender, key):
-        if key == "blog":
-            # TODO: use the bare when all blogs can be retrieved
-            node = JID(sender.jid).node
-            web_panel = WebPanel(self.host, "/blog/%s" % node)
-            self.host.addTab("%s's blog" % node, web_panel)
-        else:
-            sender.onClick(sender)
-
-    def add(self, jid, name=None):
-        def item_cb(item):
-            self.context_menu.registerRightClickSender(item)
-        GenericContactList.add(self, jid, name, item_cb)
-
-    def setState(self, jid, type_, state):
-        """Change the appearance of the contact, according to the state
-        @param jid: jid which need to change state
-        @param type_: one of availability, messageWaiting
-        @param state:
-            - for messageWaiting type:
-                True if message are waiting
-            - for availability type:
-                'unavailable' if not connected, else presence like RFC6121 #4.7.2.1"""
-        _item = self.getContactLabel(jid)
-        if _item:
-            if type_ == 'availability':
-                setPresenceStyle(_item, state)
-            elif type_ == 'messageWaiting':
-                _item.setMessageWaiting(state)
-
-
-class ContactTitleLabel(DragLabel, Label, ClickHandler):
-    def __init__(self, host, text):
-        Label.__init__(self, text) #, Element=DOM.createElement('div')
-        self.host = host
-        self.setStyleName('contactTitle')
-        DragLabel.__init__(self, text, "CONTACT_TITLE")
-        ClickHandler.__init__(self)
-        self.addClickListener(self)
-
-    def onClick(self, sender):
-        self.host.getOrCreateLiberviaWidget(MicroblogPanel, None)
-
-
-class ContactPanel(SimplePanel):
-    """Manage the contacts and groups"""
-
-    def __init__(self, host):
-        SimplePanel.__init__(self)
-
-        self.scroll_panel = ScrollPanel()
-
-        self.host = host
-        self.groups = {}
-        self.connected = {}  # jid connected as key and their status
-
-        self.vPanel = VerticalPanel()
-        _title = ContactTitleLabel(host, 'Contacts')
-        DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer")
-
-        self._contact_list = ContactList(host)
-        self._contact_list.setStyleName('contactList')
-        self._groupList = GroupList(self)
-        self._groupList.setStyleName('groupList')
-
-        self.vPanel.add(_title)
-        self.vPanel.add(self._groupList)
-        self.vPanel.add(self._contact_list)
-        self.scroll_panel.add(self.vPanel)
-        self.add(self.scroll_panel)
-        self.setStyleName('contactBox')
-        Window.addWindowResizeListener(self)
-
-    def onWindowResized(self, width, height):
-        contact_panel_elt = self.getElement()
-        classname = 'widgetsPanel' if isinstance(self.getParent().getParent(), UniBoxPanel) else'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, attributes, groups):
-        """Add a contact to the panel if it doesn't exist, update it else
-        @param jid: jid userhost as unicode
-        @attributes: cf SàT Bridge API's newContact
-        @param groups: list of groups"""
-        _current_groups = self.getContactGroups(jid)
-        _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)
-            if not self.groups[group]:
-                # The group is now empty, we must remove it
-                del self.groups[group]
-                self._groupList.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 not group in self.groups.keys():
-                self.groups[group] = set()
-                self._groupList.add(group)
-                if self.host.uni_box:
-                    self.host.uni_box.addKey(_key % group)
-            self.groups[group].add(jid)
-
-        # We add the contact to contact list, it will check if contact already exists
-        self._contact_list.add(jid)
-
-    def removeContact(self, jid):
-        """Remove contacts from groups where he is and contact list"""
-        self.updateContact(jid, {}, [])  # we remove contact from every group
-        self._contact_list.remove(jid)
-
-    def setConnected(self, jid, resource, availability, priority, statuses):
-        """Set connection status
-        @param jid: JID userhost as unicode
-        """
-        if availability == 'unavailable':
-            if jid in self.connected:
-                if resource in self.connected[jid]:
-                    del self.connected[jid][resource]
-                if not self.connected[jid]:
-                    del self.connected[jid]
-        else:
-            if not jid in self.connected:
-                self.connected[jid] = {}
-            self.connected[jid][resource] = (availability, priority, statuses)
-
-        # check if the contact is connected with another resource, use the one with highest priority
-        if jid in self.connected:
-            max_resource = max_priority = None
-            for tmp_resource in self.connected[jid]:
-                if max_priority is None or self.connected[jid][tmp_resource][1] >= max_priority:
-                    max_resource = tmp_resource
-                    max_priority = self.connected[jid][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][max_resource][0]
-        if jid 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._contact_list.setState(jid, "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()
-
-    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._contact_list.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):
-        """Get groups where contact is
-       @param group: string of single group, or list of string
-       @param contact_jid: jid to test
-        """
-        result = set()
-        for group in self.groups:
-            if self.isContactInGroup(group, contact_jid):
-                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_label in self._contact_list:
-            if contact_jid == _contact_label.jid:
-                return True
-        return False
-
-    def getContacts(self):
-        return self._contact_list.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):
-            for contact in self._contact_list:
-                if contact.jid in self.groups[sender.group]:
-                    contact.addStyleName("selected")
-
-    def onMouseLeave(self, sender):
-        if isinstance(sender, GroupLabel):
-            for contact in self._contact_list:
-                if contact.jid in self.groups[sender.group]:
-                    contact.removeStyleName("selected")
-
--- a/browser_side/contact_group.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,236 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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/>.
-
-from pyjamas.ui.FlexTable import FlexTable
-from pyjamas.ui.DockPanel import DockPanel
-from pyjamas.Timer import Timer
-from pyjamas.ui.Button import Button
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.DialogBox import DialogBox
-from pyjamas.ui import HasAlignment
-from browser_side.dialog import ConfirmDialog, InfoDialog
-from list_manager import ListManager
-from browser_side import dialog
-from browser_side import contact
-
-
-class ContactGroupManager(ListManager):
-    """A manager for sub-panels to assign contacts to each group."""
-
-    def __init__(self, parent, keys_dict, contact_list, offsets, style):
-        ListManager.__init__(self, parent, keys_dict, contact_list, offsets, style)
-        self.registerPopupMenuPanel(entries={"Remove group": {}},
-                                    callback=lambda sender, key: Timer(5, lambda timer: self.removeContactKey(sender, key)))
-
-    def removeContactKey(self, sender, key):
-        key = sender.getText()
-
-        def confirm_cb(answer):
-            if answer:
-                ListManager.removeContactKey(self, key)
-                self._parent.removeKeyFromAddGroupPanel(key)
-
-        _dialog = ConfirmDialog(confirm_cb, text="Do you really want to delete the group '%s'?" % key)
-        _dialog.show()
-
-    def removeFromRemainingList(self, contacts):
-        ListManager.removeFromRemainingList(self, contacts)
-        self._parent.updateContactList(contacts=contacts)
-
-    def addToRemainingList(self, contacts, ignore_key=None):
-        ListManager.addToRemainingList(self, contacts, ignore_key)
-        self._parent.updateContactList(contacts=contacts)
-
-
-class ContactGroupEditor(DockPanel):
-    """Panel for the contact groups manager."""
-
-    def __init__(self, host, parent=None, onCloseCallback=None):
-        DockPanel.__init__(self)
-        self.host = host
-
-        # eventually display in a popup
-        if parent is None:
-            parent = DialogBox(autoHide=False, centered=True)
-            parent.setHTML("Manage contact groups")
-        self._parent = parent
-        self._on_close_callback = onCloseCallback
-        self.all_contacts = self.host.contact_panel.getContacts()
-
-        groups_list = self.host.contact_panel.groups.keys()
-        groups_list.sort()
-
-        self.add_group_panel = self.getAddGroupPanel(groups_list)
-        south_panel = self.getCloseSaveButtons()
-        center_panel = self.getContactGroupManager(groups_list)
-        east_panel = self.getContactList()
-
-        self.add(self.add_group_panel, DockPanel.CENTER)
-        self.add(east_panel, DockPanel.EAST)
-        self.add(center_panel, DockPanel.NORTH)
-        self.add(south_panel, DockPanel.SOUTH)
-
-        self.setCellHorizontalAlignment(center_panel, HasAlignment.ALIGN_LEFT)
-        self.setCellVerticalAlignment(center_panel, HasAlignment.ALIGN_TOP)
-        self.setCellHorizontalAlignment(east_panel, HasAlignment.ALIGN_RIGHT)
-        self.setCellVerticalAlignment(east_panel, HasAlignment.ALIGN_TOP)
-        self.setCellVerticalAlignment(self.add_group_panel, HasAlignment.ALIGN_BOTTOM)
-        self.setCellHorizontalAlignment(self.add_group_panel, HasAlignment.ALIGN_LEFT)
-        self.setCellVerticalAlignment(south_panel, HasAlignment.ALIGN_BOTTOM)
-        self.setCellHorizontalAlignment(south_panel, HasAlignment.ALIGN_CENTER)
-
-        # need to be done after the contact list has been initialized
-        self.groups.setContacts(self.host.contact_panel.groups)
-        self.toggleContacts(showAll=True)
-
-        # Hide the contacts list from the main panel to not confuse the user
-        self.restore_contact_panel = False
-        if self.host.contact_panel.getVisible():
-            self.restore_contact_panel = True
-            self.host.panel._contactsSwitch()
-
-        parent.add(self)
-        parent.setVisible(True)
-        if isinstance(parent, DialogBox):
-            parent.center()
-
-    def getContactGroupManager(self, groups_list):
-        """Set the list manager for the groups"""
-        flex_table = FlexTable(len(groups_list), 2)
-        flex_table.addStyleName('contactGroupEditor')
-        # overwrite the default style which has been set for rich text editor
-        style = {
-           "keyItem": "group",
-           "popupMenuItem": "popupMenuItem",
-           "removeButton": "contactGroupRemoveButton",
-           "buttonCell": "contactGroupButtonCell",
-           "keyPanel": "contactGroupPanel"
-        }
-        self.groups = ContactGroupManager(flex_table, groups_list, self.all_contacts, style=style)
-        self.groups.createWidgets()  # widgets are automatically added to FlexTable
-        # FIXME: clean that part which is dangerous
-        flex_table.updateContactList = self.updateContactList
-        flex_table.removeKeyFromAddGroupPanel = self.add_group_panel.groups.remove
-        return flex_table
-
-    def getAddGroupPanel(self, groups_list):
-        """Add the 'Add group' panel to the FlexTable"""
-
-        def add_group_cb(text):
-            self.groups.addContactKey(text)
-            self.add_group_panel.textbox.setFocus(True)
-
-        add_group_panel = dialog.AddGroupPanel(groups_list, add_group_cb)
-        add_group_panel.addStyleName("addContactGroupPanel")
-        return add_group_panel
-
-    def getCloseSaveButtons(self):
-        """Add the buttons to close the dialog / save the groups"""
-        buttons = HorizontalPanel()
-        buttons.addStyleName("marginAuto")
-        buttons.add(Button("Save", listener=self.closeAndSave))
-        buttons.add(Button("Cancel", listener=self.cancelWithoutSaving))
-        return buttons
-
-    def getContactList(self):
-        """Add the contact list to the DockPanel"""
-        self.toggle = Button("", self.toggleContacts)
-        self.toggle.addStyleName("toggleAssignedContacts")
-        self.contacts = contact.GenericContactList(self.host)
-        for contact_ in self.all_contacts:
-            self.contacts.add(contact_)
-        contact_panel = VerticalPanel()
-        contact_panel.add(self.toggle)
-        contact_panel.add(self.contacts)
-        return contact_panel
-
-    def toggleContacts(self, sender=None, showAll=None):
-        """Callback for the toggle button"""
-        if sender is None:
-            sender = self.toggle
-        sender.showAll = showAll if showAll is not None else not sender.showAll
-        if sender.showAll:
-            sender.setText("Hide assigned")
-        else:
-            sender.setText("Show assigned")
-        self.updateContactList(sender)
-
-    def updateContactList(self, sender=None, contacts=None):
-        """Update the contact list regarding the toggle button"""
-        if not hasattr(self, "toggle") or not hasattr(self.toggle, "showAll"):
-            return
-        sender = self.toggle
-        if contacts is not None:
-            if not isinstance(contacts, list):
-                contacts = [contacts]
-            for contact_ in contacts:
-                if contact_ not in self.all_contacts:
-                    contacts.remove(contact_)
-        else:
-            contacts = self.all_contacts
-        for contact_ in contacts:
-            if sender.showAll:
-                self.contacts.getContactLabel(contact_).setVisible(True)
-            else:
-                if contact_ in self.groups.remaining_list:
-                    self.contacts.getContactLabel(contact_).setVisible(True)
-                else:
-                    self.contacts.getContactLabel(contact_).setVisible(False)
-
-    def __close(self):
-        """Remove the widget from parent or close the popup."""
-        if isinstance(self._parent, DialogBox):
-            self._parent.hide()
-        self._parent.remove(self)
-        if self._on_close_callback is not None:
-            self._on_close_callback()
-        if self.restore_contact_panel:
-            self.host.panel._contactsSwitch()
-
-    def cancelWithoutSaving(self):
-        """Ask for confirmation before closing the dialog."""
-        def confirm_cb(answer):
-            if answer:
-                self.__close()
-
-        _dialog = ConfirmDialog(confirm_cb, text="Do you really want to cancel without saving?")
-        _dialog.show()
-
-    def closeAndSave(self):
-        """Call bridge methods to save the changes and close the dialog"""
-        map_ = {}
-        for contact_ in self.all_contacts:
-            map_[contact_] = set()
-        contacts = self.groups.getContacts()
-        for group in contacts.keys():
-            for contact_ in contacts[group]:
-                try:
-                    map_[contact_].add(group)
-                except KeyError:
-                    InfoDialog("Invalid contact",
-                           "The contact '%s' is not your contact list but it has been assigned to the group '%s'." % (contact_, group) +
-                           "Your changes could not be saved: please check your assignments and save again.", Width="400px").center()
-                    return
-        for contact_ in map_.keys():
-            groups = map_[contact_]
-            current_groups = self.host.contact_panel.getContactGroups(contact_)
-            if groups != current_groups:
-                self.host.bridge.call('updateContact', None, contact_, '', list(groups))
-        self.__close()
--- a/browser_side/dialog.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,546 +0,0 @@
-#!/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/>.
-
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.Grid import Grid
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.PopupPanel import PopupPanel
-from pyjamas.ui.DialogBox import DialogBox
-from pyjamas.ui.ListBox import ListBox
-from pyjamas.ui.Button import Button
-from pyjamas.ui.TextBox import TextBox
-from pyjamas.ui.Label import Label
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.RadioButton import RadioButton
-from pyjamas.ui import HasAlignment
-from pyjamas.ui.KeyboardListener import KEY_ESCAPE, KEY_ENTER
-from pyjamas.ui.MouseListener import MouseWheelHandler
-from pyjamas import Window
-
-from base_panels import ToggleStackPanel
-
-from sat_frontends.tools.misc import DEFAULT_MUC
-
-# List here the patterns that are not allowed in contact group names
-FORBIDDEN_PATTERNS_IN_GROUP = ()
-
-
-class RoomChooser(Grid):
-    """Select a room from the rooms you already joined, or create a new one"""
-
-    GENERATE_MUC = "<use random name>"
-
-    def __init__(self, host, default_room=DEFAULT_MUC):
-        Grid.__init__(self, 2, 2, Width='100%')
-        self.host = host
-
-        self.new_radio = RadioButton("room", "Discussion room:")
-        self.new_radio.setChecked(True)
-        self.box = TextBox(Width='95%')
-        self.box.setText(self.GENERATE_MUC if default_room == "" else default_room)
-        self.exist_radio = RadioButton("room", "Already joined:")
-        self.rooms_list = ListBox(Width='95%')
-
-        self.add(self.new_radio, 0, 0)
-        self.add(self.box, 0, 1)
-        self.add(self.exist_radio, 1, 0)
-        self.add(self.rooms_list, 1, 1)
-
-        self.box.addFocusListener(self)
-        self.rooms_list.addFocusListener(self)
-
-        self.exist_radio.setVisible(False)
-        self.rooms_list.setVisible(False)
-        self.setRooms()
-
-    def onFocus(self, sender):
-        if sender == self.rooms_list:
-            self.exist_radio.setChecked(True)
-        elif sender == self.box:
-            if self.box.getText() == self.GENERATE_MUC:
-                self.box.setText("")
-            self.new_radio.setChecked(True)
-
-    def onLostFocus(self, sender):
-        if sender == self.box:
-            if self.box.getText() == "":
-                self.box.setText(self.GENERATE_MUC)
-
-    def setRooms(self):
-        for room in self.host.room_list:
-            self.rooms_list.addItem(room.bare)
-        if len(self.host.room_list) > 0:
-            self.exist_radio.setVisible(True)
-            self.rooms_list.setVisible(True)
-            self.exist_radio.setChecked(True)
-
-    def getRoom(self):
-        if self.exist_radio.getChecked():
-            values = self.rooms_list.getSelectedValues()
-            return "" if values == [] else values[0]
-        value = self.box.getText()
-        return "" if value == self.GENERATE_MUC else value
-
-
-class ContactsChooser(VerticalPanel):
-    """Select one or several connected contacts"""
-
-    def __init__(self, host, nb_contact=None, ok_button=None):
-        """
-        @param host: SatWebFrontend instance
-        @param nb_contact: number of contacts that have to be selected, None for no limit
-        If a tuple is given instead of an integer, nb_contact[0] is the minimal and
-        nb_contact[1] is the maximal number of contacts to be chosen.
-        """
-        self.host = host
-        if isinstance(nb_contact, tuple):
-            if len(nb_contact) == 0:
-                nb_contact = None
-            elif len(nb_contact) == 1:
-                nb_contact = (nb_contact[0], nb_contact[0])
-        elif nb_contact is not None:
-            nb_contact = (nb_contact, nb_contact)
-        if nb_contact is None:
-            log.warning("Need to select as many contacts as you want")
-        else:
-            log.warning("Need to select between %d and %d contacts" % nb_contact)
-        self.nb_contact = nb_contact
-        self.ok_button = ok_button
-        VerticalPanel.__init__(self, Width='100%')
-        self.contacts_list = ListBox()
-        self.contacts_list.setMultipleSelect(True)
-        self.contacts_list.setWidth("95%")
-        self.contacts_list.addStyleName('contactsChooser')
-        self.contacts_list.addChangeListener(self.onChange)
-        self.add(self.contacts_list)
-        self.setContacts()
-        self.onChange()
-
-    def onChange(self, sender=None):
-        if self.ok_button is None:
-            return
-        if self.nb_contact:
-            selected = len(self.contacts_list.getSelectedValues(True))
-            if  selected >= self.nb_contact[0] and selected <= self.nb_contact[1]:
-                self.ok_button.setEnabled(True)
-            else:
-                self.ok_button.setEnabled(False)
-
-    def setContacts(self, selected=[]):
-        """Fill the list with the connected contacts
-        @param select: list of the contacts to select by default
-        """
-        self.contacts_list.clear()
-        contacts = self.host.contact_panel.getConnected(filter_muc=True)
-        self.contacts_list.setVisibleItemCount(10 if len(contacts) > 5 else 5)
-        self.contacts_list.addItem("")
-        for contact in contacts:
-            if contact not in [room.bare for room in self.host.room_list]:
-                self.contacts_list.addItem(contact)
-        self.contacts_list.setItemTextSelection(selected)
-
-    def getContacts(self):
-        return self.contacts_list.getSelectedValues(True)
-
-
-class RoomAndContactsChooser(DialogBox):
-    """Select a room and some users to invite in"""
-
-    def __init__(self, host, callback, nb_contact=None, ok_button="OK", title="Discussion groups",
-                 title_room="Join room", title_invite="Invite contacts", visible=(True, True)):
-        DialogBox.__init__(self, centered=True)
-        self.host = host
-        self.callback = callback
-        self.title_room = title_room
-        self.title_invite = title_invite
-
-        button_panel = HorizontalPanel()
-        button_panel.addStyleName("marginAuto")
-        ok_button = Button("OK", self.onOK)
-        button_panel.add(ok_button)
-        button_panel.add(Button("Cancel", self.onCancel))
-
-        self.room_panel = RoomChooser(host, "" if visible == (False, True) else DEFAULT_MUC)
-        self.contact_panel = ContactsChooser(host, nb_contact, ok_button)
-
-        self.stack_panel = ToggleStackPanel(Width="100%")
-        self.stack_panel.add(self.room_panel, visible=visible[0])
-        self.stack_panel.add(self.contact_panel, visible=visible[1])
-        self.stack_panel.addStackChangeListener(self)
-        self.onStackChanged(self.stack_panel, 0, visible[0])
-        self.onStackChanged(self.stack_panel, 1, visible[1])
-
-        main_panel = VerticalPanel()
-        main_panel.setStyleName("room-contact-chooser")
-        main_panel.add(self.stack_panel)
-        main_panel.add(button_panel)
-
-        self.setWidget(main_panel)
-        self.setHTML(title)
-        self.show()
-
-        # needed to update the contacts list when someone logged in/out
-        self.host.room_contacts_chooser = self
-
-    def getRoom(self, asSuffix=False):
-        room = self.room_panel.getRoom()
-        if asSuffix:
-            return room if room == "" else ": %s" % room
-        else:
-            return room
-
-    def getContacts(self, asSuffix=False):
-        contacts = self.contact_panel.getContacts()
-        if asSuffix:
-            return "" if contacts == [] else ": %s" % ", ".join(contacts)
-        else:
-            return contacts
-
-    def onStackChanged(self, sender, index, visible=None):
-        if visible is None:
-            visible = sender.getWidget(index).getVisible()
-        if index == 0:
-            sender.setStackText(0, self.title_room + ("" if visible else self.getRoom(True)))
-        elif index == 1:
-            sender.setStackText(1, self.title_invite + ("" if visible else self.getContacts(True)))
-
-    def resetContacts(self):
-        """Called when someone log in/out to update the list"""
-        self.contact_panel.setContacts(self.getContacts())
-
-    def onOK(self, sender):
-        room_jid = self.getRoom()
-        if room_jid != "" and "@" not in room_jid:
-            Window.alert('You must enter a room jid in the form room@chat.%s' % self.host._defaultDomain)
-            return
-        self.hide()
-        self.callback(room_jid, self.getContacts())
-
-    def onCancel(self, sender):
-        self.hide()
-
-    def hide(self):
-        self.host.room_contacts_chooser = None
-        DialogBox.hide(self, autoClosed=True)
-
-
-class GenericConfirmDialog(DialogBox):
-
-    def __init__(self, widgets, callback, title='Confirmation', prompt=None, **kwargs):
-        """
-        Dialog to confirm an action
-        @param widgets: widgets to attach
-        @param callback: method to call when a button is clicked
-        @param title: title of the dialog
-        @param prompt: textbox from which to retrieve the string value to be passed to the callback when
-        OK button is pressed. If None, OK button will return "True". Cancel button always returns "False".
-        """
-        self.callback = callback
-        DialogBox.__init__(self, centered=True, **kwargs)
-
-        content = VerticalPanel()
-        content.setWidth('100%')
-        for wid in widgets:
-            content.add(wid)
-            if wid == prompt:
-                wid.setWidth('100%')
-        button_panel = HorizontalPanel()
-        button_panel.addStyleName("marginAuto")
-        self.confirm_button = Button("OK", self.onConfirm)
-        button_panel.add(self.confirm_button)
-        self.cancel_button = Button("Cancel", self.onCancel)
-        button_panel.add(self.cancel_button)
-        content.add(button_panel)
-        self.setHTML(title)
-        self.setWidget(content)
-        self.prompt = prompt
-
-    def onConfirm(self, sender):
-        self.hide()
-        self.callback(self.prompt.getText() if self.prompt else True)
-
-    def onCancel(self, sender):
-        self.hide()
-        self.callback(False)
-
-    def show(self):
-        DialogBox.show(self)
-        if self.prompt:
-            self.prompt.setFocus(True)
-
-
-class ConfirmDialog(GenericConfirmDialog):
-
-    def __init__(self, callback, text='Are you sure ?', title='Confirmation', **kwargs):
-        GenericConfirmDialog.__init__(self, [HTML(text)], callback, title, **kwargs)
-
-
-class GenericDialog(DialogBox):
-    """Dialog which just show a widget and a close button"""
-
-    def __init__(self, title, main_widget, callback=None, options=None, **kwargs):
-        """Simple notice dialog box
-        @param title: HTML put in the header
-        @param main_widget: widget put in the body
-        @param callback: method to call on closing
-        @param options: one or more of the following options:
-                        - NO_CLOSE: don't add a close button"""
-        DialogBox.__init__(self, centered=True, **kwargs)
-        self.callback = callback
-        if not options:
-            options = []
-        _body = VerticalPanel()
-        _body.setSize('100%','100%')
-        _body.setSpacing(4)
-        _body.add(main_widget)
-        _body.setCellWidth(main_widget, '100%')
-        _body.setCellHeight(main_widget, '100%')
-        if not 'NO_CLOSE' in options:
-            _close_button = Button("Close", self.onClose)
-            _body.add(_close_button)
-            _body.setCellHorizontalAlignment(_close_button, HasAlignment.ALIGN_CENTER)
-        self.setHTML(title)
-        self.setWidget(_body)
-        self.panel.setSize('100%', '100%') #Need this hack to have correct size in Gecko & Webkit
-
-    def close(self):
-        """Same effect as clicking the close button"""
-        self.onClose(None)
-
-    def onClose(self, sender):
-        self.hide()
-        if self.callback:
-            self.callback()
-
-
-class InfoDialog(GenericDialog):
-
-    def __init__(self, title, body, callback=None, options=None, **kwargs):
-        GenericDialog.__init__(self, title, HTML(body), callback, options, **kwargs)
-
-
-class PromptDialog(GenericConfirmDialog):
-
-    def __init__(self, callback, text='', title='User input', **kwargs):
-        prompt = TextBox()
-        prompt.setText(text)
-        GenericConfirmDialog.__init__(self, [prompt], callback, title, prompt, **kwargs)
-
-
-class PopupPanelWrapper(PopupPanel):
-    """This wrapper catch Escape event to avoid request cancellation by Firefox"""
-
-    def onEventPreview(self, event):
-        if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE:
-            #needed to prevent request cancellation in Firefox
-            event.preventDefault()
-        return PopupPanel.onEventPreview(self, event)
-
-
-class ExtTextBox(TextBox):
-    """Extended TextBox"""
-
-    def __init__(self, *args, **kwargs):
-        if 'enter_cb' in kwargs:
-            self.enter_cb = kwargs['enter_cb']
-            del kwargs['enter_cb']
-        TextBox.__init__(self, *args, **kwargs)
-        self.addKeyboardListener(self)
-
-    def onKeyUp(self, sender, keycode, modifiers):
-        pass
-
-    def onKeyDown(self, sender, keycode, modifiers):
-        pass
-
-    def onKeyPress(self, sender, keycode, modifiers):
-        if self.enter_cb and keycode == KEY_ENTER:
-            self.enter_cb(self)
-
-
-class GroupSelector(DialogBox):
-
-    def __init__(self, top_widgets, initial_groups, selected_groups,
-                 ok_title="OK", ok_cb=None, cancel_cb=None):
-        DialogBox.__init__(self, centered=True)
-        main_panel = VerticalPanel()
-        self.ok_cb = ok_cb
-        self.cancel_cb = cancel_cb
-
-        for wid in top_widgets:
-            main_panel.add(wid)
-
-        main_panel.add(Label('Select in which groups your contact is:'))
-        self.list_box = ListBox()
-        self.list_box.setMultipleSelect(True)
-        self.list_box.setVisibleItemCount(5)
-        self.setAvailableGroups(initial_groups)
-        self.setGroupsSelected(selected_groups)
-        main_panel.add(self.list_box)
-
-        def cb(text):
-            self.list_box.addItem(text)
-            self.list_box.setItemSelected(self.list_box.getItemCount() - 1, "selected")
-
-        main_panel.add(AddGroupPanel(initial_groups, cb))
-
-        button_panel = HorizontalPanel()
-        button_panel.addStyleName("marginAuto")
-        button_panel.add(Button(ok_title, self.onOK))
-        button_panel.add(Button("Cancel", self.onCancel))
-        main_panel.add(button_panel)
-
-        self.setWidget(main_panel)
-
-    def getSelectedGroups(self):
-        """Return a list of selected groups"""
-        return self.list_box.getSelectedValues()
-
-    def setAvailableGroups(self, groups):
-        _groups = list(set(groups))
-        _groups.sort()
-        self.list_box.clear()
-        for group in _groups:
-            self.list_box.addItem(group)
-
-    def setGroupsSelected(self, selected_groups):
-        self.list_box.setItemTextSelection(selected_groups)
-
-    def onOK(self, sender):
-        self.hide()
-        if self.ok_cb:
-            self.ok_cb(self)
-
-    def onCancel(self, sender):
-        self.hide()
-        if self.cancel_cb:
-            self.cancel_cb(self)
-
-
-class AddGroupPanel(HorizontalPanel):
-    def __init__(self, groups, cb=None):
-        """
-        @param groups: list of the already existing groups
-        """
-        HorizontalPanel.__init__(self)
-        self.groups = groups
-        self.add(Label('Add group:'))
-        self.textbox = ExtTextBox(enter_cb=self.onGroupInput)
-        self.add(self.textbox)
-        self.add(Button("add", lambda sender: self.onGroupInput(self.textbox)))
-        self.cb = cb
-
-    def onGroupInput(self, sender):
-        text = sender.getText()
-        if text == "":
-            return
-        for group in self.groups:
-            if text == group:
-                Window.alert("The group '%s' already exists." % text)
-                return
-        for pattern in FORBIDDEN_PATTERNS_IN_GROUP:
-            if pattern in text:
-                Window.alert("The pattern '%s' is not allowed in group names." % pattern)
-                return
-        sender.setText('')
-        self.groups.append(text)
-        if self.cb is not None:
-            self.cb(text)
-
-
-class WheelTextBox(TextBox, MouseWheelHandler):
-
-    def __init__(self, *args, **kwargs):
-        TextBox.__init__(self, *args, **kwargs)
-        MouseWheelHandler.__init__(self)
-
-
-class IntSetter(HorizontalPanel):
-    """This class show a bar with button to set an int value"""
-
-    def __init__(self, label, value=0, value_max=None, visible_len=3):
-        """initialize the intSetter
-        @param label: text shown in front of the setter
-        @param value: initial value
-        @param value_max: limit value, None or 0 for unlimited"""
-        HorizontalPanel.__init__(self)
-        self.value = value
-        self.value_max = value_max
-        _label = Label(label)
-        self.add(_label)
-        self.setCellWidth(_label, "100%")
-        minus_button = Button("-", self.onMinus)
-        self.box = WheelTextBox()
-        self.box.setVisibleLength(visible_len)
-        self.box.setText(str(value))
-        self.box.addInputListener(self)
-        self.box.addMouseWheelListener(self)
-        plus_button = Button("+", self.onPlus)
-        self.add(minus_button)
-        self.add(self.box)
-        self.add(plus_button)
-        self.valueChangedListener = []
-
-    def addValueChangeListener(self, listener):
-        self.valueChangedListener.append(listener)
-
-    def removeValueChangeListener(self, listener):
-        if listener in self.valueChangedListener:
-            self.valueChangedListener.remove(listener)
-
-    def _callListeners(self):
-        for listener in self.valueChangedListener:
-            listener(self.value)
-
-    def setValue(self, value):
-        """Change the value and fire valueChange listeners"""
-        self.value = value
-        self.box.setText(str(value))
-        self._callListeners()
-
-    def onMinus(self, sender, step=1):
-        self.value = max(0, self.value - step)
-        self.box.setText(str(self.value))
-        self._callListeners()
-
-    def onPlus(self, sender, step=1):
-        self.value += step
-        if self.value_max:
-            self.value = min(self.value, self.value_max)
-        self.box.setText(str(self.value))
-        self._callListeners()
-
-    def onInput(self, sender):
-        """Accept only valid integer && normalize print (no leading 0)"""
-        try:
-            self.value = int(self.box.getText()) if self.box.getText() else 0
-        except ValueError:
-            pass
-        if self.value_max:
-            self.value = min(self.value, self.value_max)
-        self.box.setText(str(self.value))
-        self._callListeners()
-
-    def onMouseWheel(self, sender, velocity):
-        if velocity > 0:
-            self.onMinus(sender, 10)
-        else:
-            self.onPlus(sender, 10)
--- a/browser_side/file_tools.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,148 +0,0 @@
-#!/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/>.
-
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from pyjamas.ui.FileUpload import FileUpload
-from pyjamas.ui.FormPanel import FormPanel
-from pyjamas import Window
-from pyjamas import DOM
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.Button import Button
-from pyjamas.ui.Label import Label
-
-
-class FilterFileUpload(FileUpload):
-
-    def __init__(self, name, max_size, types=None):
-        """
-        @param name: the input element name and id
-        @param max_size: maximum file size in MB
-        @param types: allowed types as a list of couples (x, y, z):
-        - x: MIME content type e.g. "audio/ogg"
-        - y: file extension e.g. "*.ogg"
-        - z: description for the user e.g. "Ogg Vorbis Audio"
-        If types is None, all file format are accepted
-        """
-        FileUpload.__init__(self)
-        self.setName(name)
-        while DOM.getElementById(name):
-            name = "%s_" % name
-        self.setID(name)
-        self._id = name
-        self.max_size = max_size
-        self.types = types
-
-    def getFileInfo(self):
-        from __pyjamas__ import JS
-        JS("var file = top.document.getElementById(this._id).files[0]; return [file.size, file.type]")
-
-    def check(self):
-        if self.getFilename() == "":
-            return False
-        (size, filetype) = self.getFileInfo()
-        if self.types and filetype not in [x for (x, y, z) in self.types]:
-            types = []
-            for type_ in ["- %s (%s)" % (z, y) for (x, y, z) in self.types]:
-                if type_ not in types:
-                    types.append(type_)
-            Window.alert('This file type is not accepted.\nAccepted file types are:\n\n%s' % "\n".join(types))
-            return False
-        if size > self.max_size * pow(2, 20):
-            Window.alert('This file is too big!\nMaximum file size: %d MB.' % self.max_size)
-            return False
-        return True
-
-
-class FileUploadPanel(FormPanel):
-
-    def __init__(self, action_url, input_id, max_size, texts=None, close_cb=None):
-        """Build a form panel to upload a file.
-        @param action_url: the form action URL
-        @param input_id: the input element name and id
-        @param max_size: maximum file size in MB
-        @param texts: a dict to ovewrite the default textual values
-        @param close_cb: the close button callback method
-        """
-        FormPanel.__init__(self)
-        self.texts = {'ok_button': 'Upload file',
-                     'cancel_button': 'Cancel',
-                     'body': 'Please select a file.',
-                     'submitting': '<strong>Submitting, please wait...</strong>',
-                     'errback': "Your file has been rejected...",
-                     'body_errback': 'Please select another file.',
-                     'callback': "Your file has been accepted!"}
-        if isinstance(texts, dict):
-            self.texts.update(texts)
-        self.close_cb = close_cb
-        self.setEncoding(FormPanel.ENCODING_MULTIPART)
-        self.setMethod(FormPanel.METHOD_POST)
-        self.setAction(action_url)
-        self.vPanel = VerticalPanel()
-        self.message = HTML(self.texts['body'])
-        self.vPanel.add(self.message)
-
-        hPanel = HorizontalPanel()
-        hPanel.setSpacing(5)
-        hPanel.setStyleName('marginAuto')
-        self.file_upload = FilterFileUpload(input_id, max_size)
-        self.vPanel.add(self.file_upload)
-
-        self.upload_btn = Button(self.texts['ok_button'], getattr(self, "onSubmitBtnClick"))
-        hPanel.add(self.upload_btn)
-        hPanel.add(Button(self.texts['cancel_button'], getattr(self, "onCloseBtnClick")))
-
-        self.status = Label()
-        hPanel.add(self.status)
-
-        self.vPanel.add(hPanel)
-
-        self.add(self.vPanel)
-        self.addFormHandler(self)
-
-    def setCloseCb(self, close_cb):
-        self.close_cb = close_cb
-
-    def onCloseBtnClick(self):
-        if self.close_cb:
-            self.close_cb()
-        else:
-            log.warning("no close method defined")
-
-    def onSubmitBtnClick(self):
-        if not self.file_upload.check():
-            return
-        self.message.setHTML(self.texts['submitting'])
-        self.upload_btn.setEnabled(False)
-        self.submit()
-
-    def onSubmit(self, event):
-        pass
-
-    def onSubmitComplete(self, event):
-        result = event.getResults()
-        if result != "OK":
-            Window.alert(self.texts['errback'])
-            self.message.setHTML(self.texts['body_errback'])
-            self.upload_btn.setEnabled(True)
-        else:
-            Window.alert(self.texts['callback'])
-            self.close_cb()
--- a/browser_side/html_tools.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-#!/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/>.
-
-from nativedom import NativeDOM
-from sat_frontends.tools import xmltools
-import re
-
-dom = NativeDOM()
-
-
-def html_sanitize(html):
-    """Naive sanitization of HTML"""
-    return html.replace('<', '&lt;').replace('>', '&gt;')
-
-
-def html_strip(html):
-    """Strip leading/trailing white spaces, HTML line breaks and &nbsp; sequences."""
-    cleaned = re.sub(r"^(<br/?>|&nbsp;|\s)+", "", html)
-    cleaned = re.sub(r"(<br/?>|&nbsp;|\s)+$", "", cleaned)
-    return cleaned
-
-
-def inlineRoot(xhtml):
-    """ make root element inline """
-    doc = dom.parseString(xhtml)
-    return xmltools.inlineRoot(doc)
-
-
-def convertNewLinesToXHTML(text):
-    return text.replace('\n', '<br/>')
--- a/browser_side/jid.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-#!/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/>.
-
-
-class JID(object):
-    """This class help manage JID (Node@Domaine/Resource)"""
-
-    def __init__(self, jid):
-        self.__raw = str(jid)
-        self.__parse()
-
-    def __parse(self):
-        """find node domaine and resource"""
-        node_end = self.__raw.find('@')
-        if node_end < 0:
-            node_end = 0
-        domain_end = self.__raw.find('/')
-        if domain_end < 1:
-            domain_end = len(self.__raw)
-        self.node = self.__raw[:node_end]
-        self.domain = self.__raw[(node_end + 1) if node_end else 0:domain_end]
-        self.resource = self.__raw[domain_end + 1:]
-        if not node_end:
-            self.bare = self.__raw
-        else:
-            self.bare = self.node + '@' + self.domain
-
-    def __str__(self):
-        return self.__raw.__str__()
-
-    def is_valid(self):
-        """return True if the jid is xmpp compliant"""
-        #FIXME: always return True for the moment
-        return True
-
-    def __eq__(self, other):
-        """Redefine equality operator to implement the naturally expected test"""
-        return self.node == other.node and self.domain == other.domain and self.resource == other.resource
--- a/browser_side/list_manager.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,608 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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/>.
-
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from pyjamas.ui.Grid import Grid
-from pyjamas.ui.Button import Button
-from pyjamas.ui.ListBox import ListBox
-from pyjamas.ui.FlowPanel import FlowPanel
-from pyjamas.ui.AutoComplete import AutoCompleteTextBox
-from pyjamas.ui.Label import Label
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.DialogBox import DialogBox
-from pyjamas.ui.KeyboardListener import KEY_ENTER
-from pyjamas.ui.MouseListener import MouseHandler
-from pyjamas.ui.FocusListener import FocusHandler
-from pyjamas.ui.DropWidget import DropWidget
-from pyjamas.Timer import Timer
-from pyjamas import DOM
-
-from base_panels import PopupMenuPanel
-from base_widget import DragLabel
-
-# HTML content for the removal button (image or text)
-REMOVE_BUTTON = '<span class="recipientRemoveIcon">x</span>'
-
-# Item to be considered for an empty list box selection.
-# Could be whatever which doesn't look like a JID or a group name.
-EMPTY_SELECTION_ITEM = ""
-
-
-class ListManager():
-    """A manager for sub-panels to assign elements to lists."""
-
-    def __init__(self, parent, keys_dict={}, contact_list=[], offsets={}, style={}):
-        """
-        @param parent: FlexTable parent widget for the manager
-        @param keys_dict: dict with the contact keys mapped to data
-        @param contact_list: list of string (the contact JID userhosts)
-        @param offsets: dict to set widget positions offset within parent
-        - "x_first": the x offset for the first widget's row on the grid
-        - "x": the x offset for all widgets rows, except the first one if "x_first" is defined
-        - "y": the y offset for all widgets columns on the grid
-        """
-        self._parent = parent
-        if isinstance(keys_dict, set) or isinstance(keys_dict, list):
-            tmp = {}
-            for key in keys_dict:
-                tmp[key] = {}
-            keys_dict = tmp
-        self.__keys_dict = keys_dict
-        if isinstance(contact_list, set):
-            contact_list = list(contact_list)
-        self.__list = contact_list
-        self.__list.sort()
-        # store the list of contacts that are not assigned yet
-        self.__remaining_list = []
-        self.__remaining_list.extend(self.__list)
-        # mark a change to sort the list before it's used
-        self.__remaining_list_sorted = True
-
-        self.offsets = {"x_first": 0, "x": 0, "y": 0}
-        if "x" in offsets and not "x_first" in offsets:
-            offsets["x_first"] = offsets["x"]
-        self.offsets.update(offsets)
-
-        self.style = {
-           "keyItem": "recipientTypeItem",
-           "popupMenuItem": "recipientTypeItem",
-           "buttonCell": "recipientButtonCell",
-           "dragoverPanel": "dragover-recipientPanel",
-           "keyPanel": "recipientPanel",
-           "textBox": "recipientTextBox",
-           "textBox-invalid": "recipientTextBox-invalid",
-           "removeButton": "recipientRemoveButton",
-        }
-        self.style.update(style)
-
-    def createWidgets(self, title_format="%s"):
-        """Fill the parent grid with all the widgets (some may be hidden during the initialization)."""
-        self.__children = {}
-        for key in self.__keys_dict:
-            self.addContactKey(key, title_format=title_format)
-
-    def addContactKey(self, key, dict_={}, title_format="%s"):
-        if key not in self.__keys_dict:
-            self.__keys_dict[key] = dict_
-        # copy the key to its associated sub-map
-        self.__keys_dict[key]["title"] = key
-        self._addChild(self.__keys_dict[key], title_format)
-
-    def removeContactKey(self, key):
-        """Remove a list panel and all its associated data."""
-        contacts = self.__children[key]["panel"].getContacts()
-        (y, x) = self._parent.getIndex(self.__children[key]["button"])
-        self._parent.removeRow(y)
-        del self.__children[key]
-        del self.__keys_dict[key]
-        self.addToRemainingList(contacts)
-
-    def _addChild(self, entry, title_format):
-        """Add a button and FlowPanel for the corresponding map entry."""
-        button = Button(title_format % entry["title"])
-        button.setStyleName(self.style["keyItem"])
-        if hasattr(entry, "desc"):
-            button.setTitle(entry["desc"])
-        if not "optional" in entry:
-            entry["optional"] = False
-        button.setVisible(not entry["optional"])
-        y = len(self.__children) + self.offsets["y"]
-        x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"]
-
-        self._parent.insertRow(y)
-        self._parent.setWidget(y, x, button)
-        self._parent.getCellFormatter().setStyleName(y, x, self.style["buttonCell"])
-
-        _child = ListPanel(self, entry, self.style)
-        self._parent.setWidget(y, x + 1, _child)
-
-        self.__children[entry["title"]] = {}
-        self.__children[entry["title"]]["button"] = button
-        self.__children[entry["title"]]["panel"] = _child
-
-        if hasattr(self, "popup_menu"):
-            # this is done if self.registerPopupMenuPanel has been called yet
-            self.popup_menu.registerClickSender(button)
-
-    def _refresh(self, visible=True):
-        """Set visible the sub-panels that are non optional or non empty, hide the rest."""
-        for key in self.__children:
-            self.setContactPanelVisible(key, False)
-        if not visible:
-            return
-        _map = self.getContacts()
-        for key in _map:
-            if len(_map[key]) > 0 or not self.__keys_dict[key]["optional"]:
-                self.setContactPanelVisible(key, True)
-
-    def setVisible(self, visible):
-        self._refresh(visible)
-
-    def setContactPanelVisible(self, key, visible=True, sender=None):
-        """Do not remove the "sender" param as it is needed for the context menu."""
-        self.__children[key]["button"].setVisible(visible)
-        self.__children[key]["panel"].setVisible(visible)
-
-    @property
-    def list(self):
-        """Return the full list of potential contacts."""
-        return self.__list
-
-    @property
-    def keys(self):
-        return self.__keys_dict.keys()
-
-    @property
-    def keys_dict(self):
-        return self.__keys_dict
-
-    @property
-    def remaining_list(self):
-        """Return the contacts that have not been selected yet."""
-        if not self.__remaining_list_sorted:
-            self.__remaining_list_sorted = True
-            self.__remaining_list.sort()
-        return self.__remaining_list
-
-    def setRemainingListUnsorted(self):
-        """Mark a change (deletion) so the list will be sorted before it's used."""
-        self.__remaining_list_sorted = False
-
-    def removeFromRemainingList(self, contacts):
-        """Remove contacts after they have been added to a sub-panel."""
-        if not isinstance(contacts, list):
-            contacts = [contacts]
-        for contact_ in contacts:
-            if contact_ in self.__remaining_list:
-                self.__remaining_list.remove(contact_)
-
-    def addToRemainingList(self, contacts, ignore_key=None):
-        """Add contacts after they have been removed from a sub-panel."""
-        if not isinstance(contacts, list):
-            contacts = [contacts]
-        assigned_contacts = set()
-        assigned_map = self.getContacts()
-        for key_ in assigned_map.keys():
-            if ignore_key is not None and key_ == ignore_key:
-                continue
-            assigned_contacts.update(assigned_map[key_])
-        for contact_ in contacts:
-            if contact_ not in self.__list or contact_ in self.__remaining_list:
-                continue
-            if contact_ in assigned_contacts:
-                continue  # the contact is assigned somewhere else
-            self.__remaining_list.append(contact_)
-            self.setRemainingListUnsorted()
-
-    def setContacts(self, _map={}):
-        """Set the contacts for each contact key."""
-        for key in self.__keys_dict:
-            if key in _map:
-                self.__children[key]["panel"].setContacts(_map[key])
-            else:
-                self.__children[key]["panel"].setContacts([])
-        self._refresh()
-
-    def getContacts(self):
-        """Get the contacts for all the lists.
-        @return: a mapping between keys and contact lists."""
-        _map = {}
-        for key in self.__children:
-            _map[key] = self.__children[key]["panel"].getContacts()
-        return _map
-
-    @property
-    def target_drop_cell(self):
-        """@return: the panel where something has been dropped."""
-        return self._target_drop_cell
-
-    def setTargetDropCell(self, target_drop_cell):
-        """@param: target_drop_cell: the panel where something has been dropped."""
-        self._target_drop_cell = target_drop_cell
-
-    def registerPopupMenuPanel(self, entries, hide, callback):
-        "Register a popup menu panel that will be bound to all contact keys elements."
-        self.popup_menu = PopupMenuPanel(entries=entries, hide=hide, callback=callback, style={"item": self.style["popupMenuItem"]})
-
-
-class DragAutoCompleteTextBox(AutoCompleteTextBox, DragLabel, MouseHandler, FocusHandler):
-    """A draggable AutoCompleteTextBox which is used for representing a contact.
-    This class is NOT generic because of the onDragEnd method which call methods
-    from ListPanel. It's probably not reusable for another scenario.
-    """
-
-    def __init__(self, parent, event_cbs, style):
-        AutoCompleteTextBox.__init__(self)
-        DragLabel.__init__(self, '', 'CONTACT_TEXTBOX')  # The group prefix "@" is already in text so we use only the "CONTACT_TEXTBOX" type
-        self._parent = parent
-        self.event_cbs = event_cbs
-        self.style = style
-        self.addMouseListener(self)
-        self.addFocusListener(self)
-        self.addChangeListener(self)
-        self.addStyleName(style["textBox"])
-        self.reset()
-
-    def reset(self):
-        self.setText("")
-        self.setValid()
-
-    def setValid(self, valid=True):
-        if self.getText() == "":
-            valid = True
-        if valid:
-            self.removeStyleName(self.style["textBox-invalid"])
-        else:
-            self.addStyleName(self.style["textBox-invalid"])
-        self.valid = valid
-
-    def onDragStart(self, event):
-        self._text = self.getText()
-        DragLabel.onDragStart(self, event)
-        self._parent.setTargetDropCell(None)
-        self.setSelectionRange(len(self.getText()), 0)
-
-    def onDragEnd(self, event):
-        target = self._parent.target_drop_cell  # parent or another ListPanel
-        if self.getText() == "" or target is None:
-            return
-        self.event_cbs["drop"](self, target)
-
-    def setRemoveButton(self):
-
-        def remove_cb(sender):
-            """Callback for the button to remove this contact."""
-            self._parent.remove(self)
-            self._parent.remove(self.remove_btn)
-            self.event_cbs["remove"](self)
-
-        self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False)
-        self.remove_btn.setStyleName(self.style["removeButton"])
-        self._parent.add(self.remove_btn)
-
-    def removeOrReset(self):
-        if hasattr(self, "remove_btn"):
-            self.remove_btn.click()
-        else:
-            self.reset()
-
-    def onMouseMove(self, sender):
-        """Mouse enters the area of a DragAutoCompleteTextBox."""
-        if hasattr(sender, "remove_btn"):
-            sender.remove_btn.setVisible(True)
-
-    def onMouseLeave(self, sender):
-        """Mouse leaves the area of a DragAutoCompleteTextBox."""
-        if hasattr(sender, "remove_btn"):
-            Timer(1500, lambda timer: sender.remove_btn.setVisible(False))
-
-    def onFocus(self, sender):
-        sender.setSelectionRange(0, len(self.getText()))
-        self.event_cbs["focus"](sender)
-
-    def validate(self):
-        self.setSelectionRange(len(self.getText()), 0)
-        self.event_cbs["validate"](self)
-
-    def onChange(self, sender):
-        """The textbox or list selection is changed"""
-        if isinstance(sender, ListBox):
-            AutoCompleteTextBox.onChange(self, sender)
-        self.validate()
-
-    def onClick(self, sender):
-        """The list is clicked"""
-        AutoCompleteTextBox.onClick(self, sender)
-        self.validate()
-
-    def onKeyUp(self, sender, keycode, modifiers):
-        """Listen for ENTER key stroke"""
-        AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers)
-        if keycode == KEY_ENTER:
-            self.validate()
-
-
-class DropCell(DropWidget):
-    """A cell where you can drop widgets. This class is NOT generic because of
-    onDrop which uses methods from ListPanel. It has been created to
-    separate the drag and drop methods from the others and add a bit of
-    lisibility, but it's probably not reusable for another scenario.
-    """
-
-    def __init__(self, drop_cbs):
-        DropWidget.__init__(self)
-        self.drop_cbs = drop_cbs
-
-    def onDragEnter(self, event):
-        self.addStyleName(self.style["dragoverPanel"])
-        DOM.eventPreventDefault(event)
-
-    def onDragLeave(self, event):
-        if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop()\
-            or event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1\
-            or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1:
-            # We check that we are inside widget's box, and we don't remove the style in this case because
-            # if the mouse is over a widget inside the DropWidget, we don't want the style to be removed
-            self.removeStyleName(self.style["dragoverPanel"])
-
-    def onDragOver(self, event):
-        DOM.eventPreventDefault(event)
-
-    def onDrop(self, event):
-        DOM.eventPreventDefault(event)
-        dt = event.dataTransfer
-        # 'text', 'text/plain', and 'Text' are equivalent.
-        item, item_type = dt.getData("text/plain").split('\n')  # Workaround for webkit, only text/plain seems to be managed
-        if item_type and item_type[-1] == '\0':  # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
-            item_type = item_type[:-1]           # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
-        if item_type in self.drop_cbs.keys():
-            self.drop_cbs[item_type](self, item)
-        self.removeStyleName(self.style["dragoverPanel"])
-
-
-VALID = 1
-INVALID = 2
-DELETE = 3
-
-
-class ListPanel(FlowPanel, DropCell):
-    """Sub-panel used for each contact key. Beware that pyjamas.ui.FlowPanel
-    is not fully implemented yet and can not be used with pyjamas.ui.Label."""
-
-    def __init__(self, parent, entry, style={}):
-        """Initialization with a button and a DragAutoCompleteTextBox."""
-        FlowPanel.__init__(self, Visible=(False if entry["optional"] else True))
-        drop_cbs = {"GROUP": lambda panel, item: self.addContact("@%s" % item),
-                    "CONTACT": lambda panel, item: self.addContact(item),
-                    "CONTACT_TITLE": lambda panel, item: self.addContact('@@'),
-                    "CONTACT_TEXTBOX": lambda panel, item: self.setTargetDropCell(panel)
-                    }
-        DropCell.__init__(self, drop_cbs)
-        self.style = style
-        self.addStyleName(self.style["keyPanel"])
-        self._parent = parent
-        self.key = entry["title"]
-        self._addTextBox()
-
-    def _addTextBox(self, switchPrevious=False):
-        """Add a text box to the last position. If switchPrevious is True, simulate
-        an insertion before the current last textbox by copying the text and valid state.
-        @return: the created textbox or the previous one if switchPrevious is True.
-        """
-        if hasattr(self, "_last_textbox"):
-            if self._last_textbox.getText() == "":
-                return
-            self._last_textbox.setRemoveButton()
-        else:
-            switchPrevious = False
-
-        def focus_cb(sender):
-            if sender != self._last_textbox:
-                # save the current value before it's being modified
-                self._parent.addToRemainingList(sender.getText(), ignore_key=self.key)
-            sender.setCompletionItems(self._parent.remaining_list)
-
-        def remove_cb(sender):
-            """Callback for the button to remove this contact."""
-            self._parent.addToRemainingList(sender.getText())
-            self._parent.setRemainingListUnsorted()
-            self._last_textbox.setFocus(True)
-
-        def drop_cb(sender, target):
-            """Callback when the textbox is drag-n-dropped."""
-            parent = sender._parent
-            if target != parent and target.addContact(sender.getText()):
-                sender.removeOrReset()
-            else:
-                parent._parent.removeFromRemainingList(sender.getText())
-
-        events_cbs = {"focus": focus_cb, "validate": self.addContact, "remove": remove_cb, "drop": drop_cb}
-        textbox = DragAutoCompleteTextBox(self, events_cbs, self.style)
-        self.add(textbox)
-        if switchPrevious:
-            textbox.setText(self._last_textbox.getText())
-            textbox.setValid(self._last_textbox.valid)
-            self._last_textbox.reset()
-            previous = self._last_textbox
-        self._last_textbox = textbox
-        return previous if switchPrevious else textbox
-
-    def _checkContact(self, contact, modify):
-        """
-        @param contact: the contact to check
-        @param modify: True if the contact is being modified
-        @return:
-        - VALID if the contact is valid
-        - INVALID if the contact is not valid but can be displayed
-        - DELETE if the contact should not be displayed at all
-        """
-        def countItemInList(list_, item):
-            """For some reason the built-in count function doesn't work..."""
-            count = 0
-            for elem in list_:
-                if elem == item:
-                    count += 1
-            return count
-        if contact is None or contact == "":
-            return DELETE
-        if countItemInList(self.getContacts(), contact) > (1 if modify else 0):
-            return DELETE
-        return VALID if contact in self._parent.list else INVALID
-
-    def addContact(self, contact, sender=None):
-        """The first parameter type is checked, so it is also possible to call addContact(sender).
-        If contact is not defined, sender.getText() is used. If sender is not defined, contact will
-        be written to the last textbox and a new textbox is added afterward.
-        @param contact: unicode
-        @param sender: DragAutoCompleteTextBox instance
-        """
-        if isinstance(contact, DragAutoCompleteTextBox):
-            sender = contact
-            contact = sender.getText()
-        valid = self._checkContact(contact, sender is not None)
-        if sender is None:
-            # method has been called to modify but to add a contact
-            if valid == VALID:
-                # eventually insert before the last textbox if it's not empty
-                sender = self._addTextBox(True) if self._last_textbox.getText() != "" else self._last_textbox
-                sender.setText(contact)
-        else:
-            sender.setValid(valid == VALID)
-        if valid != VALID:
-            if sender is not None and valid == DELETE:
-                sender.removeOrReset()
-            return False
-        if sender == self._last_textbox:
-            self._addTextBox()
-        try:
-            sender.setVisibleLength(len(contact))
-        except:
-            # IndexSizeError: Index or size is negative or greater than the allowed amount
-            log.warning("FIXME: len(%s) returns %d... javascript bug?" % (contact, len(contact)))
-        self._parent.removeFromRemainingList(contact)
-        self._last_textbox.setFocus(True)
-        return True
-
-    def emptyContacts(self):
-        """Empty the list of contacts."""
-        for child in self.getChildren():
-            if hasattr(child, "remove_btn"):
-                child.remove_btn.click()
-
-    def setContacts(self, tab):
-        """Set the contacts."""
-        self.emptyContacts()
-        if isinstance(tab, set):
-            tab = list(tab)
-        tab.sort()
-        for contact in tab:
-            self.addContact(contact)
-
-    def getContacts(self):
-        """Get the contacts
-        @return: an array of string"""
-        tab = []
-        for widget in self.getChildren():
-            if isinstance(widget, DragAutoCompleteTextBox):
-                # not to be mixed with EMPTY_SELECTION_ITEM
-                if widget.getText() != "":
-                    tab.append(widget.getText())
-        return tab
-
-    @property
-    def target_drop_cell(self):
-        """@return: the panel where something has been dropped."""
-        return self._parent.target_drop_cell
-
-    def setTargetDropCell(self, target_drop_cell):
-        """
-        XXX: Property setter here would not make it, you need a proper method!
-        @param target_drop_cell: the panel where something has been dropped."""
-        self._parent.setTargetDropCell(target_drop_cell)
-
-
-class ContactChooserPanel(DialogBox):
-    """Display the contacts chooser dialog. This has been implemented while
-    prototyping and is currently not used. Left for an eventual later use.
-    Replaced by the popup menu which allows to add a panel for Cc or Bcc.
-    """
-
-    def __init__(self, manager, **kwargs):
-        """Display a listbox for each contact key"""
-        DialogBox.__init__(self, autoHide=False, centered=True, **kwargs)
-        self.setHTML("Select contacts")
-        self.manager = manager
-        self.listboxes = {}
-        self.contacts = manager.getContacts()
-
-        container = VerticalPanel(Visible=True)
-        container.addStyleName("marginAuto")
-
-        grid = Grid(2, len(self.manager.keys_dict))
-        index = -1
-        for key in self.manager.keys_dict:
-            index += 1
-            grid.add(Label("%s:" % self.manager.keys_dict[key]["desc"]), 0, index)
-            listbox = ListBox()
-            listbox.setMultipleSelect(True)
-            listbox.setVisibleItemCount(15)
-            listbox.addItem(EMPTY_SELECTION_ITEM)
-            for element in manager.list:
-                listbox.addItem(element)
-            self.listboxes[key] = listbox
-            grid.add(listbox, 1, index)
-        self._reset()
-
-        buttons = HorizontalPanel()
-        buttons.addStyleName("marginAuto")
-        btn_close = Button("Cancel", self.hide)
-        buttons.add(btn_close)
-        btn_reset = Button("Reset", self._reset)
-        buttons.add(btn_reset)
-        btn_ok = Button("OK", self._validate)
-        buttons.add(btn_ok)
-
-        container.add(grid)
-        container.add(buttons)
-
-        self.add(container)
-        self.center()
-
-    def _reset(self):
-        """Reset the selections."""
-        for key in self.manager.keys_dict:
-            listbox = self.listboxes[key]
-            for i in xrange(0, listbox.getItemCount()):
-                if listbox.getItemText(i) in self.contacts[key]:
-                    listbox.setItemSelected(i, "selected")
-                else:
-                    listbox.setItemSelected(i, "")
-
-    def _validate(self):
-        """Sets back the selected contacts to the good sub-panels."""
-        _map = {}
-        for key in self.manager.keys_dict:
-            selections = self.listboxes[key].getSelectedItemText()
-            if EMPTY_SELECTION_ITEM in selections:
-                selections.remove(EMPTY_SELECTION_ITEM)
-            _map[key] = selections
-        self.manager.setContacts(_map)
-        self.hide()
--- a/browser_side/logging.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-#!/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/>.
-"""This module configure logs for Libervia browser side"""
-
-from __pyjamas__ import console
-from constants import Const as C
-from sat.core import log # XXX: we don't use core.log_config here to avoid the impossible imports in pyjamas
-
-
-class LiberviaLogger(log.Logger):
-
-    def out(self, message, level=None):
-        if level == C.LOG_LVL_DEBUG:
-            console.debug(message)
-        elif level == C.LOG_LVL_INFO:
-            console.info(message)
-        elif level == C.LOG_LVL_WARNING:
-            console.warn(message)
-        else:
-            console.error(message)
-
-
-def configure():
-    fmt = '[%(name)s] %(message)s'
-    log.configure(C.LOG_BACKEND_CUSTOM,
-                  logger_class = LiberviaLogger,
-                  level = C.LOG_LVL_DEBUG,
-                  fmt = fmt,
-                  output = None,
-                  logger = None,
-                  colors = False,
-                  force_colors = False)
-    # FIXME: workaround for Pyjamas, need to be removed when Pyjamas is fixed
-    LiberviaLogger.fmt = fmt
--- a/browser_side/menu.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,267 +0,0 @@
-#!/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 pyjamas.ui.SimplePanel import SimplePanel
-from pyjamas.ui.MenuBar import MenuBar
-from pyjamas.ui.MenuItem import MenuItem
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.Frame import Frame
-from pyjamas import Window
-from jid import JID
-from file_tools import FileUploadPanel
-from xmlui import XMLUI
-from browser_side import panels
-from browser_side import dialog
-from contact_group import ContactGroupEditor
-from sat.core.i18n import _
-
-
-class MenuCmd:
-
-    def __init__(self, object_, handler):
-        self._object = object_
-        self._handler = handler
-
-    def execute(self):
-        handler = getattr(self._object, self._handler)
-        handler()
-
-
-class PluginMenuCmd:
-
-    def __init__(self, host, action_id):
-        self.host = host
-        self.action_id = action_id
-
-    def execute(self):
-        self.host.launchAction(self.action_id, None)
-
-
-class LiberviaMenuBar(MenuBar):
-
-    def __init__(self):
-        MenuBar.__init__(self, vertical=False)
-        self.setStyleName('gwt-MenuBar-horizontal') # XXX: workaround for the Pyjamas' class name fix (it's now "gwt-MenuBar gwt-MenuBar-horizontal")
-                                                    # TODO: properly adapt CSS to the new class name
-
-    def doItemAction(self, item, fireCommand):
-        MenuBar.doItemAction(self, item, fireCommand)
-        if item == self.items[-1] and self.popup:
-            self.popup.setPopupPosition(Window.getClientWidth() -
-                                        self.popup.getOffsetWidth() - 22,
-                                        self.getAbsoluteTop() +
-                                        self.getOffsetHeight() - 1)
-            self.popup.addStyleName('menuLastPopup')
-
-
-class AvatarUpload(FileUploadPanel):
-    def __init__(self):
-        texts = {'ok_button': 'Upload avatar',
-                 'body': 'Please select an image to show as your avatar...<br>Your picture must be a square and will be resized to 64x64 pixels if necessary.',
-                 'errback': "Can't open image... did you actually submit an image?",
-                 'body_errback': 'Please select another image file.',
-                 'callback': "Your new profile picture has been set!"}
-        FileUploadPanel.__init__(self, 'upload_avatar', 'avatar_path', 2, texts)
-
-
-class Menu(SimplePanel):
-
-    def __init__(self, host):
-        self.host = host
-        SimplePanel.__init__(self)
-        self.setStyleName('menuContainer')
-
-    def createMenus(self, add_menus):
-        _item_tpl = "<img src='media/icons/menu/%s_menu_red.png' />%s"
-        menus_dict = {}
-        menus_order = []
-
-        def addMenu(menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd):
-            """ add a menu to menu_dict """
-            log.info("addMenu: %s %s %s %s %s" % (menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd))
-            try:
-                menu_bar = menus_dict[menu_name]
-            except KeyError:
-                menu_bar = menus_dict[menu_name] = MenuBar(vertical=True)
-                menus_order.append((menu_name, menu_name_i18n, icon))
-            if item_name_i18n and menu_cmd:
-                menu_bar.addItem(item_name_i18n, menu_cmd)
-
-        addMenu("General", _("General"), _("Web widget"), 'home', MenuCmd(self, "onWebWidget"))
-        addMenu("General", _("General"), _("Disconnect"), 'home', MenuCmd(self, "onDisconnect"))
-        addMenu("Contacts", _("Contacts"), None, 'social', None)
-        addMenu("Groups", _("Groups"), _("Discussion"), 'social', MenuCmd(self, "onJoinRoom"))
-        addMenu("Groups", _("Groups"), _("Collective radio"), 'social', MenuCmd(self, "onCollectiveRadio"))
-        addMenu("Games", _("Games"), _("Tarot"), 'games', MenuCmd(self, "onTarotGame"))
-        addMenu("Games", _("Games"), _("Xiangqi"), 'games', MenuCmd(self, "onXiangqiGame"))
-
-        # additional menus
-        for action_id, type_, path, path_i18n in add_menus:
-            if not path:
-                log.warning("skipping menu without path")
-                continue
-            if len(path) != len(path_i18n):
-                log.error("inconsistency between menu paths")
-                continue
-            menu_name = path[0]
-            menu_name_i18n = path_i18n[0]
-            item_name = path[1:]
-            if not item_name:
-                log.warning("skipping menu with a path of lenght 1 [%s]" % path[0])
-                continue
-            item_name_i18n = ' | '.join(path_i18n[1:])
-            addMenu(menu_name, menu_name_i18n, item_name_i18n, 'plugins', PluginMenuCmd(self.host, action_id))
-
-        # menu items that should be displayed after the automatically added ones
-        addMenu("Contacts", _("Contacts"), _("Manage groups"), 'social', MenuCmd(self, "onManageContactGroups"))
-
-        menus_order.append(None)  # we add separator
-
-        addMenu("Help", _("Help"), _("Social contract"), 'help', MenuCmd(self, "onSocialContract"))
-        addMenu("Help", _("Help"), _("About"), 'help', MenuCmd(self, "onAbout"))
-        addMenu("Settings", _("Settings"), _("Account"), 'settings', MenuCmd(self, "onAccount"))
-        addMenu("Settings", _("Settings"), _("Parameters"), 'settings', MenuCmd(self, "onParameters"))
-
-        # XXX: temporary, will change when a full profile will be managed in SàT
-        addMenu("Settings", _("Settings"), _("Upload avatar"), 'settings', MenuCmd(self, "onAvatarUpload"))
-
-        menubar = LiberviaMenuBar()
-
-        for menu_data in menus_order:
-            if menu_data is None:
-                _separator = MenuItem('', None)
-                _separator.setStyleName('menuSeparator')
-                menubar.addItem(_separator, None)
-            else:
-                menu_name, menu_name_i18n, icon = menu_data
-                menubar.addItem(MenuItem(_item_tpl % (icon, menu_name_i18n), True, menus_dict[menu_name]))
-
-        self.add(menubar)
-
-    #General menu
-    def onWebWidget(self):
-        web_panel = panels.WebPanel(self.host, "http://www.goffi.org")
-        self.host.addWidget(web_panel)
-        self.host.setSelected(web_panel)
-
-    def onDisconnect(self):
-        def confirm_cb(answer):
-            if answer:
-                log.info("disconnection")
-                self.host.bridge.call('disconnect', None)
-        _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to disconnect ?")
-        _dialog.show()
-
-    def onSocialContract(self):
-        _frame = Frame('contrat_social.html')
-        _frame.setStyleName('infoFrame')
-        _dialog = dialog.GenericDialog("Contrat Social", _frame)
-        _dialog.setSize('80%', '80%')
-        _dialog.show()
-
-    def onAbout(self):
-        _about = HTML("""<b>Libervia</b>, a Salut &agrave; Toi project<br />
-<br />
-You can contact the author at <a href="mailto:goffi@goffi.org">goffi@goffi.org</a><br />
-Blog available (mainly in french) at <a href="http://www.goffi.org" target="_blank">http://www.goffi.org</a><br />
-Project page: <a href="http://sat.goffi.org"target="_blank">http://sat.goffi.org</a><br />
-<br />
-Any help welcome :)
-<p style='font-size:small;text-align:center'>This project is dedicated to Roger Poisson</p>
-""")
-        _dialog = dialog.GenericDialog("About", _about)
-        _dialog.show()
-
-    #Contact menu
-    def onManageContactGroups(self):
-        """Open the contact groups manager."""
-
-        def onCloseCallback():
-            pass
-
-        ContactGroupEditor(self.host, None, onCloseCallback)
-
-    #Group menu
-    def onJoinRoom(self):
-
-        def invite(room_jid, contacts):
-            for contact in contacts:
-                self.host.bridge.call('inviteMUC', None, contact, room_jid)
-
-        def join(room_jid, contacts):
-            if self.host.whoami:
-                nick = self.host.whoami.node
-                if room_jid not in [room.bare for room in self.host.room_list]:
-                    self.host.bridge.call('joinMUC', lambda room_jid: invite(room_jid, contacts), room_jid, nick)
-                else:
-                    self.host.getOrCreateLiberviaWidget(panels.ChatPanel, (room_jid, "group"), True, JID(room_jid).bare)
-                    invite(room_jid, contacts)
-
-        dialog.RoomAndContactsChooser(self.host, join, ok_button="Join", visible=(True, False))
-
-    def onCollectiveRadio(self):
-        def callback(room_jid, contacts):
-            self.host.bridge.call('launchRadioCollective', None, contacts, room_jid)
-        dialog.RoomAndContactsChooser(self.host, callback, ok_button="Choose", title="Collective Radio", visible=(False, True))
-
-    #Game menu
-    def onTarotGame(self):
-        def onPlayersSelected(room_jid, other_players):
-            self.host.bridge.call('launchTarotGame', None, other_players, room_jid)
-        dialog.RoomAndContactsChooser(self.host, onPlayersSelected, 3, title="Tarot", title_invite="Please select 3 other players", visible=(False, True))
-
-    def onXiangqiGame(self):
-        Window.alert("A Xiangqi game is planed, but not available yet")
-
-    #Settings menu
-
-    def onAccount(self):
-        def gotUI(xmlui):
-            if not xmlui:
-                return
-            body = XMLUI(self.host, xmlui)
-            _dialog = dialog.GenericDialog("Manage your XMPP account", body, options=['NO_CLOSE'])
-            body.setCloseCb(_dialog.close)
-            _dialog.show()
-        self.host.bridge.call('getAccountDialogUI', gotUI)
-
-    def onParameters(self):
-        def gotParams(xmlui):
-            if not xmlui:
-                return
-            body = XMLUI(self.host, xmlui)
-            _dialog = dialog.GenericDialog("Parameters", body, options=['NO_CLOSE'])
-            body.setCloseCb(_dialog.close)
-            _dialog.setSize('80%', '80%')
-            _dialog.show()
-        self.host.bridge.call('getParamsUI', gotParams)
-
-    def removeItemParams(self):
-        """Remove the Parameters item from the Settings menu bar."""
-        self.menu_settings.removeItem(self.item_params)
-
-    def onAvatarUpload(self):
-        body = AvatarUpload()
-        _dialog = dialog.GenericDialog("Avatar upload", body, options=['NO_CLOSE'])
-        body.setCloseCb(_dialog.close)
-        _dialog.setWidth('40%')
-        _dialog.show()
--- a/browser_side/nativedom.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,108 +0,0 @@
-#!/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/>.
-
-"""
-This class provide basic DOM parsing based on native javascript parser
-__init__ code comes from Tim Down at http://stackoverflow.com/a/8412989
-"""
-
-from __pyjamas__ import JS
-
-
-class Node():
-
-    def __init__(self, js_node):
-        self._node = js_node
-
-    def _jsNodesList2List(self, js_nodes_list):
-        ret=[]
-        for i in range(len(js_nodes_list)):
-            #ret.append(Element(js_nodes_list.item(i)))
-            ret.append(self.__class__(js_nodes_list.item(i))) # XXX: Ugly, but used to word around a Pyjamas's bug
-        return ret
-
-    @property
-    def nodeName(self):
-        return self._node.nodeName
-
-    @property
-    def wholeText(self):
-        return self._node.wholeText
-
-    @property
-    def childNodes(self):
-        return self._jsNodesList2List(self._node.childNodes)
-
-    def getAttribute(self, attr):
-        return self._node.getAttribute(attr)
-
-    def setAttribute(self, attr, value):
-        return self._node.setAttribute(attr, value)
-
-    def hasAttribute(self, attr):
-        return self._node.hasAttribute(attr)
-
-    def toxml(self):
-        return JS("""this._node.outerHTML || new XMLSerializer().serializeToString(this._node);""")
-
-
-class Element(Node):
-
-    def __init__(self, js_node):
-        Node.__init__(self, js_node)
-
-    def getElementsByTagName(self, tagName):
-        return self._jsNodesList2List(self._node.getElementsByTagName(tagName))
-
-
-class Document(Node):
-
-    def __init__(self, js_document):
-        Node.__init__(self, js_document)
-
-    @property
-    def documentElement(self):
-        return Element(self._node.documentElement)
-
-
-class NativeDOM:
-
-    def __init__(self):
-        JS("""
-
-        if (typeof window.DOMParser != "undefined") {
-            this.parseXml = function(xmlStr) {
-                return ( new window.DOMParser() ).parseFromString(xmlStr, "text/xml");
-            };
-        } else if (typeof window.ActiveXObject != "undefined" &&
-               new window.ActiveXObject("Microsoft.XMLDOM")) {
-            this.parseXml = function(xmlStr) {
-                var xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM");
-                xmlDoc.async = "false";
-                xmlDoc.loadXML(xmlStr);
-                return xmlDoc;
-            };
-        } else {
-            throw new Error("No XML parser found");
-        }
-        """)
-
-    def parseString(self, xml):
-        return Document(self.parseXml(xml))
-
--- a/browser_side/notification.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,120 +0,0 @@
-from __pyjamas__ import JS, wnd
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from pyjamas import Window
-from pyjamas.Timer import Timer
-from browser_side import dialog
-from sat.core.i18n import _
-
-TIMER_DELAY = 5000
-
-
-class Notification(object):
-    """
-    If the browser supports it, the user allowed it to and the tab is in the
-    background, send desktop notifications on messages.
-
-    Requires both Web Notifications and Page Visibility API.
-    """
-
-    def __init__(self):
-        self.enabled = False
-        user_agent = None
-        notif_permission = None
-        JS("""
-        if (!('hidden' in document))
-            document.hidden = false;
-
-        user_agent = navigator.userAgent
-
-        if (!('Notification' in window))
-            return;
-
-        notif_permission = Notification.permission
-
-        if (Notification.permission === 'granted')
-            this.enabled = true;
-
-        else if (Notification.permission === 'default') {
-            Notification.requestPermission(function(permission){
-                if (permission !== 'granted')
-                    return;
-
-                self.enabled = true; //need to use self instead of this
-            });
-        }
-        """)
-
-        if "Chrome" in user_agent and notif_permission not in ['granted', 'denied']:
-            self.user_agent = user_agent
-            self._installChromiumWorkaround()
-
-        wnd().onfocus = self.onFocus
-        # wnd().onblur = self.onBlur
-        self._notif_count = 0
-        self._orig_title = Window.getTitle()
-
-    def _installChromiumWorkaround(self):
-        # XXX: Workaround for Chromium behaviour, it's doens't manage requestPermission on onLoad event
-        # see https://code.google.com/p/chromium/issues/detail?id=274284
-        # FIXME: need to be removed if Chromium behaviour changes
-        try:
-            version_full = [s for s in self.user_agent.split() if "Chrome" in s][0].split('/')[1]
-            version = int(version_full.split('.')[0])
-        except (IndexError, ValueError):
-            log.warning("Can't find Chromium version")
-            version = 0
-        log.info("Chromium version: %d" % (version,))
-        if version < 22:
-            log.info("Notification use the old prefixed version or are unmanaged")
-            return
-        if version < 32:
-            dialog.InfoDialog(_("Notifications activation for Chromium"), _('You need to activate notifications manually for your Chromium version.<br/>To activate notifications, click on the favicon on the left of the address bar')).show()
-            return
-
-        log.info("==> Installing Chromium notifications request workaround <==")
-        self._old_click = wnd().onclick
-        wnd().onclick = self._chromiumWorkaround
-
-    def _chromiumWorkaround(self):
-        log.info("Activating workaround")
-        JS("""
-            Notification.requestPermission(function(permission){
-                if (permission !== 'granted')
-                    return;
-                self.enabled = true; //need to use self instead of this
-            });
-        """)
-        wnd().onclick = self._old_click
-
-    def onFocus(self):
-        Window.setTitle(self._orig_title)
-        self._notif_count = 0
-
-    # def onBlur(self):
-    #     pass
-
-    def isHidden(self):
-        JS("""return document.hidden;""")
-
-    def _notify(self, title, body, icon):
-        if not self.enabled:
-            return
-        notification = None
-        JS("""
-           notification = new Notification(title, {body: body, icon: icon});
-           // Probably won’t work, but it doesn’t hurt to try.
-           notification.addEventListener('click', function() {
-               window.focus();
-           });
-           """)
-        notification.onshow = lambda: Timer(TIMER_DELAY, lambda timer: notification.close())
-
-    def highlightTab(self):
-        self._notif_count += 1
-        Window.setTitle("%s (%d)" % (self._orig_title, self._notif_count))
-
-    def notify(self, title, body, icon='/media/icons/apps/48/sat.png'):
-        if self.isHidden():
-            self._notify(title, body, icon)
-            self.highlightTab()
--- a/browser_side/panels.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1408 +0,0 @@
-#!/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 pyjamas.ui.SimplePanel import SimplePanel
-from pyjamas.ui.AbsolutePanel import AbsolutePanel
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.HTMLPanel import HTMLPanel
-from pyjamas.ui.Frame import Frame
-from pyjamas.ui.TextArea import TextArea
-from pyjamas.ui.Label import Label
-from pyjamas.ui.Button import Button
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.Image import Image
-from pyjamas.ui.ClickListener import ClickHandler
-from pyjamas.ui.FlowPanel import FlowPanel
-from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN, KeyboardHandler
-from pyjamas.ui.MouseListener import MouseHandler
-from pyjamas.ui.FocusListener import FocusHandler
-from pyjamas.Timer import Timer
-from pyjamas import DOM
-from pyjamas import Window
-from __pyjamas__ import doc
-
-from datetime import datetime
-from time import time
-from jid import JID
-
-from html_tools import html_sanitize
-from base_panels import ChatText, OccupantsList, PopupMenuPanel, BaseTextEditor, LightTextEditor, HTMLTextEditor
-from card_game import CardPanel
-from radiocol import RadioColPanel
-from menu import Menu
-from browser_side import dialog
-from browser_side import base_widget
-from browser_side import richtext
-from browser_side import contact
-
-from constants import Const as C
-from plugin_xep_0085 import ChatStateMachine
-from sat_frontends.tools.strings import addURLToText
-from sat_frontends.tools.games import SYMBOLS
-from sat.core.i18n import _
-
-
-# TODO: at some point we should decide which behaviors to keep and remove these two constants
-TOGGLE_EDITION_USE_ICON = False  # set to True to use an icon inside the "toggle syntax" button
-NEW_MESSAGE_USE_BUTTON = False  # set to True to display the "New message" button instead of an empty entry
-
-
-class UniBoxPanel(HorizontalPanel):
-    """Panel containing the UniBox"""
-
-    def __init__(self, host):
-        HorizontalPanel.__init__(self)
-        self.host = host
-        self.setStyleName('uniBoxPanel')
-        self.unibox = None
-
-    def refresh(self):
-        """Enable or disable this panel. Contained widgets are created when necessary."""
-        enable = self.host.params_ui['unibox']['value']
-        self.setVisible(enable)
-        if enable and not self.unibox:
-            self.button = Button('<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>')
-            self.button.setTitle('Open the rich text editor')
-            self.button.addStyleName('uniBoxButton')
-            self.add(self.button)
-            self.unibox = UniBox(self.host)
-            self.add(self.unibox)
-            self.setCellWidth(self.unibox, '100%')
-            self.button.addClickListener(self.openRichMessageEditor)
-            self.unibox.addKey("@@: ")
-            self.unibox.onSelectedChange(self.host.getSelected())
-
-    def openRichMessageEditor(self):
-        """Open the rich text editor."""
-        self.button.setVisible(False)
-        self.unibox.setVisible(False)
-        self.setCellWidth(self.unibox, '0px')
-        self.host.panel._contactsMove(self)
-
-        def afterEditCb():
-            Window.removeWindowResizeListener(self)
-            self.host.panel._contactsMove(self.host.panel._hpanel)
-            self.setCellWidth(self.unibox, '100%')
-            self.button.setVisible(True)
-            self.unibox.setVisible(True)
-            self.host.resize()
-
-        richtext.RichMessageEditor.getOrCreate(self.host, self, afterEditCb)
-        Window.addWindowResizeListener(self)
-        self.host.resize()
-
-    def onWindowResized(self, width, height):
-        right = self.host.panel.menu.getAbsoluteLeft() + self.host.panel.menu.getOffsetWidth()
-        left = self.host.panel._contacts.getAbsoluteLeft() + self.host.panel._contacts.getOffsetWidth()
-        ideal_width = right - left - 40
-        self.host.richtext.setWidth("%spx" % ideal_width)
-
-
-class MessageBox(TextArea):
-    """A basic text area for entering messages"""
-
-    def __init__(self, host):
-        TextArea.__init__(self)
-        self.host = host
-        self.__size = (0, 0)
-        self.setStyleName('messageBox')
-        self.addKeyboardListener(self)
-        MouseHandler.__init__(self)
-        self.addMouseListener(self)
-        self._selected_cache = None
-
-    def onBrowserEvent(self, event):
-        # XXX: woraroung a pyjamas bug: self.currentEvent is not set
-        #     so the TextBox's cancelKey doens't work. This is a workaround
-        #     FIXME: fix the bug upstream
-        self.currentEvent = event
-        TextArea.onBrowserEvent(self, event)
-
-    def onKeyPress(self, sender, keycode, modifiers):
-        _txt = self.getText()
-
-        def history_cb(text):
-            self.setText(text)
-            Timer(5, lambda timer: self.setCursorPos(len(text)))
-
-        if keycode == KEY_ENTER:
-            if _txt:
-                self._selected_cache.onTextEntered(_txt)
-                self.host._updateInputHistory(_txt)
-            self.setText('')
-            sender.cancelKey()
-        elif keycode == KEY_UP:
-            self.host._updateInputHistory(_txt, -1, history_cb)
-        elif keycode == KEY_DOWN:
-            self.host._updateInputHistory(_txt, +1, history_cb)
-        else:
-            self.__onComposing()
-
-    def __onComposing(self):
-        """Callback when the user is composing a text."""
-        if hasattr(self._selected_cache, "target"):
-            self._selected_cache.state_machine._onEvent("composing")
-
-    def onMouseUp(self, sender, x, y):
-        size = (self.getOffsetWidth(), self.getOffsetHeight())
-        if size != self.__size:
-            self.__size = size
-            self.host.resize()
-
-    def onSelectedChange(self, selected):
-        self._selected_cache = selected
-
-
-class UniBox(MessageBox, MouseHandler): #AutoCompleteTextBox):
-    """This text box is used as a main typing point, for message, microblog, etc"""
-
-    def __init__(self, host):
-        MessageBox.__init__(self, host)
-        #AutoCompleteTextBox.__init__(self)
-        self.setStyleName('uniBox')
-        host.addSelectedListener(self.onSelectedChange)
-
-    def addKey(self, key):
-        return
-        #self.getCompletionItems().completions.append(key)
-
-    def removeKey(self, key):
-        return
-        # TODO: investigate why AutoCompleteTextBox doesn't work here,
-        # maybe it can work on a TextBox but no TextArea. Remove addKey
-        # and removeKey methods if they don't serve anymore.
-        try:
-            self.getCompletionItems().completions.remove(key)
-        except KeyError:
-            log.warning("trying to remove an unknown key")
-
-    def _getTarget(self, txt):
-        """ Say who will receive the messsage
-        @return: a tuple (selected, target_type, target info) with:
-            - target_hook: None if we use the selected widget, (msg, data) if we have a hook (e.g. "@@: " for a public blog), where msg is the parsed message (i.e. without the "hook key: "@@: bla" become ("bla", None))
-            - target_type: one of PUBLIC, GROUP, ONE2ONE, STATUS, MISC
-            - msg: HTML message which will appear in the privacy warning banner """
-        target = self._selected_cache
-
-        def getSelectedOrStatus():
-            if target and target.isSelectable():
-                _type, msg = target.getWarningData()
-                target_hook = None  # we use the selected widget, not a hook
-            else:
-                _type, msg = "STATUS", "This will be your new status message"
-                target_hook = (txt, None)
-            return (target_hook, _type, msg)
-
-        if not txt.startswith('@'):
-            target_hook, _type, msg = getSelectedOrStatus()
-        elif txt.startswith('@@: '):
-            _type = "PUBLIC"
-            msg = MicroblogPanel.warning_msg_public
-            target_hook = (txt[4:], None)
-        elif txt.startswith('@'):
-            _end = txt.find(': ')
-            if _end == -1:
-                target_hook, _type, msg = getSelectedOrStatus()
-            else:
-                group = txt[1:_end]  # only one target group is managed for the moment
-                if not group or not group in self.host.contact_panel.getGroups():
-                    # the group doesn't exists, we ignore the key
-                    group = None
-                    target_hook, _type, msg = getSelectedOrStatus()
-                else:
-                    _type = "GROUP"
-                    msg = MicroblogPanel.warning_msg_group % group
-                    target_hook = (txt[_end + 2:], group)
-        else:
-            log.error("Unknown target")
-            target_hook, _type, msg = getSelectedOrStatus()
-
-        return (target_hook, _type, msg)
-
-    def onKeyPress(self, sender, keycode, modifiers):
-        _txt = self.getText()
-        target_hook, type_, msg = self._getTarget(_txt)
-
-        if keycode == KEY_ENTER:
-            if _txt:
-                if target_hook:
-                    parsed_txt, data = target_hook
-                    self.host.send([(type_, data)], parsed_txt)
-                    self.host._updateInputHistory(_txt)
-                    self.setText('')
-            self.host.showWarning(None, None)
-        else:
-            self.host.showWarning(type_, msg)
-        MessageBox.onKeyPress(self, sender, keycode, modifiers)
-
-    def getTargetAndData(self):
-        """For external use, to get information about the (hypothetical) message
-        that would be sent if we press Enter right now in the unibox.
-        @return a tuple (target, data) with:
-          - data: what would be the content of the message (body)
-          - target: JID, group with the prefix "@" or the public entity "@@"
-        """
-        _txt = self.getText()
-        target_hook, _type, _msg = self._getTarget(_txt)
-        if target_hook:
-            data, target = target_hook
-            if target is None:
-                return target_hook
-            return (data, "@%s" % (target if target != "" else "@"))
-        if isinstance(self._selected_cache, MicroblogPanel):
-            groups = self._selected_cache.accepted_groups
-            target = "@%s" % (groups[0] if len(groups) > 0 else "@")
-            if len(groups) > 1:
-                Window.alert("Sole the first group of the selected panel is taken in consideration: '%s'" % groups[0])
-        elif isinstance(self._selected_cache, ChatPanel):
-            target = self._selected_cache.target
-        else:
-            target = None
-        return (_txt, target)
-
-    def onWidgetClosed(self, lib_wid):
-        """Called when a libervia widget is closed"""
-        if self._selected_cache == lib_wid:
-            self.onSelectedChange(None)
-
-    """def complete(self):
-
-        #self.visible=False #XXX: self.visible is not unset in pyjamas when ENTER is pressed and a completion is done
-        #XXX: fixed directly on pyjamas, if the patch is accepted, no need to walk around this
-        return AutoCompleteTextBox.complete(self)"""
-
-
-class WarningPopup():
-
-    def __init__(self):
-        self._popup = None
-        self._timer = Timer(notify=self._timeCb)
-
-    def showWarning(self, type_=None, msg=None, duration=2000):
-        """Display a popup information message, e.g. to notify the recipient of a message being composed.
-        If type_ is None, a popup being currently displayed will be hidden.
-        @type_: a type determining the CSS style to be applied (see __showWarning)
-        @msg: message to be displayed
-        """
-        if type_ is None:
-            self.__removeWarning()
-            return
-        if not self._popup:
-            self.__showWarning(type_, msg)
-        elif (type_, msg) != self._popup.target_data:
-            self._timeCb(None)  # we remove the popup
-            self.__showWarning(type_, msg)
-
-        self._timer.schedule(duration)
-
-    def __showWarning(self, type_, msg):
-        """Display a popup information message, e.g. to notify the recipient of a message being composed.
-        @type_: a type determining the CSS style to be applied. For now the defined styles are
-        "NONE" (will do nothing), "PUBLIC", "GROUP", "STATUS" and "ONE2ONE".
-        @msg: message to be displayed
-        """
-        if type_ == "NONE":
-            return
-        if not msg:
-            log.warning("no msg set uniBox warning")
-            return
-        if type_ == "PUBLIC":
-            style = "targetPublic"
-        elif type_ == "GROUP":
-            style = "targetGroup"
-        elif type_ == "STATUS":
-            style = "targetStatus"
-        elif type_ == "ONE2ONE":
-            style = "targetOne2One"
-        else:
-            log.error("unknown message type")
-            return
-        contents = HTML(msg)
-
-        self._popup = dialog.PopupPanelWrapper(autoHide=False, modal=False)
-        self._popup.target_data = (type_, msg)
-        self._popup.add(contents)
-        self._popup.setStyleName("warningPopup")
-        if style:
-            self._popup.addStyleName(style)
-
-        left = 0
-        top = 0  # max(0, self.getAbsoluteTop() - contents.getOffsetHeight() - 2)
-        self._popup.setPopupPosition(left, top)
-        self._popup.show()
-
-    def _timeCb(self, timer):
-        if self._popup:
-            self._popup.hide()
-            del self._popup
-            self._popup = None
-
-    def __removeWarning(self):
-        """Remove the popup"""
-        self._timeCb(None)
-
-
-class MicroblogItem():
-    # XXX: should be moved in a separated module
-
-    def __init__(self, data):
-        self.id = data['id']
-        self.type = data.get('type', 'main_item')
-        self.empty = data.get('new', False)
-        self.title = data.get('title', '')
-        self.title_xhtml = data.get('title_xhtml', '')
-        self.content = data.get('content', '')
-        self.content_xhtml = data.get('content_xhtml', '')
-        self.author = data['author']
-        self.updated = float(data.get('updated', 0))  # XXX: int doesn't work here
-        self.published = float(data.get('published', self.updated))  # XXX: int doesn't work here
-        self.service = data.get('service', '')
-        self.node = data.get('node', '')
-        self.comments = data.get('comments', False)
-        self.comments_service = data.get('comments_service', '')
-        self.comments_node = data.get('comments_node', '')
-
-
-class MicroblogEntry(SimplePanel, ClickHandler, FocusHandler, KeyboardHandler):
-
-    def __init__(self, blog_panel, data):
-        """
-        @param blog_panel: the parent panel
-        @param data: dict containing the blog item data, or a MicroblogItem instance.
-        """
-        self._base_item = data if isinstance(data, MicroblogItem) else MicroblogItem(data)
-        for attr in ['id', 'type', 'empty', 'title', 'title_xhtml', 'content', 'content_xhtml',
-                     'author', 'updated', 'published', 'comments', 'service', 'node',
-                     'comments_service', 'comments_node']:
-            getter = lambda attr: lambda inst: getattr(inst._base_item, attr)
-            setter = lambda attr: lambda inst, value: setattr(inst._base_item, attr, value)
-            setattr(MicroblogEntry, attr, property(getter(attr), setter(attr)))
-
-        SimplePanel.__init__(self)
-        self._blog_panel = blog_panel
-
-        self.panel = FlowPanel()
-        self.panel.setStyleName('mb_entry')
-
-        self.header = HTMLPanel('')
-        self.panel.add(self.header)
-
-        self.entry_actions = VerticalPanel()
-        self.entry_actions.setStyleName('mb_entry_actions')
-        self.panel.add(self.entry_actions)
-
-        entry_avatar = SimplePanel()
-        entry_avatar.setStyleName('mb_entry_avatar')
-        self.avatar = Image(self._blog_panel.host.getAvatar(self.author))
-        entry_avatar.add(self.avatar)
-        self.panel.add(entry_avatar)
-
-        if TOGGLE_EDITION_USE_ICON:
-            self.entry_dialog = HorizontalPanel()
-        else:
-            self.entry_dialog = VerticalPanel()
-        self.entry_dialog.setStyleName('mb_entry_dialog')
-        self.panel.add(self.entry_dialog)
-
-        self.add(self.panel)
-        ClickHandler.__init__(self)
-        self.addClickListener(self)
-
-        self.__pub_data = (self.service, self.node, self.id)
-        self.__setContent()
-
-    def __setContent(self):
-        """Actually set the entry content (header, icons, bubble...)"""
-        self.delete_label = self.update_label = self.comment_label = None
-        self.bubble = self._current_comment = None
-        self.__setHeader()
-        self.__setBubble()
-        self.__setIcons()
-
-    def __setHeader(self):
-        """Set the entry header"""
-        if self.empty:
-            return
-        update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated)
-        self.header.setHTML("""<div class='mb_entry_header'>
-                                   <span class='mb_entry_author'>%(author)s</span> on
-                                   <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s
-                               </div>""" % {'author': html_sanitize(self.author),
-                                            'published': datetime.fromtimestamp(self.published),
-                                            'updated': update_text if self.published != self.updated else ''
-                                            }
-                            )
-
-    def __setIcons(self):
-        """Set the entry icons (delete, update, comment)"""
-        if self.empty:
-            return
-
-        def addIcon(label, title):
-            label = Label(label)
-            label.setTitle(title)
-            label.addClickListener(self)
-            self.entry_actions.add(label)
-            return label
-
-        if self.comments:
-            self.comment_label = addIcon(u"↶", "Comment this message")
-            self.comment_label.setStyleName('mb_entry_action_larger')
-        is_publisher = self.author == self._blog_panel.host.whoami.bare
-        if is_publisher:
-            self.update_label = addIcon(u"✍", "Edit this message")
-        if is_publisher or str(self.node).endswith(self._blog_panel.host.whoami.bare):
-            self.delete_label = addIcon(u"✗", "Delete this message")
-
-    def updateAvatar(self, new_avatar):
-        """Change the avatar of the entry
-        @param new_avatar: path to the new image"""
-        self.avatar.setUrl(new_avatar)
-
-    def onClick(self, sender):
-        if sender == self:
-            try:  # prevent re-selection of the main entry after a comment has been focused
-                if self.__ignoreNextEvent:
-                    self.__ignoreNextEvent = False
-                    return
-            except AttributeError:
-                pass
-            self._blog_panel.setSelectedEntry(self)
-        elif sender == self.delete_label:
-            self._delete()
-        elif sender == self.update_label:
-            self.edit(True)
-        elif sender == self.comment_label:
-            self.__ignoreNextEvent = True
-            self._comment()
-
-    def __modifiedCb(self, content):
-        """Send the new content to the backend
-        @return: False to restore the original content if a deletion has been cancelled
-        """
-        if not content['text']:  # previous content has been emptied
-            self._delete(True)
-            return False
-        extra = {'published': str(self.published)}
-        if isinstance(self.bubble, richtext.RichTextEditor):
-            # TODO: if the user change his parameters after the message edition started,
-            # the message syntax could be different then the current syntax: pass the
-            # message syntax in extra for the frontend to use it instead of current syntax.
-            extra.update({'content_rich': content['text'], 'title': content['title']})
-        if self.empty:
-            if self.type == 'main_item':
-                self._blog_panel.host.bridge.call('sendMblog', None, None, self._blog_panel.accepted_groups, content['text'], extra)
-            else:
-                self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, content['text'], extra)
-        else:
-            self._blog_panel.host.bridge.call('updateMblog', None, self.__pub_data, self.comments, content['text'], extra)
-        return True
-
-    def __afterEditCb(self, content):
-        """Remove the entry if it was an empty one (used for creating a new blog post).
-        Data for the actual new blog post will be received from the bridge"""
-        if self.empty:
-            self._blog_panel.removeEntry(self.type, self.id)
-            if self.type == 'main_item':  # restore the "New message" button
-                self._blog_panel.refresh()
-            else:  # allow to create a new comment
-                self._parent_entry._current_comment = None
-        self.entry_dialog.setWidth('auto')
-        try:
-            self.toggle_syntax_button.removeFromParent()
-        except TypeError:
-            pass
-
-    def __setBubble(self, edit=False):
-        """Set the bubble displaying the initial content."""
-        content = {'text': self.content_xhtml if self.content_xhtml else self.content,
-                   'title': self.title_xhtml if self.title_xhtml else self.title}
-        if self.content_xhtml:
-            content.update({'syntax': C.SYNTAX_XHTML})
-            if self.author != self._blog_panel.host.whoami.bare:
-                options = ['read_only']
-            else:
-                options = [] if self.empty else ['update_msg']
-            self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options)
-        else:  # assume raw text message have no title
-            self.bubble = LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True})
-        self.bubble.addStyleName("bubble")
-        try:
-            self.toggle_syntax_button.removeFromParent()
-        except TypeError:
-            pass
-        self.entry_dialog.add(self.bubble)
-        self.edit(edit)
-        self.bubble.addEditListener(self.__showWarning)
-
-    def __showWarning(self, sender, keycode):
-        if keycode == KEY_ENTER:
-            self._blog_panel.host.showWarning(None, None)
-        else:
-            self._blog_panel.host.showWarning(*self._blog_panel.getWarningData(self.type == 'comment'))
-
-    def _delete(self, empty=False):
-        """Ask confirmation for deletion.
-        @return: False if the deletion has been cancelled."""
-        def confirm_cb(answer):
-            if answer:
-                self._blog_panel.host.bridge.call('deleteMblog', None, self.__pub_data, self.comments)
-            else:  # restore the text if it has been emptied during the edition
-                self.bubble.setContent(self.bubble._original_content)
-
-        if self.empty:
-            text = _("New ") + (_("message") if self.comments else _("comment")) + _(" without body has been cancelled.")
-            dialog.InfoDialog(_("Information"), text).show()
-            return
-        text = ""
-        if empty:
-            text = (_("Message") if self.comments else _("Comment")) + _(" body has been emptied.<br/>")
-        target = _('message and all its comments') if self.comments else _('comment')
-        text += _("Do you really want to delete this %s?") % target
-        dialog.ConfirmDialog(confirm_cb, text=text).show()
-
-    def _comment(self):
-        """Add an empty entry for a new comment"""
-        if self._current_comment:
-            self._current_comment.bubble.setFocus(True)
-            self._blog_panel.setSelectedEntry(self._current_comment)
-            return
-        data = {'id': str(time()),
-                'new': True,
-                'type': 'comment',
-                'author': self._blog_panel.host.whoami.bare,
-                'service': self.comments_service,
-                'node': self.comments_node
-                }
-        entry = self._blog_panel.addEntry(data)
-        if entry is None:
-            log.info("The entry of id %s can not be commented" % self.id)
-            return
-        entry._parent_entry = self
-        self._current_comment = entry
-        self.edit(True, entry)
-        self._blog_panel.setSelectedEntry(entry)
-
-    def edit(self, edit, entry=None):
-        """Toggle the bubble between display and edit mode
-        @edit: boolean value
-        @entry: MicroblogEntry instance, or None to use self
-        """
-        if entry is None:
-            entry = self
-        try:
-            entry.toggle_syntax_button.removeFromParent()
-        except TypeError:
-            pass
-        entry.bubble.edit(edit)
-        if edit:
-            if isinstance(entry.bubble, richtext.RichTextEditor):
-                image = '<a class="richTextIcon">A</a>'
-                html = '<a style="color: blue;">raw text</a>'
-                title = _('Switch to raw text edition')
-            else:
-                image = '<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>'
-                html = '<a style="color: blue;">rich text</a>'
-                title = _('Switch to rich text edition')
-            if TOGGLE_EDITION_USE_ICON:
-                entry.entry_dialog.setWidth('80%')
-                entry.toggle_syntax_button = Button(image, entry.toggleContentSyntax)
-                entry.toggle_syntax_button.setTitle(title)
-                entry.entry_dialog.add(entry.toggle_syntax_button)
-            else:
-                entry.toggle_syntax_button = HTML(html)
-                entry.toggle_syntax_button.addClickListener(entry.toggleContentSyntax)
-                entry.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax')
-                entry.entry_dialog.add(entry.toggle_syntax_button)
-                entry.toggle_syntax_button.setStyleAttribute('top', '-20px')  # XXX: need to force CSS
-                entry.toggle_syntax_button.setStyleAttribute('left', '-20px')
-
-    def toggleContentSyntax(self):
-        """Toggle the editor between raw and rich text"""
-        original_content = self.bubble.getOriginalContent()
-        rich = not isinstance(self.bubble, richtext.RichTextEditor)
-        if rich:
-            original_content['syntax'] = C.SYNTAX_XHTML
-
-        def setBubble(text):
-            self.content = text
-            self.content_xhtml = text if rich else ''
-            self.content_title = self.content_title_xhtml = ''
-            self.bubble.removeFromParent()
-            self.__setBubble(True)
-            self.bubble.setOriginalContent(original_content)
-            if rich:
-                self.bubble.setDisplayContent()  # needed in case the edition is aborted, to not end with an empty bubble
-
-        text = self.bubble.getContent()['text']
-        if not text:
-            setBubble(' ')  # something different than empty string is needed to initialize the rich text editor
-            return
-        if not rich:
-            def confirm_cb(answer):
-                if answer:
-                    self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT)
-            dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show()
-        else:
-            self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML)
-
-
-class MicroblogPanel(base_widget.LiberviaWidget):
-    warning_msg_public = "This message will be PUBLIC and everybody will be able to see it, even people you don't know"
-    warning_msg_group = "This message will be published for all the people of the group <span class='warningTarget'>%s</span>"
-
-    def __init__(self, host, accepted_groups):
-        """Panel used to show microblog
-        @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts
-        """
-        base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True)
-        self.setAcceptedGroup(accepted_groups)
-        self.host = host
-        self.entries = {}
-        self.comments = {}
-        self.selected_entry = None
-        self.vpanel = VerticalPanel()
-        self.vpanel.setStyleName('microblogPanel')
-        self.setWidget(self.vpanel)
-
-    def refresh(self):
-        """Refresh the display of this widget. If the unibox is disabled,
-        display the 'New message' button or an empty bubble on top of the panel"""
-        if hasattr(self, 'new_button'):
-            self.new_button.setVisible(self.host.uni_box is None)
-            return
-        if self.host.uni_box is None:
-            def addBox():
-                if hasattr(self, 'new_button'):
-                    self.new_button.setVisible(False)
-                data = {'id': str(time()),
-                        'new': True,
-                        'author': self.host.whoami.bare,
-                        }
-                entry = self.addEntry(data)
-                entry.edit(True)
-            if NEW_MESSAGE_USE_BUTTON:
-                self.new_button = Button("New message", listener=addBox)
-                self.new_button.setStyleName("microblogNewButton")
-                self.vpanel.insert(self.new_button, 0)
-            else:
-                addBox()
-
-    @classmethod
-    def registerClass(cls):
-        base_widget.LiberviaWidget.addDropKey("GROUP", cls.createPanel)
-        base_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", cls.createMetaPanel)
-
-    @classmethod
-    def createPanel(cls, host, item):
-        """Generic panel creation for one, several or all groups (meta).
-        @parem host: the SatWebFrontend instance
-        @param item: single group as a string, list of groups
-         (as an array) or None (for the meta group = "all groups")
-        @return: the created MicroblogPanel
-        """
-        _items = item if isinstance(item, list) else ([] if item is None else [item])
-        _type = 'ALL' if _items == [] else 'GROUP'
-        # XXX: pyjamas doesn't support use of cls directly
-        _new_panel = MicroblogPanel(host, _items)
-        host.FillMicroblogPanel(_new_panel)
-        host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, _type, _items, 10)
-        host.setSelected(_new_panel)
-        _new_panel.refresh()
-        return _new_panel
-
-    @classmethod
-    def createMetaPanel(cls, host, item):
-        """Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group"""
-        return MicroblogPanel.createPanel(host, None)
-
-    @property
-    def accepted_groups(self):
-        return self._accepted_groups
-
-    def matchEntity(self, entity):
-        """
-        @param entity: single group as a string, list of groups
-        (as an array) or None (for the meta group = "all groups")
-        @return: True if self matches the given entity
-        """
-        entity = entity if isinstance(entity, list) else ([] if entity is None else [entity])
-        entity.sort()  # sort() do not return the sorted list: do it here, not on the "return" line
-        return self.accepted_groups == entity
-
-    def getWarningData(self, comment=None):
-        """
-        @param comment: True if the composed message is a comment. If None, consider we are
-        composing from the unibox and guess the message type from self.selected_entry
-        @return: a couple (type, msg) for calling self.host.showWarning"""
-        if comment is None:  # composing from the unibox
-            if self.selected_entry and not self.selected_entry.comments:
-                log.error("an item without comment is selected")
-                return ("NONE", None)
-            comment = self.selected_entry is not None
-        if comment:
-            return ("PUBLIC", "This is a <span class='warningTarget'>comment</span> and keep the initial post visibility, so it is potentialy public")
-        elif not self._accepted_groups:
-            # we have a meta MicroblogPanel, we publish publicly
-            return ("PUBLIC", self.warning_msg_public)
-        else:
-            # we only accept one group at the moment
-            # FIXME: manage several groups
-            return ("GROUP", self.warning_msg_group % self._accepted_groups[0])
-
-    def onTextEntered(self, text):
-        if self.selected_entry:
-            # we are entering a comment
-            comments_url = self.selected_entry.comments
-            if not comments_url:
-                raise Exception("ERROR: the comments URL is empty")
-            target = ("COMMENT", comments_url)
-        elif not self._accepted_groups:
-            # we are entering a public microblog
-            target = ("PUBLIC", None)
-        else:
-            # we are entering a microblog restricted to a group
-            # FIXME: manage several groups
-            target = ("GROUP", self._accepted_groups[0])
-        self.host.send([target], text)
-
-    def accept_all(self):
-        return not self._accepted_groups  # we accept every microblog only if we are not filtering by groups
-
-    def getEntries(self):
-        """Ask all the entries for the currenly accepted groups,
-        and fill the panel"""
-
-    def massiveInsert(self, mblogs):
-        """Insert several microblogs at once
-        @param mblogs: dictionary of microblogs, as the result of getMassiveLastGroupBlogs
-        """
-        log.debug("Massive insertion of %d microblogs" % len(mblogs))
-        for publisher in mblogs:
-            log.debug("adding blogs for [%s]" % publisher)
-            for mblog in mblogs[publisher]:
-                if not "content" in mblog:
-                    log.warning("No content found in microblog [%s]", mblog)
-                    continue
-                self.addEntry(mblog)
-
-    def mblogsInsert(self, mblogs):
-        """ Insert several microblogs at once
-        @param mblogs: list of microblogs
-        """
-        for mblog in mblogs:
-            if not "content" in mblog:
-                log.warning("No content found in microblog [%s]", mblog)
-                continue
-            self.addEntry(mblog)
-
-    def _chronoInsert(self, vpanel, entry, reverse=True):
-        """ Insert an entry in chronological order
-        @param vpanel: VerticalPanel instance
-        @param entry: MicroblogEntry
-        @param reverse: more recent entry on top if True, chronological order else"""
-        if entry.empty:
-            entry.published = time()
-        # we look for the right index to insert our entry:
-        # if reversed, we insert the entry above the first entry
-        # in the past
-        idx = 0
-
-        for child in vpanel.children:
-            if not isinstance(child, MicroblogEntry):
-                idx += 1
-                continue
-            if reverse:
-                if child.published < entry.published:
-                    break
-            else:
-                if child.published > entry.published:
-                    break
-            idx += 1
-
-        vpanel.insert(entry, idx)
-
-    def addEntry(self, data):
-        """Add an entry to the panel
-        @param data: dict containing the item data
-        @return: the added entry, or None
-        """
-        _entry = MicroblogEntry(self, data)
-        if _entry.type == "comment":
-            comments_hash = (_entry.service, _entry.node)
-            if not comments_hash in self.comments:
-                # The comments node is not known in this panel
-                return None
-            parent = self.comments[comments_hash]
-            parent_idx = self.vpanel.getWidgetIndex(parent)
-            # we find or create the panel where the comment must be inserted
-            try:
-                sub_panel = self.vpanel.getWidget(parent_idx + 1)
-            except IndexError:
-                sub_panel = None
-            if not sub_panel or not isinstance(sub_panel, VerticalPanel):
-                sub_panel = VerticalPanel()
-                sub_panel.setStyleName('microblogPanel')
-                sub_panel.addStyleName('subPanel')
-                self.vpanel.insert(sub_panel, parent_idx + 1)
-            for idx in xrange(0, len(sub_panel.getChildren())):
-                comment = sub_panel.getIndexedChild(idx)
-                if comment.id == _entry.id:
-                    # update an existing comment
-                    sub_panel.remove(comment)
-                    sub_panel.insert(_entry, idx)
-                    return _entry
-            # we want comments to be inserted in chronological order
-            self._chronoInsert(sub_panel, _entry, reverse=False)
-            return _entry
-
-        if _entry.id in self.entries:  # update
-            idx = self.vpanel.getWidgetIndex(self.entries[_entry.id])
-            self.vpanel.remove(self.entries[_entry.id])
-            self.vpanel.insert(_entry, idx)
-        else:  # new entry
-            self._chronoInsert(self.vpanel, _entry)
-        self.entries[_entry.id] = _entry
-
-        if _entry.comments:
-            # entry has comments, we keep the comments service/node as a reference
-            comments_hash = (_entry.comments_service, _entry.comments_node)
-            self.comments[comments_hash] = _entry
-            self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node)
-
-        return _entry
-
-    def removeEntry(self, type_, id_):
-        """Remove an entry from the panel
-        @param type_: entry type ('main_item' or 'comment')
-        @param id_: entry id
-        """
-        for child in self.vpanel.getChildren():
-            if isinstance(child, MicroblogEntry) and type_ == 'main_item':
-                if child.id == id_:
-                    main_idx = self.vpanel.getWidgetIndex(child)
-                    try:
-                        sub_panel = self.vpanel.getWidget(main_idx + 1)
-                        if isinstance(sub_panel, VerticalPanel):
-                            sub_panel.removeFromParent()
-                    except IndexError:
-                        pass
-                    child.removeFromParent()
-                    self.selected_entry = None
-                    break
-            elif isinstance(child, VerticalPanel) and type_ == 'comment':
-                for comment in child.getChildren():
-                    if comment.id == id_:
-                        comment.removeFromParent()
-                        self.selected_entry = None
-                        break
-
-    def setSelectedEntry(self, entry):
-        try:
-            self.vpanel.getParent().ensureVisible(entry)  # scroll to the clicked entry
-        except AttributeError:
-            log.warning("FIXME: MicroblogPanel.vpanel should be wrapped in a ScrollPanel!")
-        removeStyle = lambda entry: entry.removeStyleName('selected_entry')
-        if not self.host.uni_box or not entry.comments:
-            entry.addStyleName('selected_entry')  # blink the clicked entry
-            clicked_entry = entry  # entry may be None when the timer is done
-            Timer(500, lambda timer: removeStyle(clicked_entry))
-        if not self.host.uni_box:
-            return  # unibox is disabled
-        # from here the previous behavior (toggle main item selection) is conserved
-        entry = entry if entry.comments else None
-        if self.selected_entry == entry:
-            entry = None
-        if self.selected_entry:
-            removeStyle(self.selected_entry)
-        if entry:
-            log.debug("microblog entry selected (author=%s)" % entry.author)
-            entry.addStyleName('selected_entry')
-        self.selected_entry = entry
-
-    def updateValue(self, type_, jid, value):
-        """Update a jid value in entries
-        @param type_: one of 'avatar', 'nick'
-        @param jid: jid concerned
-        @param value: new value"""
-        def updateVPanel(vpanel):
-            for child in vpanel.children:
-                if isinstance(child, MicroblogEntry) and child.author == jid:
-                    child.updateAvatar(value)
-                elif isinstance(child, VerticalPanel):
-                    updateVPanel(child)
-        if type_ == 'avatar':
-            updateVPanel(self.vpanel)
-
-    def setAcceptedGroup(self, group):
-        """Add one or more group(s) which can be displayed in this panel.
-        Prevent from duplicate values and keep the list sorted.
-        @param group: string of the group, or list of string
-        """
-        if not hasattr(self, "_accepted_groups"):
-            self._accepted_groups = []
-        groups = group if isinstance(group, list) else [group]
-        for _group in groups:
-            if _group not in self._accepted_groups:
-                self._accepted_groups.append(_group)
-        self._accepted_groups.sort()
-
-    def isJidAccepted(self, jid):
-        """Tell if a jid is actepted and shown in this panel
-        @param jid: jid
-        @return: True if the jid is accepted"""
-        if self.accept_all():
-            return True
-        for group in self._accepted_groups:
-            if self.host.contact_panel.isContactInGroup(group, jid):
-                return True
-        return False
-
-
-class StatusPanel(HTMLTextEditor):
-
-    EMPTY_STATUS = '&lt;click to set a status&gt;'
-
-    def __init__(self, host, status=''):
-        self.host = host
-        modifiedCb = lambda content: self.host.bridge.call('setStatus', None, self.host.status_panel.presence, content['text']) or True
-        HTMLTextEditor.__init__(self, {'text': status}, modifiedCb, options={'no_xhtml': True, 'listen_focus': True, 'listen_click': True})
-        self.edit(False)
-        self.setStyleName('statusPanel')
-
-    @property
-    def status(self):
-        return self._original_content['text']
-
-    def __cleanContent(self, content):
-        status = content['text']
-        if status == self.EMPTY_STATUS or status in C.PRESENCE.values():
-            content['text'] = ''
-        return content
-
-    def getContent(self):
-        return self.__cleanContent(HTMLTextEditor.getContent(self))
-
-    def setContent(self, content):
-        content = self.__cleanContent(content)
-        BaseTextEditor.setContent(self, content)
-
-    def setDisplayContent(self):
-        status = self._original_content['text']
-        try:
-            presence = self.host.status_panel.presence
-        except AttributeError:  # during initialization
-            presence = None
-        if not status:
-            if presence and presence in C.PRESENCE:
-                status = C.PRESENCE[presence]
-            else:
-                status = self.EMPTY_STATUS
-        self.display.setHTML(addURLToText(status))
-
-
-class PresenceStatusPanel(HorizontalPanel, ClickHandler):
-
-    def __init__(self, host, presence="", status=""):
-        self.host = host
-        HorizontalPanel.__init__(self, Width='100%')
-        self.presence_button = Label(u"◉")
-        self.presence_button.setStyleName("presence-button")
-        self.status_panel = StatusPanel(host, status=status)
-        self.setPresence(presence)
-        entries = {}
-        for value in C.PRESENCE.keys():
-            entries.update({C.PRESENCE[value]: {"value": value}})
-
-        def callback(sender, key):
-            self.setPresence(entries[key]["value"])  # order matters
-            self.host.send([("STATUS", None)], self.status_panel.status)
-
-        self.presence_list = PopupMenuPanel(entries, callback=callback, style={"menu": "gwt-ListBox"})
-        self.presence_list.registerClickSender(self.presence_button)
-
-        panel = HorizontalPanel()
-        panel.add(self.presence_button)
-        panel.add(self.status_panel)
-        panel.setCellVerticalAlignment(self.presence_button, 'baseline')
-        panel.setCellVerticalAlignment(self.status_panel, 'baseline')
-        panel.setStyleName("marginAuto")
-        self.add(panel)
-
-        self.status_panel.edit(False)
-
-        ClickHandler.__init__(self)
-        self.addClickListener(self)
-
-    @property
-    def presence(self):
-        return self._presence
-
-    @property
-    def status(self):
-        return self.status_panel._original_content['text']
-
-    def setPresence(self, presence):
-        self._presence = presence
-        contact.setPresenceStyle(self.presence_button, self._presence)
-
-    def setStatus(self, status):
-        self.status_panel.setContent({'text': status})
-        self.status_panel.setDisplayContent()
-
-    def onClick(self, sender):
-        # As status is the default target of uniBar, we don't want to select anything if click on it
-        self.host.setSelected(None)
-
-
-class ChatPanel(base_widget.LiberviaWidget):
-
-    def __init__(self, host, target, type_='one2one'):
-        """Panel used for conversation (one 2 one or group chat)
-        @param host: SatWebFrontend instance
-        @param target: entity (JID) with who we have a conversation (contact's jid for one 2 one chat, or MUC room)
-        @param type: one2one for simple conversation, group for MUC"""
-        base_widget.LiberviaWidget.__init__(self, host, title=target.bare, selectable=True)
-        self.vpanel = VerticalPanel()
-        self.vpanel.setSize('100%', '100%')
-        self.type = type_
-        self.nick = None
-        if not target:
-            log.error("Empty target !")
-            return
-        self.target = target
-        self.__body = AbsolutePanel()
-        self.__body.setStyleName('chatPanel_body')
-        chat_area = HorizontalPanel()
-        chat_area.setStyleName('chatArea')
-        if type_ == 'group':
-            self.occupants_list = OccupantsList()
-            chat_area.add(self.occupants_list)
-        self.__body.add(chat_area)
-        self.content = AbsolutePanel()
-        self.content.setStyleName('chatContent')
-        self.content_scroll = base_widget.ScrollPanelWrapper(self.content)
-        chat_area.add(self.content_scroll)
-        chat_area.setCellWidth(self.content_scroll, '100%')
-        self.vpanel.add(self.__body)
-        self.vpanel.setCellHeight(self.__body, '100%')
-        self.addStyleName('chatPanel')
-        self.setWidget(self.vpanel)
-        self.state_machine = ChatStateMachine(self.host, str(self.target))
-        self._state = None
-
-    @classmethod
-    def registerClass(cls):
-        base_widget.LiberviaWidget.addDropKey("CONTACT", cls.createPanel)
-
-    @classmethod
-    def createPanel(cls, host, item):
-        _contact = item if isinstance(item, JID) else JID(item)
-        host.contact_panel.setContactMessageWaiting(_contact.bare, False)
-        _new_panel = ChatPanel(host, _contact)  # XXX: pyjamas doesn't seems to support creating with cls directly
-        _new_panel.historyPrint()
-        host.setSelected(_new_panel)
-        _new_panel.refresh()
-        return _new_panel
-
-    def refresh(self):
-        """Refresh the display of this widget. If the unibox is disabled,
-        add a message box at the bottom of the panel"""
-        self.host.contact_panel.setContactMessageWaiting(self.target.bare, False)
-        self.content_scroll.scrollToBottom()
-
-        enable_box = self.host.uni_box is None
-        if hasattr(self, 'message_box'):
-            self.message_box.setVisible(enable_box)
-            return
-        if enable_box:
-            self.message_box = MessageBox(self.host)
-            self.message_box.onSelectedChange(self)
-            self.vpanel.add(self.message_box)
-
-    def matchEntity(self, entity):
-        """
-        @param entity: target jid as a string or JID instance.
-        Could also be a couple with a type in the second element.
-        @return: True if self matches the given entity
-        """
-        if isinstance(entity, tuple):
-            entity, type_ = entity if len(entity) > 1 else (entity[0], self.type)
-        else:
-            type_ = self.type
-        entity = entity if isinstance(entity, JID) else JID(entity)
-        try:
-            return self.target.bare == entity.bare and self.type == type_
-        except AttributeError as e:
-            e.include_traceback()
-            return False
-
-    def getWarningData(self):
-        if self.type not in ["one2one", "group"]:
-            raise Exception("Unmanaged type !")
-        if self.type == "one2one":
-            msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % self.target
-        elif self.type == "group":
-            msg = "This message will be sent to all the participants of the multi-user room <span class='warningTarget'>%s</span>" % self.target
-        return ("ONE2ONE" if self.type == "one2one" else "GROUP", msg)
-
-    def onTextEntered(self, text):
-        self.host.send([("groupchat" if self.type == 'group' else "chat", str(self.target))], text)
-        self.state_machine._onEvent("active")
-
-    def onQuit(self):
-        base_widget.LiberviaWidget.onQuit(self)
-        if self.type == 'group':
-            self.host.bridge.call('mucLeave', None, self.target.bare)
-
-    def setUserNick(self, nick):
-        """Set the nick of the user, usefull for e.g. change the color of the user"""
-        self.nick = nick
-
-    def setPresents(self, nicks):
-        """Set the users presents in this room
-        @param occupants: list of nicks (string)"""
-        self.occupants_list.clear()
-        for nick in nicks:
-            self.occupants_list.addOccupant(nick)
-
-    def userJoined(self, nick, data):
-        self.occupants_list.addOccupant(nick)
-        self.printInfo("=> %s has joined the room" % nick)
-
-    def userLeft(self, nick, data):
-        self.occupants_list.removeOccupant(nick)
-        self.printInfo("<= %s has left the room" % nick)
-
-    def changeUserNick(self, old_nick, new_nick):
-        assert(self.type == "group")
-        self.occupants_list.removeOccupant(old_nick)
-        self.occupants_list.addOccupant(new_nick)
-        self.printInfo(_("%(old_nick)s is now known as %(new_nick)s") % {'old_nick': old_nick, 'new_nick': new_nick})
-
-    def historyPrint(self, size=20):
-        """Print the initial history"""
-        def getHistoryCB(history):
-            # display day change
-            day_format = "%A, %d %b %Y"
-            previous_day = datetime.now().strftime(day_format)
-            for line in history:
-                timestamp, from_jid, to_jid, message, mess_type, extra = line
-                message_day = datetime.fromtimestamp(float(timestamp or time())).strftime(day_format)
-                if previous_day != message_day:
-                    self.printInfo("* " + message_day)
-                    previous_day = message_day
-                self.printMessage(from_jid, message, extra, timestamp)
-        self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, self.target.bare, size, True)
-
-    def printInfo(self, msg, type_='normal', link_cb=None):
-        """Print general info
-        @param msg: message to print
-        @param type_: one of:
-            "normal": general info like "toto has joined the room"
-            "link": general info that is clickable like "click here to join the main room"
-            "me": "/me" information like "/me clenches his fist" ==> "toto clenches his fist"
-        @param link_cb: method to call when the info is clicked, ignored if type_ is not 'link'
-        """
-        _wid = HTML(msg) if type_ == 'link' else Label(msg)
-        if type_ == 'normal':
-            _wid.setStyleName('chatTextInfo')
-        elif type_ == 'link':
-            _wid.setStyleName('chatTextInfo-link')
-            if link_cb:
-                _wid.addClickListener(link_cb)
-        elif type_ == 'me':
-            _wid.setStyleName('chatTextMe')
-        else:
-            _wid.setStyleName('chatTextInfo')
-        self.content.add(_wid)
-
-    def printMessage(self, from_jid, msg, extra, timestamp=None):
-        """Print message in chat window. Must be implemented by child class"""
-        _jid = JID(from_jid)
-        nick = _jid.node if self.type == 'one2one' else _jid.resource
-        mymess = _jid.resource == self.nick if self.type == "group" else _jid.bare == self.host.whoami.bare  # mymess = True if message comes from local user
-        if msg.startswith('/me '):
-            self.printInfo('* %s %s' % (nick, msg[4:]), type_='me')
-            return
-        self.content.add(ChatText(timestamp, nick, mymess, msg, extra.get('xhtml')))
-        self.content_scroll.scrollToBottom()
-
-    def startGame(self, game_type, waiting, referee, players, *args):
-        """Configure the chat window to start a game"""
-        classes = {"Tarot": CardPanel, "RadioCol": RadioColPanel}
-        if game_type not in classes.keys():
-            return  # unknown game
-        attr = game_type.lower()
-        self.occupants_list.updateSpecials(players, SYMBOLS[attr])
-        if waiting or not self.nick in players:
-            return  # waiting for player or not playing
-        attr = "%s_panel" % attr
-        if hasattr(self, attr):
-            return
-        log.info("%s Game Started \o/" % game_type)
-        panel = classes[game_type](self, referee, self.nick, players, *args)
-        setattr(self, attr, panel)
-        self.vpanel.insert(panel, 0)
-        self.vpanel.setCellHeight(panel, panel.getHeight())
-
-    def getGame(self, game_type):
-        """Return class managing the game type"""
-        # TODO: check that the game is launched, and manage errors
-        if game_type == "Tarot":
-            return self.tarot_panel
-        elif game_type == "RadioCol":
-            return self.radiocol_panel
-
-    def setState(self, state, nick=None):
-        """Set the chat state (XEP-0085) of the contact. Leave nick to None
-        to set the state for a one2one conversation, or give a nickname or
-        C.ALL_OCCUPANTS to set the state of a participant within a MUC.
-        @param state: the new chat state
-        @param nick: None for one2one, the MUC user nick or ALL_OCCUPANTS
-        """
-        if nick:
-            assert(self.type == 'group')
-            occupants = self.occupants_list.occupants_list.keys() if nick == C.ALL_OCCUPANTS else [nick]
-            for occupant in occupants:
-                self.occupants_list.occupants_list[occupant].setState(state)
-        else:
-            assert(self.type == 'one2one')
-            self._state = state
-            self.refreshTitle()
-        self.state_machine.started = not not state  # start to send "composing" state from now
-
-    def refreshTitle(self):
-        """Refresh the title of this ChatPanel dialog"""
-        if self._state:
-            self.setTitle(self.target.bare + " (" + self._state + ")")
-        else:
-            self.setTitle(self.target.bare)
-
-
-class WebPanel(base_widget.LiberviaWidget):
-    """ (mini)browser like widget """
-
-    def __init__(self, host, url=None):
-        """
-        @param host: SatWebFrontend instance
-        """
-        base_widget.LiberviaWidget.__init__(self, host)
-        self._vpanel = VerticalPanel()
-        self._vpanel.setSize('100%', '100%')
-        self._url = dialog.ExtTextBox(enter_cb=self.onUrlClick)
-        self._url.setText(url or "")
-        self._url.setWidth('100%')
-        hpanel = HorizontalPanel()
-        hpanel.add(self._url)
-        btn = Button("Go", self.onUrlClick)
-        hpanel.setCellWidth(self._url, "100%")
-        #self.setCellWidth(btn, "10%")
-        hpanel.add(self._url)
-        hpanel.add(btn)
-        self._vpanel.add(hpanel)
-        self._vpanel.setCellHeight(hpanel, '20px')
-        self._frame = Frame(url or "")
-        self._frame.setSize('100%', '100%')
-        DOM.setStyleAttribute(self._frame.getElement(), "position", "relative")
-        self._vpanel.add(self._frame)
-        self.setWidget(self._vpanel)
-
-    def onUrlClick(self, sender):
-        self._frame.setUrl(self._url.getText())
-
-
-class MainPanel(AbsolutePanel):
-
-    def __init__(self, host):
-        self.host = host
-        AbsolutePanel.__init__(self)
-
-        # menu
-        self.menu = Menu(host)
-
-        # unibox
-        self.unibox_panel = UniBoxPanel(host)
-        self.unibox_panel.setVisible(False)
-
-        # contacts
-        self._contacts = HorizontalPanel()
-        self._contacts.addStyleName('globalLeftArea')
-        self.contacts_switch = Button(u'«', self._contactsSwitch)
-        self.contacts_switch.addStyleName('contactsSwitch')
-        self._contacts.add(self.contacts_switch)
-        self._contacts.add(self.host.contact_panel)
-
-        # tabs
-        self.tab_panel = base_widget.MainTabPanel(host)
-        self.discuss_panel = base_widget.WidgetsPanel(self.host, locked=True)
-        self.tab_panel.add(self.discuss_panel, "Discussions")
-        self.tab_panel.selectTab(0)
-
-        self.header = AbsolutePanel()
-        self.header.add(self.menu)
-        self.header.add(self.unibox_panel)
-        self.header.add(self.host.status_panel)
-        self.header.setStyleName('header')
-        self.add(self.header)
-
-        self._hpanel = HorizontalPanel()
-        self._hpanel.add(self._contacts)
-        self._hpanel.add(self.tab_panel)
-        self.add(self._hpanel)
-
-        self.setWidth("100%")
-        Window.addWindowResizeListener(self)
-
-    def _contactsSwitch(self, btn=None):
-        """ (Un)hide contacts panel """
-        if btn is None:
-            btn = self.contacts_switch
-        cpanel = self.host.contact_panel
-        cpanel.setVisible(not cpanel.getVisible())
-        btn.setText(u"«" if cpanel.getVisible() else u"»")
-        self.host.resize()
-
-    def _contactsMove(self, parent):
-        """Move the contacts container (containing the contact list and
-        the "hide/show" button) to another parent, but always as the
-        first child position (insert at index 0).
-        """
-        if self._contacts.getParent():
-            if self._contacts.getParent() == parent:
-                return
-            self._contacts.removeFromParent()
-        parent.insert(self._contacts, 0)
-
-    def onWindowResized(self, width, height):
-        _elts = doc().getElementsByClassName('gwt-TabBar')
-        if not _elts.length:
-            tab_bar_h = 0
-        else:
-            tab_bar_h = _elts.item(0).offsetHeight
-        ideal_height = Window.getClientHeight() - tab_bar_h
-        self.setHeight("%s%s" % (ideal_height, "px"))
-
-    def refresh(self):
-        """Refresh the main panel"""
-        self.unibox_panel.refresh()
--- a/browser_side/plugin_xep_0085.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# SAT plugin for Chat State Notifications Protocol (xep-0085)
-# Copyright (C) 2013, 2014 Adrien Cossa (souliane@mailoo.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/>.
-
-from pyjamas.Timer import Timer
-
-
-# Copy of the map from sat/src/plugins/plugin_xep_0085
-TRANSITIONS = {"active": {"next_state": "inactive", "delay": 120},
-               "inactive": {"next_state": "gone", "delay": 480},
-               "gone": {"next_state": "", "delay": 0},
-               "composing": {"next_state": "paused", "delay": 30},
-               "paused": {"next_state": "inactive", "delay": 450}
-               }
-
-
-class ChatStateMachine:
-    """This is an adapted version of the ChatStateMachine from sat/src/plugins/plugin_xep_0085
-    which manage a timer on the web browser and keep it synchronized with the timer that runs
-    on the backend. This is only needed to avoid calling the bridge method chatStateComposing
-    too often ; accuracy is not needed so we can ignore the delay of the communication between
-    the web browser and the backend (the timer on the web browser always starts a bit before).
-    /!\ Keep this file up to date if you modify the one in the sat plugins directory.
-    """
-    def __init__(self, host, target_s):
-
-        self.host = host
-        self.target_s = target_s
-        self.started = False
-        self.state = None
-        self.timer = None
-
-    def _onEvent(self, state):
-        """Pyjamas callback takes no extra argument so we need this trick"""
-        # Here we should check the value of the parameter "Send chat state notifications"
-        # but this costs two messages. It's even better to call chatStateComposing
-        # with a doubt, it will be checked by the back-end anyway before sending
-        # the actual notifications to the other client.
-        if state == "composing" and not self.started:
-            return
-        self.started = True
-        self.next_state = state
-        self.__onEvent(None)
-
-    def __onEvent(self, timer):
-        # print "on event %s" % self.next_state
-        state = self.next_state
-        self.next_state = ""
-        if state != self.state and state == "composing":
-            self.host.bridge.call('chatStateComposing', None, self.target_s)
-        self.state = state
-        if not self.timer is None:
-            self.timer.cancel()
-
-        if not state in TRANSITIONS:
-            return
-        if not "next_state" in TRANSITIONS[state]:
-            return
-        if not "delay" in TRANSITIONS[state]:
-            return
-        next_state = TRANSITIONS[state]["next_state"]
-        delay = TRANSITIONS[state]["delay"]
-        if next_state == "" or delay < 0:
-            return
-        self.next_state = next_state
-        # pyjamas timer in milliseconds
-        self.timer = Timer(delay * 1000, self.__onEvent)
--- a/browser_side/radiocol.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,321 +0,0 @@
-#!/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 pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.FlexTable import FlexTable
-from pyjamas.ui.FormPanel import FormPanel
-from pyjamas.ui.Label import Label
-from pyjamas.ui.Button import Button
-from pyjamas.ui.ClickListener import ClickHandler
-from pyjamas.ui.Hidden import Hidden
-from pyjamas.ui.CaptionPanel import CaptionPanel
-from pyjamas.media.Audio import Audio
-from pyjamas import Window
-from pyjamas.Timer import Timer
-
-from html_tools import html_sanitize
-from file_tools import FilterFileUpload
-from sat_frontends.tools.misc import DEFAULT_MUC
-from sat.core.i18n import _
-
-
-class MetadataPanel(FlexTable):
-
-    def __init__(self):
-        FlexTable.__init__(self)
-        title_lbl = Label("title:")
-        title_lbl.setStyleName('radiocol_metadata_lbl')
-        artist_lbl = Label("artist:")
-        artist_lbl.setStyleName('radiocol_metadata_lbl')
-        album_lbl = Label("album:")
-        album_lbl.setStyleName('radiocol_metadata_lbl')
-        self.title = Label("")
-        self.title.setStyleName('radiocol_metadata')
-        self.artist = Label("")
-        self.artist.setStyleName('radiocol_metadata')
-        self.album = Label("")
-        self.album.setStyleName('radiocol_metadata')
-        self.setWidget(0, 0, title_lbl)
-        self.setWidget(1, 0, artist_lbl)
-        self.setWidget(2, 0, album_lbl)
-        self.setWidget(0, 1, self.title)
-        self.setWidget(1, 1, self.artist)
-        self.setWidget(2, 1, self.album)
-        self.setStyleName("radiocol_metadata_pnl")
-
-    def setTitle(self, title):
-        self.title.setText(title)
-
-    def setArtist(self, artist):
-        self.artist.setText(artist)
-
-    def setAlbum(self, album):
-        self.album.setText(album)
-
-
-class ControlPanel(FormPanel):
-    """Panel used to show controls to add a song, or vote for the current one"""
-
-    def __init__(self, parent):
-        FormPanel.__init__(self)
-        self.setEncoding(FormPanel.ENCODING_MULTIPART)
-        self.setMethod(FormPanel.METHOD_POST)
-        self.setAction("upload_radiocol")
-        self.timer_on = False
-        self._parent = parent
-        vPanel = VerticalPanel()
-
-        types = [('audio/ogg', '*.ogg', 'Ogg Vorbis Audio'),
-                 ('video/ogg', '*.ogv', 'Ogg Vorbis Video'),
-                 ('application/ogg', '*.ogx', 'Ogg Vorbis Multiplex'),
-                 ('audio/mpeg', '*.mp3', 'MPEG-Layer 3'),
-                 ('audio/mp3', '*.mp3', 'MPEG-Layer 3'),
-                 ]
-        self.file_upload = FilterFileUpload("song", 10, types)
-        vPanel.add(self.file_upload)
-
-        hPanel = HorizontalPanel()
-        self.upload_btn = Button("Upload song", getattr(self, "onBtnClick"))
-        hPanel.add(self.upload_btn)
-        self.status = Label()
-        self.updateStatus()
-        hPanel.add(self.status)
-        #We need to know the filename and the referee
-        self.filename_field = Hidden('filename', '')
-        hPanel.add(self.filename_field)
-        referee_field = Hidden('referee', self._parent.referee)
-        hPanel.add(self.filename_field)
-        hPanel.add(referee_field)
-        vPanel.add(hPanel)
-
-        self.add(vPanel)
-        self.addFormHandler(self)
-
-    def updateStatus(self):
-        if self.timer_on:
-            return
-        # TODO: the status should be different if a song is being played or not
-        queue = self._parent.getQueueSize()
-        queue_data = self._parent.queue_data
-        if queue < queue_data[0]:
-            left = queue_data[0] - queue
-            self.status.setText("[we need %d more song%s]" % (left, "s" if left > 1 else ""))
-        elif queue < queue_data[1]:
-            left = queue_data[1] - queue
-            self.status.setText("[%d available spot%s]" % (left, "s" if left > 1 else ""))
-        elif queue >= queue_data[1]:
-                self.status.setText("[The queue is currently full]")
-        self.status.setStyleName('radiocol_status')
-
-    def onBtnClick(self):
-        if self.file_upload.check():
-            self.status.setText('[Submitting, please wait...]')
-            self.filename_field.setValue(self.file_upload.getFilename())
-            if self.file_upload.getFilename().lower().endswith('.mp3'):
-                self._parent._parent.host.showWarning('STATUS', 'For a better support, it is recommended to submit Ogg Vorbis file instead of MP3. You can convert your files easily, ask for help if needed!', 5000)
-            self.submit()
-            self.file_upload.setFilename("")
-
-    def onSubmit(self, event):
-        pass
-
-    def blockUpload(self):
-        self.file_upload.setVisible(False)
-        self.upload_btn.setEnabled(False)
-
-    def unblockUpload(self):
-        self.file_upload.setVisible(True)
-        self.upload_btn.setEnabled(True)
-
-    def setTemporaryStatus(self, text, style):
-        self.status.setText(text)
-        self.status.setStyleName('radiocol_upload_status_%s' % style)
-        self.timer_on = True
-
-        def cb(timer):
-            self.timer_on = False
-            self.updateStatus()
-
-        Timer(5000, cb)
-
-    def onSubmitComplete(self, event):
-        result = event.getResults()
-        if result == "OK":
-            # the song can still be rejected (not readable, full queue...)
-            self.setTemporaryStatus('[Your song has been submitted to the radio]', "ok")
-        elif result == "KO":
-            self.setTemporaryStatus('[Something went wrong during your song upload]', "ko")
-            self._parent.radiocolSongRejected(_("The uploaded file has been rejected, only Ogg Vorbis and MP3 songs are accepted."))
-            # TODO: would be great to re-use the original Exception class and message
-            # but it is lost in the middle of the traceback and encapsulated within
-            # a DBusException instance --> extract the data from the traceback?
-        else:
-            Window.alert('Submit error: %s' % result)
-            self.status.setText('')
-
-
-class Player(Audio):
-
-    def __init__(self, player_id, metadata_panel):
-        Audio.__init__(self)
-        self._id = player_id
-        self.metadata = metadata_panel
-        self.timestamp = ""
-        self.title = ""
-        self.artist = ""
-        self.album = ""
-        self.filename = None
-        self.played = False  # True when the song is playing/has played, becomes False on preload
-        self.setAutobuffer(True)
-        self.setAutoplay(False)
-        self.setVisible(False)
-
-
-    def preload(self, timestamp, filename, title, artist, album):
-        """preload the song but doesn't play it"""
-        self.timestamp = timestamp
-        self.filename = filename
-        self.title = title
-        self.artist = artist
-        self.album = album
-        self.played = False
-        self.setSrc("radiocol/%s" % html_sanitize(filename))
-        log.debug("preloading %s in %s" % (title, self._id))
-
-    def play(self, play=True):
-        """Play or pause the song
-        @param play: set to True to play or to False to pause
-        """
-        if play:
-            self.played = True
-            self.metadata.setTitle(self.title)
-            self.metadata.setArtist(self.artist)
-            self.metadata.setAlbum(self.album)
-            Audio.play(self)
-        else:
-            self.pause()
-
-
-class RadioColPanel(HorizontalPanel, ClickHandler):
-
-    def __init__(self, parent, referee, player_nick, players, queue_data):
-        """
-        @param parent
-        @param referee
-        @param player_nick
-        @param players
-        @param queue_data: list of integers (queue to start, queue limit)
-        """
-        # We need to set it here and not in the CSS :(
-        HorizontalPanel.__init__(self, Height="90px")
-        ClickHandler.__init__(self)
-        self._parent = parent
-        self.referee = referee
-        self.queue_data = queue_data
-        self.setStyleName("radiocolPanel")
-
-        # Now we set up the layout
-        self.metadata_panel = MetadataPanel()
-        self.add(CaptionPanel("Now playing", self.metadata_panel))
-        self.playlist_panel = VerticalPanel()
-        self.add(CaptionPanel("Songs queue", self.playlist_panel))
-        self.control_panel = ControlPanel(self)
-        self.add(CaptionPanel("Controls", self.control_panel))
-
-        self.next_songs = []
-        self.players = [Player("player_%d" % i, self.metadata_panel) for i in xrange(queue_data[1] + 1)]
-        self.current_player = None
-        for player in self.players:
-            self.add(player)
-        self.addClickListener(self)
-
-        help_msg = """Accepted file formats: Ogg Vorbis (recommended), MP3.<br />
-        Please do not submit files that are protected by copyright.<br />
-        Click <a style="color: red;">here</a> if you need some support :)"""
-        link_cb = lambda: self._parent.host.bridge.call('joinMUC', None, DEFAULT_MUC, self._parent.nick)
-        self._parent.printInfo(help_msg, type_='link', link_cb=link_cb)
-
-    def pushNextSong(self, title):
-        """Add a song to the left panel's next songs queue"""
-        next_song = Label(title)
-        next_song.setStyleName("radiocol_next_song")
-        self.next_songs.append(next_song)
-        self.playlist_panel.append(next_song)
-        self.control_panel.updateStatus()
-
-    def popNextSong(self):
-        """Remove the first song of next songs list
-        should be called when the song is played"""
-        #FIXME: should check that the song we remove is the one we play
-        next_song = self.next_songs.pop(0)
-        self.playlist_panel.remove(next_song)
-        self.control_panel.updateStatus()
-
-    def getQueueSize(self):
-        return len(self.playlist_panel.getChildren())
-
-    def radiocolCheckPreload(self, timestamp):
-        for player in self.players:
-            if player.timestamp == timestamp:
-                return False
-        return True
-
-    def radiocolPreload(self, timestamp, filename, title, artist, album, sender):
-        if not self.radiocolCheckPreload(timestamp):
-            return  # song already preloaded
-        preloaded = False
-        for player in self.players:
-            if not player.filename or \
-               (player.played and player != self.current_player):
-                #if player has no file loaded, or it has already played its song
-                #we use it to preload the next one
-                player.preload(timestamp, filename, title, artist, album)
-                preloaded = True
-                break
-        if not preloaded:
-            log.warning("Can't preload song, we are getting too many songs to preload, we shouldn't have more than %d at once" % self.queue_data[1])
-        else:
-            self.pushNextSong(title)
-            self._parent.printInfo(_('%(user)s uploaded %(artist)s - %(title)s') % {'user': sender, 'artist': artist, 'title': title})
-
-    def radiocolPlay(self, filename):
-        found = False
-        for player in self.players:
-            if not found and player.filename == filename:
-                player.play()
-                self.popNextSong()
-                self.current_player = player
-                found = True
-            else:
-                player.play(False)  # in case the previous player was not sync
-        if not found:
-            log.error("Song not found in queue, can't play it. This should not happen")
-
-    def radiocolNoUpload(self):
-        self.control_panel.blockUpload()
-
-    def radiocolUploadOk(self):
-        self.control_panel.unblockUpload()
-
-    def radiocolSongRejected(self, reason):
-        Window.alert("Song rejected: %s" % reason)
--- a/browser_side/register.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,249 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
-# Copyright (C) 2011, 2012  Adrien Vigneron <adrienvigneron@mailoo.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/>.
-
-#This page manage subscription and new account creation
-import pyjd # this is dummy in pyjs
-
-from constants import Const as C
-from sat.core.i18n import _
-from pyjamas.ui.SimplePanel import SimplePanel
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.TabPanel import TabPanel
-from pyjamas.ui.TabBar import TabBar
-from pyjamas.ui.PasswordTextBox import PasswordTextBox
-from pyjamas.ui.TextBox import TextBox
-from pyjamas.ui.FormPanel import FormPanel
-from pyjamas.ui.Button import Button
-from pyjamas.ui.Label import Label
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.PopupPanel import PopupPanel
-from pyjamas.ui.Image import Image
-from pyjamas.ui.Hidden import Hidden
-from pyjamas import Window
-from pyjamas.ui.KeyboardListener import KEY_ENTER
-import re
-from pyjamas.Timer import Timer
-
-
-class RegisterPanel(FormPanel):
-
-    def __init__(self, callback):
-        """
-        @param callback: method to call if login successful
-        """
-        FormPanel.__init__(self)
-        self.setSize('600px', '350px')
-        self.callback = callback
-        self.setMethod(FormPanel.METHOD_POST)
-        main_panel = HorizontalPanel()
-        main_panel.setStyleName('registerPanel_main')
-        left_side = Image("media/libervia/register_left.png")
-        main_panel.add(left_side)
-
-        ##TabPanel##
-        tab_bar = TabBar()
-        tab_bar.setStyleName('registerPanel_tabs')
-        self.right_side = TabPanel(tab_bar)
-        self.right_side.setStyleName('registerPanel_right_side')
-        main_panel.add(self.right_side)
-        main_panel.setCellWidth(self.right_side, '100%')
-
-        ##Login tab##
-        login_tab = SimplePanel()
-        login_tab.setStyleName('registerPanel_content')
-        login_vpanel = VerticalPanel()
-        login_tab.setWidget(login_vpanel)
-
-        self.login_warning_msg = Label('')
-        self.login_warning_msg.setVisible(False)
-        self.login_warning_msg.setStyleName('formWarning')
-        login_vpanel.add(self.login_warning_msg)
-
-        login_label = Label('Login:')
-        self.login_box = TextBox()
-        self.login_box.setName("login")
-        self.login_box.addKeyboardListener(self)
-        login_pass_label = Label('Password:')
-        self.login_pass_box = PasswordTextBox()
-        self.login_pass_box.setName("login_password")
-        self.login_pass_box.addKeyboardListener(self)
-
-        login_vpanel.add(login_label)
-        login_vpanel.add(self.login_box)
-        login_vpanel.add(login_pass_label)
-        login_vpanel.add(self.login_pass_box)
-        login_but = Button("Log in", getattr(self, "onLogin"))
-        login_but.setStyleName('button')
-        login_but.addStyleName('red')
-        login_vpanel.add(login_but)
-
-        #The hidden submit_type field
-        self.submit_type = Hidden('submit_type')
-        login_vpanel.add(self.submit_type)
-
-        ##Register tab##
-        register_tab = SimplePanel()
-        register_tab.setStyleName('registerPanel_content')
-        register_vpanel = VerticalPanel()
-        register_tab.setWidget(register_vpanel)
-
-        self.register_warning_msg = HTML('')
-        self.register_warning_msg.setVisible(False)
-        self.register_warning_msg.setStyleName('formWarning')
-        register_vpanel.add(self.register_warning_msg)
-
-        register_login_label = Label('Login:')
-        self.register_login_box = TextBox()
-        self.register_login_box.setName("register_login")
-        self.register_login_box.addKeyboardListener(self)
-        email_label = Label('E-mail:')
-        self.email_box = TextBox()
-        self.email_box.setName("email")
-        self.email_box.addKeyboardListener(self)
-        register_pass_label = Label('Password:')
-        self.register_pass_box = PasswordTextBox()
-        self.register_pass_box.setName("register_password")
-        self.register_pass_box.addKeyboardListener(self)
-        register_vpanel.add(register_login_label)
-        register_vpanel.add(self.register_login_box)
-        register_vpanel.add(email_label)
-        register_vpanel.add(self.email_box)
-        register_vpanel.add(register_pass_label)
-        register_vpanel.add(self.register_pass_box)
-
-        register_but = Button("Register", getattr(self, "onRegister"))
-        register_but.setStyleName('button')
-        register_but.addStyleName('red')
-        register_vpanel.add(register_but)
-
-        self.right_side.add(login_tab, 'Login')
-        self.right_side.add(register_tab, 'Register')
-        self.right_side.addTabListener(self)
-        self.right_side.selectTab(1)
-        login_tab.setWidth(None)
-        register_tab.setWidth(None)
-
-        self.add(main_panel)
-        self.addFormHandler(self)
-        self.setAction('register_api/login')
-
-    def onBeforeTabSelected(self, sender, tabIndex):
-        return True
-
-    def onTabSelected(self, sender, tabIndex):
-        if tabIndex == 0:
-            self.login_box.setFocus(True)
-        elif tabIndex == 1:
-            self.register_login_box.setFocus(True)
-
-    def onKeyPress(self, sender, keycode, modifiers):
-        if keycode == KEY_ENTER:
-            # Browsers offer an auto-completion feature to any
-            # text box, but the selected value is not set when
-            # the widget looses the focus. Using a timer with
-            # any delay value > 0 would do the trick.
-            if sender == self.login_box:
-                Timer(5, lambda timer: self.login_pass_box.setFocus(True))
-            elif sender == self.login_pass_box:
-                self.onLogin(None)
-            elif sender == self.register_login_box:
-                Timer(5, lambda timer: self.email_box.setFocus(True))
-            elif sender == self.email_box:
-                Timer(5, lambda timer: self.register_pass_box.setFocus(True))
-            elif sender == self.register_pass_box:
-                self.onRegister(None)
-
-    def onKeyUp(self, sender, keycode, modifiers):
-        pass
-
-    def onKeyDown(self, sender, keycode, modifiers):
-        pass
-
-    def onLogin(self, button):
-        if not re.match(r'^[a-z0-9_-]+$', self.login_box.getText(), re.IGNORECASE):
-            self.login_warning_msg.setText('Invalid login, valid characters are a-z A-Z 0-9 _ -')
-            self.login_warning_msg.setVisible(True)
-        else:
-            self.submit_type.setValue('login')
-            self.submit()
-
-    def onRegister(self, button):
-        if not re.match(r'^[a-z0-9_-]+$', self.register_login_box.getText(), re.IGNORECASE):
-            self.register_warning_msg.setHTML(_('Invalid login, valid characters<br>are a-z A-Z 0-9 _ -'))
-            self.register_warning_msg.setVisible(True)
-        elif not re.match(r'^.+@.+\..+', self.email_box.getText(), re.IGNORECASE):
-            self.register_warning_msg.setHTML(_('Invalid email address'))
-            self.register_warning_msg.setVisible(True)
-        elif len(self.register_pass_box.getText()) < C.PASSWORD_MIN_LENGTH:
-            self.register_warning_msg.setHTML(_('Your password must contain<br>at least %d characters') % C.PASSWORD_MIN_LENGTH)
-            self.register_warning_msg.setVisible(True)
-        else:
-            self.register_warning_msg.setVisible(False)
-            self.submit_type.setValue('register')
-            self.submit()
-
-    def onSubmit(self, event):
-        pass
-
-    def onSubmitComplete(self, event):
-        result = event.getResults()
-        if result == "AUTH ERROR":
-            Window.alert('Your login and/or password is incorrect. Please try again')
-        elif result == "LOGGED":
-            self.callback()
-        elif result == "SESSION_ACTIVE":
-            Window.alert('Session already active, this should not happen, please contact the author to fix it')
-        elif result == "ALREADY EXISTS":
-            self.register_warning_msg.setHTML('This login already exists,<br>please choose another one')
-            self.register_warning_msg.setVisible(True)
-        elif result == "INTERNAL":
-            self.register_warning_msg.setHTML('SERVER ERROR: something went wrong during registration process, please contact the server administrator')
-            self.register_warning_msg.setVisible(True)
-        elif result == "REGISTRATION":
-            self.login_warning_msg.setVisible(False)
-            self.register_warning_msg.setVisible(False)
-            self.login_box.setText(self.register_login_box.getText())
-            self.login_pass_box.setText('')
-            self.register_login_box.setText('')
-            self.register_pass_box.setText('')
-            self.email_box.setText('')
-            self.right_side.selectTab(0)
-            self.login_pass_box.setFocus(True)
-            Window.alert('An email has been sent to you with your login informations\nPlease remember that this is ONLY A TECHNICAL DEMO')
-        else:
-            Window.alert('Submit error: %s' % result)
-
-
-class RegisterBox(PopupPanel):
-
-    def __init__(self, callback, *args, **kwargs):
-        PopupPanel.__init__(self, *args, **kwargs)
-        self._form = RegisterPanel(callback)
-        self.setWidget(self._form)
-
-    def onWindowResized(self, width, height):
-        super(RegisterBox, self).onWindowResized(width, height)
-        self.centerBox()
-
-    def show(self):
-        super(RegisterBox, self).show()
-        self.centerBox()
-        self._form.login_box.setFocus(True)
--- a/browser_side/richtext.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,536 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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/>.
-
-from pyjamas.ui.TextArea import TextArea
-from pyjamas.ui.Button import Button
-from pyjamas.ui.CheckBox import CheckBox
-from pyjamas.ui.DialogBox import DialogBox
-from pyjamas.ui.Label import Label
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.FlexTable import FlexTable
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas import Window
-from pyjamas.ui.KeyboardListener import KeyboardHandler
-from __pyjamas__ import doc
-
-from constants import Const as C
-from dialog import ConfirmDialog, InfoDialog
-from base_panels import TitlePanel, BaseTextEditor, HTMLTextEditor
-from list_manager import ListManager
-from html_tools import html_sanitize
-from browser_side import panels
-
-from sat_frontends.tools import composition
-from sat.core.i18n import _
-
-
-class RichTextEditor(BaseTextEditor, FlexTable):
-    """Panel for the rich text editor."""
-
-    def __init__(self, host, content=None, modifiedCb=None, afterEditCb=None, options=None, style=None):
-        """
-        @param host: the SatWebFrontend instance
-        @param content: dict with at least a 'text' key
-        @param modifiedCb: method to be called when the text has been modified
-        @param afterEditCb: method to be called when the edition is done
-        @param options: list of UI options (see self.readOptions)
-        """
-        self.host = host
-        self._debug = False  # TODO: don't forget to set  it False before commit
-        self.wysiwyg = False
-        self.__readOptions(options)
-        self.style = {'main': 'richTextEditor',
-                      'title': 'richTextTitle',
-                      'toolbar': 'richTextToolbar',
-                      'textarea': 'richTextArea'}
-        if isinstance(style, dict):
-            self.style.update(style)
-        self._prepareUI()
-        BaseTextEditor.__init__(self, content, None, modifiedCb, afterEditCb)
-
-    def __readOptions(self, options):
-        """Set the internal flags according to the given options."""
-        if options is None:
-            options = []
-        self.read_only = 'read_only' in options
-        self.update_msg = 'update_msg' in options
-        self.no_title = 'no_title' in options or self.read_only
-        self.no_command = 'no_command' in options or self.read_only
-
-    def _prepareUI(self, y_offset=0):
-        """Prepare the UI to host title panel, toolbar, text area...
-        @param y_offset: Y offset to start from (extra rows on top)"""
-        if not self.read_only:
-            self.title_offset = y_offset
-            self.toolbar_offset = self.title_offset + (0 if self.no_title else 1)
-            self.content_offset = self.toolbar_offset + (len(composition.RICH_SYNTAXES) if self._debug else 1)
-            self.command_offset = self.content_offset + 1
-        else:
-            self.title_offset = self.toolbar_offset = self.content_offset = y_offset
-            self.command_offset = self.content_offset + 1
-        FlexTable.__init__(self, self.command_offset + (0 if self.no_command else 1), 2)
-        self.addStyleName(self.style['main'])
-
-    def addEditListener(self, listener):
-        """Add a method to be called whenever the text is edited.
-        @param listener: method taking two arguments: sender, keycode"""
-        BaseTextEditor.addEditListener(self, listener)
-        if hasattr(self, 'display'):
-            self.display.addEditListener(listener)
-
-    def refresh(self, edit=None):
-        """Refresh the UI for edition/display mode
-        @param edit: set to True to display the edition mode"""
-        if edit is None:
-            edit = hasattr(self, 'textarea') and self.textarea.getVisible()
-
-        for widget in ['title_panel', 'command']:
-            if hasattr(self, widget):
-                getattr(self, widget).setVisible(edit)
-
-        if hasattr(self, 'toolbar'):
-            self.toolbar.setVisible(False)
-        if not hasattr(self, 'display'):
-            self.display = HTMLTextEditor(options={'enhance_display': False, 'listen_keyboard': False})  # for display mode
-            for listener in self.edit_listeners:
-                self.display.addEditListener(listener)
-        if not self.read_only and not hasattr(self, 'textarea'):
-            self.textarea = EditTextArea(self)  # for edition mode
-            self.textarea.addStyleName(self.style['textarea'])
-
-        self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2)
-        if edit and not self.wysiwyg:
-            self.textarea.setWidth('100%')  # CSS width doesn't do it, don't know why
-            self.setWidget(self.content_offset, 0, self.textarea)
-        else:
-            self.setWidget(self.content_offset, 0, self.display)
-        if not edit:
-            return
-
-        if not self.no_title and not hasattr(self, 'title_panel'):
-            self.title_panel = TitlePanel()
-            self.title_panel.addStyleName(self.style['title'])
-            self.getFlexCellFormatter().setColSpan(self.title_offset, 0, 2)
-            self.setWidget(self.title_offset, 0, self.title_panel)
-
-        if not self.no_command and not hasattr(self, 'command'):
-            self.command = HorizontalPanel()
-            self.command.addStyleName("marginAuto")
-            self.command.add(Button("Cancel", lambda: self.edit(True, True)))
-            self.command.add(Button("Update" if self.update_msg else "Send message", lambda: self.edit(False)))
-            self.getFlexCellFormatter().setColSpan(self.command_offset, 0, 2)
-            self.setWidget(self.command_offset, 0, self.command)
-
-    def setToolBar(self, syntax):
-        """This method is called asynchronously after the parameter
-        holding the rich text syntax is retrieved. It is called at
-        each call of self.edit(True) because the user may
-        have change his setting since the last time."""
-        if syntax is None or syntax not in composition.RICH_SYNTAXES.keys():
-            syntax = composition.RICH_SYNTAXES.keys()[0]
-        if hasattr(self, "toolbar") and self.toolbar.syntax == syntax:
-            self.toolbar.setVisible(True)
-            return
-        count = 0
-        for syntax in composition.RICH_SYNTAXES.keys() if self._debug else [syntax]:
-            self.toolbar = HorizontalPanel()
-            self.toolbar.syntax = syntax
-            self.toolbar.addStyleName(self.style['toolbar'])
-            for key in composition.RICH_SYNTAXES[syntax].keys():
-                self.addToolbarButton(syntax, key)
-            self.wysiwyg_button = CheckBox(_('WYSIWYG edition'))
-            wysiywgCb = lambda sender: self.setWysiwyg(sender.getChecked())
-            self.wysiwyg_button.addClickListener(wysiywgCb)
-            self.toolbar.add(self.wysiwyg_button)
-            self.syntax_label = Label(_("Syntax: %s") % syntax)
-            self.syntax_label.addStyleName("richTextSyntaxLabel")
-            self.toolbar.add(self.syntax_label)
-            self.toolbar.setCellWidth(self.syntax_label, "100%")
-            self.getFlexCellFormatter().setColSpan(self.toolbar_offset + count, 0, 2)
-            self.setWidget(self.toolbar_offset + count, 0, self.toolbar)
-            count += 1
-
-    def setWysiwyg(self, wysiwyg, init=False):
-        """Toggle the edition mode between rich content syntax and wysiwyg.
-        @param wysiwyg: boolean value
-        @param init: set to True to re-init without switching the widgets."""
-        def setWysiwyg():
-            self.wysiwyg = wysiwyg
-            try:
-                self.wysiwyg_button.setChecked(wysiwyg)
-            except TypeError:
-                pass
-            try:
-                if wysiwyg:
-                    self.syntax_label.addStyleName('transparent')
-                else:
-                    self.syntax_label.removeStyleName('transparent')
-            except TypeError:
-                pass
-            if not wysiwyg:
-                self.display.removeStyleName('richTextWysiwyg')
-
-        if init:
-            setWysiwyg()
-            return
-
-        self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2)
-        if wysiwyg:
-            def syntaxConvertCb(text):
-                self.display.setContent({'text': text})
-                self.textarea.removeFromParent()  # XXX: force as it is not always done...
-                self.setWidget(self.content_offset, 0, self.display)
-                self.display.addStyleName('richTextWysiwyg')
-                self.display.edit(True)
-            content = self.getContent()
-            if content['text'] and content['syntax'] != C.SYNTAX_XHTML:
-                self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax'], C.SYNTAX_XHTML)
-            else:
-                syntaxConvertCb(content['text'])
-        else:
-            syntaxConvertCb = lambda text: self.textarea.setText(text)
-            text = self.display.getContent()['text']
-            if text and self.toolbar.syntax != C.SYNTAX_XHTML:
-                self.host.bridge.call('syntaxConvert', syntaxConvertCb, text)
-            else:
-                syntaxConvertCb(text)
-            self.setWidget(self.content_offset, 0, self.textarea)
-            self.textarea.setWidth('100%')  # CSS width doesn't do it, don't know why
-
-        setWysiwyg()  # do it in the end because it affects self.getContent
-
-    def addToolbarButton(self, syntax, key):
-        """Add a button with the defined parameters."""
-        button = Button('<img src="%s" class="richTextIcon" />' %
-                        composition.RICH_BUTTONS[key]["icon"])
-        button.setTitle(composition.RICH_BUTTONS[key]["tip"])
-        button.addStyleName('richTextToolButton')
-        self.toolbar.add(button)
-
-        def buttonCb():
-            """Generic callback for a toolbar button."""
-            text = self.textarea.getText()
-            cursor_pos = self.textarea.getCursorPos()
-            selection_length = self.textarea.getSelectionLength()
-            data = composition.RICH_SYNTAXES[syntax][key]
-            if selection_length == 0:
-                middle_text = data[1]
-            else:
-                middle_text = text[cursor_pos:cursor_pos + selection_length]
-            self.textarea.setText(text[:cursor_pos]
-                                  + data[0]
-                                  + middle_text
-                                  + data[2]
-                                  + text[cursor_pos + selection_length:])
-            self.textarea.setCursorPos(cursor_pos + len(data[0]) + len(middle_text))
-            self.textarea.setFocus(True)
-            self.textarea.onKeyDown()
-
-        def wysiwygCb():
-            """Callback for a toolbar button while wysiwyg mode is enabled."""
-            data = composition.COMMANDS[key]
-
-            def execCommand(command, arg):
-                self.display.setFocus(True)
-                doc().execCommand(command, False, arg.strip() if arg else '')
-            # use Window.prompt instead of dialog.PromptDialog to not loose the focus
-            prompt = lambda command, text: execCommand(command, Window.prompt(text))
-            if isinstance(data, tuple) or isinstance(data, list):
-                if data[1]:
-                    prompt(data[0], data[1])
-                else:
-                    execCommand(data[0], data[2])
-            else:
-                execCommand(data, False, '')
-            self.textarea.onKeyDown()
-
-        button.addClickListener(lambda: wysiwygCb() if self.wysiwyg else buttonCb())
-
-    def getContent(self):
-        assert(hasattr(self, 'textarea'))
-        assert(hasattr(self, 'toolbar'))
-        if self.wysiwyg:
-            content = {'text': self.display.getContent()['text'], 'syntax': C.SYNTAX_XHTML}
-        else:
-            content = {'text': self.strproc(self.textarea.getText()), 'syntax': self.toolbar.syntax}
-        if hasattr(self, 'title_panel'):
-            content.update({'title': self.strproc(self.title_panel.getText())})
-        return content
-
-    def edit(self, edit=False, abort=False, sync=False):
-        """
-        Remark: the editor must be visible before you call this method.
-        @param edit: set to True to edit the content or False to only display it
-        @param abort: set to True to cancel the edition and loose the changes.
-        If edit and abort are both True, self.abortEdition can be used to ask for a
-        confirmation. When edit is False and abort is True, abortion is actually done.
-        @param sync: set to True to cancel the edition after the content has been saved somewhere else
-        """
-        if not (edit and abort):
-            self.refresh(edit)  # not when we are asking for a confirmation
-        BaseTextEditor.edit(self, edit, abort, sync)  # after the UI has been refreshed
-        if (edit and abort):
-            return  # self.abortEdition is called by BaseTextEditor.edit
-        self.setWysiwyg(False, init=True)  # after BaseTextEditor (it affects self.getContent)
-        if sync:
-            return
-        # the following must NOT be done at each UI refresh!
-        content = self._original_content
-        if edit:
-            def getParamCb(syntax):
-                # set the editable text in the current user-selected syntax
-                def syntaxConvertCb(text=None):
-                    if text is not None:
-                        # Important: this also update self._original_content
-                        content.update({'text': text})
-                    content.update({'syntax': syntax})
-                    self.textarea.setText(content['text'])
-                    if hasattr(self, 'title_panel') and 'title' in content:
-                        self.title_panel.setText(content['title'])
-                        self.title_panel.setStackVisible(0, content['title'] != '')
-                    self.setToolBar(syntax)
-                if content['text'] and content['syntax'] != syntax:
-                    self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax'])
-                else:
-                    syntaxConvertCb()
-            self.host.bridge.call('asyncGetParamA', getParamCb, composition.PARAM_NAME_SYNTAX, composition.PARAM_KEY_COMPOSITION)
-        else:
-            if not self.initialized:
-                # set the display text in XHTML only during init because a new MicroblogEntry instance is created after each modification
-                self.setDisplayContent()
-            self.display.edit(False)
-
-    def setDisplayContent(self):
-        """Set the content of the HTMLTextEditor which is used for display/wysiwyg"""
-        content = self._original_content
-        text = content['text']
-        if 'title' in content and content['title']:
-            text = '<h1>%s</h1>%s' % (html_sanitize(content['title']), content['text'])
-        self.display.setContent({'text': text})
-
-    def setFocus(self, focus):
-        self.textarea.setFocus(focus)
-
-    def abortEdition(self, content):
-        """Ask for confirmation before closing the dialog."""
-        def confirm_cb(answer):
-            if answer:
-                self.edit(False, True)
-        _dialog = ConfirmDialog(confirm_cb, text="Do you really want to %s?" % ("cancel your changes" if self.update_msg else "cancel this message"))
-        _dialog.cancel_button.setText(_("No"))
-        _dialog.show()
-
-
-class RichMessageEditor(RichTextEditor):
-    """Use the rich text editor for sending messages with extended addressing.
-    Recipient panels are on top and data may be synchronized from/to the unibox."""
-
-    @classmethod
-    def getOrCreate(cls, host, parent=None, callback=None):
-        """Get or create the message editor associated to that host.
-        Add it to parent if parent is not None, otherwise display it
-        in a popup dialog.
-        @param host: the host
-        @param parent: parent panel (or None to display in a popup).
-        @return: the RichTextEditor instance if parent is not None,
-        otherwise a popup DialogBox containing the RichTextEditor.
-        """
-        if not hasattr(host, 'richtext'):
-            modifiedCb = lambda content: True
-
-            def afterEditCb(content):
-                if hasattr(host.richtext, 'popup'):
-                    host.richtext.popup.hide()
-                else:
-                    host.richtext.setVisible(False)
-                callback()
-            options = ['no_title']
-            style = {'main': 'richMessageEditor', 'textarea': 'richMessageArea'}
-            host.richtext = RichMessageEditor(host, None, modifiedCb, afterEditCb, options, style)
-
-        def add(widget, parent):
-            if widget.getParent() is not None:
-                if widget.getParent() != parent:
-                    widget.removeFromParent()
-                    parent.add(widget)
-            else:
-                parent.add(widget)
-            widget.setVisible(True)
-            widget.initialized = False  # fake a new creation
-            widget.edit(True)
-
-        if parent is None:
-            if not hasattr(host.richtext, 'popup'):
-                host.richtext.popup = DialogBox(autoHide=False, centered=True)
-                host.richtext.popup.setHTML("Compose your message")
-                host.richtext.popup.add(host.richtext)
-            add(host.richtext, host.richtext.popup)
-            host.richtext.popup.center()
-        else:
-            add(host.richtext, parent)
-        return host.richtext.popup if parent is None else host.richtext
-
-    def _prepareUI(self, y_offset=0):
-        """Prepare the UI to host recipients panel, toolbar, text area...
-        @param y_offset: Y offset to start from (extra rows on top)"""
-        self.recipient_offset = y_offset
-        self.recipient_spacer_offset = self.recipient_offset + len(composition.RECIPIENT_TYPES)
-        RichTextEditor._prepareUI(self, self.recipient_spacer_offset + 1)
-
-    def refresh(self, edit=None):
-        """Refresh the UI between edition/display mode
-        @param edit: set to True to display the edition mode"""
-        if edit is None:
-            edit = hasattr(self, 'textarea') and self.textarea.getVisible()
-        RichTextEditor.refresh(self, edit)
-
-        for widget in ['recipient', 'recipient_spacer']:
-            if hasattr(self, widget):
-                getattr(self, widget).setVisible(edit)
-
-        if not edit:
-            return
-
-        if not hasattr(self, 'recipient'):
-            # recipient types sub-panels are automatically added by the manager
-            self.recipient = RecipientManager(self, self.recipient_offset)
-            self.recipient.createWidgets(title_format="%s: ")
-            self.recipient_spacer = HTML('')
-            self.recipient_spacer.setStyleName('recipientSpacer')
-            self.getFlexCellFormatter().setColSpan(self.recipient_spacer_offset, 0, 2)
-            self.setWidget(self.recipient_spacer_offset, 0, self.recipient_spacer)
-
-        if not hasattr(self, 'sync_button'):
-            self.sync_button = Button("Back to quick box", lambda: self.edit(True, sync=True))
-            self.command.insert(self.sync_button, 1)
-
-    def syncToEditor(self):
-        """Synchronize from unibox."""
-        def setContent(target, data):
-            if hasattr(self, 'recipient'):
-                self.recipient.setContacts({"To": [target]} if target else {})
-            self.setContent({'text': data if data else '', 'syntax': ''})
-            self.textarea.setText(data if data else '')
-        data, target = self.host.uni_box.getTargetAndData() if self.host.uni_box else (None, None)
-        setContent(target, data)
-
-    def __syncToUniBox(self, recipients=None, emptyText=False):
-        """Synchronize to unibox if a maximum of one recipient is set.
-        @return True if the sync could be done, False otherwise"""
-        if not self.host.uni_box:
-            return
-        setText = lambda: self.host.uni_box.setText("" if emptyText else self.getContent()['text'])
-        if not hasattr(self, 'recipient'):
-            setText()
-            return True
-        if recipients is None:
-            recipients = self.recipient.getContacts()
-        target = ""
-        # we could eventually allow more in the future
-        allowed = 1
-        for key in recipients:
-            count = len(recipients[key])
-            if count == 0:
-                continue
-            allowed -= count
-            if allowed < 0:
-                return False
-            # TODO: change this if later more then one recipients are allowed
-            target = recipients[key][0]
-        setText()
-        if target == "":
-            return True
-        if target.startswith("@"):
-            _class = panels.MicroblogPanel
-            target = None if target == "@@" else target[1:]
-        else:
-            _class = panels.ChatPanel
-        self.host.getOrCreateLiberviaWidget(_class, target)
-        return True
-
-    def syncFromEditor(self, content):
-        """Synchronize to unibox and close the dialog afterward. Display
-        a message and leave the dialog open if the sync was not possible."""
-        if self.__syncToUniBox():
-            self._afterEditCb(content)
-            return
-        InfoDialog("Too many recipients",
-                   "A message with more than one direct recipient (To)," +
-                   " or with any special recipient (Cc or Bcc), could not be" +
-                   " stored in the quick box.\n\nPlease finish your composing" +
-                   " in the rich text editor, and send your message directly" +
-                   " from here.", Width="400px").center()
-
-    def edit(self, edit=True, abort=False, sync=False):
-        if not edit and not abort and not sync:  # force sending message even when the text has not been modified
-            if not self.__sendMessage():  # message has not been sent (missing information), do nothing
-                return
-        RichTextEditor.edit(self, edit, abort, sync)
-
-    def __sendMessage(self):
-        """Send the message."""
-        recipients = self.recipient.getContacts()
-        targets = []
-        for addr in recipients:
-            for recipient in recipients[addr]:
-                if recipient.startswith("@"):
-                    targets.append(("PUBLIC", None, addr) if recipient == "@@" else ("GROUP", recipient[1:], addr))
-                else:
-                    targets.append(("chat", recipient, addr))
-        # check that we actually have a message target and data
-        content = self.getContent()
-        if content['text'] == "" or len(targets) == 0:
-            InfoDialog("Missing information",
-                       "Some information are missing and the message hasn't been sent.", Width="400px").center()
-            return None
-        self.__syncToUniBox(recipients, emptyText=True)
-        extra = {'content_rich': content['text']}
-        if hasattr(self, 'title_panel'):
-            extra.update({'title': content['title']})
-        self.host.send(targets, content['text'], extra=extra)
-        return True
-
-
-class RecipientManager(ListManager):
-    """A manager for sub-panels to set the recipients for each recipient type."""
-
-    def __init__(self, parent, y_offset=0):
-        # TODO: be sure we also display empty groups and disconnected contacts + their groups
-        # store the full list of potential recipients (groups and contacts)
-        list_ = []
-        list_.append("@@")
-        list_.extend("@%s" % group for group in parent.host.contact_panel.getGroups())
-        list_.extend(contact for contact in parent.host.contact_panel.getContacts())
-        ListManager.__init__(self, parent, composition.RECIPIENT_TYPES, list_, {'y': y_offset})
-
-        self.registerPopupMenuPanel(entries=composition.RECIPIENT_TYPES,
-                                    hide=lambda sender, key: self.__children[key]["panel"].isVisible(),
-                                    callback=self.setContactPanelVisible)
-
-
-class EditTextArea(TextArea, KeyboardHandler):
-    def __init__(self, _parent):
-        TextArea.__init__(self)
-        self._parent = _parent
-        KeyboardHandler.__init__(self)
-        self.addKeyboardListener(self)
-
-    def onKeyDown(self, sender=None, keycode=None, modifiers=None):
-        for listener in self._parent.edit_listeners:
-            listener(self, keycode)
--- a/browser_side/xmlui.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,418 +0,0 @@
-#!/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/>.
-
-from sat.core.log import getLogger
-log = getLogger(__name__)
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.TabPanel import TabPanel
-from pyjamas.ui.Grid import Grid
-from pyjamas.ui.Label import Label
-from pyjamas.ui.TextBox import TextBox
-from pyjamas.ui.PasswordTextBox import PasswordTextBox
-from pyjamas.ui.TextArea import TextArea
-from pyjamas.ui.CheckBox import CheckBox
-from pyjamas.ui.ListBox import ListBox
-from pyjamas.ui.Button import Button
-from pyjamas.ui.HTML import HTML
-from nativedom import NativeDOM
-from sat_frontends.tools import xmlui
-
-
-class EmptyWidget(xmlui.EmptyWidget, Label):
-
-    def __init__(self, parent):
-        Label.__init__(self, '')
-
-
-class TextWidget(xmlui.TextWidget, Label):
-
-    def __init__(self, parent, value):
-        Label.__init__(self, value)
-
-
-class LabelWidget(xmlui.LabelWidget, TextWidget):
-
-    def __init__(self, parent, value):
-        TextWidget.__init__(self, parent, value+": ")
-
-
-class JidWidget(xmlui.JidWidget, TextWidget):
-
-    def __init__(self, parent, value):
-        TextWidget.__init__(self, parent, value)
-
-
-class DividerWidget(xmlui.DividerWidget, HTML):
-
-    def __init__(self, parent, style='line'):
-        """Add a divider
-
-        @param parent
-        @param style (string): one of:
-                               - line: a simple line
-                               - dot: a line of dots
-                               - dash: a line of dashes
-                               - plain: a full thick line
-                               - blank: a blank line/space
-        """
-        HTML.__init__(self, "<hr/>")
-        self.addStyleName(style)
-
-
-class StringWidget(xmlui.StringWidget, TextBox):
-
-    def __init__(self, parent, value):
-        TextBox.__init__(self)
-        self.setText(value)
-
-    def _xmluiSetValue(self, value):
-        self.setText(value)
-
-    def _xmluiGetValue(self):
-        return self.getText()
-
-    def _xmluiOnChange(self, callback):
-        self.addChangeListener(callback)
-
-
-class PasswordWidget(xmlui.PasswordWidget, PasswordTextBox):
-
-    def __init__(self, parent, value):
-        PasswordTextBox.__init__(self)
-        self.setText(value)
-
-    def _xmluiSetValue(self, value):
-        self.setText(value)
-
-    def _xmluiGetValue(self):
-        return self.getText()
-
-    def _xmluiOnChange(self, callback):
-        self.addChangeListener(callback)
-
-
-class TextBoxWidget(xmlui.TextBoxWidget, TextArea):
-
-    def __init__(self, parent, value):
-        TextArea.__init__(self)
-        self.setText(value)
-
-    def _xmluiSetValue(self, value):
-        self.setText(value)
-
-    def _xmluiGetValue(self):
-        return self.getText()
-
-    def _xmluiOnChange(self, callback):
-        self.addChangeListener(callback)
-
-
-class BoolWidget(xmlui.BoolWidget, CheckBox):
-
-    def __init__(self, parent, state):
-        CheckBox.__init__(self)
-        self.setChecked(state)
-
-    def _xmluiSetValue(self, value):
-        self.setChecked(value == "true")
-
-    def _xmluiGetValue(self):
-        return "true" if self.isChecked() else "false"
-
-    def _xmluiOnChange(self, callback):
-        self.addClickListener(callback)
-
-
-class ButtonWidget(xmlui.ButtonWidget, Button):
-
-    def __init__(self, parent, value, click_callback):
-        Button.__init__(self, value, click_callback)
-
-    def _xmluiOnClick(self, callback):
-        self.addClickListener(callback)
-
-
-class ListWidget(xmlui.ListWidget, ListBox):
-
-    def __init__(self, parent, options, selected, flags):
-        ListBox.__init__(self)
-        multi_selection = 'single' not in flags
-        self.setMultipleSelect(multi_selection)
-        if multi_selection:
-            self.setVisibleItemCount(5)
-        for option in options:
-            self.addItem(option[1])
-        self._xmlui_attr_map = {label: value for value, label in options}
-        self._xmluiSelectValues(selected)
-
-    def _xmluiSelectValue(self, value):
-        """Select a value checking its item"""
-        try:
-            label = [label for label, _value in self._xmlui_attr_map.items() if _value == value][0]
-        except IndexError:
-            log.warning("Can't find value [%s] to select" % value)
-            return
-        self.selectItem(label)
-
-    def _xmluiSelectValues(self, values):
-        """Select multiple values, ignore the items"""
-        self.setValueSelection(values)
-
-    def _xmluiGetSelectedValues(self):
-        ret = []
-        for label in self.getSelectedItemText():
-            ret.append(self._xmlui_attr_map[label])
-        return ret
-
-    def _xmluiOnChange(self, callback):
-        self.addChangeListener(callback)
-
-    def _xmluiAddValues(self, values, select=True):
-        selected = self._xmluiGetSelectedValues()
-        for value in values:
-            if value not in self._xmlui_attr_map.values():
-                self.addItem(value)
-                self._xmlui_attr_map[value] = value
-            if value not in selected:
-                selected.append(value)
-        self._xmluiSelectValues(selected)
-
-
-class LiberviaContainer(object):
-
-    def _xmluiAppend(self, widget):
-        self.append(widget)
-
-
-class AdvancedListContainer(xmlui.AdvancedListContainer, Grid):
-
-    def __init__(self, parent, columns, selectable='no'):
-        Grid.__init__(self, 0, columns)
-        self.columns = columns
-        self.row = -1
-        self.col = 0
-        self._xmlui_rows_idx = []
-        self._xmlui_selectable = selectable != 'no'
-        self._xmlui_selected_row = None
-        self.addTableListener(self)
-        if self._xmlui_selectable:
-            self.addStyleName('AdvancedListSelectable')
-
-    def onCellClicked(self, grid, row, col):
-        if not self._xmlui_selectable:
-            return
-        self._xmlui_selected_row = row
-        try:
-            self._xmlui_select_cb(self)
-        except AttributeError:
-            log.warning("no select callback set")
-
-
-    def _xmluiAppend(self, widget):
-        self.setWidget(self.row, self.col, widget)
-        self.col += 1
-
-    def _xmluiAddRow(self, idx):
-        self.row += 1
-        self.col = 0
-        self._xmlui_rows_idx.insert(self.row, idx)
-        self.resizeRows(self.row+1)
-
-    def _xmluiGetSelectedWidgets(self):
-        return [self.getWidget(self._xmlui_selected_row, col) for col in range(self.columns)]
-
-    def _xmluiGetSelectedIndex(self):
-        try:
-            return self._xmlui_rows_idx[self._xmlui_selected_row]
-        except TypeError:
-            return None
-
-    def _xmluiOnSelect(self, callback):
-        self._xmlui_select_cb = callback
-
-
-class PairsContainer(xmlui.PairsContainer, Grid):
-
-    def __init__(self, parent):
-        Grid.__init__(self, 0, 0)
-        self.row = 0
-        self.col = 0
-
-    def _xmluiAppend(self, widget):
-        if self.col == 0:
-            self.resize(self.row+1, 2)
-        self.setWidget(self.row, self.col, widget)
-        self.col += 1
-        if self.col == 2:
-            self.row +=1
-            self.col = 0
-
-
-
-class TabsContainer(LiberviaContainer, xmlui.TabsContainer, TabPanel):
-
-    def __init__(self, parent):
-        TabPanel.__init__(self)
-        self.setStyleName('liberviaTabPanel')
-
-    def _xmluiAddTab(self, label):
-        tab_panel = VerticalContainer(self)
-        self.add(tab_panel, label)
-        if len(self.getChildren()) == 1:
-            self.selectTab(0)
-        return tab_panel
-
-
-class VerticalContainer(LiberviaContainer, xmlui.VerticalContainer, VerticalPanel):
-    __bases__ = (LiberviaContainer, xmlui.VerticalContainer, VerticalPanel)
-
-    def __init__(self, parent):
-        VerticalPanel.__init__(self)
-
-
-class WidgetFactory(object):
-    # XXX: __getattr__ doesn't work here for an unknown reason
-
-    def createVerticalContainer(self, *args, **kwargs):
-        instance = VerticalContainer(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createPairsContainer(self, *args, **kwargs):
-        instance = PairsContainer(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createTabsContainer(self, *args, **kwargs):
-        instance = TabsContainer(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createAdvancedListContainer(self, *args, **kwargs):
-        instance = AdvancedListContainer(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createEmptyWidget(self, *args, **kwargs):
-        instance = EmptyWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createTextWidget(self, *args, **kwargs):
-        instance = TextWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createLabelWidget(self, *args, **kwargs):
-        instance = LabelWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createJidWidget(self, *args, **kwargs):
-        instance = JidWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createDividerWidget(self, *args, **kwargs):
-        instance = DividerWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createStringWidget(self, *args, **kwargs):
-        instance = StringWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createPasswordWidget(self, *args, **kwargs):
-        instance = PasswordWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createTextBoxWidget(self, *args, **kwargs):
-        instance = TextBoxWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createBoolWidget(self, *args, **kwargs):
-        instance = BoolWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createButtonWidget(self, *args, **kwargs):
-        instance = ButtonWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-    def createListWidget(self, *args, **kwargs):
-        instance = ListWidget(*args, **kwargs)
-        instance._xmlui_main = self._xmlui_main
-        return instance
-
-
-    # def __getattr__(self, attr):
-    #     if attr.startswith("create"):
-    #         cls = globals()[attr[6:]]
-    #         cls._xmlui_main = self._xmlui_main
-    #         return cls
-
-
-class XMLUI(xmlui.XMLUI, VerticalPanel):
-    widget_factory = WidgetFactory()
-
-    def __init__(self, host, xml_data, title = None, flags = None):
-        self.widget_factory._xmlui_main = self
-        self.dom = NativeDOM()
-        dom_parse = lambda xml_data: self.dom.parseString(xml_data)
-        VerticalPanel.__init__(self)
-        self.setSize('100%', '100%')
-        xmlui.XMLUI.__init__(self, host, xml_data, title, flags, dom_parse)
-
-    def setCloseCb(self, close_cb):
-        self.close_cb = close_cb
-
-    def _xmluiClose(self):
-        if self.close_cb:
-            self.close_cb()
-        else:
-            log.warning("no close method defined")
-
-    def _xmluiLaunchAction(self, action_id, data):
-        self.host.launchAction(action_id, data)
-
-    def _xmluiSetParam(self, name, value, category):
-        self.host.bridge.call('setParam', None, name, value, category)
-
-    def constructUI(self, xml_data):
-        super(XMLUI, self).constructUI(xml_data)
-        self.add(self.main_cont)
-        self.setCellHeight(self.main_cont, '100%')
-        if self.type == 'form':
-            hpanel = HorizontalPanel()
-            hpanel.setStyleName('marginAuto')
-            hpanel.add(Button('Submit',self.onFormSubmitted))
-            if not 'NO_CANCEL' in self.flags:
-                hpanel.add(Button('Cancel',self.onFormCancelled))
-            self.add(hpanel)
-        elif self.type == 'param':
-            assert(isinstance(self.children[0][0],TabPanel))
-            hpanel = HorizontalPanel()
-            hpanel.add(Button('Save', self.onSaveParams))
-            hpanel.add(Button('Cancel', lambda ignore: self._xmluiClose()))
-            self.add(hpanel)
--- a/constants.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Primitivus: a SAT frontend
-# Copyright (C) 2009, 2010, 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/>.
-
-from sat.core.i18n import D_
-from sat_frontends import constants
-
-
-class Const(constants.Const):
-
-    APP_NAME = 'Libervia'
-    SERVICE_PROFILE = 'libervia'  # the SàT profile that is used for exporting the service
-
-    TIMEOUT = 300  # Session's time out, after that the user will be disconnected
-    LIBERVIA_DIR = "output/"
-    MEDIA_DIR = "media/"
-    AVATARS_DIR = "avatars/"
-    CARDS_DIR = "games/cards/tarot"
-
-    ERRNUM_BRIDGE_ERRBACK = 0  # FIXME
-    ERRNUM_LIBERVIA = 0  # FIXME
-
-    # Security limit for Libervia (get/set params)
-    SECURITY_LIMIT = 5
-
-    # Security limit for Libervia server_side
-    SERVER_SECURITY_LIMIT = constants.Const.NO_SECURITY_LIMIT
-
-    # Frontend parameters
-    ENABLE_UNIBOX_KEY = D_("Composition")
-    ENABLE_UNIBOX_PARAM = D_("Enable unibox")
-
-    # MISC
-    PASSWORD_MIN_LENGTH = 6  # for new account creation
-    LOG_OPT_SECTION = APP_NAME.lower()
--- a/libervia.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,903 +0,0 @@
-#!/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
-
-### logging configuration ###
-from browser_side import logging
-logging.configure()
-from sat.core.log import getLogger
-log = getLogger(__name__)
-###
-
-from pyjamas.ui.RootPanel import RootPanel
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.KeyboardListener import KEY_ESCAPE
-from pyjamas.Timer import Timer
-from pyjamas import Window, DOM
-from pyjamas.JSONService import JSONProxy
-
-from browser_side.register import RegisterBox
-from browser_side.contact import ContactPanel
-from browser_side.base_widget import WidgetsPanel
-from browser_side.panels import MicroblogItem
-from browser_side import panels, dialog
-from browser_side.jid import JID
-from browser_side.xmlui import XMLUI
-from browser_side.html_tools import html_sanitize
-from browser_side.notification import Notification
-
-from sat_frontends.tools.misc import InputHistory
-from sat_frontends.tools.strings import getURLParams
-from sat.core.i18n import _
-from constants import Const as C
-
-
-MAX_MBLOG_CACHE = 500  # Max microblog entries kept in memories
-
-# Set to true to not create a new LiberviaWidget when a similar one
-# already exist (i.e. a chat panel with the same target). Instead
-# the existing widget will be eventually removed from its parent
-# and added to new WidgetsPanel, or replaced to the expected
-# position if the previous and the new parent are the same.
-REUSE_EXISTING_LIBERVIA_WIDGETS = True
-
-
-class LiberviaJsonProxy(JSONProxy):
-    def __init__(self, *args, **kwargs):
-        JSONProxy.__init__(self, *args, **kwargs)
-        self.handler = self
-        self.cb = {}
-        self.eb = {}
-
-    def call(self, method, cb, *args):
-        _id = self.callMethod(method, args)
-        if cb:
-            if isinstance(cb, tuple):
-                if len(cb) != 2:
-                    log.error("tuple syntax for bridge.call is (callback, errback), aborting")
-                    return
-                if cb[0] is not None:
-                    self.cb[_id] = cb[0]
-                self.eb[_id] = cb[1]
-            else:
-                self.cb[_id] = cb
-
-    def onRemoteResponse(self, response, request_info):
-        if request_info.id in self.cb:
-            _cb = self.cb[request_info.id]
-            # if isinstance(_cb, tuple):
-            #     #we have arguments attached to the callback
-            #     #we send them after the answer
-            #     callback, args = _cb
-            #     callback(response, *args)
-            # else:
-            #     #No additional argument, we call directly the callback
-            _cb(response)
-            del self.cb[request_info.id]
-            if request_info.id in self.eb:
-                del self.eb[request_info.id]
-
-    def onRemoteError(self, code, errobj, request_info):
-        """def dump(obj):
-            print "\n\nDUMPING %s\n\n" % obj
-            for i in dir(obj):
-                print "%s: %s" % (i, getattr(obj,i))"""
-        if request_info.id in self.eb:
-            _eb = self.eb[request_info.id]
-            _eb((code, errobj))
-            del self.cb[request_info.id]
-            del self.eb[request_info.id]
-        else:
-            if code != 0:
-                log.error("Internal server error")
-                """for o in code, error, request_info:
-                    dump(o)"""
-            else:
-                if isinstance(errobj['message'], dict):
-                    log.error("Error %s: %s" % (errobj['message']['faultCode'], errobj['message']['faultString']))
-                else:
-                    log.error("%s" % errobj['message'])
-
-
-class RegisterCall(LiberviaJsonProxy):
-    def __init__(self):
-        LiberviaJsonProxy.__init__(self, "/register_api",
-                        ["isRegistered", "isConnected", "connect", "registerParams", "getMenus"])
-
-
-class BridgeCall(LiberviaJsonProxy):
-    def __init__(self):
-        LiberviaJsonProxy.__init__(self, "/json_api",
-                        ["getContacts", "addContact", "sendMessage", "sendMblog", "sendMblogComment",
-                         "getLastMblogs", "getMassiveLastMblogs", "getMblogComments", "getProfileJid",
-                         "getHistory", "getPresenceStatuses", "joinMUC", "mucLeave", "getRoomsJoined",
-                         "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady",
-                         "tarotGamePlayCards", "launchRadioCollective", "getMblogs", "getMblogsWithComments",
-                         "getWaitingSub", "subscription", "delContact", "updateContact", "getCard",
-                         "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction",
-                         "disconnect", "chatStateComposing", "getNewAccountDomain", "confirmationAnswer",
-                         "syntaxConvert", "getAccountDialogUI",
-                        ])
-
-
-class BridgeSignals(LiberviaJsonProxy):
-    RETRY_BASE_DELAY = 1000
-
-    def __init__(self, host):
-        self.host = host
-        self.retry_delay = self.RETRY_BASE_DELAY
-        LiberviaJsonProxy.__init__(self, "/json_signal_api",
-                        ["getSignals"])
-
-    def onRemoteResponse(self, response, request_info):
-        self.retry_delay = self.RETRY_BASE_DELAY
-        LiberviaJsonProxy.onRemoteResponse(self, response, request_info)
-
-    def onRemoteError(self, code, errobj, request_info):
-        if errobj['message'] == 'Empty Response':
-            Window.getLocation().reload()  # XXX: reset page in case of session ended.
-                                           # FIXME: Should be done more properly without hard reload
-        LiberviaJsonProxy.onRemoteError(self, code, errobj, request_info)
-        #we now try to reconnect
-        if isinstance(errobj['message'], dict) and errobj['message']['faultCode'] == 0:
-            Window.alert('You are not allowed to connect to server')
-        else:
-            def _timerCb(timer):
-                self.host.bridge_signals.call('getSignals', self.host._getSignalsCB)
-            Timer(notify=_timerCb).schedule(self.retry_delay)
-            self.retry_delay *= 2
-
-
-class SatWebFrontend(InputHistory):
-    def onModuleLoad(self):
-        log.info("============ onModuleLoad ==============")
-        panels.ChatPanel.registerClass()
-        panels.MicroblogPanel.registerClass()
-        self.whoami = None
-        self._selected_listeners = set()
-        self.bridge = BridgeCall()
-        self.bridge_signals = BridgeSignals(self)
-        self.uni_box = None
-        self.status_panel = HTML('<br />')
-        self.contact_panel = ContactPanel(self)
-        self.panel = panels.MainPanel(self)
-        self.discuss_panel = self.panel.discuss_panel
-        self.tab_panel = self.panel.tab_panel
-        self.tab_panel.addTabListener(self)
-        self.libervia_widgets = set()  # keep track of all actives LiberviaWidgets
-        self.room_list = []  # list of rooms
-        self.mblog_cache = []  # used to keep our own blog entries in memory, to show them in new mblog panel
-        self.avatars_cache = {}  # keep track of jid's avatar hash (key=jid, value=file)
-        self._register_box = None
-        RootPanel().add(self.panel)
-        self.notification = Notification()
-        DOM.addEventPreview(self)
-        self._register = RegisterCall()
-        self._register.call('getMenus', self.panel.menu.createMenus)
-        self._register.call('registerParams', None)
-        self._register.call('isRegistered', self._isRegisteredCB)
-        self.initialised = False
-        self.init_cache = []  # used to cache events until initialisation is done
-        # define here the parameters that have an incidende to UI refresh
-        self.params_ui = {"unibox": {"name": C.ENABLE_UNIBOX_PARAM,
-                                     "category": C.ENABLE_UNIBOX_KEY,
-                                     "cast": lambda value: value == 'true',
-                                     "value": None
-                                     }
-                          }
-
-    def addSelectedListener(self, callback):
-        self._selected_listeners.add(callback)
-
-    def getSelected(self):
-        wid = self.tab_panel.getCurrentPanel()
-        if not isinstance(wid, WidgetsPanel):
-            log.error("Tab widget is not a WidgetsPanel, can't get selected widget")
-            return None
-        return wid.selected
-
-    def setSelected(self, widget):
-        """Define the selected widget"""
-        widgets_panel = self.tab_panel.getCurrentPanel()
-        if not isinstance(widgets_panel, WidgetsPanel):
-            return
-
-        selected = widgets_panel.selected
-
-        if selected == widget:
-            return
-
-        if selected:
-            selected.removeStyleName('selected_widget')
-
-        widgets_panel.selected = widget
-
-        if widget:
-            widgets_panel.selected.addStyleName('selected_widget')
-
-        for callback in self._selected_listeners:
-            callback(widget)
-
-    def resize(self):
-        """Resize elements"""
-        Window.onResize()
-
-    def onBeforeTabSelected(self, sender, tab_index):
-        return True
-
-    def onTabSelected(self, sender, tab_index):
-        selected = self.getSelected()
-        for callback in self._selected_listeners:
-            callback(selected)
-
-    def onEventPreview(self, event):
-        if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE:
-            #needed to prevent request cancellation in Firefox
-            event.preventDefault()
-        return True
-
-    def getAvatar(self, jid_str):
-        """Return avatar of a jid if in cache, else ask for it"""
-        def dataReceived(result):
-            if 'avatar' in result:
-                self._entityDataUpdatedCb(jid_str, 'avatar', result['avatar'])
-            else:
-                self.bridge.call("getCard", None, jid_str)
-
-        def avatarError(error_data):
-            # The jid is maybe not in our roster, we ask for the VCard
-            self.bridge.call("getCard", None, jid_str)
-
-        if jid_str not in self.avatars_cache:
-            self.bridge.call('getEntityData', (dataReceived, avatarError), jid_str, ['avatar'])
-            self.avatars_cache[jid_str] = "/media/icons/tango/emotes/64/face-plain.png"
-        return self.avatars_cache[jid_str]
-
-    def registerWidget(self, wid):
-        log.debug("Registering %s" % wid.getDebugName())
-        self.libervia_widgets.add(wid)
-
-    def unregisterWidget(self, wid):
-        try:
-            self.libervia_widgets.remove(wid)
-        except KeyError:
-            log.warning('trying to remove a non registered Widget: %s' % wid.getDebugName())
-
-    def refresh(self):
-        """Refresh the general display."""
-        self.panel.refresh()
-        if self.params_ui['unibox']['value']:
-            self.uni_box = self.panel.unibox_panel.unibox
-        else:
-            self.uni_box = None
-        for lib_wid in self.libervia_widgets:
-            lib_wid.refresh()
-        self.resize()
-
-    def addTab(self, label, wid, select=True):
-        """Create a new tab and eventually add a widget in
-        @param label: label of the tab
-        @param wid: LiberviaWidget to add
-        @param select: True to select the added tab
-        """
-        widgets_panel = WidgetsPanel(self)
-        self.tab_panel.add(widgets_panel, label)
-        widgets_panel.addWidget(wid)
-        if select:
-            self.tab_panel.selectTab(self.tab_panel.getWidgetCount() - 1)
-        return widgets_panel
-
-    def addWidget(self, wid, tab_index=None):
-        """ Add a widget at the bottom of the current or specified tab
-        @param wid: LiberviaWidget to add
-        @param tab_index: index of the tab to add the widget to"""
-        if tab_index is None or tab_index < 0 or tab_index >= self.tab_panel.getWidgetCount():
-            panel = self.tab_panel.getCurrentPanel()
-        else:
-            panel = self.tab_panel.tabBar.getTabWidget(tab_index)
-        panel.addWidget(wid)
-
-    def displayNotification(self, title, body):
-        self.notification.notify(title, body)
-
-    def _isRegisteredCB(self, result):
-        registered, warning = result
-        if not registered:
-            self._register_box = RegisterBox(self.logged)
-            self._register_box.centerBox()
-            self._register_box.show()
-            if warning:
-                dialog.InfoDialog(_('Security warning'), warning).show()
-            self._tryAutoConnect(skip_validation=not not warning)
-        else:
-            self._register.call('isConnected', self._isConnectedCB)
-
-    def _isConnectedCB(self, connected):
-        if not connected:
-            self._register.call('connect', lambda x: self.logged())
-        else:
-            self.logged()
-
-    def logged(self):
-        if self._register_box:
-            self._register_box.hide()
-            del self._register_box  # don't work if self._register_box is None
-
-        # display the real presence status panel
-        self.panel.header.remove(self.status_panel)
-        self.status_panel = panels.PresenceStatusPanel(self)
-        self.panel.header.add(self.status_panel)
-
-        #it's time to fill the page
-        self.bridge.call('getContacts', self._getContactsCB)
-        self.bridge.call('getParamsUI', self._getParamsUICB)
-        self.bridge_signals.call('getSignals', self._getSignalsCB)
-        #We want to know our own jid
-        self.bridge.call('getProfileJid', self._getProfileJidCB)
-
-        def domain_cb(value):
-            self._defaultDomain = value
-            log.info("new account domain: %s" % value)
-
-        def domain_eb(value):
-            self._defaultDomain = "libervia.org"
-
-        self.bridge.call("getNewAccountDomain", (domain_cb, domain_eb))
-        self.discuss_panel.addWidget(panels.MicroblogPanel(self, []))
-
-        # get ui params and refresh the display
-        count = 0  # used to do something similar to DeferredList
-
-        def params_ui_cb(param, value=None):
-            count += 1
-            refresh = count == len(self.params_ui)
-            self._paramUpdate(param['name'], value, param['category'], refresh)
-        for param in self.params_ui:
-            self.bridge.call('asyncGetParamA', lambda value: params_ui_cb(self.params_ui[param], value),
-                             self.params_ui[param]['name'], self.params_ui[param]['category'])
-
-    def _tryAutoConnect(self, skip_validation=False):
-        """This method retrieve the eventual URL parameters to auto-connect the user.
-        @param skip_validation: if True, set the form values but do not validate it
-        """
-        params = getURLParams(Window.getLocation().getSearch())
-        if "login" in params:
-            self._register_box._form.login_box.setText(params["login"])
-            self._register_box._form.login_pass_box.setFocus(True)
-            if "passwd" in params:
-                # try to connect
-                self._register_box._form.login_pass_box.setText(params["passwd"])
-                if not skip_validation:
-                    self._register_box._form.onLogin(None)
-                return True
-            else:
-                # this would eventually set the browser saved password
-                Timer(5, lambda: self._register_box._form.login_pass_box.setFocus(True))
-
-    def _actionCb(self, data):
-        if not data:
-            # action was a one shot, nothing to do
-            pass
-        elif "xmlui" in data:
-            ui = XMLUI(self, xml_data = data['xmlui'])
-            options = ['NO_CLOSE'] if ui.type == 'form' else []
-            _dialog = dialog.GenericDialog(ui.title, ui, options=options)
-            ui.setCloseCb(_dialog.close)
-            _dialog.show()
-        else:
-            dialog.InfoDialog("Error",
-                              "Unmanaged action result", Width="400px").center()
-
-    def _actionEb(self, err_data):
-        err_code, err_obj = err_data
-        dialog.InfoDialog("Error",
-                          str(err_obj), Width="400px").center()
-
-    def launchAction(self, callback_id, data):
-        """ Launch a dynamic action
-        @param callback_id: id of the action to launch
-        @param data: data needed only for certain actions
-
-        """
-        if data is None:
-            data = {}
-        self.bridge.call('launchAction', (self._actionCb, self._actionEb), callback_id, data)
-
-    def _getContactsCB(self, contacts_data):
-        for contact in contacts_data:
-            jid, attributes, groups = contact
-            self._newContactCb(jid, attributes, groups)
-
-    def _getSignalsCB(self, signal_data):
-        self.bridge_signals.call('getSignals', self._getSignalsCB)
-        log.debug("Got signal ==> name: %s, params: %s" % (signal_data[0], signal_data[1]))
-        name, args = signal_data
-        if name == 'personalEvent':
-            self._personalEventCb(*args)
-        elif name == 'newMessage':
-            self._newMessageCb(*args)
-        elif name == 'presenceUpdate':
-            self._presenceUpdateCb(*args)
-        elif name == 'paramUpdate':
-            self._paramUpdate(*args)
-        elif name == 'roomJoined':
-            self._roomJoinedCb(*args)
-        elif name == 'roomLeft':
-            self._roomLeftCb(*args)
-        elif name == 'roomUserJoined':
-            self._roomUserJoinedCb(*args)
-        elif name == 'roomUserLeft':
-            self._roomUserLeftCb(*args)
-        elif name == 'roomUserChangedNick':
-            self._roomUserChangedNickCb(*args)
-        elif name == 'askConfirmation':
-            self._askConfirmation(*args)
-        elif name == 'newAlert':
-            self._newAlert(*args)
-        elif name == 'tarotGamePlayers':
-            self._tarotGameStartedCb(True, *args)
-        elif name == 'tarotGameStarted':
-            self._tarotGameStartedCb(False, *args)
-        elif name == 'tarotGameNew' or \
-             name == 'tarotGameChooseContrat' or \
-             name == 'tarotGameShowCards' or \
-             name == 'tarotGameInvalidCards' or \
-             name == 'tarotGameCardsPlayed' or \
-             name == 'tarotGameYourTurn' or \
-             name == 'tarotGameScore':
-            self._tarotGameGenericCb(name, args[0], args[1:])
-        elif name == 'radiocolPlayers':
-            self._radioColStartedCb(True, *args)
-        elif name == 'radiocolStarted':
-            self._radioColStartedCb(False, *args)
-        elif name == 'radiocolPreload':
-            self._radioColGenericCb(name, args[0], args[1:])
-        elif name == 'radiocolPlay':
-            self._radioColGenericCb(name, args[0], args[1:])
-        elif name == 'radiocolNoUpload':
-            self._radioColGenericCb(name, args[0], args[1:])
-        elif name == 'radiocolUploadOk':
-            self._radioColGenericCb(name, args[0], args[1:])
-        elif name == 'radiocolSongRejected':
-            self._radioColGenericCb(name, args[0], args[1:])
-        elif name == 'subscribe':
-            self._subscribeCb(*args)
-        elif name == 'contactDeleted':
-            self._contactDeletedCb(*args)
-        elif name == 'newContact':
-            self._newContactCb(*args)
-        elif name == 'entityDataUpdated':
-            self._entityDataUpdatedCb(*args)
-        elif name == 'chatStateReceived':
-            self._chatStateReceivedCb(*args)
-
-    def _getParamsUICB(self, xmlui):
-        """Hide the parameters item if there's nothing to display"""
-        if not xmlui:
-            self.panel.menu.removeItemParams()
-
-    def _ownBlogsFills(self, mblogs):
-        #put our own microblogs in cache, then fill all panels with them
-        for publisher in mblogs:
-            for mblog in mblogs[publisher]:
-                if not mblog.has_key('content'):
-                    log.warning("No content found in microblog [%s]" % mblog)
-                    continue
-                if mblog.has_key('groups'):
-                    _groups = set(mblog['groups'].split() if mblog['groups'] else [])
-                else:
-                    _groups = None
-                mblog_entry = MicroblogItem(mblog)
-                self.mblog_cache.append((_groups, mblog_entry))
-
-        if len(self.mblog_cache) > MAX_MBLOG_CACHE:
-            del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)]
-        for lib_wid in self.libervia_widgets:
-            if isinstance(lib_wid, panels.MicroblogPanel):
-                self.FillMicroblogPanel(lib_wid)
-        self.initialised = True # initialisation phase is finished here
-        for event_data in self.init_cache: # so we have to send all the cached events
-            self._personalEventCb(*event_data)
-        del self.init_cache
-
-    def _getProfileJidCB(self, jid):
-        self.whoami = JID(jid)
-        #we can now ask our status
-        self.bridge.call('getPresenceStatuses', self._getPresenceStatusesCb)
-        #the rooms where we are
-        self.bridge.call('getRoomsJoined', self._getRoomsJoinedCb)
-        #and if there is any subscription request waiting for us
-        self.bridge.call('getWaitingSub', self._getWaitingSubCb)
-        #we fill the panels already here
-        for lib_wid in self.libervia_widgets:
-            if isinstance(lib_wid, panels.MicroblogPanel):
-                if lib_wid.accept_all():
-                    self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'ALL', [], 10)
-                else:
-                    self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'GROUP', lib_wid.accepted_groups, 10)
-
-        #we ask for our own microblogs:
-        self.bridge.call('getMassiveLastMblogs', self._ownBlogsFills, 'JID', [self.whoami.bare], 10)
-
-    ## Signals callbacks ##
-
-    def _personalEventCb(self, sender, event_type, data):
-        if not self.initialised:
-            self.init_cache.append((sender, event_type, data))
-            return
-        sender = JID(sender).bare
-        if event_type == "MICROBLOG":
-            if not 'content' in data:
-                log.warning("No content found in microblog data")
-                return
-            if 'groups' in data:
-                _groups = set(data['groups'].split() if data['groups'] else [])
-            else:
-                _groups = None
-            mblog_entry = MicroblogItem(data)
-
-            for lib_wid in self.libervia_widgets:
-                if isinstance(lib_wid, panels.MicroblogPanel):
-                    self.addBlogEntry(lib_wid, sender, _groups, mblog_entry)
-
-            if sender == self.whoami.bare:
-                found = False
-                for index in xrange(0, len(self.mblog_cache)):
-                    entry = self.mblog_cache[index]
-                    if entry[1].id == mblog_entry.id:
-                        # replace existing entry
-                        self.mblog_cache.remove(entry)
-                        self.mblog_cache.insert(index, (_groups, mblog_entry))
-                        found = True
-                        break
-                if not found:
-                    self.mblog_cache.append((_groups, mblog_entry))
-                    if len(self.mblog_cache) > MAX_MBLOG_CACHE:
-                        del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)]
-        elif event_type == 'MICROBLOG_DELETE':
-            for lib_wid in self.libervia_widgets:
-                if isinstance(lib_wid, panels.MicroblogPanel):
-                    lib_wid.removeEntry(data['type'], data['id'])
-            log.debug("%s %s %s" % (self.whoami.bare, sender, data['type']))
-
-            if sender == self.whoami.bare and data['type'] == 'main_item':
-                for index in xrange(0, len(self.mblog_cache)):
-                    entry = self.mblog_cache[index]
-                    if entry[1].id == data['id']:
-                        self.mblog_cache.remove(entry)
-                        break
-
-    def addBlogEntry(self, mblog_panel, sender, _groups, mblog_entry):
-        """Check if an entry can go in MicroblogPanel and add to it
-        @param mblog_panel: MicroblogPanel instance
-        @param sender: jid of the entry sender
-        @param _groups: groups which can receive this entry
-        @param mblog_entry: MicroblogItem instance"""
-        if mblog_entry.type == "comment" or mblog_panel.isJidAccepted(sender) or (_groups == None and self.whoami and sender == self.whoami.bare) \
-           or (_groups and _groups.intersection(mblog_panel.accepted_groups)):
-            mblog_panel.addEntry(mblog_entry)
-
-    def FillMicroblogPanel(self, mblog_panel):
-        """Fill a microblog panel with entries in cache
-        @param mblog_panel: MicroblogPanel instance
-        """
-        #XXX: only our own entries are cached
-        for cache_entry in self.mblog_cache:
-            _groups, mblog_entry = cache_entry
-            self.addBlogEntry(mblog_panel, self.whoami.bare, *cache_entry)
-
-    def getEntityMBlog(self, entity):
-        log.info("geting mblog for entity [%s]" % (entity,))
-        for lib_wid in self.libervia_widgets:
-            if isinstance(lib_wid, panels.MicroblogPanel):
-                if lib_wid.isJidAccepted(entity):
-                    self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'JID', [entity], 10)
-
-    def getLiberviaWidget(self, class_, entity, ignoreOtherTabs=True):
-        """Get the corresponding panel if it exists.
-        @param class_: class of the panel (ChatPanel, MicroblogPanel...)
-        @param entity: polymorphic parameter, see class_.matchEntity.
-        @param ignoreOtherTabs: if True, the widgets that are not
-        contained by the currently selected tab will be ignored
-        @return: the existing widget that has been found or None."""
-        selected_tab = self.tab_panel.getCurrentPanel()
-        for lib_wid in self.libervia_widgets:
-            parent = lib_wid.getWidgetsPanel(verbose=False)
-            if parent is None or (ignoreOtherTabs and parent != selected_tab):
-                # do not return a widget that is not in the currently selected tab
-                continue
-            if isinstance(lib_wid, class_):
-                try:
-                    if lib_wid.matchEntity(entity):
-                        log.debug("existing widget found: %s" % lib_wid.getDebugName())
-                        return lib_wid
-                except AttributeError as e:
-                    e.stack_list()
-                    return None
-        return None
-
-    def getOrCreateLiberviaWidget(self, class_, entity, select=True, new_tab=None):
-        """Get the matching LiberviaWidget if it exists, or create a new one.
-        @param class_: class of the panel (ChatPanel, MicroblogPanel...)
-        @param entity: polymorphic parameter, see class_.matchEntity.
-        @param select: if True, select the widget that has been found or created
-        @param new_tab: if not None, a widget which is created is created in
-        a new tab. In that case new_tab is a unicode to label that new tab.
-        If new_tab is not None and a widget is found, no tab is created.
-        @return: the newly created wigdet if REUSE_EXISTING_LIBERVIA_WIDGETS
-         is set to False or if the widget has not been found, the existing
-         widget that has been found otherwise."""
-        lib_wid = None
-        tab = None
-        if REUSE_EXISTING_LIBERVIA_WIDGETS:
-            lib_wid = self.getLiberviaWidget(class_, entity, new_tab is None)
-        if lib_wid is None:  # create a new widget
-            lib_wid = class_.createPanel(self, entity[0] if isinstance(entity, tuple) else entity)
-            if new_tab is None:
-                self.addWidget(lib_wid)
-            else:
-                tab = self.addTab(new_tab, lib_wid, False)
-        else:  # reuse existing widget
-            tab = lib_wid.getWidgetsPanel(verbose=False)
-            if new_tab is None:
-                if tab is not None:
-                    tab.removeWidget(lib_wid)
-                self.addWidget(lib_wid)
-        if select:
-            if new_tab is not None:
-                self.tab_panel.selectTab(tab)
-            # must be done after the widget is added,
-            # for example to scroll to the bottom
-            self.setSelected(lib_wid)
-            lib_wid.refresh()
-        return lib_wid
-
-    def _newMessageCb(self, from_jid, msg, msg_type, to_jid, extra):
-        _from = JID(from_jid)
-        _to = JID(to_jid)
-        other = _to if _from.bare == self.whoami.bare else _from
-        lib_wid = self.getLiberviaWidget(panels.ChatPanel, other, ignoreOtherTabs=False)
-        self.displayNotification(_from, msg)
-        if lib_wid is not None:
-            lib_wid.printMessage(_from, msg, extra)
-        else:
-            # The message has not been shown, we must indicate it
-            self.contact_panel.setContactMessageWaiting(other.bare, True)
-
-    def _presenceUpdateCb(self, entity, show, priority, statuses):
-        entity_jid = JID(entity)
-        if self.whoami and self.whoami == entity_jid:  # XXX: QnD way to get our presence/status
-            self.status_panel.setPresence(show)
-            if statuses:
-                self.status_panel.setStatus(statuses.values()[0])
-        else:
-            self.contact_panel.setConnected(entity_jid.bare, entity_jid.resource, show, priority, statuses)
-
-    def _roomJoinedCb(self, room_jid, room_nicks, user_nick):
-        _target = JID(room_jid)
-        if _target not in self.room_list:
-            self.room_list.append(_target)
-        chat_panel = panels.ChatPanel(self, _target, type_='group')
-        chat_panel.setUserNick(user_nick)
-        if _target.node.startswith('sat_tarot_'): #XXX: it's not really beautiful, but it works :)
-            self.addTab("Tarot", chat_panel)
-        elif _target.node.startswith('sat_radiocol_'):
-            self.addTab("Radio collective", chat_panel)
-        else:
-            self.addTab(_target.node, chat_panel)
-        chat_panel.setPresents(room_nicks)
-        chat_panel.historyPrint()
-        chat_panel.refresh()
-
-    def _roomLeftCb(self, room_jid, room_nicks, user_nick):
-        # FIXME: room_list contains JID instances so why MUST we do
-        # 'remove(room_jid)' and not 'remove(JID(room_jid))' ????!!
-        # This looks like a pyjamas bug --> check/report
-        try:
-            self.room_list.remove(room_jid)
-        except KeyError:
-            pass
-
-    def _roomUserJoinedCb(self, room_jid_s, user_nick, user_data):
-        for lib_wid in self.libervia_widgets:
-            if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
-                lib_wid.userJoined(user_nick, user_data)
-
-    def _roomUserLeftCb(self, room_jid_s, user_nick, user_data):
-        for lib_wid in self.libervia_widgets:
-            if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
-                lib_wid.userLeft(user_nick, user_data)
-
-    def _roomUserChangedNickCb(self, room_jid_s, old_nick, new_nick):
-        """Called when an user joined a MUC room"""
-        for lib_wid in self.libervia_widgets:
-            if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
-                lib_wid.changeUserNick(old_nick, new_nick)
-
-    def _tarotGameStartedCb(self, waiting, room_jid_s, referee, players):
-        for lib_wid in self.libervia_widgets:
-            if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
-                lib_wid.startGame("Tarot", waiting, referee, players)
-
-    def _tarotGameGenericCb(self, event_name, room_jid_s, args):
-        for lib_wid in self.libervia_widgets:
-            if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
-                getattr(lib_wid.getGame("Tarot"), event_name)(*args)
-
-    def _radioColStartedCb(self, waiting, room_jid_s, referee, players, queue_data):
-        for lib_wid in self.libervia_widgets:
-            if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
-                lib_wid.startGame("RadioCol", waiting, referee, players, queue_data)
-
-    def _radioColGenericCb(self, event_name, room_jid_s, args):
-        for lib_wid in self.libervia_widgets:
-            if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
-                getattr(lib_wid.getGame("RadioCol"), event_name)(*args)
-
-    def _getPresenceStatusesCb(self, presence_data):
-        for entity in presence_data:
-            for resource in presence_data[entity]:
-                args = presence_data[entity][resource]
-                self._presenceUpdateCb("%s/%s" % (entity, resource), *args)
-
-    def _getRoomsJoinedCb(self, room_data):
-        for room in room_data:
-            self._roomJoinedCb(*room)
-
-    def _getWaitingSubCb(self, waiting_sub):
-        for sub in waiting_sub:
-            self._subscribeCb(waiting_sub[sub], sub)
-
-    def _subscribeCb(self, sub_type, entity):
-        if sub_type == 'subscribed':
-            dialog.InfoDialog('Subscription confirmation', 'The contact <b>%s</b> has added you to his/her contact list' % html_sanitize(entity)).show()
-            self.getEntityMBlog(entity)
-
-        elif sub_type == 'unsubscribed':
-            dialog.InfoDialog('Subscription refusal', 'The contact <b>%s</b> has refused to add you in his/her contact list' % html_sanitize(entity)).show()
-            #TODO: remove microblogs from panels
-
-        elif sub_type == 'subscribe':
-            #The user want to subscribe to our presence
-            _dialog = None
-            msg = HTML('The contact <b>%s</b> want to add you in his/her contact list, do you accept ?' % html_sanitize(entity))
-
-            def ok_cb(ignore):
-                self.bridge.call('subscription', None, "subscribed", entity, '', _dialog.getSelectedGroups())
-            def cancel_cb(ignore):
-                self.bridge.call('subscription', None, "unsubscribed", entity, '', '')
-
-            _dialog = dialog.GroupSelector([msg], self.contact_panel.getGroups(), [], "Add", ok_cb, cancel_cb)
-            _dialog.setHTML('<b>Add contact request</b>')
-            _dialog.show()
-
-    def _contactDeletedCb(self, entity):
-        self.contact_panel.removeContact(entity)
-
-    def _newContactCb(self, contact, attributes, groups):
-        self.contact_panel.updateContact(contact, attributes, groups)
-
-    def _entityDataUpdatedCb(self, entity_jid_s, key, value):
-        if key == "avatar":
-            avatar = '/avatars/%s' % value
-
-            self.avatars_cache[entity_jid_s] = avatar
-
-            for lib_wid in self.libervia_widgets:
-                if isinstance(lib_wid, panels.MicroblogPanel):
-                    if lib_wid.isJidAccepted(entity_jid_s) or (self.whoami and entity_jid_s == self.whoami.bare):
-                        lib_wid.updateValue('avatar', entity_jid_s, avatar)
-
-    def _chatStateReceivedCb(self, from_jid_s, state):
-        """Callback when a new chat state is received.
-        @param from_jid_s: JID of the contact who sent his state, or '@ALL@'
-        @param state: new state (string)
-        """
-        if from_jid_s == '@ALL@':
-            target = '@ALL@'
-            nick = C.ALL_OCCUPANTS
-        else:
-            from_jid = JID(from_jid_s)
-            target = from_jid.bare
-            nick = from_jid.resource
-
-        for lib_wid in self.libervia_widgets:
-            if isinstance(lib_wid, panels.ChatPanel):
-                if target == '@ALL' or target == lib_wid.target.bare:
-                    if lib_wid.type == 'one2one':
-                        lib_wid.setState(state)
-                    elif lib_wid.type == 'group':
-                        lib_wid.setState(state, nick=nick)
-
-    def _askConfirmation(self, confirmation_id, confirmation_type, data):
-        answer_data = {}
-
-        def confirm_cb(result):
-            self.bridge.call('confirmationAnswer', None, confirmation_id, result, answer_data)
-
-        if confirmation_type == "YES/NO":
-            dialog.ConfirmDialog(confirm_cb, text=data["message"], title=data["title"]).show()
-
-    def _newAlert(self, message, title, alert_type):
-        dialog.InfoDialog(title, message).show()
-
-    def _paramUpdate(self, name, value, category, refresh=True):
-        """This is called when the paramUpdate signal is received, but also
-        during initialization when the UI parameters values are retrieved.
-        @param refresh: set to True to refresh the general UI
-        """
-        for param in self.params_ui:
-            if name == self.params_ui[param]['name']:
-                self.params_ui[param]['value'] = self.params_ui[param]['cast'](value)
-                if refresh:
-                    self.refresh()
-                break
-
-    def sendError(self, errorData):
-        dialog.InfoDialog("Error while sending message",
-                          "Your message can't be sent", Width="400px").center()
-        log.error("sendError: %s" % str(errorData))
-
-    def send(self, targets, text, extra={}):
-        """Send a message to any target type.
-        @param targets: list of tuples (type, entities, addr) with:
-        - type in ("PUBLIC", "GROUP", "COMMENT", "STATUS" , "groupchat" , "chat")
-        - entities could be a JID, a list groups, a node hash... depending the target
-        - addr in ("To", "Cc", "Bcc") - ignore case
-        @param text: the message content
-        @param extra: options
-        """
-        # FIXME: too many magic strings, we should use constants instead
-        addresses = []
-        for target in targets:
-            type_, entities, addr = target[0], target[1], 'to' if len(target) < 3 else target[2].lower()
-            if type_ in ("PUBLIC", "GROUP"):
-                self.bridge.call("sendMblog", None, type_, entities if type_ == "GROUP" else None, text, extra)
-            elif type_ == "COMMENT":
-                self.bridge.call("sendMblogComment", None, entities, text, extra)
-            elif type_ == "STATUS":
-                self.bridge.call('setStatus', None, self.status_panel.presence, text)
-            elif type_ in ("groupchat", "chat"):
-                addresses.append((addr, entities))
-            else:
-                log.error("Unknown target type")
-        if addresses:
-            if len(addresses) == 1 and addresses[0][0] == 'to':
-                self.bridge.call('sendMessage', (None, self.sendError), addresses[0][1], text, '', type_, extra)
-            else:
-                extra.update({'address': '\n'.join([('%s:%s' % entry) for entry in addresses])})
-                self.bridge.call('sendMessage', (None, self.sendError), self.whoami.domain, text, '', type_, extra)
-
-    def showWarning(self, type_=None, msg=None):
-        """Display a popup information message, e.g. to notify the recipient of a message being composed.
-        If type_ is None, a popup being currently displayed will be hidden.
-        @type_: a type determining the CSS style to be applied (see WarningPopup.showWarning)
-        @msg: message to be displayed
-        """
-        if not hasattr(self, "warning_popup"):
-            self.warning_popup = panels.WarningPopup()
-        self.warning_popup.showWarning(type_, msg)
-
-
-if __name__ == '__main__':
-    pyjd.setup("http://localhost:8080/libervia.html")
-    app = SatWebFrontend()
-    app.onModuleLoad()
-    pyjd.run()
--- a/libervia_server/__init__.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1142 +0,0 @@
-#!/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/>.
-
-from twisted.application import service
-from twisted.internet import glib2reactor
-glib2reactor.install()
-from twisted.internet import reactor, defer
-from twisted.web import server
-from twisted.web.static import File
-from twisted.web.resource import Resource, NoResource
-from twisted.web.util import Redirect, redirectTo
-from twisted.python.components import registerAdapter
-from twisted.python.failure import Failure
-from twisted.words.protocols.jabber.jid import JID
-from txjsonrpc.web import jsonrpc
-from txjsonrpc import jsonrpclib
-
-from sat.core.log import getLogger
-log = getLogger(__name__)
-import re
-import glob
-import os.path
-import sys
-import tempfile
-import shutil
-import uuid
-from zope.interface import Interface, Attribute, implements
-from xml.dom import minidom
-from httplib import HTTPS_PORT
-
-from constants import Const as C
-from libervia_server.blog import MicroBlog
-from sat_frontends.bridge.DBus import DBusBridgeFrontend, BridgeExceptionNoService
-from sat.core.i18n import _, D_
-from sat.tools.xml_tools import paramsXML2XMLUI
-try:
-    import OpenSSL
-    from twisted.internet import ssl
-    ssl_available = True
-except:
-    ssl_available = False
-
-
-class ISATSession(Interface):
-    profile = Attribute("Sat profile")
-    jid = Attribute("JID associated with the profile")
-
-class SATSession(object):
-    implements(ISATSession)
-    def __init__(self, session):
-        self.profile = None
-        self.jid = None
-
-class LiberviaSession(server.Session):
-    sessionTimeout = C.TIMEOUT
-
-    def __init__(self, *args, **kwargs):
-        self.__lock = False
-        server.Session.__init__(self, *args, **kwargs)
-
-    def lock(self):
-        """Prevent session from expiring"""
-        self.__lock = True
-        self._expireCall.reset(sys.maxint)
-
-    def unlock(self):
-        """Allow session to expire again, and touch it"""
-        self.__lock = False
-        self.touch()
-
-    def touch(self):
-        if not self.__lock:
-            server.Session.touch(self)
-
-class ProtectedFile(File):
-    """A File class which doens't show directory listing"""
-
-    def directoryListing(self):
-        return NoResource()
-
-class SATActionIDHandler(object):
-    """Manage SàT action action_id lifecycle"""
-    ID_LIFETIME = 30 #after this time (in seconds), action_id will be suppressed and action result will be ignored
-
-    def __init__(self):
-        self.waiting_ids = {}
-
-    def waitForId(self, callback, action_id, profile, *args, **kwargs):
-        """Wait for an action result
-        @param callback: method to call when action gave a result back
-        @param action_id: action_id to wait for
-        @param profile: %(doc_profile)s
-        @param *args: additional argument to pass to callback
-        @param **kwargs: idem"""
-        action_tuple = (action_id, profile)
-        self.waiting_ids[action_tuple] = (callback, args, kwargs)
-        reactor.callLater(self.ID_LIFETIME, self.purgeID, action_tuple)
-
-    def purgeID(self, action_tuple):
-        """Called when an action_id has not be handled in time"""
-        if action_tuple in self.waiting_ids:
-            log.warning ("action of action_id %s [%s] has not been managed, action_id is now ignored" % action_tuple)
-            del self.waiting_ids[action_tuple]
-
-    def actionResultCb(self, answer_type, action_id, data, profile):
-        """Manage the actionResult signal"""
-        action_tuple = (action_id, profile)
-        if action_tuple in self.waiting_ids:
-            callback, args, kwargs = self.waiting_ids[action_tuple]
-            del self.waiting_ids[action_tuple]
-            callback(answer_type, action_id, data, *args, **kwargs)
-
-class JSONRPCMethodManager(jsonrpc.JSONRPC):
-
-    def __init__(self, sat_host):
-        jsonrpc.JSONRPC.__init__(self)
-        self.sat_host=sat_host
-
-    def asyncBridgeCall(self, method_name, *args, **kwargs):
-        """Call an asynchrone bridge method and return a deferred
-        @param method_name: name of the method as a unicode
-        @return: a deferred which trigger the result
-
-        """
-        d = defer.Deferred()
-
-        def _callback(*args):
-            if not args:
-                d.callback(None)
-            else:
-                if len(args) != 1:
-                    Exception("Multiple return arguments not supported")
-                d.callback(args[0])
-
-        def _errback(result):
-            d.errback(Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(result))))
-
-        kwargs["callback"] = _callback
-        kwargs["errback"] = _errback
-        getattr(self.sat_host.bridge, method_name)(*args, **kwargs)
-        return d
-
-
-class MethodHandler(JSONRPCMethodManager):
-
-    def __init__(self, sat_host):
-        JSONRPCMethodManager.__init__(self, sat_host)
-        self.authorized_params = None
-
-    def render(self, request):
-        self.session = request.getSession()
-        profile = ISATSession(self.session).profile
-        if not profile:
-            #user is not identified, we return a jsonrpc fault
-            parsed = jsonrpclib.loads(request.content.read())
-            fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia
-            return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))
-        return jsonrpc.JSONRPC.render(self, request)
-
-    def jsonrpc_getProfileJid(self):
-        """Return the jid of the profile"""
-        sat_session = ISATSession(self.session)
-        profile = sat_session.profile
-        sat_session.jid = JID(self.sat_host.bridge.getParamA("JabberID", "Connection", profile_key=profile))
-        return sat_session.jid.full()
-
-    def jsonrpc_disconnect(self):
-        """Disconnect the profile"""
-        sat_session = ISATSession(self.session)
-        profile = sat_session.profile
-        self.sat_host.bridge.disconnect(profile)
-
-    def jsonrpc_getContacts(self):
-        """Return all passed args."""
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.getContacts(profile)
-
-    def jsonrpc_addContact(self, entity, name, groups):
-        """Subscribe to contact presence, and add it to the given groups"""
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.addContact(entity, profile)
-        self.sat_host.bridge.updateContact(entity, name, groups, profile)
-
-    def jsonrpc_delContact(self, entity):
-        """Remove contact from contacts list"""
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.delContact(entity, profile)
-
-    def jsonrpc_updateContact(self, entity, name, groups):
-        """Update contact's roster item"""
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.updateContact(entity, name, groups, profile)
-
-    def jsonrpc_subscription(self, sub_type, entity, name, groups):
-        """Confirm (or infirm) subscription,
-        and setup user roster in case of subscription"""
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.subscription(sub_type, entity, profile)
-        if sub_type == 'subscribed':
-            self.sat_host.bridge.updateContact(entity, name, groups, profile)
-
-    def jsonrpc_getWaitingSub(self):
-        """Return list of room already joined by user"""
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.getWaitingSub(profile)
-
-    def jsonrpc_setStatus(self, presence, status):
-        """Change the presence and/or status
-        @param presence: value from ("", "chat", "away", "dnd", "xa")
-        @param status: any string to describe your status
-        """
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.setPresence('', presence, {'': status}, profile)
-
-
-    def jsonrpc_sendMessage(self, to_jid, msg, subject, type_, options={}):
-        """send message"""
-        profile = ISATSession(self.session).profile
-        return self.asyncBridgeCall("sendMessage", to_jid, msg, subject, type_, options, profile)
-
-    def jsonrpc_sendMblog(self, type_, dest, text, extra={}):
-        """ Send microblog message
-        @param type_: one of "PUBLIC", "GROUP"
-        @param dest: destinees (list of groups, ignored for "PUBLIC")
-        @param text: microblog's text
-        """
-        profile = ISATSession(self.session).profile
-        extra['allow_comments'] = 'True'
-
-        if not type_:  # auto-detect
-            type_ = "PUBLIC" if dest == [] else "GROUP"
-
-        if type_ in ("PUBLIC", "GROUP") and text:
-            if type_ == "PUBLIC":
-                #This text if for the public microblog
-                print "sending public blog"
-                return self.sat_host.bridge.sendGroupBlog("PUBLIC", [], text, extra, profile)
-            else:
-                print "sending group blog"
-                dest = dest if isinstance(dest, list) else [dest]
-                return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra, profile)
-        else:
-            raise Exception("Invalid data")
-
-    def jsonrpc_deleteMblog(self, pub_data, comments):
-        """Delete a microblog node
-        @param pub_data: a tuple (service, comment node identifier, item identifier)
-        @param comments: comments node identifier (for main item) or False
-        """
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile)
-
-    def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}):
-        """Modify a microblog node
-        @param pub_data: a tuple (service, comment node identifier, item identifier)
-        @param comments: comments node identifier (for main item) or False
-        @param message: new message
-        @param extra: dict which option name as key, which can be:
-            - allow_comments: True to accept an other level of comments, False else (default: False)
-            - rich: if present, contain rich text in currently selected syntax
-        """
-        profile = ISATSession(self.session).profile
-        if comments:
-            extra['allow_comments'] = 'True'
-        return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile)
-
-    def jsonrpc_sendMblogComment(self, node, text, extra={}):
-        """ Send microblog message
-        @param node: url of the comments node
-        @param text: comment
-        """
-        profile = ISATSession(self.session).profile
-        if node and text:
-            return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile)
-        else:
-            raise Exception("Invalid data")
-
-    def jsonrpc_getMblogs(self, publisher_jid, item_ids):
-        """Get specified microblogs posted by a contact
-        @param publisher_jid: jid of the publisher
-        @param item_ids: list of microblogs items IDs
-        @return list of microblog data (dict)"""
-        profile = ISATSession(self.session).profile
-        d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, profile)
-        return d
-
-    def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids):
-        """Get specified microblogs posted by a contact and their comments
-        @param publisher_jid: jid of the publisher
-        @param item_ids: list of microblogs items IDs
-        @return list of couple (microblog data, list of microblog data)"""
-        profile = ISATSession(self.session).profile
-        d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, profile)
-        return d
-
-    def jsonrpc_getLastMblogs(self, publisher_jid, max_item):
-        """Get last microblogs posted by a contact
-        @param publisher_jid: jid of the publisher
-        @param max_item: number of items to ask
-        @return list of microblog data (dict)"""
-        profile = ISATSession(self.session).profile
-        d = self.asyncBridgeCall("getLastGroupBlogs", publisher_jid, max_item, profile)
-        return d
-
-    def jsonrpc_getMassiveLastMblogs(self, publishers_type, publishers_list, max_item):
-        """Get lasts microblogs posted by several contacts at once
-        @param publishers_type: one of "ALL", "GROUP", "JID"
-        @param publishers_list: list of publishers type (empty list of all, list of groups or list of jids)
-        @param max_item: number of items to ask
-        @return: dictionary key=publisher's jid, value=list of microblog data (dict)"""
-        profile = ISATSession(self.session).profile
-        d = self.asyncBridgeCall("getMassiveLastGroupBlogs", publishers_type, publishers_list, max_item, profile)
-        self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers_list, profile)
-        return d
-
-    def jsonrpc_getMblogComments(self, service, node):
-        """Get all comments of given node
-        @param service: jid of the service hosting the node
-        @param node: comments node
-        """
-        profile = ISATSession(self.session).profile
-        d = self.asyncBridgeCall("getGroupBlogComments", service, node, profile)
-        return d
-
-
-    def jsonrpc_getPresenceStatuses(self):
-        """Get Presence information for connected contacts"""
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.getPresenceStatuses(profile)
-
-    def jsonrpc_getHistory(self, from_jid, to_jid, size, between):
-        """Return history for the from_jid/to_jid couple"""
-        sat_session = ISATSession(self.session)
-        profile = sat_session.profile
-        sat_jid = sat_session.jid
-        if not sat_jid:
-            log.error("No jid saved for this profile")
-            return {}
-        if JID(from_jid).userhost() != sat_jid.userhost() and JID(to_jid).userhost() != sat_jid.userhost():
-            log.error("Trying to get history from a different jid, maybe a hack attempt ?")
-            return {}
-        d = self.asyncBridgeCall("getHistory", from_jid, to_jid, size, between, profile)
-        def show(result_dbus):
-            result = []
-            for line in result_dbus:
-                #XXX: we have to do this stupid thing because Python D-Bus use its own types instead of standard types
-                #     and txJsonRPC doesn't accept D-Bus types, resulting in a empty query
-                timestamp, from_jid, to_jid, message, mess_type, extra = line
-                result.append((float(timestamp), unicode(from_jid), unicode(to_jid), unicode(message), unicode(mess_type), dict(extra)))
-            return result
-        d.addCallback(show)
-        return d
-
-    def jsonrpc_joinMUC(self, room_jid, nick):
-        """Join a Multi-User Chat room
-        @room_jid: leave empty string to generate a unique name
-        """
-        profile = ISATSession(self.session).profile
-        try:
-            if room_jid != "":
-                room_jid = JID(room_jid).userhost()
-        except:
-            log.warning('Invalid room jid')
-            return
-        d = self.asyncBridgeCall("joinMUC", room_jid, nick, {}, profile)
-        return d
-
-    def jsonrpc_inviteMUC(self, contact_jid, room_jid):
-        """Invite a user to a Multi-User Chat room"""
-        profile = ISATSession(self.session).profile
-        try:
-            room_jid = JID(room_jid).userhost()
-        except:
-            log.warning('Invalid room jid')
-            return
-        room_id = room_jid.split("@")[0]
-        service = room_jid.split("@")[1]
-        self.sat_host.bridge.inviteMUC(contact_jid, service, room_id, {}, profile)
-
-    def jsonrpc_mucLeave(self, room_jid):
-        """Quit a Multi-User Chat room"""
-        profile = ISATSession(self.session).profile
-        try:
-            room_jid = JID(room_jid)
-        except:
-            log.warning('Invalid room jid')
-            return
-        self.sat_host.bridge.mucLeave(room_jid.userhost(), profile)
-
-    def jsonrpc_getRoomsJoined(self):
-        """Return list of room already joined by user"""
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.getRoomsJoined(profile)
-
-    def jsonrpc_launchTarotGame(self, other_players, room_jid=""):
-        """Create a room, invite the other players and start a Tarot game
-        @param room_jid: leave empty string to generate a unique room name
-        """
-        profile = ISATSession(self.session).profile
-        try:
-            if room_jid != "":
-                room_jid = JID(room_jid).userhost()
-        except:
-            log.warning('Invalid room jid')
-            return
-        self.sat_host.bridge.tarotGameLaunch(other_players, room_jid, profile)
-
-    def jsonrpc_getTarotCardsPaths(self):
-        """Give the path of all the tarot cards"""
-        _join = os.path.join
-        _media_dir = _join(self.sat_host.media_dir,'')
-        return map(lambda x: _join(C.MEDIA_DIR, x[len(_media_dir):]), glob.glob(_join(_media_dir, C.CARDS_DIR, '*_*.png')));
-
-    def jsonrpc_tarotGameReady(self, player, referee):
-        """Tell to the server that we are ready to start the game"""
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.tarotGameReady(player, referee, profile)
-
-    def jsonrpc_tarotGamePlayCards(self, player_nick, referee, cards):
-        """Tell to the server the cards we want to put on the table"""
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.tarotGamePlayCards(player_nick, referee, cards, profile)
-
-    def jsonrpc_launchRadioCollective(self, invited, room_jid=""):
-        """Create a room, invite people, and start a radio collective
-        @param room_jid: leave empty string to generate a unique room name
-        """
-        profile = ISATSession(self.session).profile
-        try:
-            if room_jid != "":
-                room_jid = JID(room_jid).userhost()
-        except:
-            log.warning('Invalid room jid')
-            return
-        self.sat_host.bridge.radiocolLaunch(invited, room_jid, profile)
-
-    def jsonrpc_getEntityData(self, jid, keys):
-        """Get cached data for an entit
-        @param jid: jid of contact from who we want data
-        @param keys: name of data we want (list)
-        @return: requested data"""
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.getEntityData(jid, keys, profile)
-
-    def jsonrpc_getCard(self, jid):
-        """Get VCard for entiry
-        @param jid: jid of contact from who we want data
-        @return: id to retrieve the profile"""
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.getCard(jid, profile)
-
-    def jsonrpc_getAccountDialogUI(self):
-        """Get the dialog for managing user account
-        @return: XML string of the XMLUI"""
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.getAccountDialogUI(profile)
-
-    def jsonrpc_getParamsUI(self):
-        """Return the parameters XML for profile"""
-        profile = ISATSession(self.session).profile
-        d = self.asyncBridgeCall("getParams", C.SECURITY_LIMIT, C.APP_NAME, profile)
-
-        def setAuthorizedParams(params_xml):
-            if self.authorized_params is None:
-                self.authorized_params = {}
-                for cat in minidom.parseString(params_xml.encode('utf-8')).getElementsByTagName("category"):
-                    params = cat.getElementsByTagName("param")
-                    params_list = [param.getAttribute("name") for param in params]
-                    self.authorized_params[cat.getAttribute("name")] = params_list
-            if self.authorized_params:
-                return params_xml
-            else:
-                return None
-
-        d.addCallback(setAuthorizedParams)
-
-        d.addCallback(lambda params_xml: paramsXML2XMLUI(params_xml) if params_xml else "")
-
-        return d
-
-    def jsonrpc_asyncGetParamA(self, param, category, attribute="value"):
-        """Return the parameter value for profile"""
-        profile = ISATSession(self.session).profile
-        d = self.asyncBridgeCall("asyncGetParamA", param, category, attribute, C.SECURITY_LIMIT, profile_key=profile)
-        return d
-
-    def jsonrpc_setParam(self, name, value, category):
-        profile = ISATSession(self.session).profile
-        if category in self.authorized_params and name in self.authorized_params[category]:
-            return self.sat_host.bridge.setParam(name, value, category, C.SECURITY_LIMIT, profile)
-        else:
-            log.warning("Trying to set parameter '%s' in category '%s' without authorization!!!"
-                    % (name, category))
-
-    def jsonrpc_launchAction(self, callback_id, data):
-        #FIXME: any action can be launched, this can be a huge security issue if callback_id can be guessed
-        #       a security system with authorised callback_id must be implemented, similar to the one for authorised params
-        profile = ISATSession(self.session).profile
-        d = self.asyncBridgeCall("launchAction", callback_id, data, profile)
-        return d
-
-    def jsonrpc_chatStateComposing(self, to_jid_s):
-        """Call the method to process a "composing" state.
-        @param to_jid_s: contact the user is composing to
-        """
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.chatStateComposing(to_jid_s, profile)
-
-    def jsonrpc_getNewAccountDomain(self):
-        """@return: the domain for new account creation"""
-        d = self.asyncBridgeCall("getNewAccountDomain")
-        return d
-
-    def jsonrpc_confirmationAnswer(self, confirmation_id, result, answer_data):
-        """Send the user's answer to any previous 'askConfirmation' signal"""
-        profile = ISATSession(self.session).profile
-        self.sat_host.bridge.confirmationAnswer(confirmation_id, result, answer_data, profile)
-
-    def jsonrpc_syntaxConvert(self, text, syntax_from=C.SYNTAX_XHTML, syntax_to=C.SYNTAX_CURRENT):
-        """ Convert a text between two syntaxes
-        @param text: text to convert
-        @param syntax_from: source syntax (e.g. "markdown")
-        @param syntax_to: dest syntax (e.g.: "XHTML")
-        @param safe: clean resulting XHTML to avoid malicious code if True (forced here)
-        @return: converted text """
-        profile = ISATSession(self.session).profile
-        return self.sat_host.bridge.syntaxConvert(text, syntax_from, syntax_to, True, profile)
-
-
-class Register(JSONRPCMethodManager):
-    """This class manage the registration procedure with SàT
-    It provide an api for the browser, check password and setup the web server"""
-
-    def __init__(self, sat_host):
-        JSONRPCMethodManager.__init__(self, sat_host)
-        self.profiles_waiting={}
-        self.request=None
-
-    def getWaitingRequest(self, profile):
-        """Tell if a profile is trying to log in"""
-        if self.profiles_waiting.has_key(profile):
-            return self.profiles_waiting[profile]
-        else:
-            return None
-
-    def render(self, request):
-        """
-        Render method with some hacks:
-           - if login is requested, try to login with form data
-           - except login, every method is jsonrpc
-           - user doesn't need to be authentified for explicitely listed methods, but must be for all others
-        """
-        if request.postpath == ['login']:
-            return self.loginOrRegister(request)
-        _session = request.getSession()
-        parsed = jsonrpclib.loads(request.content.read())
-        method = parsed.get("method")
-        if  method not in ['isRegistered', 'registerParams', 'getMenus']:
-            #if we don't call these methods, we need to be identified
-            profile = ISATSession(_session).profile
-            if not profile:
-                #user is not identified, we return a jsonrpc fault
-                fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia
-                return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))
-        self.request = request
-        return jsonrpc.JSONRPC.render(self, request)
-
-    def loginOrRegister(self, request):
-        """This method is called with the POST information from the registering form.
-
-        @param request: request of the register form
-        @return: a constant indicating the state:
-            - BAD REQUEST: something is wrong in the request (bad arguments)
-            - a return value from self._loginAccount or self._registerNewAccount
-        """
-        try:
-            submit_type = request.args['submit_type'][0]
-        except KeyError:
-            return "BAD REQUEST"
-
-        if submit_type == 'register':
-            return self._registerNewAccount(request)
-        elif submit_type == 'login':
-            return self._loginAccount(request)
-        return Exception('Unknown submit type')
-
-    def _loginAccount(self, request):
-        """Try to authenticate the user with the request information.
-        @param request: request of the register form
-        @return: a constant indicating the state:
-            - BAD REQUEST: something is wrong in the request (bad arguments)
-            - AUTH ERROR: either the profile (login) or the password is wrong
-            - ALREADY WAITING: a request has already been submitted for this profile
-            - server.NOT_DONE_YET: the profile is being processed, the return
-                value will be given by self._logged or self._logginError
-        """
-        try:
-            login_ = request.args['login'][0]
-            password_ = request.args['login_password'][0]
-        except KeyError:
-            return "BAD REQUEST"
-
-        if login_.startswith('@'):
-            raise Exception('No profile_key allowed')
-
-        profile_check = self.sat_host.bridge.getProfileName(login_)
-        if not profile_check or profile_check != login_ or not password_:
-            # profiles with empty passwords are restricted to local frontends
-            return "AUTH ERROR"
-
-        if login_ in self.profiles_waiting:
-            return "ALREADY WAITING"
-
-        def auth_eb(ignore=None):
-            self.__cleanWaiting(login_)
-            log.info("Profile %s doesn't exist or the submitted password is wrong" % login_)
-            request.write("AUTH ERROR")
-            request.finish()
-
-        self.profiles_waiting[login_] = request
-        d = self.asyncBridgeCall("asyncConnect", login_, password_)
-        d.addCallbacks(lambda connected: self._logged(login_, request) if connected else None, auth_eb)
-
-        return server.NOT_DONE_YET
-
-    def _registerNewAccount(self, request):
-        """Create a new account, or return error
-        @param request: request of the register form
-        @return: a constant indicating the state:
-            - BAD REQUEST: something is wrong in the request (bad arguments)
-            - REGISTRATION: new account has been successfully registered
-            - ALREADY EXISTS: the given profile already exists
-            - INTERNAL or 'Unknown error (...)'
-            - server.NOT_DONE_YET: the profile is being processed, the return
-                value will be given later (one of those previously described)
-        """
-        try:
-            profile = login = request.args['register_login'][0]
-            password = request.args['register_password'][0]
-            email = request.args['email'][0]
-        except KeyError:
-            return "BAD REQUEST"
-        if not re.match(r'^[a-z0-9_-]+$', login, re.IGNORECASE) or \
-           not re.match(r'^.+@.+\..+', email, re.IGNORECASE) or \
-           len(password) < C.PASSWORD_MIN_LENGTH:
-            return "BAD REQUEST"
-
-        def registered(result):
-            request.write('REGISTRATION')
-            request.finish()
-
-        def registeringError(failure):
-            reason = failure.value.faultString
-            if reason == "ConflictError":
-                request.write('ALREADY EXISTS')
-            elif reason == "InternalError":
-                request.write('INTERNAL')
-            else:
-                log.error('Unknown registering error: %s' % (reason,))
-                request.write('Unknown error (%s)' % reason)
-            request.finish()
-
-        d = self.asyncBridgeCall("registerSatAccount", email, password, profile)
-        d.addCallback(registered)
-        d.addErrback(registeringError)
-        return server.NOT_DONE_YET
-
-    def __cleanWaiting(self, login):
-        """Remove login from waiting queue"""
-        try:
-            del self.profiles_waiting[login]
-        except KeyError:
-            pass
-
-    def _logged(self, profile, request):
-        """Set everything when a user just logged in
-
-        @param profile
-        @param request
-        @return: a constant indicating the state:
-            - LOGGED
-            - SESSION_ACTIVE
-        """
-        self.__cleanWaiting(profile)
-        _session = request.getSession()
-        sat_session = ISATSession(_session)
-        if sat_session.profile:
-            log.error(('/!\\ Session has already a profile, this should NEVER happen!'))
-            request.write('SESSION_ACTIVE')
-            request.finish()
-            return
-        sat_session.profile = profile
-        self.sat_host.prof_connected.add(profile)
-
-        def onExpire():
-            log.info("Session expired (profile=%s)" % (profile,))
-            try:
-                #We purge the queue
-                del self.sat_host.signal_handler.queue[profile]
-            except KeyError:
-                pass
-            #and now we disconnect the profile
-            self.sat_host.bridge.disconnect(profile)
-
-        _session.notifyOnExpire(onExpire)
-
-        request.write('LOGGED')
-        request.finish()
-
-    def _logginError(self, login, request, error_type):
-        """Something went wrong during logging in
-        @return: error
-        """
-        self.__cleanWaiting(login)
-        return error_type
-
-    def jsonrpc_isConnected(self):
-        _session = self.request.getSession()
-        profile = ISATSession(_session).profile
-        return self.sat_host.bridge.isConnected(profile)
-
-    def jsonrpc_connect(self):
-        _session = self.request.getSession()
-        profile = ISATSession(_session).profile
-        if self.profiles_waiting.has_key(profile):
-            raise jsonrpclib.Fault(1,'Already waiting') #FIXME: define some standard error codes for libervia
-        self.profiles_waiting[profile] = self.request
-        self.sat_host.bridge.connect(profile)
-        return server.NOT_DONE_YET
-
-    def jsonrpc_isRegistered(self):
-        """
-        @return: a couple (registered, message) with:
-        - registered: True if the user is already registered, False otherwise
-        - message: a security warning message if registered is False *and* the connection is unsecure, None otherwise
-        """
-        _session = self.request.getSession()
-        profile = ISATSession(_session).profile
-        if bool(profile):
-            return (True, None)
-        return (False, self.__getSecurityWarning())
-
-    def jsonrpc_registerParams(self):
-        """Register the frontend specific parameters"""
-        params = """
-        <params>
-        <individual>
-        <category name="%(category_name)s" label="%(category_label)s">
-            <param name="%(param_name)s" label="%(param_label)s" value="false" type="bool" security="0"/>
-         </category>
-        </individual>
-        </params>
-        """ % {
-            'category_name': C.ENABLE_UNIBOX_KEY,
-            'category_label': _(C.ENABLE_UNIBOX_KEY),
-            'param_name': C.ENABLE_UNIBOX_PARAM,
-            'param_label': _(C.ENABLE_UNIBOX_PARAM)
-        }
-
-        self.sat_host.bridge.paramsRegisterApp(params, C.SECURITY_LIMIT, C.APP_NAME)
-
-    def jsonrpc_getMenus(self):
-        """Return the parameters XML for profile"""
-        # XXX: we put this method in Register because we get menus before being logged
-        return self.sat_host.bridge.getMenus('', C.SECURITY_LIMIT)
-
-    def __getSecurityWarning(self):
-        """@return: a security warning message, or None if the connection is secure"""
-        if self.request.URLPath().scheme == 'https' or not self.sat_host.security_warning:
-            return None
-        text = D_("You are about to connect to an unsecured service.")
-        if self.sat_host.connection_type == 'both':
-            new_port = (':%s' % self.sat_host.port_https_ext) if self.sat_host.port_https_ext != HTTPS_PORT else ''
-            url = "https://%s" % self.request.URLPath().netloc.replace(':%s' % self.sat_host.port, new_port)
-            text += D_('<br />Secure version of this website: <a href="%(url)s">%(url)s</a>') % {'url': url}
-        return text
-
-
-class SignalHandler(jsonrpc.JSONRPC):
-
-    def __init__(self, sat_host):
-        Resource.__init__(self)
-        self.register=None
-        self.sat_host=sat_host
-        self.signalDeferred = {}
-        self.queue = {}
-
-    def plugRegister(self, register):
-        self.register = register
-
-    def jsonrpc_getSignals(self):
-        """Keep the connection alive until a signal is received, then send it
-        @return: (signal, *signal_args)"""
-        _session = self.request.getSession()
-        profile = ISATSession(_session).profile
-        if profile in self.queue: #if we have signals to send in queue
-            if self.queue[profile]:
-                return self.queue[profile].pop(0)
-            else:
-                #the queue is empty, we delete the profile from queue
-                del self.queue[profile]
-        _session.lock() #we don't want the session to expire as long as this connection is active
-        def unlock(signal, profile):
-            _session.unlock()
-            try:
-                source_defer = self.signalDeferred[profile]
-                if source_defer.called and source_defer.result[0] == "disconnected":
-                    log.info(u"[%s] disconnected" % (profile,))
-                    _session.expire()
-            except IndexError:
-                log.error("Deferred result should be a tuple with fonction name first")
-
-        self.signalDeferred[profile] = defer.Deferred()
-        self.request.notifyFinish().addBoth(unlock, profile)
-        return self.signalDeferred[profile]
-
-    def getGenericCb(self, function_name):
-        """Return a generic function which send all params to signalDeferred.callback
-        function must have profile as last argument"""
-        def genericCb(*args):
-            profile = args[-1]
-            if not profile in self.sat_host.prof_connected:
-                return
-            if profile in self.signalDeferred:
-                self.signalDeferred[profile].callback((function_name,args[:-1]))
-                del self.signalDeferred[profile]
-            else:
-                if not self.queue.has_key(profile):
-                    self.queue[profile] = []
-                self.queue[profile].append((function_name, args[:-1]))
-        return genericCb
-
-    def connected(self, profile):
-        assert(self.register)  # register must be plugged
-        request = self.register.getWaitingRequest(profile)
-        if request:
-            self.register._logged(profile, request)
-
-    def disconnected(self, profile):
-        if not profile in self.sat_host.prof_connected:
-            log.error("'disconnected' signal received for a not connected profile")
-            return
-        self.sat_host.prof_connected.remove(profile)
-        if profile in self.signalDeferred:
-            self.signalDeferred[profile].callback(("disconnected",))
-            del self.signalDeferred[profile]
-        else:
-            if not self.queue.has_key(profile):
-                self.queue[profile] = []
-            self.queue[profile].append(("disconnected",))
-
-
-    def connectionError(self, error_type, profile):
-        assert(self.register) #register must be plugged
-        request = self.register.getWaitingRequest(profile)
-        if request: #The user is trying to log in
-            if error_type == "AUTH_ERROR":
-                _error_t = "AUTH ERROR"
-            else:
-                _error_t = "UNKNOWN"
-            self.register._logginError(profile, request, _error_t)
-
-    def render(self, request):
-        """
-        Render method wich reject access if user is not identified
-        """
-        _session = request.getSession()
-        parsed = jsonrpclib.loads(request.content.read())
-        profile = ISATSession(_session).profile
-        if not profile:
-            #user is not identified, we return a jsonrpc fault
-            fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia
-            return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))
-        self.request = request
-        return jsonrpc.JSONRPC.render(self, request)
-
-class UploadManager(Resource):
-    """This class manage the upload of a file
-    It redirect the stream to SàT core backend"""
-    isLeaf = True
-    NAME = 'path' #name use by the FileUpload
-
-    def __init__(self, sat_host):
-        self.sat_host=sat_host
-        self.upload_dir = tempfile.mkdtemp()
-        self.sat_host.addCleanup(shutil.rmtree, self.upload_dir)
-
-    def getTmpDir(self):
-        return self.upload_dir
-
-    def _getFileName(self, request):
-        """Generate unique filename for a file"""
-        raise NotImplementedError
-
-    def _fileWritten(self, request, filepath):
-        """Called once the file is actually written on disk
-        @param request: HTTP request object
-        @param filepath: full filepath on the server
-        @return: a tuple with the name of the async bridge method
-        to be called followed by its arguments.
-        """
-        raise NotImplementedError
-
-    def render(self, request):
-        """
-        Render method with some hacks:
-           - if login is requested, try to login with form data
-           - except login, every method is jsonrpc
-           - user doesn't need to be authentified for isRegistered, but must be for all other methods
-        """
-        filename = self._getFileName(request)
-        filepath = os.path.join(self.upload_dir, filename)
-        #FIXME: the uploaded file is fully loaded in memory at form parsing time so far
-        #       (see twisted.web.http.Request.requestReceived). A custom requestReceived should
-        #       be written in the futur. In addition, it is not yet possible to get progression informations
-        #       (see http://twistedmatrix.com/trac/ticket/288)
-
-        with open(filepath,'w') as f:
-            f.write(request.args[self.NAME][0])
-
-        def finish(d):
-            error = isinstance(d, Exception) or isinstance (d, Failure)
-            request.write('KO' if error else 'OK')
-            # TODO: would be great to re-use the original Exception class and message
-            # but it is lost in the middle of the backtrace and encapsulated within
-            # a DBusException instance --> extract the data from the backtrace?
-            request.finish()
-
-        d = JSONRPCMethodManager(self.sat_host).asyncBridgeCall(*self._fileWritten(request, filepath))
-        d.addCallbacks(lambda d: finish(d), lambda failure: finish(failure))
-        return server.NOT_DONE_YET
-
-
-class UploadManagerRadioCol(UploadManager):
-    NAME = 'song'
-
-    def _getFileName(self, request):
-        extension = os.path.splitext(request.args['filename'][0])[1]
-        return "%s%s" % (str(uuid.uuid4()), extension)  # XXX: chromium doesn't seem to play song without the .ogg extension, even with audio/ogg mime-type
-
-    def _fileWritten(self, request, filepath):
-        """Called once the file is actually written on disk
-        @param request: HTTP request object
-        @param filepath: full filepath on the server
-        @return: a tuple with the name of the async bridge method
-        to be called followed by its arguments.
-        """
-        profile = ISATSession(request.getSession()).profile
-        return ("radiocolSongAdded", request.args['referee'][0], filepath, profile)
-
-
-class UploadManagerAvatar(UploadManager):
-    NAME = 'avatar_path'
-
-    def _getFileName(self, request):
-        return str(uuid.uuid4())
-
-    def _fileWritten(self, request, filepath):
-        """Called once the file is actually written on disk
-        @param request: HTTP request object
-        @param filepath: full filepath on the server
-        @return: a tuple with the name of the async bridge method
-        to be called followed by its arguments.
-        """
-        profile = ISATSession(request.getSession()).profile
-        return ("setAvatar", filepath, profile)
-
-
-def coerceConnectionType(value):  # called from Libervia.OPT_PARAMETERS
-    allowed_values = ('http', 'https', 'both')
-    if value not in allowed_values:
-        raise ValueError("Invalid parameter value, not in %s" % str(allowed_values))
-    return value
-
-
-class Libervia(service.Service):
-
-    OPT_PARAMETERS = [['connection_type', 't', 'https', "'http', 'https' or 'both' (to launch both servers).", coerceConnectionType],
-                      ['port', 'p', 8080, 'The port number to listen HTTP on.', int],
-                      ['port_https', 's', 8443, 'The port number to listen HTTPS on.', int],
-                      ['port_https_ext', 'e', 0, 'The external port number used for HTTPS (0 means port_https value).', int],
-                      ['ssl_certificate', 'c', 'libervia.pem', 'PEM certificate with both private and public parts.', str],
-                      ['redirect_to_https', 'r', 1, 'automatically redirect from HTTP to HTTPS.', int],
-                      ['security_warning', 'w', 1, 'warn user that he is about to connect on HTTP.', int],
-                      ['passphrase', 'k', '', u"passphrase for the SàT profile named '%s'" % C.SERVICE_PROFILE, str],
-                      ]
-
-    def __init__(self, *args, **kwargs):
-        if not kwargs:
-            # During the loading of the twisted plugins, we just need the default values.
-            # This part is not executed when the plugin is actually started.
-            for name, value in [(option[0], option[2]) for option in self.OPT_PARAMETERS]:
-                kwargs[name] = value
-        self.initialised = defer.Deferred()
-        self.connection_type = kwargs['connection_type']
-        self.port = kwargs['port']
-        self.port_https = kwargs['port_https']
-        self.port_https_ext = kwargs['port_https_ext']
-        if not self.port_https_ext:
-            self.port_https_ext = self.port_https
-        self.ssl_certificate = kwargs['ssl_certificate']
-        self.redirect_to_https = kwargs['redirect_to_https']
-        self.security_warning = kwargs['security_warning']
-        self.passphrase = kwargs['passphrase']
-        self._cleanup = []
-        root = ProtectedFile(C.LIBERVIA_DIR)
-        self.signal_handler = SignalHandler(self)
-        _register = Register(self)
-        _upload_radiocol = UploadManagerRadioCol(self)
-        _upload_avatar = UploadManagerAvatar(self)
-        self.signal_handler.plugRegister(_register)
-        self.sessions = {} #key = session value = user
-        self.prof_connected = set() #Profiles connected
-        self.action_handler = SATActionIDHandler()
-        ## bridge ##
-        try:
-            self.bridge=DBusBridgeFrontend()
-        except BridgeExceptionNoService:
-            print(u"Can't connect to SàT backend, are you sure it's launched ?")
-            sys.exit(1)
-        def backendReady(dummy):
-            self.bridge.register("connected", self.signal_handler.connected)
-            self.bridge.register("disconnected", self.signal_handler.disconnected)
-            self.bridge.register("connectionError", self.signal_handler.connectionError)
-            self.bridge.register("actionResult", self.action_handler.actionResultCb)
-            #core
-            for signal_name in ['presenceUpdate', 'newMessage', 'subscribe', 'contactDeleted', 'newContact', 'entityDataUpdated', 'askConfirmation', 'newAlert', 'paramUpdate']:
-                self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name))
-            #plugins
-            for signal_name in ['personalEvent', 'roomJoined', 'roomUserJoined', 'roomUserLeft', 'tarotGameStarted', 'tarotGameNew', 'tarotGameChooseContrat',
-                                'tarotGameShowCards', 'tarotGameInvalidCards', 'tarotGameCardsPlayed', 'tarotGameYourTurn', 'tarotGameScore', 'tarotGamePlayers',
-                                'radiocolStarted', 'radiocolPreload', 'radiocolPlay', 'radiocolNoUpload', 'radiocolUploadOk', 'radiocolSongRejected', 'radiocolPlayers',
-                                'roomLeft', 'roomUserChangedNick', 'chatStateReceived']:
-                self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name), "plugin")
-            self.media_dir = self.bridge.getConfig('', 'media_dir')
-            self.local_dir = self.bridge.getConfig('', 'local_dir')
-            root.putChild('', Redirect('libervia.html'))
-            root.putChild('json_signal_api', self.signal_handler)
-            root.putChild('json_api', MethodHandler(self))
-            root.putChild('register_api', _register)
-            root.putChild('upload_radiocol', _upload_radiocol)
-            root.putChild('upload_avatar', _upload_avatar)
-            root.putChild('blog', MicroBlog(self))
-            root.putChild('css', ProtectedFile("server_css/"))
-            root.putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir))
-            root.putChild(os.path.dirname(C.AVATARS_DIR), ProtectedFile(os.path.join(self.local_dir, C.AVATARS_DIR)))
-            root.putChild('radiocol', ProtectedFile(_upload_radiocol.getTmpDir(), defaultType="audio/ogg")) #We cheat for PoC because we know we are on the same host, so we use directly upload dir
-            self.site = server.Site(root)
-            self.site.sessionFactory = LiberviaSession
-
-        self.bridge.getReady(lambda: self.initialised.callback(None),
-                             lambda failure: self.initialised.errback(Exception(failure)))
-        self.initialised.addCallback(backendReady)
-        self.initialised.addErrback(lambda failure: log.error("Init error: %s" % failure))
-
-    def addCleanup(self, callback, *args, **kwargs):
-        """Add cleaning method to call when service is stopped
-        cleaning method will be called in reverse order of they insertion
-        @param callback: callable to call on service stop
-        @param *args: list of arguments of the callback
-        @param **kwargs: list of keyword arguments of the callback"""
-        self._cleanup.insert(0, (callback, args, kwargs))
-
-    def startService(self):
-        """Connect the profile for Libervia and start the HTTP(S) server(s)"""
-        def eb(e):
-            log.error(_("Connection failed: %s") % e)
-            self.stop()
-
-        def initOk(dummy):
-            if not self.bridge.isConnected(C.SERVICE_PROFILE):
-                self.bridge.asyncConnect(C.SERVICE_PROFILE, self.passphrase,
-                                         callback=self._startService, errback=eb)
-
-        self.initialised.addCallback(initOk)
-
-    def _startService(self, dummy):
-        """Actually start the HTTP(S) server(s) after the profile for Libervia is connected"""
-        if self.connection_type in ('https', 'both'):
-            if not ssl_available:
-                raise(ImportError(_("Python module pyOpenSSL is not installed!")))
-            try:
-                with open(os.path.expanduser(self.ssl_certificate)) as keyAndCert:
-                    try:
-                        cert = ssl.PrivateCertificate.loadPEM(keyAndCert.read())
-                    except OpenSSL.crypto.Error as e:
-                        log.error(_("The file '%s' must contain both private and public parts of the certificate") % self.ssl_certificate)
-                        raise e
-            except IOError as e:
-                log.error(_("The file '%s' doesn't exist") % self.ssl_certificate)
-                raise e
-            reactor.listenSSL(self.port_https, self.site, cert.options())
-        if self.connection_type in ('http', 'both'):
-            if self.connection_type == 'both' and self.redirect_to_https:
-                reactor.listenTCP(self.port, server.Site(RedirectToHTTPS(self.port, self.port_https_ext)))
-            else:
-                reactor.listenTCP(self.port, self.site)
-
-    def stopService(self):
-        print "launching cleaning methods"
-        for callback, args, kwargs in self._cleanup:
-            callback(*args, **kwargs)
-        self.bridge.disconnect(C.SERVICE_PROFILE)
-
-    def run(self):
-        reactor.run()
-
-    def stop(self):
-        reactor.stop()
-
-
-class RedirectToHTTPS(Resource):
-
-    def __init__(self, old_port, new_port):
-        Resource.__init__(self)
-        self.isLeaf = True
-        self.old_port = old_port
-        self.new_port = new_port
-
-    def render(self, request):
-        netloc = request.URLPath().netloc.replace(':%s' % self.old_port, ':%s' % self.new_port)
-        url = "https://" + netloc + request.uri
-        return redirectTo(url, request)
-
-
-registerAdapter(SATSession, server.Session, ISATSession)
--- a/libervia_server/blog.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,224 +0,0 @@
-#!/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/>.
-
-from sat.core.i18n import _
-from sat_frontends.tools.strings import addURLToText
-from libervia_server.html_tools import sanitizeHtml
-from twisted.internet import defer
-from twisted.web import server
-from twisted.web.resource import Resource
-from twisted.words.protocols.jabber.jid import JID
-from datetime import datetime
-from constants import Const as C
-import uuid
-import re
-
-
-class MicroBlog(Resource):
-    isLeaf = True
-
-    ERROR_TEMPLATE = """
-                <html>
-                <head profile="http://www.w3.org/2005/10/profile">
-                    <link rel="icon" type="image/png" href="%(root)ssat_logo_16.png">
-                    <title>MICROBLOG ERROR</title>
-                </head>
-                <body>
-                    <h1 style='text-align: center; color: red;'>%(message)s</h1>
-                </body>
-                </html>
-                """
-
-    def __init__(self, host):
-        self.host = host
-        Resource.__init__(self)
-
-    def render_GET(self, request):
-        if not request.postpath:
-            return MicroBlog.ERROR_TEMPLATE % {'root': '',
-                                               'message': "You must indicate a nickname"}
-        else:
-            prof_requested = request.postpath[0]
-            #TODO: char check: only use alphanumerical chars + some extra(_,-,...) here
-            prof_found = self.host.bridge.getProfileName(prof_requested)
-            if not prof_found or prof_found == 'libervia':
-                return MicroBlog.ERROR_TEMPLATE % {'root': '../' * len(request.postpath),
-                                                    'message': "Invalid nickname"}
-            else:
-                def got_jid(pub_jid_s):
-                    pub_jid = JID(pub_jid_s)
-                    d2 = defer.Deferred()
-                    item_id = None
-                    if len(request.postpath) > 1:
-                        if request.postpath[1] == 'atom.xml':  # return the atom feed
-                            d2.addCallbacks(self.render_atom_feed, self.render_error_blog, [request], None, [request, prof_found], None)
-                            self.host.bridge.getLastGroupBlogsAtom(pub_jid.userhost(), 10, 'libervia', d2.callback, d2.errback)
-                            return
-                        try:  # check if the given path is a valid UUID
-                            uuid.UUID(request.postpath[1])
-                            item_id = request.postpath[1]
-                        except ValueError:
-                            pass
-                    d2.addCallbacks(self.render_html_blog, self.render_error_blog, [request, prof_found], None, [request, prof_found], None)
-                    if item_id:  # display one message and its comments
-                        self.host.bridge.getGroupBlogsWithComments(pub_jid.userhost(), [item_id], 'libervia', d2.callback, d2.errback)
-                    else:  # display the last messages without comment
-                        self.host.bridge.getLastGroupBlogs(pub_jid.userhost(), 10, 'libervia', d2.callback, d2.errback)
-
-                d1 = defer.Deferred()
-                JID(self.host.bridge.asyncGetParamA('JabberID', 'Connection', 'value', C.SERVER_SECURITY_LIMIT, prof_found, callback=d1.callback, errback=d1.errback))
-                d1.addCallbacks(got_jid)
-
-                return server.NOT_DONE_YET
-
-    def render_html_blog(self, mblog_data, request, profile):
-        """Retrieve the user parameters before actually rendering the static blog
-        @param mblog_data: list of microblog data or list of couple (microblog data, list of microblog data)
-        @param request: HTTP request
-        @param profile
-        """
-        d_list = []
-        style = {}
-
-        def getCallback(param_name):
-            d = defer.Deferred()
-            d.addCallback(lambda value: style.update({param_name: value}))
-            d_list.append(d)
-            return d.callback
-
-        eb = lambda failure: self.render_error_blog(failure, request, profile)
-
-        for param_name in (C.STATIC_BLOG_PARAM_TITLE, C.STATIC_BLOG_PARAM_BANNER, C.STATIC_BLOG_PARAM_KEYWORDS, C.STATIC_BLOG_PARAM_DESCRIPTION):
-            self.host.bridge.asyncGetParamA(param_name, C.STATIC_BLOG_KEY, 'value', C.SERVER_SECURITY_LIMIT, profile, callback=getCallback(param_name), errback=eb)
-
-        cb = lambda dummy: self.__render_html_blog(mblog_data, style, request, profile)
-        defer.DeferredList(d_list).addCallback(cb)
-
-    def __render_html_blog(self, mblog_data, style, request, profile):
-        """Actually render the static blog. If mblog_data is a list of dict, we are missing
-        the comments items so we just display the main items. If mblog_data is a list of couple,
-        each couple is associating a main item data with the list of its comments, so we render all.
-        @param mblog_data: list of microblog data or list of couple (microblog data, list of microblog data)
-        @param style: dict defining the blog's rendering parameters
-        @param request: the HTTP request
-        @profile
-        """
-        if not isinstance(style, dict):
-            style = {}
-        user = sanitizeHtml(profile).encode('utf-8')
-        root_url = '../' * len(request.postpath)
-        base_url = root_url + 'blog/' + user
-
-        def getFromData(key):
-            return sanitizeHtml(style[key]).encode('utf-8') if key in style else ''
-
-        def getImageFromData(key, alt):
-            """regexp from http://answers.oreilly.com/topic/280-how-to-validate-urls-with-regular-expressions/"""
-            url = style[key].encode('utf-8') if key in style else ''
-            regexp = r"^(https?|ftp)://[a-z0-9-]+(\.[a-z0-9-]+)+(/[\w-]+)*/[\w-]+\.(gif|png|jpg)$"
-            return "<img src='%(url)s' alt='%(alt)s'/>" % {'alt': alt, 'url': url} if re.match(regexp, url) else alt
-
-        request.write("""
-            <html>
-            <head>
-                <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-                <meta name="keywords" content="%(keywords)s">
-                <meta name="description" content="%(description)s">
-                <link rel="alternate" type="application/atom+xml" href="%(base)s/atom.xml"/>
-                <link rel="stylesheet" type="text/css" href="%(root)scss/blog.css" />
-                <link rel="icon" type="image/png" href="%(root)ssat_logo_16.png">
-                <title>%(title)s</title>
-            </head>
-            <body>
-            <div class="mblog_title"><a href="%(base)s">%(banner_elt)s</a></div>
-            """ % {'base': base_url,
-                   'root': root_url,
-                   'user': user,
-                   'keywords': getFromData(C.STATIC_BLOG_PARAM_KEYWORDS),
-                   'description': getFromData(C.STATIC_BLOG_PARAM_DESCRIPTION),
-                   'title': getFromData(C.STATIC_BLOG_PARAM_TITLE) or "%s's microblog" % user,
-                   'banner_elt': getImageFromData(C.STATIC_BLOG_PARAM_BANNER, user)})
-        mblog_data = [(entry if isinstance(entry, tuple) else (entry, [])) for entry in mblog_data]
-        mblog_data = sorted(mblog_data, key=lambda entry: (-float(entry[0].get('published', 0))))
-        for entry in mblog_data:
-            self.__render_html_entry(entry[0], base_url, request)
-            comments = sorted(entry[1], key=lambda entry: (float(entry.get('published', 0))))
-            for comment in comments:
-                self.__render_html_entry(comment, base_url, request)
-        request.write('</body></html>')
-        request.finish()
-
-    def __render_html_entry(self, entry, base_url, request):
-        """Render one microblog entry.
-        @param entry: the microblog entry
-        @param base_url: the base url of the blog
-        @param request: the HTTP request
-        """
-        timestamp = float(entry.get('published', 0))
-        datetime_ = datetime.fromtimestamp(timestamp)
-        is_comment = entry['type'] == 'comment'
-        if is_comment:
-            author = (_("comment from %s") % entry['author']).encode('utf-8')
-            item_link = ''
-        else:
-            author = '&nbsp;'
-            item_link = ("%(base)s/%(item_id)s" % {'base': base_url, 'item_id': entry['id']}).encode('utf-8')
-
-        def getText(key):
-            if ('%s_xhtml' % key) in entry:
-                return entry['%s_xhtml' % key].encode('utf-8')
-            elif key in entry:
-                processor = addURLToText if key.startswith('content') else sanitizeHtml
-                return processor(entry[key]).encode('utf-8')
-            return ''
-
-        def addMainItemLink(elem):
-            if not item_link or not elem:
-                return elem
-            return """<a href="%(link)s" class="item_link">%(elem)s</a>""" % {'link': item_link, 'elem': elem}
-
-        header = addMainItemLink("""<div class="mblog_header">
-                                      <div class="mblog_metadata">
-                                        <div class="mblog_author">%(author)s</div>
-                                        <div class="mblog_timestamp">%(date)s</div>
-                                      </div>
-                                    </div>""" % {'author': author, 'date': datetime_})
-
-        title = addMainItemLink(getText('title'))
-        body = getText('content')
-        if title:  # insert the title within the body
-            body = """<h1>%(title)s</h1>\n%(body)s""" % {'title': title, 'body': body}
-
-        request.write("""<div class="mblog_entry %(extra_style)s">
-                           %(header)s
-                           <span class="mblog_content">%(content)s</span>
-                         </div>""" %
-                         {'extra_style': 'mblog_comment' if entry['type'] == 'comment' else '',
-                          'item_link': item_link,
-                          'header': header,
-                          'content': body})
-
-    def render_atom_feed(self, feed, request):
-        request.write(feed.encode('utf-8'))
-        request.finish()
-
-    def render_error_blog(self, error, request, profile):
-        request.write(MicroBlog.ERROR_TEMPLATE % {'root': '../' * len(request.postpath),
-                                                  'message': "Can't access requested data"})
-        request.finish()
--- a/libervia_server/html_tools.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,32 +0,0 @@
-#!/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/>.
-
-def sanitizeHtml(text):
-    """Sanitize HTML by escaping everything"""
-    #this code comes from official python wiki: http://wiki.python.org/moin/EscapingHtml
-    html_escape_table = {
-        "&": "&amp;",
-        '"': "&quot;",
-        "'": "&apos;",
-        ">": "&gt;",
-        "<": "&lt;",
-        }
-
-    return "".join(html_escape_table.get(c,c) for c in text)
-
--- a/libervia_server/libervia.sh	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,77 +0,0 @@
-#!/bin/sh
-
-DEBUG=""
-PYTHON="python2"
-
-kill_process() {
-    # $1 is the file containing the PID to kill, $2 is the process name
-    if [ -f $1 ]; then
-        PID=`cat $1`
-        if ps -p $PID > /dev/null; then
-            echo "Terminating $2... "
-            kill -INT $PID
-        else
-            echo "No running process of ID $PID... removing PID file"
-            rm -f $1
-        fi
-    else
-        echo "$2 is probably not running (PID file doesn't exist)"
-    fi
-}
-
-#We use python to parse config files
-eval `"$PYTHON" << PYTHONEND
-
-from sat.core.constants import Const as C
-from sat.memory.memory import fixLocalDir
-from ConfigParser import SafeConfigParser
-from os.path import expanduser, join
-import sys
-
-fixLocalDir()  # XXX: tmp update code, will be removed in the future
-
-config = SafeConfigParser(defaults=C.DEFAULT_CONFIG)
-try:
-    config.read(C.CONFIG_FILES)
-except:
-    print ("echo \"/!\\ Can't read main config ! Please check the syntax\";")
-    print ("exit 1")
-    sys.exit()
-
-env=[]
-env.append("PID_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'pid_dir')),''))
-env.append("LOG_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'log_dir')),''))
-print ";".join(env)
-PYTHONEND
-`
-APP_NAME="Libervia"  # FIXME: the import from Python needs libervia module to be in PYTHONPATH
-APP_NAME_FILE="libervia"
-PID_FILE="$PID_DIR$APP_NAME_FILE.pid"
-LOG_FILE="$LOG_DIR$APP_NAME_FILE.log"
-
-# if there is one argument which is "stop", then we kill Libervia
-if [ $# -eq 1 ];then
-    if [ $1 = "stop" ];then
-        kill_process $PID_FILE "$APP_NAME"
-        exit 0
-    fi
-    if [ $1 = "debug" ];then
-        echo "Launching $APP_NAME in debug mode"
-        DEBUG="--debug"
-    fi
-fi
-
-DAEMON="n"
-MAIN_OPTIONS="-${DAEMON}o"
-
-#Don't change the next line
-AUTO_OPTIONS=""
-ADDITIONAL_OPTIONS="--pidfile $PID_FILE --logfile $LOG_FILE $AUTO_OPTIONS $DEBUG"
-
-log_dir=`dirname "$LOG_FILE"`
-if [ ! -d $log_dir ] ; then
-    mkdir $log_dir
-fi
-
-echo "Starting $APP_NAME..."
-twistd $MAIN_OPTIONS $ADDITIONAL_OPTIONS $APP_NAME_FILE
--- a/public/contrat_social.html	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,110 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
-<html><head>
-  
-  <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type">
-  <title>Salut  Toi: Contrat Social</title>
-
-  
-</head><body>
-Le projet  Salut  Toi  est n d'un besoin de protection de nos
-liberts, de notre vie prive et de notre indpendance. Il se veut
-garant des droits et liberts qu'un utilisateur a vis  vis de ses
-propres informations, des informations numriques sur sa vie ou celles
-de ses connaissances, des donnes qu'il manipule; et se veut galement
-un point de contact humain, ne se substituant pas aux rapports rels,
-mais au contraire les facilitant.<br>
-
-Salut  Toi lutte et luttera toujours contre toute forme de main mise
-sur les technologies par des intrts privs. Le rseau global doit
-appartenir  tous, et tre un point d'expression et de libert pour
-l'Humanit.<br>
-
-<br>
-
- ce titre,  Salut  Toi  et ceux qui y participent se basent sur un
-contrat social, un engagement vis  vis de ceux qui l'utilisent. Ce
-contrat consiste en les points suivants:<br>
-
-<ul>
-
-  <li>nous plaons la <span style="font-style: italic;">Libert</span> en tte de nos priorits: libert de
-l'utilisateur, libert vis  vis de ses donnes. Pour cela,  Salut 
-Toi  est un logiciel Libre - condition essentielle -, et son
-infrastructure se base galement sur des logiciels Libres, c'est  dire
-des logiciels qui respectent ces 4 liberts fondamentales
-    <ul>
-
-    <li>la libert d'excuter le programme, pour tous les usages,</li>
-  
-    </ul>
-    <ul>
-
-    <li>la libert d'tudier le fonctionnement du programme et de
-l'adapter  ses besoins,</li>
-  
-    </ul>
-    <ul>
-
-    <li>la libert de redistribuer des copies du programme,</li>
-  
-    </ul>
-    <ul>
-
-    <li>la libert d'amliorer le programme et de distribuer ces
-amliorations au public.<br>
-</li>
-  
-    </ul>
-</li>
-  
-  
-  
-  
-
-Vous avez ainsi la possibilit d'installer votre propre version de 
-Salut  Toi  sur votre propre machine, d'en vrifier - et de
-comprendre - ainsi son fonctionnement, de l'adapter  vos besoins, d'en
-faire profiter vos amis.
-
-  <li>Les informations vous concernant vous appartiennent, et nous
-n'aurons pas la prtention - et l'indcence ! - de considrer le
-contenu que vous produisez ou faites circuler via  Salut  Toi  comme
-nous appartenant. De mme, nous nous engageons  ne jamais faire de
-profit en revendant vos informations personnelles.</li>
-  <li>Nous incitons fortement  la <span style="text-decoration: underline;">dcentralisation gnralise</span>. 
-Salut  Toi  tant bas sur un protocole dcentralis (XMPP), il l'est
-lui-mme par nature. La dcentralisation est essentielle pour une
-meilleure protection de vos informations, une meilleure rsistance  la
-censure ou aux pannes, et pour viter les drives autoritaires.</li>
-  <li>Luttant contre les tentatives de contrle priv et les abus
-commerciaux du rseau global, et afin de garder notre indpendance,
-nous nous refusons  toute forme de publicit: vous ne verrez <span style="font-weight: bold;">jamais</span>
-de forme de rclame commerciale de notre fait.</li>
-  <li>L'<span style="font-style: italic;">galit</span> des utilisateurs est essentielle pour nous, nous
-refusons toute forme de discrimination, que ce soit pour une zone
-gographique, une catgorie de la population, ou tout autre raison.</li>
-  <li>Nous ferons tout notre possible pour lutter contre toute
-tentative de censure. Le rseau global doit tre un moyen d'expression
-pour tous.</li>
-  <li>Nous refusons toute ide d'autorit absolue en ce qui concerne
-les dcisions prises pour  Salut  Toi  et son fonctionnement, et le
-choix de la dcentralisation et l'utilisation de logiciel Libre permet
-de lutter contre toute forme de hirarchie.</li>
-  
-  <li>L'ide de <span style="font-style: italic;">Fraternit</span> est essentielle, aussi:
-    <ul>
-      <li>nous ferons notre
-possible pour aider les utilisateurs, quel que soit leur niveau</li>
-      <li>de mme, des efforts seront fait quant 
-l'accessibilit aux personnes victimes d'un handicap</li>
-      <li> Salut  Toi ,
-XMPP, et les technologies utilises facilitent les changes
-lectroniques, mais nous dsirons mettre l'accent sur les rencontres
-relles et humaines: nous favoriserons toujours le rel sur le virtuel.</li>
-    </ul>
-</li>
-  
-  
-</ul>
-
-</body></html>
--- a/public/libervia.css	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1580 +0,0 @@
-/*
-Libervia: a Salut à Toi frontend
-Copyright (C) 2011, 2012, 2013, 2014  Jérôme Poisson <goffi@goffi.org>
-Copyright (C) 2011  Adrien Vigneron <adrienvigneron@mailoo.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/>.
-*/
-
-
-/*
- * CSS Reset: see http://pyjs.org/wiki/csshellandhowtodealwithit/
- */
-
-/* reset/default styles */
-
-html, body, div, span, applet, object, iframe,
-p, blockquote, pre,
-a, abbr, acronym, address, big, cite, code,
-del, dfn, em, font, img, ins, kbd, q, s, samp,
-small, strike, strong, sub, sup, tt, var,
-b, u, i, center, dl, dt, dd, li,
-fieldset, form, label, legend, table, caption,
-tbody, tfoot, thead, tr, th, td {
-    margin: 0;
-    padding: 0;
-    border: 0;
-    outline: 0;
-    font-size: 100%;
-    vertical-align: baseline;
-    background: transparent;
-    color: #444;
-}
-
-/* styles for displaying rich text - START */
-h1, h2, h3, h4, h5, h6 {
-    margin: 0;
-    padding: 0;
-    border: 0;
-    outline: 0;
-    vertical-align: baseline;
-    background: transparent;
-    color: #444;
-    border-bottom: 1px solid rgb(170, 170, 170);
-    margin-bottom: 0.6em;
-}
-ol, ul {
-    margin: 0;
-    border: 0;
-    outline: 0;
-    font-size: 100%;
-    vertical-align: baseline;
-    background: transparent;
-    color: #444;
-}
-a:link {
-    color: blue;
-}
-.bubble p {
-    margin: 0.4em 0em;
-}
-.bubble img {
-    /* /!\ setting a max-width percentage value affects the toolbar icons */
-    max-width: 600px;
-}
-
-/* styles for displaying rich text - END */
-
-blockquote, q { quotes: none; }
-
-blockquote:before, blockquote:after,
-q:before, q:after {
-    content: '';
-    content: none;
-}
-
-:focus { outline: 0; }
-ins { text-decoration: none; }
-del { text-decoration: line-through; }
-
-table {
-    border-collapse: collapse;
-    border-spacing: 0;
-}
-
-/* pyjamas iframe hide */
-iframe { position: absolute; }
-
-
-html, body {
-    width: 100%;
-    height: 100%;
-    min-height: 100%;
-
-}
-
-body {
-    line-height: 1em;
-    font-size: 1em;
-    overflow: auto;
-
-}
-
-.scrollpanel {
-   margin-bottom: -10000px;
-
-}
-
-.iescrollpanelfix {
-   position: relative;
-   top: 100%;
-   margin-bottom: -10000px;
-
-}
-
-/* undo part of the above (non-IE) */
-html>body .iescrollpanelfix { position: static; }
-
-/* CSS Reset END */
-
-body {
-    background-color: #fff;
-    font: normal 0.8em/1.5em Arial, Helvetica, sans-serif;
-}
-
-.header {
-    background-color: #eee;
-    border-bottom: 1px solid #ddd;
-}
-
-/* Misc Pyjamas stuff */
-
-.menuContainer {
-    margin: 0 32px 0 20px;   
-}
-
-.gwt-MenuBar,.gwt-MenuBar-horizontal {
-  /*background-color: #01FF78;
-  border: 1px solid #87B3FF;
-  cursor: default;*/
-    width: 100%;
-    height: 28px;
-    margin: 0;
-    padding: 5px 5px 0 5px;    
-    line-height: 100%;
-    box-shadow: 0px 1px 4px #000;
-    -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
-    -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
-    border: 1px solid #ddd;
-    border-radius: 0 0 1em 1em;
-    -webkit-border-radius: 0 0 1em 1em;
-    -moz-border-radius: 0 0 1em 1em;
-    background-color: #222;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’444444′, endColorstr=’#222222’);
-    background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222));
-    background: -moz-linear-gradient(top, #444444, #222222);
-    background-image: -o-linear-gradient(#444444,#222222);
-    display: inline-block;
-}
-
-.gwt-MenuBar-horizontal .gwt-MenuItem {
-    text-decoration: none;    
-    font-weight: bold;
-    height: 100%;
-    color: #e7e5e5;
-    padding: 3px 15px;
-    /*display: block;*/
-    border-radius: 1em 1em 1em 1em;
-    -webkit-border-radius: 1em 1em 1em 1em;
-    -moz-border-radius: 1em 1em 1em 1em;
-    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
-    -webkit-transition: color 0.2s linear; 
-    -moz-transition: color 0.2s linear; 
-    -o-transition: color 0.2s linear;
-}
-
-.gwt-MenuItem img {
-    padding-right: 2px;
-}
-
-.gwt-MenuBar-horizontal .gwt-MenuItem-selected {
-     background-color: #eee;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#eee’, endColorstr=’#aaa′);
-    background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa));
-    background: -moz-linear-gradient(top, #eee, #aaa);
-    background-image: -o-linear-gradient(#eee,#aaa);
-    color: #444;
-    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);    
-    cursor: pointer;
-}
-
-.menuSeparator {
-    width: 100%;
-}
-
-.menuSeparator.gwt-MenuItem-selected {
-    border: 0;
-    background: inherit;
-    cursor: default;
-}
-
-.gwt-MenuBar {
-    background-color: #fff;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#fff’, endColorstr=’#ccc’);
-    background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc));
-    background: -moz-linear-gradient(top, #fff, #ccc);
-    background-image: -o-linear-gradient(#fff,#ccc);
-    /*display: none;*/
-    height: 100%;
-    min-width: 148px;
-    margin: 0;
-    padding: 0;
-    /*min-width: 148px;
-    top: 28px;*/
-    border: solid 1px #aaa;
-    -webkit-border-radius: 0 0 10px 10px;
-    -moz-border-radius: 0 0 10px 10px;
-    border-radius: 0 0 10px 10px;
-    -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
-    -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
-    box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
-}
-
-.gwt-MenuBar table {
-    width: 100%;
-    display: inline-table;
-}
-
-.gwt-MenuBar .gwt-MenuItem {
-    padding: 8px 15px;
-}
-
-
-.gwt-MenuBar .gwt-MenuItem-selected {
-    background: #cf2828 !important;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828’, endColorstr=’#981a1a’) !important;
-    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)) !important;
-    background: -moz-linear-gradient(top, #cf2828, #981a1a) !important;
-    background-image: -o-linear-gradient(#cf2828,#981a1a) !important;
-    color: #fff !important;
-    -webkit-border-radius: 0 0 0 0;
-    -moz-border-radius: 0 0 0 0;
-    border-radius: 0 0 0 0;
-    text-shadow: 0 1px 1px rgba(0, 0, 0, .1);
-    transition: color 0.2s linear;
-    -webkit-transition: color 0.2s linear; 
-    -moz-transition: color 0.2s linear; 
-    -o-transition: color 0.2s linear; 
-    cursor: pointer;
-}
-
-/*.menuLastPopup div tr:first-child td{
-    border-radius: 0 0 9px 9px !important; 
-    -webkit-border-radius: 0 0 9px 9px !important; 
-    -moz-border-radius: 0 0 9px 9px !important; 
-}*/
-
-.gwt-MenuBar tr:last-child td {
-    border-radius: 0 0 9px 9px !important;
-    -webkit-border-radius: 0 0 9px 9px !important;
-    -moz-border-radius: 0 0 9px 9px !important;
-}
-
-
-.menuLastPopup .gwt-MenuBar {
-    border-top-right-radius: 9px 9px 9px 9px;
-    -webkit-border-top-right-radius: 9px 9px 9px 9px;
-    -moz-border-top-right-radius: 9px 9px 9px 9px;
-}
-
-.gwt-AutoCompleteTextBox {
-  width: 80%;
-  border: 1px solid #87B3FF;
-  margin-top: 20px;
-}
-.gwt-DialogBox {
-  padding: 10px;
-  border: 1px solid #aaa;
-  background-color: #fff;
-  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#fff’, endColorstr=’#ccc’);
-  background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc));
-  background: -moz-linear-gradient(top, #fff, #ccc);
-  background-image: -o-linear-gradient(#fff,#ccc);
-  border-radius: 9px 9px 9px 9px; 
-  -webkit-border-radius: 9px 9px 9px 9px; 
-  -moz-border-radius: 9px 9px 9px 9px;
-  box-shadow: 0px 1px 4px #000;
-  -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
-  -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); 
-}
-
-.gwt-DialogBox .Caption {
-  height: 20px;
-  font-size: 1.3em !important;
-  background-color: #cf2828;
-  background: #cf2828 !important;
-  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828’, endColorstr=’#981a1a’);
-  background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)) !important;
-  background: -moz-linear-gradient(top, #cf2828, #981a1a) !important;
-  background-image: -o-linear-gradient(#cf2828,#981a1a);
-  color: #fff;
-  padding: 3px 3px 4px 3px;
-  margin: -10px;
-  margin-bottom: 5px;
-  font-weight: bold;
-  cursor: default;
-  text-align: center;
-  border-radius: 7px 7px 0 0; 
-  -webkit-border-radius: 7px 7px 0 0; 
-  -moz-border-radius: 7px 7px 0 0;
-}
-
-/*DIALOG: button, listbox, textbox, label */
-
-.gwt-DialogBox .gwt-button {
-    background-color: #ccc;
-    border-radius: 5px 5px 5px 5px;
-    -webkit-border-radius: 5px 5px 5px 5px;
-    -moz-border-radius: 5px 5px 5px 5px;
-    box-shadow: 0px 1px 4px #000;
-    -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6);
-    -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6);
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#444&#888;, endColorstr=’#222’); 
-    background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222)); 
-    background: -moz-linear-gradient(top, #444, #222); 
-    background-image: -o-linear-gradient(#444,#222);
-	 text-shadow: 1px 1px 1px rgba(0,0,0,0.2);
-	 padding: 3px 5px 3px 5px;
-	 margin: 10px 5px 10px 5px;
-	 color: #fff;
-	 font-weight: bold;
-	 font-size: 1em;
-	 border: none;
-	 -webkit-transition: color 0.2s linear; 
-    -moz-transition: color 0.2s linear; 
-    -o-transition: color 0.2s linear;
-}
-
-.gwt-DialogBox .gwt-button:hover {
-	background-color: #cf2828;
-	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828&#888;, endColorstr=’#981a1a’); 
-   background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); 
-   background: -moz-linear-gradient(top, #cf2828, #981a1a); 
-   background-image: -o-linear-gradient(#cf2828,#981a1a);
-   color: #fff;
-   text-shadow: 1px 1px 1px rgba(0,0,0,0.25);  
-}
-
-.gwt-DialogBox .gwt-TextBox {
-	 background-color: #fff;
-    border-radius: 5px 5px 5px 5px;
-    -webkit-border-radius: 5px 5px 5px 5px;
-    -moz-border-radius: 5px 5px 5px 5px;
-    box-shadow:inset 0px 1px 4px #000;
-    -webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.6);
-    -moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.6);
-	 padding: 3px 5px 3px 5px;
-	 margin: 10px 5px 10px 5px;
-	 color: #444;
-	 font-size: 1em;
-	 border: none;
-}
-
-.gwt-DialogBox .gwt-ListBox {
-    overflow: auto;
-    width: 100%;
-    background-color: #fff;
-    border-radius: 5px 5px 5px 5px;
-    -webkit-border-radius: 5px 5px 5px 5px;
-    -moz-border-radius: 5px 5px 5px 5px;
-    box-shadow:inset 0px 1px 4px #000;
-    -webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.6);
-    -moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.6);
-	 padding: 3px 5px 3px 5px;
-	 margin: 10px 5px 10px 5px;
-	 color: #444;
-	 font-size: 1em;
-	 border: none;
-}
-
-.gwt-DialogBox .gwt-Label {
-	margin-top: 13px;
-}
-
-/* Custom Dialogs */
-
-.formWarning { /* used when a form is not valid and must be corrected before submission */
-    font-weight: bold;
-    color: red !important;
-}
-
-.contactsChooser {
-    text-align: center;
-    margin:auto;
-    cursor: pointer;
-}
-
-.infoDialogBody {
-    width: 100%;
-    height: 100%
-}
-/* Contact List */
-
-div.contactBox {
-    width: 100%;
-	/* We want the contact panel to not use all the available height when displayed
-	   in the unibox panel (grey part), because the dialogs panels (white part) should
-	   still be visible. The setting max-height: fit-content would be appropriate here
-	   but it doesn't work with firefox 24.0. TODO: check if the current setting works
-	   with other browsers... the panel should of course not be displayed on 100px
-	   but exactly fit the contacts box.
-     */
-	max-height: 100px;
-}
-
-.contactTitle {
-    color: #cf2828;
-    font-size: 1.7em;
-    text-indent: 5px;
-    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
-    width: 200px;
-    height: 30px; 
-}
-
-.contactsSwitch {
-    /* Button used to switch contacts panel */
-    background: none;
-    border: 0;
-    padding: 0;
-    font-size: large;
-}
-
-.groupList {
-    width: 100%;    
-}
-
-.groupList tr:first-child td {
-    padding-top: 10px;
-}
-
-.group {
-    padding: 2px 15px;
-    margin: 5px;
-    display: inline-block;
-    text-decoration: none;     
-    font-weight: bold; 
-    color: #e7e5e5;
-    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); 
-    border-radius: 1em 1em 1em 1em; 
-    -webkit-border-radius: 1em 1em 1em 1em; 
-    -moz-border-radius: 1em 1em 1em 1em;
-    background-color: #eee; 
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#eee’, endColorstr=’#aaa&#888;); 
-    background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); 
-    background: -moz-linear-gradient(top, #eee, #aaa); 
-    background-image: -o-linear-gradient(#eee,#aaa);
-    color: #444;
-    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
-    box-shadow: 0px 1px 1px #000;
-    -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6);
-    -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6);
-}
-
-div.group:hover {
-    color: #fff;
-    text-shadow: 0 1px 0 rgba(0, 0, 0, 0.6);
-    background-color: #cf2828;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828&#888;, endColorstr=’#981a1a’); 
-    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); 
-    background: -moz-linear-gradient(top, #cf2828, #981a1a); 
-    background-image: -o-linear-gradient(#cf2828,#981a1a);
-    -webkit-transition: color 0.1s linear; 
-   -moz-transition: color 0.1s linear; 
-   -o-transition: color 0.1s linear;  
-}
-.contact {
-    font-size: 1em;
-    margin-top: 3px;
-    padding: 3px 10px 3px 10px;
-}
-
-.contact-menu-selected {
-    font-size: 1em;
-    margin-top: 3px;
-    padding: 3px 10px 3px 10px;
-    border-radius: 5px;
-	background-color: rgb(175, 175, 175);
-}
-
-/* START - contact presence status */
-.contact-connected {
-    color: #3c7e0c;
-    font-weight: bold;
-}
-.contact-unavailable {
-}
-.contact-chat {
-    color: #3c7e0c;
-    font-weight: bold;
-}
-.contact-away {
-    color: brown;
-    font-weight: bold;
-}
-.contact-dnd {
-    color: red;
-    font-weight: bold;
-}
-.contact-xa {
-    color: red;
-    font-weight: bold;
-}
-/* END - contact presence status */
-
-.selected {
-    color: #fff;
-    background-color: #cf2828;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828&#888;, endColorstr=’#981a1a’); 
-    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); 
-    background: -moz-linear-gradient(top, #cf2828, #981a1a); 
-    background-image: -o-linear-gradient(#cf2828,#981a1a);
-    border-radius: 1em 1em 1em 1em; 
-    -webkit-border-radius: 1em 1em 1em 1em; 
-    -moz-border-radius: 1em 1em 1em 1em;
-    -webkit-transition: color 0.2s linear; 
-    -moz-transition: color 0.2s linear; 
-    -o-transition: color 0.2s linear;
-}
-
-.messageBox {
-    width: 100%;
-    padding: 5px;
-    border: 1px solid #bbb;
-    color: #444;
-    background: #fff url('media/libervia/unibox_2.png') top bottom no-repeat;
-    box-shadow:inset 0 0 10px #ddd;
-    -webkit-box-shadow:inset 0 0 10px #ddd;
-    -moz-box-shadow:inset 0 0 10px #ddd;
-    border-radius: 0px 0px 10px 10px;
-    height: 25px;
-    margin: 0px;
-}
-
-/* UniBox & Status */
-
-.uniBoxPanel {
-    margin: 15px 22px 0 22px;
-}
-
-.uniBox {
-    width: 100%;
-    height: 45px;
-    padding: 5px;
-    border: 1px solid #bbb;
-    color: #444;
-    background: #fff url('media/libervia/unibox_2.png') top right no-repeat;    
-    box-shadow:inset 0 0 10px #ddd;
-    -webkit-box-shadow:inset 0 0 10px #ddd; 
-    -moz-box-shadow:inset 0 0 10px #ddd;
-}
-
-.uniBoxButton {
-    width:30px;
-    height:45px;
-}
-
-.statusPanel {
-    margin: auto;
-    text-align: center;
-    width: 100%;
-    padding: 5px 0px;
-    text-shadow: 0 -1px 1px rgba(255,255,255,0.25);
-    font-size: 1.2em;
-    background-color: #eee;
-    font-style: italic;
-    font-weight: bold;
-    color: #666;
-    cursor: pointer;
-}
-
-.presence-button {
-	font-size: x-large;
-	padding-right: 5px;
-	cursor: pointer;
-}
-
-/* RegisterBox */
-
-.registerPanel_main button {
-    margin: 0;
-    padding: 0;
-    border: 0;
-}
-
-.registerPanel_main div, .registerPanel_main button {
-    color: #fff;
-    text-decoration: none;
-}
-
-.registerPanel_main{
-    height: 100%;
-    border: 5px solid #222;
-    box-shadow: 0px 1px 4px #000;
-    -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
-    -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
-}
-
-.registerPanel_tabs .gwt-Label {
-    margin: 15px 7.5px 0px 7.5px;
-    cursor: pointer;
-    font-size: larger;
-}
-
-.registerPanel_tabs .gwt-TabBarItem div {
-    color: #444;
-    padding: 5px 7.5px;
-    border-radius: 5px 5px 0px 5px;
-    box-shadow: inset 0px 0px 2px 1px #9F2828;
-}
-
-.registerPanel_tabs .gwt-TabBarItem div:hover {
-    color: #fff;
-    box-shadow: inset 0px 0px 2px 2px #9F2828;
-}
-
-.registerPanel_tabs .gwt-TabBarItem-selected div {
-    color: #fff;
-    box-shadow: inset 0px 0px 2px 2px #9F2828;
-}
-
-.registerPanel_tabs .gwt-TabBarRest {
-    border-bottom: 1px #3F1818 dashed;
-}
-
-.registerPanel_right_side {
-   background: #111 url('media/libervia/register_right.png');
-   height: 100%;
-   width: 100%;
-}
-.registerPanel_content {
-   margin-left: 50px;
-   margin-top: 30px;
-}
-
-.registerPanel_content div {
-   font-size: 1em;
-   margin-left: 10px;
-   margin-top: 15px;
-   font-style: bold;
-   color: #888;
-}
-
-.registerPanel_content input {
-   height: 25px;
-   line-height: 25px;
-   width: 200px;
-   text-indent: 11px;
-
-   background: #000;
-   color: #aaa;
-   border: 1px solid #222;
-   border-radius: 15px 15px 15px 15px;
-   -webkit-border-radius: 15px 15px 15px 15px;
-   -moz-border-radius: 15px 15px 15px 15px;
-}
-
-.registerPanel_content input:focus {
-   border: 1px solid #444;
-}
-
-
-.registerPanel_content .button, .registerPanel_content .button:visited {
-   background: #222 url('media/libervia/gradient.png') repeat-x;
-   display: inline-block;
-   text-decoration: none;
-   border-radius: 6px 6px 6px 6px;
-   -moz-border-radius: 6px 6px 6px 6px;
-   -webkit-border-radius: 6px 6px 6px 6px;
-   -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.6);
-   -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.6);
-   border-bottom: 1px solid rgba(0,0,0,0.25);
-   cursor: pointer;
-   margin-top: 30px;
-}
-
-/* Fix for Opera */
-.button, .button:visited {
-    border-radius: 6px 6px 6px 6px !important;
-}
-
-.registerPanel_content .button:hover { background-color: #111; color: #fff; }
-.registerPanel_content .button:active    { top: 1px; }
-.registerPanel_content .button, .registerPanel_content .button:visited { font-size: 1em; font-weight: bold; line-height: 1; text-shadow: 0 -1px 1px rgba(0,0,0,0.25); padding: 7px 10px 8px; }
-.registerPanel_content .red.button, .registerPanel_content .red.button:visited { background-color: #000; }
-.registerPanel_content .red.button:hover { background-color: #bc0000; }
-
-/* Widgets */
-
-.widgetsPanel td {
-    vertical-align: top;
-}
-
-.widgetsPanel > div > table {
-    border-collapse: separate !important;
-    border-spacing: 7px;
-}
-
-.widgetHeader {
-    margin: auto;
-    height: 25px;
-    /*border: 1px solid #ddd;*/
-    border-radius: 10px 10px 0 0; 
-    -webkit-border-radius: 10px 10px 0 0; 
-    -moz-border-radius: 10px 10px 0 0; 
-    background-color: #222;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#444&#888;, endColorstr=’#222’); 
-    background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222)); 
-    background: -moz-linear-gradient(top, #444, #222); 
-    background-image: -o-linear-gradient(#444,#222);
-}
-
-.widgetHeader_title {
-    color: #fff;
-    font-weight: bold;
-    text-align: left;
-    text-indent: 15px;
-    margin-top: 4px;
-}
-
-.widgetHeader_buttonsWrapper {
-    position: absolute;
-    top: 0;
-    height: 100%;
-    width: 100%;
-}
-
-.widgetHeader_buttonGroup {
-    float: right;
-}
-
-.widgetHeader_buttonGroup img {
-   background-color: transparent;
-    width: 25px;
-    height: 20px;
-    padding-top: 2px;
-    padding-bottom: 3px;
-    border-left: 1px solid #666;
-    border-top: 0;
-    border-radius: 0 10px 0 0; 
-    -webkit-border-radius: 0 10px 0 0; 
-    -moz-border-radius: 0 10px 0 0; 
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#555&#888;, endColorstr=’#333’); 
-    background: -webkit-gradient(linear, left top, left bottom, from(#555), to(#333)); 
-    background: -moz-linear-gradient(top, #555, #333); 
-    background-image: -o-linear-gradient(#555,#333);
-}
-
-.widgetHeader_closeButton {
-    border-radius: 0 10px 0 0 !important; 
-    -webkit-border-radius: 0 10px 0 0 !important; 
-    -moz-border-radius: 0 10px 0 0 !important;
-}
-
-.widgetHeader_settingButton {
-    border-radius: 0 0 0 0 !important; 
-    -webkit-border-radius: 0 0 0 0 !important; 
-    -moz-border-radius: 0 0 0 0 !important;          
-}
-
-.widgetHeader_buttonGroup img:hover {
-    background-color: #cf2828;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828&#888;, endColorstr=’#981a1a’); 
-    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); 
-    background: -moz-linear-gradient(top, #cf2828, #981a1a); 
-    background-image: -o-linear-gradient(#cf2828,#981a1a);
-}
-
-.widgetBody {
-    border-radius: 0 0 10px 10px; 
-    -webkit-border-radius: 0 0 10px 10px; 
-    -moz-border-radius: 0 0 10px 10px;
-    background-color: #fff;  
-    min-width: 200px;
-    min-height: 150px;
-    box-shadow:inset 0px 0 1px #444;
-    -webkit-box-shadow:inset 0 0 1px #444;
-    -moz-box-shadow:inset 0 0 1px #444;
-}
-
-/* BorderWidgets */
-
-.bottomBorderWidget {
-    height: 10px !important;
-}
-
-.leftBorderWidget, .rightBorderWidget {
-    width: 10px !important;
-}
-
-/* Microblog */
-
-.microblogPanel {
-/*    margin: auto;
-    width: 95% !important;*/
-    width: 100%;
-}
-
-.microblogNewButton {
-    width: 100%;
-    height: 35px;
-}
-
-.subPanel {
-}
-
-.subpanel .mb_entry {
-    padding-left: 65px;
-}
-
-.mb_entry {
-	min-height: 64px;
-}
-
-.mb_entry_header
-{
-    cursor: pointer;
-}
-
-.selected_widget .selected_entry .mb_entry_header
-{
-    background: #cf2828;
-}
-
-.mb_entry_author {
-    font-weight: bold;
-    padding-left: 5px;
-}
-
-.mb_entry_avatar {
-	float: left;
-}
-
-.mb_entry_avatar img {
-	width: 48px;
-	height: 48px;
-	padding: 8px;
-}
-
-.mb_entry_dialog {
-	float: left;
-	min-height: 54px;
-	padding: 5px 20px 5px 20px;
-    border-collapse: separate;  # for the bubble queue since the entry dialog is now a HorizontalPanel
-}
-
-.bubble {
-    position: relative;
-    padding: 15px;
-    margin: 2px;
-    -webkit-border-radius:10px;
-    -moz-border-radius:10px;
-    border-radius:10px;
-    background: #EDEDED;
-    border-color: #C1C1C1;
-    border-width: 1px;
-    border-style: solid;
-    display: block;
-    border-collapse: separate;
-    min-height: 15px;  # for the bubble queue to be aligned when the bubble is empty
-}
-
-.bubble:after {
-    background: transparent url('media/libervia/bubble_after.png') top right no-repeat;
-    border: none;	
-    content: "";
-    position: absolute;
-    bottom: auto;
-    left: -20px;
-    top: 16px;
-    display: block;
-    height: 20;
-    width: 20;
-}
-
-.bubble textarea{
-    width: 100%;
-}
-
-.mb_entry_timestamp {
-    font-style: italic;
-}
-
-.mb_entry_actions {
-    float: right;
-    margin: 5px;
-    cursor: pointer;
-    font-size: large;
-}
-
-.mb_entry_action_larger {
-    font-size: x-large;
-}
-
-.mb_entry_toggle_syntax {
-    cursor: pointer; 
-    text-align: right;
-    display: block;
-    position: relative;
-    top: -20px:
-    left: -20px;
-}
-
-/* Chat & MUC Room */
-
-.chatPanel {
-    height: 100%;
-    width: 100%;
-}
-
-.chatPanel_body {
-    height: 100%;
-    width: 100%;
-}
-
-.chatContent {
-    overflow: auto;
-    padding: 5px 15px 5px 15px;
-}
-
-.chatText {
-  margin-top: 7px;
-}
-
-.chatTextInfo {
-    font-weight: bold;
-    font-style: italic;
-}
-
-.chatTextInfo-link {
-    font-weight: bold;
-    font-style: italic;
-	cursor: pointer;
-	display: inline;
-}
-
-.chatArea {
-    height:100%;
-    width:100%;
-}
-
-.chat_text_timestamp {
-    font-style: italic;
-    margin-right: -4px;
-    padding: 1px 3px 1px 3px;
-    -moz-border-radius: 15px 0 0 15px;
-    -webkit-border-radius: 15px 0 0 15px;
-    border-radius: 15px 0 0 15px;
-    background-color: #eee;
-    color: #888;
-    border: 1px solid #ddd;
-    border-right: none;
-}
-
-.chat_text_nick {
-    font-weight: bold;
-    padding: 1px 3px 1px 3px;
-    -moz-border-radius: 0 15px 15px 0;
-    -webkit-border-radius: 10 15px 15px 0;
-    border-radius: 0 15px 15px 0;
-    background-color: #eee;
-    color: #b01e1e;
-    border: 1px solid #ddd;
-    border-left: none;
-}
-
-.chat_text_msg {
-    white-space: pre-wrap;
-}
-
-.chat_text_mymess {
-    color: #006600;
-}
-
-.occupant {
-    margin-top: 10px;
-    margin-right: 4px;
-    min-width: 120px;
-    padding: 5px 15px 5px 15px;
-    font-weight: bold;
-    background-color: #eee;
-    border: 1px solid #ddd;
-    white-space: nowrap;
-}
-
-.occupantsList {
-    border-right: 2px dotted #ddd;
-    margin-left: 5px;
-    margin-right: 10px;
-    height: 100%;
-}
-
-/* Games */
-
-.cardPanel {
-    background: #02FE03;
-    margin: 0 auto;
-}
-
-.cardGamePlayerNick {
-    font-weight: bold;
-}
-
-/* Radiocol */
-
-.radiocolPanel {
-
-}
-
-.radiocol_metadata_lbl {
-    font-weight: bold;
-    padding-right: 5px;
-}
-
-.radiocol_next_song {
-    margin-right: 5px;
-    font-style:italic;
-}
-
-.radiocol_status {
-    margin-left: 10px;
-    margin-right: 10px;
-    font-weight: bold;
-	color: black;
-}
-
-.radiocol_upload_status_ok {
-    margin-left: 10px;
-    margin-right: 10px;
-    font-weight: bold;
-	color: #28F215;
-}
-
-.radiocol_upload_status_ko {
-    margin-left: 10px;
-    margin-right: 10px;
-    font-weight: bold;
-	color: #B80000;
-}
-
-/* Drag and drop */
-
-.dragover {
-    background: #cf2828 !important;
-    border-radius: 1em 1em 1em 1em !important;
-    -webkit-border-radius: 1em 1em 1em 1em !important;
-    -moz-border-radius: 1em 1em 1em 1em !important;
-}
-
-.dragover .widgetHeader, .dragover .widgetBody, .dragover .widgetBody span, .dragover .widgetHeader img {
-    background: #cf2828 !important;
-}
-
-.dragover.widgetHeader {
-    border-radius: 1em 1em 0 0 !important;
-    -webkit-border-radius: 1em 1em 0 0 !important;
-    -moz-border-radius: 1em 1em 0 0 !important;
-}
-
-.dragover.widgetBody {
-    border-radius: 0 0 1em 1em !important;
-    -webkit-border-radius: 0 0 1em 1em !important;
-    -moz-border-radius: 0 0 1em 1em !important;
-}
-
-/* Warning message */
-
-.warningPopup {
-	font-size: 1em;
-    width: 100%;
-    height: 26px;
-    text-align: center;
-    padding: 5px 0;
-    border-bottom: 1px solid #444;
-    /*background-color: #fff;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’fff′, endColorstr=’#ccc’);
-    background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc));
-    background: -moz-linear-gradient(top, #fff, #ccc);
-    background-image: -o-linear-gradient(#fff,#ccc); */
-
-}
-
-.warningTarget {
-    font-weight: bold;
-   
-}
-
-.targetPublic {
-    background-color: red; /*#cf2828;*/
-    /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828′, endColorstr=’#981a1a’);
-    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a));
-    background: -moz-linear-gradient(top, #cf2828, #981a1a);
-    background-image: -o-linear-gradient(#cf2828,#981a1a); */
-}
-
-.targetGroup {
-    background-color: #00FFFB;
-    /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’68ba0f′, endColorstr=’#40700d’);
-    background: -webkit-gradient(linear, left top, left bottom, from(#68ba0f), to(#40700d));
-    background: -moz-linear-gradient(top, #68ba0f, #40700d);
-    background-image: -o-linear-gradient(#68ba0f,#40700d); */
-}
-
-.targetOne2One {
-    background-color: #66FF00;
-    /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’444444′, endColorstr=’#222222’);
-    background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222));
-    background: -moz-linear-gradient(top, #444444, #222222);
-    background-image: -o-linear-gradient(#444444,#222222);*/ 
-}
-
-.targetStatus {
-    background-color: #fff;
-    /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’fff′, endColorstr=’#ccc’);
-    background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc));
-    background: -moz-linear-gradient(top, #fff, #ccc);
-    background-image: -o-linear-gradient(#fff,#ccc); */
-}
-
-/* Tab panel */
-
-.liberviaTabPanel {
-}
-
-.gwt-TabPanel {
-}
-
-.gwt-TabPanelBottom {
-  height: 100%;
-}
-
-.gwt-TabBar {
-  font-weight: bold;
-  text-decoration: none;
-  border-bottom: 3px solid #a01c1c;  
-}
-
-.mainTabPanel .gwt-TabBar {
-  z-index: 10;
-  position: fixed;
-  bottom: 0;
-  left: 0;
-}
-
-.gwt-TabBar .gwt-TabBarFirst {
-  height: 100%;
-}
-
-.gwt-TabBar .gwt-TabBarRest {
-}
-
-.liberviaTabPanel .gwt-TabBar {;
-}
-
-.liberviaTabPanel .gwt-TabBar .gwt-TabBarItem {
-  cursor: pointer;
-  margin-right: 5px;
-}
-
-.liberviaTabPanel .gwt-TabBarItem div {
-	color: #fff;
-}
-
-.liberviaTabPanel .gwt-TabBarItem {
-  color: #444 !important;
-  background-color: #222;
-  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#444′, endColorstr=’#222’);
-  background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222));
-  background: -moz-linear-gradient(top, #444, #222);
-  background-image: -o-linear-gradient(#444,#222);
-  box-shadow: 0px 1px 4px #000;
-  -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
-  -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
-  padding: 4px 15px 4px 15px;
-  border-radius: 1em 1em 0 0;
-  -webkit-border-radius: 1em 1em 0 0;
-  -moz-border-radius: 1em 1em 0 0;
-  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
-}
-
-.liberviaTabPanel .gwt-TabBarItem-selected {
-  color: #fff;
-  background-color: #cf2828;
-  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828′, endColorstr=’#981a1a’);
-  background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a));
-  background: -moz-linear-gradient(top, #cf2828, #981a1a);
-  background-image: -o-linear-gradient(#cf2828,#981a1a);
-  box-shadow: 0px 1px 4px #000;
-  -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
-  -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
-  padding: 4px 15px 4px 15px;
-  border-radius: 1em 1em 0 0;
-  -webkit-border-radius: 1em 1em 0 0;
-  -moz-border-radius: 1em 1em 0 0;
-  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
-}
-
-.liberviaTabPanel div.gwt-TabBarItem:hover {
-  color: #fff;
-  background-color: #cf2828;
-  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828′, endColorstr=’#981a1a’);
-  background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a));
-  background: -moz-linear-gradient(top, #cf2828, #981a1a);
-  background-image: -o-linear-gradient(#cf2828,#981a1a);
-  box-shadow: 0px 1px 4px #000;
-  -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
-  -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
-  padding: 4px 15px 4px 15px;
-  border-radius: 1em 1em 0 0;
-  -webkit-border-radius: 1em 1em 0 0;
-  -moz-border-radius: 1em 1em 0 0;
-  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); 
-}
-
-.liberviaTabPanel .gwt-TabBar .gwt-TabBarItem-selected {
-  cursor: default;
-}
-
-.globalLeftArea {
-    margin-top: 9px;
-}
-
-
-/* Misc */
-
-.selected_widget .widgetHeader  {
-    /* this property is set when a widget is the current target of the uniBox
-     * (messages entered in unibox will be sent to this widget)
-     */
-    background-color: #cf2828;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828&#888;, endColorstr=’#981a1a’); 
-    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); 
-    background: -moz-linear-gradient(top, #cf2828, #981a1a); 
-    background-image: -o-linear-gradient(#cf2828,#981a1a);
-}
-
-.infoFrame {
-    position: relative;
-    width: 100%;
-    height: 100%;
-}
-
-.marginAuto {
-    margin: auto;
-}
-
-.transparent {
-    opacity: 0;
-}
-
-/* URLs */
-
-a.url {
-    color: blue;
-    text-decoration: none
-}
-
-a:hover.url {
-    text-decoration: underline
-}
-
-/* Rich Text/Message Editor */
-
-.richTextEditor {
-}
-
-.richTextEditor tbody {
-    width: 100%;
-    display: table;
-}
-
-.richMessageEditor {
-    width: 100%;
-    margin: 9px 18px;
-}
-
-.richTextTitle {
-    margin-bottom: 5px;
-}
-
-.richTextTitle textarea {
-    height: 23px;
-    width: 99%;
-    margin: auto;
-    display: block;
-}
-
-.richTextToolbar {
-    white-space: nowrap;
-    width: 100%;
-}
-
-.richTextArea {
-    width: 100%;
-}
-
-.richMessageArea {
-    width: 100%;
-    height: 250px;
-}
-
-.richTextWysiwyg {
-    min-height: 50px;
-    background-color: white;
-    border: 1px solid #a0a0a0;
-    border-radius: 5px;
-    display: block;
-    font-size: larger;
-    white-space: pre;
-}
-
-.richTextSyntaxLabel {
-	text-align: right;
-	margin: 14px 0px 0px 14px;
-	font-size: 12px;
-}
-
-.richTextToolButton {
-	cursor: pointer;
-    width:26px;
-    height:26px;
-    vertical-align: middle;
-    margin: 2px 1px;
-    border-radius: 5px 5px 5px 5px;
-    -webkit-border-radius: 5px 5px 5px 5px;
-    -moz-border-radius: 5px 5px 5px 5px;
-    box-shadow: 0px 1px 4px #000;
-    -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
-    -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
-	border: none;
-	-webkit-transition: color 0.2s linear; 
-    -moz-transition: color 0.2s linear; 
-    -o-transition: color 0.2s linear;
-}
-
-.richTextIcon {
-    width:16px;
-    height:16px;
-    vertical-align: middle;
-}
-
-/* Recipients panel */
-
-.recipientButtonCell {
-	width:55px;
-}
-
-.recipientTypeMenu {
-}
-
-.recipientTypeItem {
-	cursor: pointer;
-    border-radius: 5px;
-    width: 50px;
-}
-
-.recipientPanel {
-}
-
-.recipientTextBox {
-	cursor: pointer;
-    width: auto;
-    border-radius: 5px 5px 5px 5px;
-    -webkit-border-radius: 5px 5px 5px 5px;
-    -moz-border-radius: 5px 5px 5px 5px;
-    box-shadow: inset 0px 1px 4px rgba(135, 179, 255, 0.6);
-    -webkit-box-shadow:inset 0 1px 4px rgba(135, 179, 255, 0.6);
-    -moz-box-shadow:inset 0 1px 4px rgba(135, 179, 255, 0.6);
-    padding: 2px 1px;
-    margin: 0px;
-    color: #444;
-    font-size: 1em;
-}
-
-.recipientTextBox-invalid {
-    box-shadow: inset 0px 1px 4px rgba(255, 0, 0, 0.6);
-    -webkit-box-shadow:inset 0 1px 4px rgba(255, 0, 0, 0.6);
-    -moz-box-shadow:inset 0 1px 4px rgba(255, 0, 0, 0.6);
-    border: 1px solid rgb(255, 0, 0);
-}
-
-.recipientRemoveButton {
-	margin: 0px 10px 0px 0px;
-	padding: 0px;
-	border: 1px dashed red;
-    border-radius: 5px 5px 5px 5px;
-}
-
-.recipientRemoveIcon {
-	color: red;
-    width:15px;
-    height:15px;
-	vertical-align: baseline;
-}
-
-.dragover-recipientPanel {
-	border-radius: 5px;
-    background: none repeat scroll 0% 0% rgb(135, 179, 255);
-    border: 1px dashed rgb(35,79,255);
-}
-
-.recipientSpacer {
-    height: 15px;
-}
-
-/* Popup (context) menu */
-
-.popupMenuItem {
-    cursor: pointer;
-    border-radius: 5px;
-    width: 100%;
-}
-
-/* Contact group manager */
-
-.contactGroupEditor {
-	width: 800px;
-	max-width:800px;
-	min-width: 800px;
-	margin-top: 9px;
-	margin-left:18px;
-}
-
-.contactGroupRemoveButton {
-	margin: 0px 10px 0px 0px;
-	padding: 0px;
-	border: 1px dashed red;
-    border-radius: 5px 5px 5px 5px;
-}
-
-.addContactGroupPanel {
-	
-}
-
-.contactGroupPanel {
-	vertical-align:middle;
-}
-
-.toggleAssignedContacts {
-    white-space: nowrap;
-}
-
-.contactGroupButtonCell {
-    vertical-align: baseline;
-    width: 55px;
-    white-space: nowrap;
-}
-
-/* Room and contacts chooser */
-
-.room-contact-chooser {
-	width:380px;
-}
-
-/* StackPanel */
-
-.gwt-StackPanel {
-}
-
-.gwt-StackPanel .gwt-StackPanelItem {
-    background-color: #222;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’444444′, endColorstr=’#222222’);
-    background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222));
-    background: -moz-linear-gradient(top, #444444, #222222);
-    background-image: -o-linear-gradient(#444444,#222222);
-    text-decoration: none;    
-    font-weight: bold;
-    height: 100%;
-    color: #e7e5e5;
-    padding: 3px 15px;
-    /*display: block;*/
-    border-radius: 1em 1em 1em 1em;
-    -webkit-border-radius: 1em 1em 1em 1em;
-    -moz-border-radius: 1em 1em 1em 1em;
-    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
-    -webkit-transition: color 0.2s linear; 
-    -moz-transition: color 0.2s linear; 
-    -o-transition: color 0.2s linear;
-}
-
-.gwt-StackPanel .gwt-StackPanelItem:hover {
-    background-color: #eee;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#eee’, endColorstr=’#aaa′);
-    background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa));
-    background: -moz-linear-gradient(top, #eee, #aaa);
-    background-image: -o-linear-gradient(#eee,#aaa);
-    color: #444;
-    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);    
-    cursor: pointer;
-}
-
-.gwt-StackPanel .gwt-StackPanelItem-selected {
-    background-color: #eee;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#eee’, endColorstr=’#aaa′);
-    background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa));
-    background: -moz-linear-gradient(top, #eee, #aaa);
-    background-image: -o-linear-gradient(#eee,#aaa);
-    color: #444;
-    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);    
-    cursor: pointer;
-}
-
-/* Caption Panel */
-
-.gwt-CaptionPanel {
-    overflow: auto;
-    background-color: #fff;
-    border-radius: 5px 5px 5px 5px;
-    -webkit-border-radius: 5px 5px 5px 5px;
-    -moz-border-radius: 5px 5px 5px 5px;
-	 padding: 3px 5px 3px 5px;
-	 margin: 10px 5px 10px 5px;
-	 color: #444;
-	 font-size: 1em;
-	 border: solid 1px gray;
-}
-
-/* Radio buttons */
-
-.gwt-RadioButton {
-	white-space: nowrap;
-}
-
-[contenteditable="true"] {
-}
-
-/* XMLUI styles */
-
-.AdvancedListSelectable tr{
-    cursor: pointer;
-}
-
-.AdvancedListSelectable tr:hover{
-    background: none repeat scroll 0 0 #EE0000;
-}
-
-.line hr {
-
-}
-
-.dot hr {
-    height: 0px;
-    border-top: 1px dotted;
-    border-bottom: 0px;
-}
-
-.dash hr {
-    height: 0px;
-    border-top: 1px dashed;
-    border-bottom: 0px;
-}
-
-.plain hr {
-    height: 10px;
-    color: black;
-    background-color: black;
-}
-
-.blank hr {
-    border: 0px;
-}
--- a/public/libervia.html	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,32 +0,0 @@
-<!--
-Libervia: a Salut à Toi frontend
-Copyright (C) 2011  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/>.
--->
-
-<html>
-<head profile="http://www.w3.org/2005/10/profile">
-<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-<meta name="pygwt:module" content="libervia">
-<link rel='stylesheet' href='libervia.css'>
-<link rel="icon" type="image/png" href="sat_logo_16.png">
-
-<title>Libervia</title>
-</head>
-<body bgcolor="white">
-<script language="javascript" src="bootstrap.js"></script>
-<iframe id='__pygwt_historyFrame' style='display:none;width:0;height:0;border:0'></iframe>
-</body>
-</html>
Binary file public/sat_logo_16.png has changed
--- a/setup.py	Fri May 16 11:51:10 2014 +0200
+++ b/setup.py	Tue May 20 06:41:16 2014 +0200
@@ -1,8 +1,9 @@
-#!/usr/bin/python
+#!/usr/bin/env python2
 # -*- coding: utf-8 -*-
 
 # Libervia: a Salut à Toi frontend
-# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
+# Copyright (C) 2011, 2012, 2013, 2014  Jérôme Poisson (goffi@goffi.org)
+# Copyright (C) 2013, 2014 Adrien Cossa (souliane@mailoo.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
@@ -26,6 +27,8 @@
 import sys
 import subprocess
 from stat import ST_MODE
+import shutil
+from src.server.constants import Const as C
 
 # seen here: http://stackoverflow.com/questions/7275295
 try:
@@ -52,6 +55,12 @@
 NAME = 'libervia'
 LAUNCH_DAEMON_COMMAND = 'libervia'
 
+ENV_LIBERVIA_INSTALL = "LIBERVIA_INSTALL"  # environment variable to customise installation
+NO_PREINSTALL_OPT = 'nopreinstall'  # skip all preinstallation checks
+AUTO_DEB_OPT = 'autodeb'  # automaticaly install debs
+CLEAN_OPT = 'clean'  # remove previous installation directories
+PURGE_OPT = 'purge'  # remove building and previous installation directories
+
 
 class MercurialException(Exception):
     pass
@@ -78,9 +87,9 @@
         with open(self.sh_script_path, 'r') as sh_file:
             for ori_line in sh_file:
                 if ori_line.startswith('DAEMON='):
-                    dest_line = 'DAEMON=""\n'  # we want to launch sat as a daemon
-                elif ori_line.startswith('TAP_PATH='):
-                    dest_line = 'TAP_PATH="%s/"\n' % run_dir
+                    dest_line = 'DAEMON=""\n'  # we want to launch libervia as a daemon
+                elif ori_line.startswith('DATA_DIR='):
+                    dest_line = 'DATA_DIR="%s"\n' % self.install_data_dir
                 elif ori_line.startswith('PYTHON='):
                     dest_line = 'PYTHON="%s"\n' % sys.executable
                 else:
@@ -93,36 +102,117 @@
     def custom_create_links(self):
         """Create symbolic links to executables"""
         # the script which launch the daemon
-        links = [(self.sh_script_path, LAUNCH_DAEMON_COMMAND)]
-        for source, dest in links:
-            dest_name, copied = copy_file(source, os.path.join(self.install_scripts, dest), link='sym')
+        for source, dest in self.sh_script_links:
+            dest = os.path.join(self.install_scripts, dest)
+            if os.path.islink(dest) and os.readlink(dest) != source:
+                os.remove(dest)  # copy_file doesn't force the link update
+            dest_name, copied = copy_file(source, dest, link='sym')
             assert (copied)
             # we change the perm in the same way as in the original install_scripts
             mode = ((os.stat(dest_name)[ST_MODE]) | 0555) & 07777
             os.chmod(dest_name, mode)
 
     def pyjs_build(self):
-        return subprocess.call('pyjsbuild libervia --no-compile-inplace -m -I %s' % self.install_lib, shell=True)
+        """Build the browser side JS files from Python source."""
+        cwd = os.getcwd()
+        os.chdir(os.path.join('src', 'browser'))
+        result = subprocess.call('pyjsbuild libervia_main --no-compile-inplace -I %s -o %s' %
+                                 (self.install_lib, self.pyjamas_output_dir), shell=True)
+        os.chdir(cwd)
+        return result
+
+    def copy_data_files(self):
+        """To copy the JS files couldn't be done with the data_files parameter
+        of setuptools.setup because all the files to be copied must exist before
+        the call. Also, we need the value of self.install_lib to build the JS
+        files (it's not easily predictable as it may vary from one system to
+        another), so we can't call pyjsbuild before setuptools.setup.
+        """
+        html = os.path.join(self.install_data_dir, C.HTML_DIR)
+        if os.path.isdir(html):
+            shutil.rmtree(html, ignore_errors=True)
+        shutil.copytree(self.pyjamas_output_dir, html)
 
     def run(self):
+        self.sh_script_path = os.path.join(self.install_lib, NAME, 'libervia.sh')
+        self.sh_script_links = [(self.sh_script_path, LAUNCH_DAEMON_COMMAND)]
+        self.install_data_dir = os.path.join(self.install_data, 'share', NAME)
+        self.pyjamas_output_dir = os.path.join(os.getcwd(), 'html')
         sys.stdout.write('running pre installation stuff\n')
         sys.stdout.flush()
-        build_result = self.pyjs_build()
+        if PURGE_OPT in install_opt:
+            self.purge()
+        elif CLEAN_OPT in install_opt:
+            self.clean()
+        install.run(self)
+        sys.stdout.write('running post installation stuff\n')
+        sys.stdout.flush()
+        build_result = self.pyjs_build()  # build after libervia.common is accessible
         if build_result == 127:  # TODO: remove magic string
             print "pyjsbuild is not installed or not accessible from the PATH of user '%s'" % os.getenv('USERNAME')
             return
         if build_result != 0:
             print "pyjsbuild failed to build libervia"
             return
-        install.run(self)
-        sys.stdout.write('running post installation stuff\n')
-        sys.stdout.flush()
-        self.sh_script_path = os.path.join(self.install_lib, 'libervia_server', 'libervia.sh')
+        self.copy_data_files()
         self.custom_auto_options()
         self.custom_create_links()
 
+    def confirm(self, message):
+        """Ask the user for a confirmation"""
+        message += 'Proceed'
+        while True:
+            res = raw_input("%s (y/n)? " % message)
+            if res not in ['y', 'Y', 'n', 'N']:
+                print "Your response ('%s') was not one of the expected responses: y, n" % res
+                message = 'Proceed'
+                continue
+            if res in ('y', 'Y'):
+                return True
+            return False
 
-def preinstall_check():
+    def clean(self, message=None, to_remove=None):
+        """Clean previous installation directories
+
+        @param message (str): to use a non-default confirmation message
+        @param to_remove (str): extra files/directories to remove
+        """
+        if message is None:
+            message = "Cleaning previous installation directories"
+        if to_remove is None:
+            to_remove = []
+        to_remove.extend([os.path.join(self.install_lib, NAME),
+                          self.install_data_dir,
+                          os.path.join(self.install_data, 'share', 'doc', NAME),
+                          os.path.join(self.install_lib, "%s-py%s.egg-info" % (self.config_vars['dist_fullname'], self.config_vars['py_version_short'])),
+                          ])
+        for source, dest in self.sh_script_links:
+            dest = os.path.join(self.install_scripts, dest)
+            if os.path.islink(dest):
+                to_remove.append(dest)
+        plugin_file = os.path.join(self.install_lib, 'twisted', 'plugins', NAME)
+        if os.path.isfile(plugin_file):
+            to_remove.append(plugin_file)
+
+        message = "%s:\n%s\n" % (message, "\n".join(["    %s" % path for path in to_remove]))
+        if not self.confirm(message):
+            return
+        sys.stdout.write('cleaning previous installation directories...\n')
+        sys.stdout.flush()
+        for path in to_remove:
+            if os.path.isdir(path):
+                shutil.rmtree(path, ignore_errors=True)
+            else:
+                os.remove(path)
+
+    def purge(self):
+        """Clean building and previous installation directories"""
+        message = "Cleaning building and previous installation directories"
+        to_remove = [os.path.join(os.getcwd(), 'build'), self.pyjamas_output_dir]
+        self.clean(message, to_remove)
+
+
+def preinstall_check(install_opt):
     """Check presence of problematic dependencies, and try to install them with package manager
     This ugly stuff is necessary as distributions are not installed correctly with setuptools/distribute
     Hope to remove this at some point"""
@@ -138,10 +228,18 @@
 
     # which modules are not installed ?
     modules_toinstall = [mod for mod in modules_tocheck if not module_installed(mod)]
+    """# is mercurial available ?
+    hg_installed = subprocess.call('which hg', stdout=open('/dev/null', 'w'), shell=True) == 0
+    if not hg_installed:
+        modules_toinstall.append('mercurial')"""  # hg can be installed from pypi
 
     if modules_toinstall:
-        # are we on a distribution using apt ?
-        apt_path = subprocess.Popen('which apt-get', stdout=subprocess.PIPE, shell=True).communicate()[0][:-1]
+        if AUTO_DEB_OPT in install_opt: # auto debian installation is requested
+            # are we on a distribution using apt ?
+            apt_path = subprocess.Popen('which apt-get', stdout=subprocess.PIPE, shell=True).communicate()[0][:-1]
+        else:
+            apt_path = None
+
         not_installed = set()
         if apt_path:
             # we have apt, we'll try to use it
@@ -164,9 +262,9 @@
 
 if sys.argv[1].lower() in ['egg_info', 'install']:
     # we only check dependencies if egg_info or install is used
-    install_opt = os.environ.get("LIBERVIA_INSTALL", "")
-    if not "nopreinstall" in install_opt:  # user can force preinstall skipping
-        preinstall_check()
+    install_opt = os.environ.get(ENV_LIBERVIA_INSTALL, "").split()
+    if not NO_PREINSTALL_OPT in install_opt:  # user can force preinstall skipping
+        preinstall_check(install_opt)
 
 setup(name=NAME,
       version='0.4.0',
@@ -181,14 +279,15 @@
                    'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
                    'Operating System :: POSIX :: Linux',
                    'Topic :: Communications :: Chat'],
-      package_dir={'libervia': '.', 'libervia_server': 'libervia_server'},
-      packages=['libervia', 'libervia.output', 'libervia_server', 'twisted.plugins'],
-      package_data={'libervia': ['libervia.py'], 'libervia.output': ['**/*.*'], 'libervia_server': ['libervia.sh']},
-      data_files=[('share/doc/%s' % NAME, ['COPYING', 'README'])],
+      package_dir={'libervia': 'src', 'twisted.plugins': 'src/twisted/plugins'},
+      packages=['libervia', 'libervia.common', 'libervia.server', 'twisted.plugins'],
+      package_data={'libervia': ['libervia.sh']},
+      data_files=[(os.path.join('share', 'doc', NAME), ['COPYING', 'README', 'INSTALL']),
+                  (os.path.join('share', NAME, C.SERVER_CSS_DIR), [os.path.join(C.SERVER_CSS_DIR, filename) for filename in os.listdir(C.SERVER_CSS_DIR)]),
+                  ],
       scripts=[],
       zip_safe=False,
       dependency_links=['http://www.blarg.net/%7Esteveha/pyfeed-0.7.4.tar.gz', 'http://www.blarg.net/%7Esteveha/xe-0.7.4.tar.gz'],
       install_requires=['sat', 'twisted', 'pyfeed', 'xe', 'txJSON-RPC', 'zope.interface', 'pyopenssl'],
       cmdclass={'install': CustomInstall},
       )
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/base_panels.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,609 @@
+#!/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.core.i18n import _
+from sat_frontends.tools.strings import addURLToText, addURLToImage
+
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.HTMLPanel import HTMLPanel
+from pyjamas.ui.Button import Button
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.PopupPanel import PopupPanel
+from pyjamas.ui.StackPanel import StackPanel
+from pyjamas.ui.TextArea import TextArea
+from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT
+from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KeyboardHandler
+from pyjamas.ui.FocusListener import FocusHandler
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas import DOM
+
+from datetime import datetime
+from time import time
+
+from html_tools import html_sanitize, html_strip, inlineRoot, convertNewLinesToXHTML
+from constants import Const as C
+
+
+class ChatText(HTMLPanel):
+
+    def __init__(self, timestamp, nick, mymess, msg, xhtml=None):
+        _date = datetime.fromtimestamp(float(timestamp or time()))
+        _msg_class = ["chat_text_msg"]
+        if mymess:
+            _msg_class.append("chat_text_mymess")
+        HTMLPanel.__init__(self, "<span class='chat_text_timestamp'>%(timestamp)s</span> <span class='chat_text_nick'>%(nick)s</span> <span class='%(msg_class)s'>%(msg)s</span>" %
+                           {"timestamp": _date.strftime("%H:%M"),
+                            "nick": "[%s]" % html_sanitize(nick),
+                            "msg_class": ' '.join(_msg_class),
+                            "msg": addURLToText(html_sanitize(msg)) if not xhtml else inlineRoot(xhtml)}  # FIXME: images and external links must be removed according to preferences
+                           )
+        self.setStyleName('chatText')
+
+
+class Occupant(HTML):
+    """Occupant of a MUC room"""
+
+    def __init__(self, nick, state=None, special=""):
+        """
+        @param nick: the user nickname
+        @param state: the user chate state (XEP-0085)
+        @param special: a string of symbols (e.g: for activities)
+        """
+        HTML.__init__(self)
+        self.nick = nick
+        self._state = state
+        self.special = special
+        self._refresh()
+
+    def __str__(self):
+        return self.nick
+
+    def setState(self, state):
+        self._state = state
+        self._refresh()
+
+    def addSpecial(self, special):
+        """@param special: unicode"""
+        if special not in self.special:
+            self.special += special
+            self._refresh()
+
+    def removeSpecials(self, special):
+        """@param special: unicode or list"""
+        if not isinstance(special, list):
+            special = [special]
+        for symbol in special:
+            self.special = self.special.replace(symbol, "")
+            self._refresh()
+
+    def _refresh(self):
+        state = (' %s' % C.MUC_USER_STATES[self._state]) if self._state else ''
+        special = "" if len(self.special) == 0 else " %s" % self.special
+        self.setHTML("<div class='occupant'>%s%s%s</div>" % (html_sanitize(self.nick), special, state))
+
+
+class OccupantsList(AbsolutePanel):
+    """Panel user to show occupants of a room"""
+
+    def __init__(self):
+        AbsolutePanel.__init__(self)
+        self.occupants_list = {}
+        self.setStyleName('occupantsList')
+
+    def addOccupant(self, nick):
+        _occupant = Occupant(nick)
+        self.occupants_list[nick] = _occupant
+        self.add(_occupant)
+
+    def removeOccupant(self, nick):
+        try:
+            self.remove(self.occupants_list[nick])
+        except KeyError:
+            log.error("trying to remove an unexisting nick")
+
+    def clear(self):
+        self.occupants_list.clear()
+        AbsolutePanel.clear(self)
+
+    def updateSpecials(self, occupants=[], html=""):
+        """Set the specified html "symbol" to the listed occupants,
+        and eventually remove it from the others (if they got it).
+        This is used for example to visualize who is playing a game.
+        @param occupants: list of the occupants that need the symbol
+        @param html: unicode symbol (actually one character or more)
+        or a list to assign different symbols of the same family.
+        """
+        index = 0
+        special = html
+        for occupant in self.occupants_list.keys():
+            if occupant in occupants:
+                if isinstance(html, list):
+                    special = html[index]
+                    index = (index + 1) % len(html)
+                self.occupants_list[occupant].addSpecial(special)
+            else:
+                self.occupants_list[occupant].removeSpecials(html)
+
+
+class PopupMenuPanel(PopupPanel):
+    """This implementation of a popup menu (context menu) allow you to assign
+    two special methods which are common to all the items, in order to hide
+    certain items and also easily define their callbacks. The menu can be
+    bound to any of the mouse button (left, middle, right).
+    """
+    def __init__(self, entries, hide=None, callback=None, vertical=True, style=None, **kwargs):
+        """
+        @param entries: a dict of dicts, where each sub-dict is representing
+        one menu item: the sub-dict key can be used as the item text and
+        description, but optional "title" and "desc" entries would be used
+        if they exists. The sub-dicts may be extended later to do
+        more complicated stuff or overwrite the common methods.
+        @param hide: function  with 2 args: widget, key as string and
+        returns True if that item should be hidden from the context menu.
+        @param callback: function with 2 args: sender, key as string
+        @param vertical: True or False, to set the direction
+        @param item_style: alternative CSS class for the menu items
+        @param menu_style: supplementary CSS class for the sender widget
+        """
+        PopupPanel.__init__(self, autoHide=True, **kwargs)
+        self._entries = entries
+        self._hide = hide
+        self._callback = callback
+        self.vertical = vertical
+        self.style = {"selected": None, "menu": "recipientTypeMenu", "item": "popupMenuItem"}
+        if isinstance(style, dict):
+            self.style.update(style)
+        self._senders = {}
+
+    def _show(self, sender):
+        """Popup the menu relative to this sender's position.
+        @param sender: the widget that has been clicked
+        """
+        menu = VerticalPanel() if self.vertical is True else HorizontalPanel()
+        menu.setStyleName(self.style["menu"])
+
+        def button_cb(item):
+            """You can not put that method in the loop and rely
+            on _key, because it is overwritten by each step.
+            You can rely on item.key instead, which is copied
+            from _key after the item creation.
+            @param item: the menu item that has been clicked
+            """
+            if self._callback is not None:
+                self._callback(sender=sender, key=item.key)
+            self.hide(autoClosed=True)
+
+        for _key in self._entries.keys():
+            entry = self._entries[_key]
+            if self._hide is not None and self._hide(sender=sender, key=_key) is True:
+                continue
+            title = entry["title"] if "title" in entry.keys() else _key
+            item = Button(title, button_cb)
+            item.key = _key
+            item.setStyleName(self.style["item"])
+            item.setTitle(entry["desc"] if "desc" in entry.keys() else title)
+            menu.add(item)
+        if len(menu.getChildren()) == 0:
+            return
+        self.add(menu)
+        if self.vertical is True:
+            x = sender.getAbsoluteLeft() + sender.getOffsetWidth()
+            y = sender.getAbsoluteTop()
+        else:
+            x = sender.getAbsoluteLeft()
+            y = sender.getAbsoluteTop() + sender.getOffsetHeight()
+        self.setPopupPosition(x, y)
+        self.show()
+        if self.style["selected"]:
+            sender.addStyleDependentName(self.style["selected"])
+
+        def _onHide(popup):
+            if self.style["selected"]:
+                sender.removeStyleDependentName(self.style["selected"])
+            return PopupPanel.onHideImpl(self, popup)
+
+        self.onHideImpl = _onHide
+
+    def registerClickSender(self, sender, button=BUTTON_LEFT):
+        """Bind the menu to the specified sender.
+        @param sender: the widget to which the menu should be bound
+        @param: BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT
+        """
+        self._senders.setdefault(sender, [])
+        self._senders[sender].append(button)
+
+        if button == BUTTON_RIGHT:
+            # WARNING: to disable the context menu is a bit tricky...
+            # The following seems to work on Firefox 24.0, but:
+            # TODO: find a cleaner way to disable the context menu
+            sender.getElement().setAttribute("oncontextmenu", "return false")
+
+        def _onBrowserEvent(event):
+            button = DOM.eventGetButton(event)
+            if DOM.eventGetType(event) == "mousedown" and button in self._senders[sender]:
+                self._show(sender)
+            return sender.__class__.onBrowserEvent(sender, event)
+
+        sender.onBrowserEvent = _onBrowserEvent
+
+    def registerMiddleClickSender(self, sender):
+        self.registerClickSender(sender, BUTTON_MIDDLE)
+
+    def registerRightClickSender(self, sender):
+        self.registerClickSender(sender, BUTTON_RIGHT)
+
+
+class ToggleStackPanel(StackPanel):
+    """This is a pyjamas.ui.StackPanel with modified behavior. All sub-panels ca be
+    visible at the same time, clicking a sub-panel header will not display it and hide
+    the others but only toggle its own visibility. The argument 'visibleStack' is ignored.
+    Note that the argument 'visible' has been added to listener's 'onStackChanged' method.
+    """
+
+    def __init__(self, **kwargs):
+        StackPanel.__init__(self, **kwargs)
+
+    def onBrowserEvent(self, event):
+        if DOM.eventGetType(event) == "click":
+            index = self.getDividerIndex(DOM.eventGetTarget(event))
+            if index != -1:
+                self.toggleStack(index)
+
+    def add(self, widget, stackText="", asHTML=False, visible=False):
+        StackPanel.add(self, widget, stackText, asHTML)
+        self.setStackVisible(self.getWidgetCount() - 1, visible)
+
+    def toggleStack(self, index):
+        if index >= self.getWidgetCount():
+            return
+        visible = not self.getWidget(index).getVisible()
+        self.setStackVisible(index, visible)
+        for listener in self.stackListeners:
+            listener.onStackChanged(self, index, visible)
+
+
+class TitlePanel(ToggleStackPanel):
+    """A toggle panel to set the message title"""
+    def __init__(self):
+        ToggleStackPanel.__init__(self, Width="100%")
+        self.text_area = TextArea()
+        self.add(self.text_area, _("Title"))
+        self.addStackChangeListener(self)
+
+    def onStackChanged(self, sender, index, visible=None):
+        if visible is None:
+            visible = sender.getWidget(index).getVisible()
+        text = self.text_area.getText()
+        suffix = "" if (visible or not text) else (": %s" % text)
+        sender.setStackText(index, _("Title") + suffix)
+
+    def getText(self):
+        return self.text_area.getText()
+
+    def setText(self, text):
+        self.text_area.setText(text)
+
+
+class BaseTextEditor(object):
+    """Basic definition of a text editor. The method edit gets a boolean parameter which
+    should be set to True when you want to edit the text and False to only display it."""
+
+    def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None):
+        """
+        Remark when inheriting this class: since the setContent method could be
+        overwritten by the child class, you should consider calling this __init__
+        after all the parameters affecting this setContent method have been set.
+        @param content: dict with at least a 'text' key
+        @param strproc: method to be applied on strings to clean the content
+        @param modifiedCb: method to be called when the text has been modified.
+        If this method returns:
+        - True: the modification will be saved and afterEditCb called;
+        - False: the modification won't be saved and afterEditCb called;
+        - None: the modification won't be saved and afterEditCb not called.
+        @param afterEditCb: method to be called when the edition is done
+        """
+        if content is None:
+            content = {'text': ''}
+        assert('text' in content)
+        if strproc is None:
+            def strproc(text):
+                try:
+                    return text.strip()
+                except (TypeError, AttributeError):
+                    return text
+        self.strproc = strproc
+        self.__modifiedCb = modifiedCb
+        self._afterEditCb = afterEditCb
+        self.initialized = False
+        self.edit_listeners = []
+        self.setContent(content)
+
+    def setContent(self, content=None):
+        """Set the editable content. The displayed content, which is set from the child class, could differ.
+        @param content: dict with at least a 'text' key
+        """
+        if content is None:
+            content = {'text': ''}
+        elif not isinstance(content, dict):
+            content = {'text': content}
+        assert('text' in content)
+        self._original_content = {}
+        for key in content:
+            self._original_content[key] = self.strproc(content[key])
+
+    def getContent(self):
+        """Get the current edited or editable content.
+        @return: dict with at least a 'text' key
+        """
+        raise NotImplementedError
+
+    def setOriginalContent(self, content):
+        """Use this method with care! Content initialization should normally be
+        done with self.setContent. This method exists to let you trick the editor,
+        e.g. for self.modified to return True also when nothing has been modified.
+        @param content: dict
+        """
+        self._original_content = content
+
+    def getOriginalContent(self):
+        """
+        @return the original content before modification (dict)
+        """
+        return self._original_content
+
+    def modified(self, content=None):
+        """Check if the content has been modified.
+        Remark: we don't use the direct comparison because we want to ignore empty elements
+        @content: content to be check against the original content or None to use the current content
+        @return: True if the content has been modified.
+        """
+        if content is None:
+            content = self.getContent()
+        # the following method returns True if one non empty element exists in a but not in b
+        diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != []
+        # the following method returns True if the values for the common keys are not equals
+        diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != []
+        # finally the combination of both to return True if a difference is found
+        diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b)
+
+        return diff(content, self._original_content)
+
+    def edit(self, edit, abort=False, sync=False):
+        """
+        Remark: the editor must be visible before you call this method.
+        @param edit: set to True to edit the content or False to only display it
+        @param abort: set to True to cancel the edition and loose the changes.
+        If edit and abort are both True, self.abortEdition can be used to ask for a
+        confirmation. When edit is False and abort is True, abortion is actually done.
+        @param sync: set to True to cancel the edition after the content has been saved somewhere else
+        """
+        if edit:
+            if not self.initialized:
+                self.syncToEditor()  # e.g.: use the selected target and unibox content
+            self.setFocus(True)
+            if abort:
+                content = self.getContent()
+                if not self.modified(content) or self.abortEdition(content):  # e.g: ask for confirmation
+                    self.edit(False, True, sync)
+                    return
+            if sync:
+                self.syncFromEditor(content)  # e.g.: save the content to unibox
+                return
+        else:
+            if not self.initialized:
+                return
+            content = self.getContent()
+            if abort:
+                self._afterEditCb(content)
+                return
+            if self.__modifiedCb and self.modified(content):
+                result = self.__modifiedCb(content)  # e.g.: send a message or update something
+                if result is not None:
+                    if self._afterEditCb:
+                        self._afterEditCb(content)  # e.g.: restore the display mode
+                    if result is True:
+                        self.setContent(content)
+            elif self._afterEditCb:
+                self._afterEditCb(content)
+
+        self.initialized = True
+
+    def setFocus(self, focus):
+        """
+        @param focus: set to True to focus the editor
+        """
+        raise NotImplementedError
+
+    def syncToEditor(self):
+        pass
+
+    def syncFromEditor(self, content):
+        pass
+
+    def abortEdition(self, content):
+        return True
+
+    def addEditListener(self, listener):
+        """Add a method to be called whenever the text is edited.
+        @param listener: method taking two arguments: sender, keycode"""
+        self.edit_listeners.append(listener)
+
+
+class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler):
+    """Base class for manage a simple text editor."""
+
+    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
+        """
+        @param content
+        @param modifiedCb
+        @param afterEditCb
+        @param options: dict with the following value:
+        - no_xhtml: set to True to clean any xhtml content.
+        - enhance_display: if True, the display text will be enhanced with addURLToText
+        - listen_keyboard: set to True to terminate the edition with <enter> or <escape>.
+        - listen_focus: set to True to terminate the edition when the focus is lost.
+        - listen_click: set to True to start the edition when you click on the widget.
+        """
+        self.options = {'no_xhtml': False,
+                        'enhance_display': True,
+                        'listen_keyboard': True,
+                        'listen_focus': False,
+                        'listen_click': False
+                        }
+        if options:
+            self.options.update(options)
+        self.__shift_down = False
+        if self.options['listen_focus']:
+            FocusHandler.__init__(self)
+        if self.options['listen_click']:
+            ClickHandler.__init__(self)
+        KeyboardHandler.__init__(self)
+        strproc = lambda text: html_sanitize(html_strip(text)) if self.options['no_xhtml'] else html_strip(text)
+        BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb)
+        self.textarea = self.display = None
+
+    def setContent(self, content=None):
+        BaseTextEditor.setContent(self, content)
+
+    def getContent(self):
+        raise NotImplementedError
+
+    def edit(self, edit, abort=False, sync=False):
+        BaseTextEditor.edit(self, edit)
+        if edit:
+            if self.options['listen_focus'] and self not in self.textarea._focusListeners:
+                self.textarea.addFocusListener(self)
+            if self.options['listen_click']:
+                self.display.clearClickListener()
+            if self not in self.textarea._keyboardListeners:
+                self.textarea.addKeyboardListener(self)
+        else:
+            self.setDisplayContent()
+            if self.options['listen_focus']:
+                try:
+                    self.textarea.removeFocusListener(self)
+                except ValueError:
+                    pass
+            if self.options['listen_click'] and self not in self.display._clickListeners:
+                self.display.addClickListener(self)
+            try:
+                self.textarea.removeKeyboardListener(self)
+            except ValueError:
+                pass
+
+    def setDisplayContent(self):
+        text = self._original_content['text']
+        if not self.options['no_xhtml']:
+            text = addURLToImage(text)
+        if self.options['enhance_display']:
+            text = addURLToText(text)
+        self.display.setHTML(convertNewLinesToXHTML(text))
+
+    def setFocus(self, focus):
+        raise NotImplementedError
+
+    def onKeyDown(self, sender, keycode, modifiers):
+        for listener in self.edit_listeners:
+            listener(self.textarea, keycode)
+        if not self.options['listen_keyboard']:
+            return
+        if keycode == KEY_SHIFT or self.__shift_down:  # allow input a new line with <shift> + <enter>
+            self.__shift_down = True
+            return
+        if keycode == KEY_ENTER:  # finish the edition
+            self.textarea.setFocus(False)
+            if not self.options['listen_focus']:
+                self.edit(False)
+
+    def onKeyUp(self, sender, keycode, modifiers):
+        if keycode == KEY_SHIFT:
+            self.__shift_down = False
+
+    def onLostFocus(self, sender):
+        """Finish the edition when focus is lost"""
+        if self.options['listen_focus']:
+            self.edit(False)
+
+    def onClick(self, sender=None):
+        """Start the edition when the widget is clicked"""
+        if self.options['listen_click']:
+            self.edit(True)
+
+    def onBrowserEvent(self, event):
+        if self.options['listen_focus']:
+            FocusHandler.onBrowserEvent(self, event)
+        if self.options['listen_click']:
+            ClickHandler.onBrowserEvent(self, event)
+        KeyboardHandler.onBrowserEvent(self, event)
+
+
+class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler):
+    """Manage a simple text editor with the HTML 5 "contenteditable" property."""
+
+    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
+        HTML.__init__(self)
+        SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
+        self.textarea = self.display = self
+
+    def getContent(self):
+        text = DOM.getInnerHTML(self.getElement())
+        return {'text': self.strproc(text) if text else ''}
+
+    def edit(self, edit, abort=False, sync=False):
+        if edit:
+            self.textarea.setHTML(self._original_content['text'])
+        self.getElement().setAttribute('contenteditable', 'true' if edit else 'false')
+        SimpleTextEditor.edit(self, edit, abort, sync)
+
+    def setFocus(self, focus):
+        if focus:
+            self.getElement().focus()
+        else:
+            self.getElement().blur()
+
+
+class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler):
+    """Manage a simple text editor with a TextArea for editing, HTML for display."""
+
+    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
+        SimplePanel.__init__(self)
+        SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
+        self.textarea = TextArea()
+        self.display = HTML()
+
+    def getContent(self):
+        text = self.textarea.getText()
+        return {'text': self.strproc(text) if text else ''}
+
+    def edit(self, edit, abort=False, sync=False):
+        if edit:
+            self.textarea.setText(self._original_content['text'])
+        self.setWidget(self.textarea if edit else self.display)
+        SimpleTextEditor.edit(self, edit, abort, sync)
+
+    def setFocus(self, focus):
+        if focus:
+            self.textarea.setCursorPos(len(self.textarea.getText()))
+        self.textarea.setFocus(focus)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/base_widget.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,730 @@
+#!/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 pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.ScrollPanel import ScrollPanel
+from pyjamas.ui.FlexTable import FlexTable
+from pyjamas.ui.TabPanel import TabPanel
+from pyjamas.ui.HTMLPanel import HTMLPanel
+from pyjamas.ui.Label import Label
+from pyjamas.ui.Button import Button
+from pyjamas.ui.Image import Image
+from pyjamas.ui.Widget import Widget
+from pyjamas.ui.DragWidget import DragWidget
+from pyjamas.ui.DropWidget import DropWidget
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui import HasAlignment
+from pyjamas import DOM
+from pyjamas import Window
+from __pyjamas__ import doc
+
+import dialog
+
+
+class DragLabel(DragWidget):
+
+    def __init__(self, text, _type):
+        DragWidget.__init__(self)
+        self._text = text
+        self._type = _type
+
+    def onDragStart(self, event):
+        dt = event.dataTransfer
+        dt.setData('text/plain', "%s\n%s" % (self._text, self._type))
+        dt.setDragImage(self.getElement(), 15, 15)
+
+
+class LiberviaDragWidget(DragLabel):
+    """ A DragLabel which keep the widget being dragged as class value """
+    current = None  # widget currently dragged
+
+    def __init__(self, text, _type, widget):
+        DragLabel.__init__(self, text, _type)
+        self.widget = widget
+
+    def onDragStart(self, event):
+        LiberviaDragWidget.current = self.widget
+        DragLabel.onDragStart(self, event)
+
+    def onDragEnd(self, event):
+        LiberviaDragWidget.current = None
+
+
+class DropCell(DropWidget):
+    """Cell in the middle grid which replace itself with the dropped widget on DnD"""
+    drop_keys = {}
+
+    def __init__(self, host):
+        DropWidget.__init__(self)
+        self.host = host
+        self.setStyleName('dropCell')
+
+    @classmethod
+    def addDropKey(cls, key, callback):
+        DropCell.drop_keys[key] = callback
+
+    def onDragEnter(self, event):
+        if self == LiberviaDragWidget.current:
+            return
+        self.addStyleName('dragover')
+        DOM.eventPreventDefault(event)
+
+    def onDragLeave(self, event):
+        if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\
+            event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1:
+            # We check that we are inside widget's box, and we don't remove the style in this case because
+            # if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we
+            # don't want that
+            self.removeStyleName('dragover')
+
+    def onDragOver(self, event):
+        DOM.eventPreventDefault(event)
+
+    def _getCellAndRow(self, grid, event):
+        """Return cell and row index where the event is occuring"""
+        cell = grid.getEventTargetCell(event)
+        row = DOM.getParent(cell)
+        return (row.rowIndex, cell.cellIndex)
+
+    def onDrop(self, event):
+        self.removeStyleName('dragover')
+        DOM.eventPreventDefault(event)
+        dt = event.dataTransfer
+        # 'text', 'text/plain', and 'Text' are equivalent.
+        try:
+            item, item_type = dt.getData("text/plain").split('\n')  # Workaround for webkit, only text/plain seems to be managed
+            if item_type and item_type[-1] == '\0':  # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
+                item_type = item_type[:-1]           # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
+            # item_type = dt.getData("type")
+            log.debug("message: %s" % item)
+            log.debug("type: %s" % item_type)
+        except:
+            log.debug("no message found")
+            item = '&nbsp;'
+            item_type = None
+        if item_type == "WIDGET":
+            if not LiberviaDragWidget.current:
+                log.error("No widget registered in LiberviaDragWidget !")
+                return
+            _new_panel = LiberviaDragWidget.current
+            if self == _new_panel:  # We can't drop on ourself
+                return
+            # we need to remove the widget from the panel as it will be inserted elsewhere
+            widgets_panel = _new_panel.getWidgetsPanel()
+            wid_row = widgets_panel.getWidgetCoords(_new_panel)[0]
+            row_wids = widgets_panel.getLiberviaRowWidgets(wid_row)
+            if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]:
+                # the dropped widget is the only one in the same row
+                # as the target widget (self), we don't do anything
+                return
+            widgets_panel.removeWidget(_new_panel)
+        elif item_type in self.drop_keys:
+            _new_panel = self.drop_keys[item_type](self.host, item)
+        else:
+            log.warning("unmanaged item type")
+            return
+        if isinstance(self, LiberviaWidget):
+            self.host.unregisterWidget(self)
+            self.onQuit()
+            if not isinstance(_new_panel, LiberviaWidget):
+                log.warning("droping an object which is not a class of LiberviaWidget")
+        _flextable = self.getParent()
+        _widgetspanel = _flextable.getParent().getParent()
+        row_idx, cell_idx = self._getCellAndRow(_flextable, event)
+        if self.host.getSelected == self:
+            self.host.setSelected(None)
+        _widgetspanel.changeWidget(row_idx, cell_idx, _new_panel)
+        """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable))
+        _width = 90/float(len(_unempty_panels) or 1)
+        #now we resize all the cell of the column
+        for panel in _unempty_panels:
+            td_elt = panel.getElement().parentNode
+            DOM.setStyleAttribute(td_elt, "width", "%s%%" % _width)"""
+        #FIXME: delete object ? Check the right way with pyjamas
+
+
+class WidgetHeader(AbsolutePanel, LiberviaDragWidget):
+
+    def __init__(self, parent, title):
+        AbsolutePanel.__init__(self)
+        self.add(title)
+        button_group_wrapper = SimplePanel()
+        button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper')
+        button_group = HorizontalPanel()
+        button_group.setStyleName('widgetHeader_buttonGroup')
+        setting_button = Image("media/icons/misc/settings.png")
+        setting_button.setStyleName('widgetHeader_settingButton')
+        setting_button.addClickListener(parent.onSetting)
+        close_button = Image("media/icons/misc/close.png")
+        close_button.setStyleName('widgetHeader_closeButton')
+        close_button.addClickListener(parent.onClose)
+        button_group.add(setting_button)
+        button_group.add(close_button)
+        button_group_wrapper.setWidget(button_group)
+        self.add(button_group_wrapper)
+        self.addStyleName('widgetHeader')
+        LiberviaDragWidget.__init__(self, "", "WIDGET", parent)
+
+
+class LiberviaWidget(DropCell, VerticalPanel, ClickHandler):
+    """Libervia's widget which can replace itself with a dropped widget on DnD"""
+
+    def __init__(self, host, title='', selectable=False):
+        """Init the widget
+        @param host: SatWebFrontend object
+        @param title: title show in the header of the widget
+        @param selectable: True is widget can be selected by user"""
+        VerticalPanel.__init__(self)
+        DropCell.__init__(self, host)
+        ClickHandler.__init__(self)
+        self.__selectable = selectable
+        self.__title_id = HTMLPanel.createUniqueId()
+        self.__setting_button_id = HTMLPanel.createUniqueId()
+        self.__close_button_id = HTMLPanel.createUniqueId()
+        self.__title = Label(title)
+        self.__title.setStyleName('widgetHeader_title')
+        self._close_listeners = []
+        header = WidgetHeader(self, self.__title)
+        self.add(header)
+        self.setSize('100%', '100%')
+        self.addStyleName('widget')
+        if self.__selectable:
+            self.addClickListener(self)
+
+            def onClose(sender):
+                """Check dynamically if the unibox is enable or not"""
+                if self.host.uni_box:
+                    self.host.uni_box.onWidgetClosed(sender)
+
+            self.addCloseListener(onClose)
+        self.host.registerWidget(self)
+
+    def getDebugName(self):
+        return "%s (%s)" % (self, self.__title.getText())
+
+    def getWidgetsPanel(self, verbose=True):
+        return self.getParent(WidgetsPanel, verbose)
+
+    def getParent(self, class_=None, verbose=True):
+        """
+        Note: this method overrides pyjamas.ui.Widget.getParent
+        @param class_: class of the ancestor to look for or None to return the first parent
+        @param verbose: set to True to log error messages # FIXME: must be removed
+        @return: the parent/ancestor or None if it has not been found
+        """
+        current = Widget.getParent(self)
+        if class_ is None:
+            return current  # this is the default behavior
+        while current is not None and not isinstance(current, class_):
+            current = Widget.getParent(current)
+        if current is None and verbose:
+            log.debug("Can't find parent %s for %s" % (class_, self))
+        return current
+
+    def onClick(self, sender):
+        self.host.setSelected(self)
+
+    def onClose(self, sender):
+        """ Called when the close button is pushed """
+        _widgetspanel = self.getWidgetsPanel()
+        _widgetspanel.removeWidget(self)
+        for callback in self._close_listeners:
+            callback(self)
+        self.onQuit()
+
+    def onQuit(self):
+        """ Called when the widget is actually ending """
+        pass
+
+    def addCloseListener(self, callback):
+        """Add a close listener to this widget
+        @param callback: function to be called from self.onClose"""
+        self._close_listeners.append(callback)
+
+    def refresh(self):
+        """This can be overwritten by a child class to refresh the display when,
+        instead of creating a new one, an existing widget is found and reused.
+        """
+        pass
+
+    def onSetting(self, sender):
+        widpanel = self.getWidgetsPanel()
+        row, col = widpanel.getIndex(self)
+        body = VerticalPanel()
+
+        # colspan & rowspan
+        colspan = widpanel.getColSpan(row, col)
+        rowspan = widpanel.getRowSpan(row, col)
+
+        def onColSpanChange(value):
+            widpanel.setColSpan(row, col, value)
+
+        def onRowSpanChange(value):
+            widpanel.setRowSpan(row, col, value)
+        colspan_setter = dialog.IntSetter("Columns span", colspan)
+        colspan_setter.addValueChangeListener(onColSpanChange)
+        colspan_setter.setWidth('100%')
+        rowspan_setter = dialog.IntSetter("Rows span", rowspan)
+        rowspan_setter.addValueChangeListener(onRowSpanChange)
+        rowspan_setter.setWidth('100%')
+        body.add(colspan_setter)
+        body.add(rowspan_setter)
+
+        # size
+        width_str = self.getWidth()
+        if width_str.endswith('px'):
+            width = int(width_str[:-2])
+        else:
+            width = 0
+        height_str = self.getHeight()
+        if height_str.endswith('px'):
+            height = int(height_str[:-2])
+        else:
+            height = 0
+
+        def onWidthChange(value):
+            if not value:
+                self.setWidth('100%')
+            else:
+                self.setWidth('%dpx' % value)
+
+        def onHeightChange(value):
+            if not value:
+                self.setHeight('100%')
+            else:
+                self.setHeight('%dpx' % value)
+        width_setter = dialog.IntSetter("width (0=auto)", width)
+        width_setter.addValueChangeListener(onWidthChange)
+        width_setter.setWidth('100%')
+        height_setter = dialog.IntSetter("height (0=auto)", height)
+        height_setter.addValueChangeListener(onHeightChange)
+        height_setter.setHeight('100%')
+        body.add(width_setter)
+        body.add(height_setter)
+
+        # reset
+        def onReset(sender):
+            colspan_setter.setValue(1)
+            rowspan_setter.setValue(1)
+            width_setter.setValue(0)
+            height_setter.setValue(0)
+
+        reset_bt = Button("Reset", onReset)
+        body.add(reset_bt)
+        body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER)
+
+        _dialog = dialog.GenericDialog("Widget setting", body)
+        _dialog.show()
+
+    def setTitle(self, text):
+        """change the title in the header of the widget
+        @param text: text of the new title"""
+        self.__title.setText(text)
+
+    def isSelectable(self):
+        return self.__selectable
+
+    def setSelectable(self, selectable):
+        if not self.__selectable:
+            try:
+                self.removeClickListener(self)
+            except ValueError:
+                pass
+        if self.selectable and not self in self._clickListeners:
+            self.addClickListener(self)
+        self.__selectable = selectable
+
+    def getWarningData(self):
+        """ Return exposition warning level when this widget is selected and something is sent to it
+        This method should be overriden by children
+        @return: tuple (warning level type/HTML msg). Type can be one of:
+            - PUBLIC
+            - GROUP
+            - ONE2ONE
+            - MISC
+            - NONE
+        """
+        if not self.__selectable:
+            log.error("getWarningLevel must not be called for an unselectable widget")
+            raise Exception
+        # TODO: cleaner warning types (more general constants)
+        return ("NONE", None)
+
+    def setWidget(self, widget, scrollable=True):
+        """Set the widget that will be in the body of the LiberviaWidget
+        @param widget: widget to put in the body
+        @param scrollable: if true, the widget will be in a ScrollPanelWrapper"""
+        if scrollable:
+            _scrollpanelwrapper = ScrollPanelWrapper()
+            _scrollpanelwrapper.setStyleName('widgetBody')
+            _scrollpanelwrapper.setWidget(widget)
+            body_wid = _scrollpanelwrapper
+        else:
+            body_wid = widget
+        self.add(body_wid)
+        self.setCellHeight(body_wid, '100%')
+
+    def doDetachChildren(self):
+        # We need to force the use of a panel subclass method here,
+        # for the same reason as doAttachChildren
+        VerticalPanel.doDetachChildren(self)
+
+    def doAttachChildren(self):
+        # We need to force the use of a panel subclass method here, else
+        # the event will not propagate to children
+        VerticalPanel.doAttachChildren(self)
+
+    def matchEntity(self, entity):
+        """This method should be overwritten by child classes."""
+        raise NotImplementedError
+
+
+class ScrollPanelWrapper(SimplePanel):
+    """Scroll Panel like component, wich use the full available space
+    to work around percent size issue, it use some of the ideas found
+    here: http://code.google.com/p/google-web-toolkit/issues/detail?id=316
+    specially in code given at comment #46, thanks to Stefan Bachert"""
+
+    def __init__(self, *args, **kwargs):
+        SimplePanel.__init__(self)
+        self.spanel = ScrollPanel(*args, **kwargs)
+        SimplePanel.setWidget(self, self.spanel)
+        DOM.setStyleAttribute(self.getElement(), "position", "relative")
+        DOM.setStyleAttribute(self.getElement(), "top", "0px")
+        DOM.setStyleAttribute(self.getElement(), "left", "0px")
+        DOM.setStyleAttribute(self.getElement(), "width", "100%")
+        DOM.setStyleAttribute(self.getElement(), "height", "100%")
+        DOM.setStyleAttribute(self.spanel.getElement(), "position", "absolute")
+        DOM.setStyleAttribute(self.spanel.getElement(), "width", "100%")
+        DOM.setStyleAttribute(self.spanel.getElement(), "height", "100%")
+
+    def setWidget(self, widget):
+        self.spanel.setWidget(widget)
+
+    def setScrollPosition(self, position):
+        self.spanel.setScrollPosition(position)
+
+    def scrollToBottom(self):
+        self.setScrollPosition(self.spanel.getElement().scrollHeight)
+
+
+class EmptyWidget(DropCell, SimplePanel):
+    """Empty dropable panel"""
+
+    def __init__(self, host):
+        SimplePanel.__init__(self)
+        DropCell.__init__(self, host)
+        #self.setWidget(HTML(''))
+        self.setSize('100%', '100%')
+
+
+class BorderWidget(EmptyWidget):
+    def __init__(self, host):
+        EmptyWidget.__init__(self, host)
+        self.addStyleName('borderPanel')
+
+
+class LeftBorderWidget(BorderWidget):
+    def __init__(self, host):
+        BorderWidget.__init__(self, host)
+        self.addStyleName('leftBorderWidget')
+
+
+class RightBorderWidget(BorderWidget):
+    def __init__(self, host):
+        BorderWidget.__init__(self, host)
+        self.addStyleName('rightBorderWidget')
+
+
+class BottomBorderWidget(BorderWidget):
+    def __init__(self, host):
+        BorderWidget.__init__(self, host)
+        self.addStyleName('bottomBorderWidget')
+
+
+class WidgetsPanel(ScrollPanelWrapper):
+
+    def __init__(self, host, locked=False):
+        ScrollPanelWrapper.__init__(self)
+        self.setSize('100%', '100%')
+        self.host = host
+        self.locked = locked  # if True: tab will not be removed when there are no more widgets inside
+        self.selected = None
+        self.flextable = FlexTable()
+        self.flextable.setSize('100%', '100%')
+        self.setWidget(self.flextable)
+        self.setStyleName('widgetsPanel')
+        _bottom = BottomBorderWidget(self.host)
+        self.flextable.setWidget(0, 0, _bottom)  # There will be always an Empty widget on the last row,
+                                                 # dropping a widget there will add a new row
+        td_elt = _bottom.getElement().parentNode
+        DOM.setStyleAttribute(td_elt, "height", "1px")  # needed so the cell adapt to the size of the border (specially in webkit)
+        self._max_cols = 1  # give the maximum number of columns i a raw
+
+    def isLocked(self):
+        return self.locked
+
+    def changeWidget(self, row, col, wid):
+        """Change the widget in the given location, add row or columns when necessary"""
+        log.debug("changing widget: %s %s %s" % (wid.getDebugName(), row, col))
+        last_row = max(0, self.flextable.getRowCount() - 1)
+        try:
+            prev_wid = self.flextable.getWidget(row, col)
+        except:
+            log.error("Trying to change an unexisting widget !")
+            return
+
+        cellFormatter = self.flextable.getFlexCellFormatter()
+
+        if isinstance(prev_wid, BorderWidget):
+            # We are on a border, we must create a row and/or columns
+            log.debug("BORDER WIDGET")
+            prev_wid.removeStyleName('dragover')
+
+            if isinstance(prev_wid, BottomBorderWidget):
+                # We are on the bottom border, we create a new row
+                self.flextable.insertRow(last_row)
+                self.flextable.setWidget(last_row, 0, LeftBorderWidget(self.host))
+                self.flextable.setWidget(last_row, 1, wid)
+                self.flextable.setWidget(last_row, 2, RightBorderWidget(self.host))
+                cellFormatter.setHorizontalAlignment(last_row, 2, HasAlignment.ALIGN_RIGHT)
+                row = last_row
+
+            elif isinstance(prev_wid, LeftBorderWidget):
+                if col != 0:
+                    log.error("LeftBorderWidget must be on the first column !")
+                    return
+                self.flextable.insertCell(row, col + 1)
+                self.flextable.setWidget(row, 1, wid)
+
+            elif isinstance(prev_wid, RightBorderWidget):
+                if col != self.flextable.getCellCount(row) - 1:
+                    log.error("RightBorderWidget must be on the last column !")
+                    return
+                self.flextable.insertCell(row, col)
+                self.flextable.setWidget(row, col, wid)
+
+        else:
+            prev_wid.removeFromParent()
+            self.flextable.setWidget(row, col, wid)
+
+        _max_cols = max(self._max_cols, self.flextable.getCellCount(row))
+        if _max_cols != self._max_cols:
+            self._max_cols = _max_cols
+            self._sizesAdjust()
+
+    def _sizesAdjust(self):
+        cellFormatter = self.flextable.getFlexCellFormatter()
+        width = 100.0 / max(1, self._max_cols - 2)  # we don't count the borders
+
+        for row_idx in xrange(self.flextable.getRowCount()):
+            for col_idx in xrange(self.flextable.getCellCount(row_idx)):
+                _widget = self.flextable.getWidget(row_idx, col_idx)
+                if not isinstance(_widget, BorderWidget):
+                    td_elt = _widget.getElement().parentNode
+                    DOM.setStyleAttribute(td_elt, "width", "%.2f%%" % width)
+
+        last_row = max(0, self.flextable.getRowCount() - 1)
+        cellFormatter.setColSpan(last_row, 0, self._max_cols)
+
+    def addWidget(self, wid):
+        """Add a widget to a new cell on the next to last row"""
+        last_row = max(0, self.flextable.getRowCount() - 1)
+        log.debug("putting widget %s at %d, %d" % (wid.getDebugName(), last_row, 0))
+        self.changeWidget(last_row, 0, wid)
+
+    def removeWidget(self, wid):
+        """Remove a widget and the cell where it is"""
+        _row, _col = self.flextable.getIndex(wid)
+        self.flextable.remove(wid)
+        self.flextable.removeCell(_row, _col)
+        if not self.getLiberviaRowWidgets(_row):  # we have no more widgets, we remove the row
+            self.flextable.removeRow(_row)
+        _max_cols = 1
+        for row_idx in xrange(self.flextable.getRowCount()):
+            _max_cols = max(_max_cols, self.flextable.getCellCount(row_idx))
+        if _max_cols != self._max_cols:
+            self._max_cols = _max_cols
+            self._sizesAdjust()
+        current = self
+
+        blank_page = self.getLiberviaWidgetsCount() == 0  # do we still have widgets on the page ?
+
+        if blank_page and not self.isLocked():
+            # we now notice the MainTabPanel that the WidgetsPanel is empty and need to be removed
+            while current is not None:
+                if isinstance(current, MainTabPanel):
+                    current.onWidgetPanelRemove(self)
+                    return
+                current = current.getParent()
+            log.error("no MainTabPanel found !")
+
+    def getWidgetCoords(self, wid):
+        return self.flextable.getIndex(wid)
+
+    def getLiberviaRowWidgets(self, row):
+        """ Return all the LiberviaWidget in the row """
+        return [wid for wid in self.getRowWidgets(row) if isinstance(wid, LiberviaWidget)]
+
+    def getRowWidgets(self, row):
+        """ Return all the widgets in the row """
+        widgets = []
+        cols = self.flextable.getCellCount(row)
+        for col in xrange(cols):
+            widgets.append(self.flextable.getWidget(row, col))
+        return widgets
+
+    def getLiberviaWidgetsCount(self):
+        """ Get count of contained widgets """
+        return len([wid for wid in self.flextable if isinstance(wid, LiberviaWidget)])
+
+    def getIndex(self, wid):
+        return self.flextable.getIndex(wid)
+
+    def getColSpan(self, row, col):
+        cellFormatter = self.flextable.getFlexCellFormatter()
+        return cellFormatter.getColSpan(row, col)
+
+    def setColSpan(self, row, col, value):
+        cellFormatter = self.flextable.getFlexCellFormatter()
+        return cellFormatter.setColSpan(row, col, value)
+
+    def getRowSpan(self, row, col):
+        cellFormatter = self.flextable.getFlexCellFormatter()
+        return cellFormatter.getRowSpan(row, col)
+
+    def setRowSpan(self, row, col, value):
+        cellFormatter = self.flextable.getFlexCellFormatter()
+        return cellFormatter.setRowSpan(row, col, value)
+
+
+class DropTab(Label, DropWidget):
+
+    def __init__(self, tab_panel, text):
+        Label.__init__(self, text)
+        DropWidget.__init__(self, tab_panel)
+        self.tab_panel = tab_panel
+        self.setStyleName('dropCell')
+        self.setWordWrap(False)
+        DOM.setStyleAttribute(self.getElement(), "min-width", "30px")
+
+    def _getIndex(self):
+        """ get current index of the DropTab """
+        # XXX: awful hack, but seems the only way to get index
+        return self.tab_panel.tabBar.panel.getWidgetIndex(self.getParent().getParent()) - 1
+
+    def onDragEnter(self, event):
+        #if self == LiberviaDragWidget.current:
+        #    return
+        self.addStyleName('dragover')
+        DOM.eventPreventDefault(event)
+
+    def onDragLeave(self, event):
+        self.removeStyleName('dragover')
+
+    def onDragOver(self, event):
+        DOM.eventPreventDefault(event)
+
+    def onDrop(self, event):
+        DOM.eventPreventDefault(event)
+        self.removeStyleName('dragover')
+        if self._getIndex() == self.tab_panel.tabBar.getSelectedTab():
+            # the widget come from the DragTab, so nothing to do, we let it there
+            return
+
+        # FIXME: quite the same stuff as in DropCell, need some factorisation
+        dt = event.dataTransfer
+        # 'text', 'text/plain', and 'Text' are equivalent.
+        try:
+            item, item_type = dt.getData("text/plain").split('\n')  # Workaround for webkit, only text/plain seems to be managed
+            if item_type and item_type[-1] == '\0':  # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
+                item_type = item_type[:-1]           # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
+            # item_type = dt.getData("type")
+            log.debug("message: %s" % item)
+            log.debug("type: %s" % item_type)
+        except:
+            log.debug("no message found")
+            item = '&nbsp;'
+            item_type = None
+        if item_type == "WIDGET":
+            if not LiberviaDragWidget.current:
+                log.error("No widget registered in LiberviaDragWidget !")
+                return
+            _new_panel = LiberviaDragWidget.current
+            _new_panel.getWidgetsPanel().removeWidget(_new_panel)
+        elif item_type in DropCell.drop_keys:
+            _new_panel = DropCell.drop_keys[item_type](self.tab_panel.host, item)
+        else:
+            log.warning("unmanaged item type")
+            return
+
+        widgets_panel = self.tab_panel.getWidget(self._getIndex())
+        widgets_panel.addWidget(_new_panel)
+
+
+class MainTabPanel(TabPanel):
+
+    def __init__(self, host):
+        TabPanel.__init__(self)
+        self.host = host
+        self.tabBar.setVisible(False)
+        self.setStyleName('liberviaTabPanel')
+        self.addStyleName('mainTabPanel')
+        Window.addWindowResizeListener(self)
+
+    def getCurrentPanel(self):
+        """ Get the panel of the currently selected tab """
+        return self.deck.visibleWidget
+
+    def onWindowResized(self, width, height):
+        tab_panel_elt = self.getElement()
+        _elts = doc().getElementsByClassName('gwt-TabBar')
+        if not _elts.length:
+            log.error("no TabBar found, it should exist !")
+            tab_bar_h = 0
+        else:
+            tab_bar_h = _elts.item(0).offsetHeight
+        ideal_height = height - DOM.getAbsoluteTop(tab_panel_elt) - tab_bar_h - 5
+        ideal_width = width - DOM.getAbsoluteLeft(tab_panel_elt) - 5
+        self.setWidth("%s%s" % (ideal_width, "px"))
+        self.setHeight("%s%s" % (ideal_height, "px"))
+
+    def add(self, widget, text=''):
+        tab = DropTab(self, text)
+        TabPanel.add(self, widget, tab, False)
+        if self.getWidgetCount() > 1:
+            self.tabBar.setVisible(True)
+            self.host.resize()
+
+    def onWidgetPanelRemove(self, panel):
+        """ Called when a child WidgetsPanel is empty and need to be removed """
+        self.remove(panel)
+        widgets_count = self.getWidgetCount()
+        if widgets_count == 1:
+            self.tabBar.setVisible(False)
+            self.host.resize()
+            self.selectTab(0)
+        else:
+            self.selectTab(widgets_count - 1)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/card_game.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,387 @@
+#!/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.tools.games import TarotCard
+from sat.core.i18n import _
+
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.DockPanel import DockPanel
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.Image import Image
+from pyjamas.ui.Label import Label
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui.MouseListener import MouseHandler
+from pyjamas.ui import HasAlignment
+from pyjamas import Window
+from pyjamas import DOM
+
+from dialog import ConfirmDialog, GenericDialog
+from xmlui import XMLUI
+
+
+CARD_WIDTH = 74
+CARD_HEIGHT = 136
+CARD_DELTA_Y = 30
+MIN_WIDTH = 950  # Minimum size of the panel
+MIN_HEIGHT = 500
+
+
+class CardWidget(TarotCard, Image, MouseHandler):
+    """This class is used to represent a card, graphically and logically"""
+
+    def __init__(self, parent, file_):
+        """@param file: path of the PNG file"""
+        self._parent = parent
+        Image.__init__(self, file_)
+        root_name = file_[file_.rfind("/") + 1:-4]
+        suit, value = root_name.split('_')
+        TarotCard.__init__(self, (suit, value))
+        MouseHandler.__init__(self)
+        self.addMouseListener(self)
+
+    def onMouseEnter(self, sender):
+        if self._parent.state == "ecart" or self._parent.state == "play":
+            DOM.setStyleAttribute(self.getElement(), "top", "0px")
+
+    def onMouseLeave(self, sender):
+        if not self in self._parent.hand:
+            return
+        if not self in list(self._parent.selected):  # FIXME: Workaround pyjs bug, must report it
+            DOM.setStyleAttribute(self.getElement(), "top", "%dpx" % CARD_DELTA_Y)
+
+    def onMouseUp(self, sender, x, y):
+        if self._parent.state == "ecart":
+            if self not in list(self._parent.selected):
+                self._parent.addToSelection(self)
+            else:
+                self._parent.removeFromSelection(self)
+        elif self._parent.state == "play":
+            self._parent.playCard(self)
+
+
+class CardPanel(DockPanel, ClickHandler):
+
+    def __init__(self, parent, referee, player_nick, players):
+        DockPanel.__init__(self)
+        ClickHandler.__init__(self)
+        self._parent = parent
+        self._autoplay = None  # XXX: use 0 to activate fake play, None else
+        self.referee = referee
+        self.players = players
+        self.player_nick = player_nick
+        self.bottom_nick = self.player_nick
+        idx = self.players.index(self.player_nick)
+        idx = (idx + 1) % len(self.players)
+        self.right_nick = self.players[idx]
+        idx = (idx + 1) % len(self.players)
+        self.top_nick = self.players[idx]
+        idx = (idx + 1) % len(self.players)
+        self.left_nick = self.players[idx]
+        self.bottom_nick = player_nick
+        self.selected = set()  # Card choosed by the player (e.g. during ecart)
+        self.hand_size = 13  # number of cards in a hand
+        self.hand = []
+        self.to_show = []
+        self.state = None
+        self.setSize("%dpx" % MIN_WIDTH, "%dpx" % MIN_HEIGHT)
+        self.setStyleName("cardPanel")
+
+        # Now we set up the layout
+        _label = Label(self.top_nick)
+        _label.setStyleName('cardGamePlayerNick')
+        self.add(_label, DockPanel.NORTH)
+        self.setCellWidth(_label, '100%')
+        self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_CENTER)
+
+        self.hand_panel = AbsolutePanel()
+        self.add(self.hand_panel, DockPanel.SOUTH)
+        self.setCellWidth(self.hand_panel, '100%')
+        self.setCellHorizontalAlignment(self.hand_panel, HasAlignment.ALIGN_CENTER)
+
+        _label = Label(self.left_nick)
+        _label.setStyleName('cardGamePlayerNick')
+        self.add(_label, DockPanel.WEST)
+        self.setCellHeight(_label, '100%')
+        self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE)
+
+        _label = Label(self.right_nick)
+        _label.setStyleName('cardGamePlayerNick')
+        self.add(_label, DockPanel.EAST)
+        self.setCellHeight(_label, '100%')
+        self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_RIGHT)
+        self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE)
+
+        self.center_panel = DockPanel()
+        self.inner_left = SimplePanel()
+        self.inner_left.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
+        self.center_panel.add(self.inner_left, DockPanel.WEST)
+        self.center_panel.setCellHeight(self.inner_left, '100%')
+        self.center_panel.setCellHorizontalAlignment(self.inner_left, HasAlignment.ALIGN_RIGHT)
+        self.center_panel.setCellVerticalAlignment(self.inner_left, HasAlignment.ALIGN_MIDDLE)
+
+        self.inner_right = SimplePanel()
+        self.inner_right.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
+        self.center_panel.add(self.inner_right, DockPanel.EAST)
+        self.center_panel.setCellHeight(self.inner_right, '100%')
+        self.center_panel.setCellVerticalAlignment(self.inner_right, HasAlignment.ALIGN_MIDDLE)
+
+        self.inner_top = SimplePanel()
+        self.inner_top.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
+        self.center_panel.add(self.inner_top, DockPanel.NORTH)
+        self.center_panel.setCellHorizontalAlignment(self.inner_top, HasAlignment.ALIGN_CENTER)
+        self.center_panel.setCellVerticalAlignment(self.inner_top, HasAlignment.ALIGN_BOTTOM)
+
+        self.inner_bottom = SimplePanel()
+        self.inner_bottom.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
+        self.center_panel.add(self.inner_bottom, DockPanel.SOUTH)
+        self.center_panel.setCellHorizontalAlignment(self.inner_bottom, HasAlignment.ALIGN_CENTER)
+        self.center_panel.setCellVerticalAlignment(self.inner_bottom, HasAlignment.ALIGN_TOP)
+
+        self.inner_center = SimplePanel()
+        self.center_panel.add(self.inner_center, DockPanel.CENTER)
+        self.center_panel.setCellHorizontalAlignment(self.inner_center, HasAlignment.ALIGN_CENTER)
+        self.center_panel.setCellVerticalAlignment(self.inner_center, HasAlignment.ALIGN_MIDDLE)
+
+        self.add(self.center_panel, DockPanel.CENTER)
+        self.setCellWidth(self.center_panel, '100%')
+        self.setCellHeight(self.center_panel, '100%')
+        self.setCellVerticalAlignment(self.center_panel, HasAlignment.ALIGN_MIDDLE)
+        self.setCellHorizontalAlignment(self.center_panel, HasAlignment.ALIGN_CENTER)
+
+        self.loadCards()
+        self.mouse_over_card = None  # contain the card to highlight
+        self.visible_size = CARD_WIDTH / 2  # number of pixels visible for cards
+        self.addClickListener(self)
+
+    def loadCards(self):
+        """Load all the cards in memory"""
+        def _getTarotCardsPathsCb(paths):
+            log.debug("_getTarotCardsPathsCb")
+            for file_ in paths:
+                log.debug("path:", file_)
+                card = CardWidget(self, file_)
+                log.debug("card:", card)
+                self.cards[(card.suit, card.value)] = card
+                self.deck.append(card)
+            self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee)
+        self.cards = {}
+        self.deck = []
+        self.cards["atout"] = {}  # As Tarot is a french game, it's more handy & logical to keep french names
+        self.cards["pique"] = {}  # spade
+        self.cards["coeur"] = {}  # heart
+        self.cards["carreau"] = {}  # diamond
+        self.cards["trefle"] = {}  # club
+        self._parent.host.bridge.call('getTarotCardsPaths', _getTarotCardsPathsCb)
+
+    def onClick(self, sender):
+        if self.state == "chien":
+            self.to_show = []
+            self.state = "wait"
+            self.updateToShow()
+        elif self.state == "wait_for_ecart":
+            self.state = "ecart"
+            self.hand.extend(self.to_show)
+            self.hand.sort()
+            self.to_show = []
+            self.updateToShow()
+            self.updateHand()
+
+    def tarotGameNew(self, hand):
+        """Start a new game, with given hand"""
+        if hand is []:  # reset the display after the scores have been showed
+            self.selected.clear()
+            del self.hand[:]
+            del self.to_show[:]
+            self.state = None
+            #empty hand
+            self.updateHand()
+            #nothing on the table
+            self.updateToShow()
+            for pos in ['top', 'left', 'bottom', 'right']:
+                getattr(self, "inner_%s" % pos).setWidget(None)
+            self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee)
+            return
+        for suit, value in hand:
+            self.hand.append(self.cards[(suit, value)])
+        self.hand.sort()
+        self.state = "init"
+        self.updateHand()
+
+    def updateHand(self):
+        """Show the cards in the hand in the hand_panel (SOUTH panel)"""
+        self.hand_panel.clear()
+        self.hand_panel.setSize("%dpx" % (self.visible_size * (len(self.hand) + 1)), "%dpx" % (CARD_HEIGHT + CARD_DELTA_Y + 10))
+        x_pos = 0
+        y_pos = CARD_DELTA_Y
+        for card in self.hand:
+            self.hand_panel.add(card, x_pos, y_pos)
+            x_pos += self.visible_size
+
+    def updateToShow(self):
+        """Show cards in the center panel"""
+        if not self.to_show:
+            _widget = self.inner_center.getWidget()
+            if _widget:
+                self.inner_center.remove(_widget)
+            return
+        panel = AbsolutePanel()
+        panel.setSize("%dpx" % ((CARD_WIDTH + 5) * len(self.to_show) - 5), "%dpx" % (CARD_HEIGHT))
+        x_pos = 0
+        y_pos = 0
+        for card in self.to_show:
+            panel.add(card, x_pos, y_pos)
+            x_pos += CARD_WIDTH + 5
+        self.inner_center.setWidget(panel)
+
+    def _ecartConfirm(self, confirm):
+        if not confirm:
+            return
+        ecart = []
+        for card in self.selected:
+            ecart.append((card.suit, card.value))
+            self.hand.remove(card)
+        self.selected.clear()
+        self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, ecart)
+        self.state = "wait"
+        self.updateHand()
+
+    def addToSelection(self, card):
+        self.selected.add(card)
+        if len(self.selected) == 6:
+            ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show()
+
+    def tarotGameInvalidCards(self, phase, played_cards, invalid_cards):
+        """Invalid cards have been played
+        @param phase: phase of the game
+        @param played_cards: all the cards played
+        @param invalid_cards: cards which are invalid"""
+
+        if phase == "play":
+            self.state = "play"
+        elif phase == "ecart":
+            self.state = "ecart"
+        else:
+            log.error("INTERNAL ERROR: unmanaged game phase") # FIXME: raise an exception here
+
+        for suit, value in played_cards:
+            self.hand.append(self.cards[(suit, value)])
+
+        self.hand.sort()
+        self.updateHand()
+        if self._autoplay == None:  # No dialog if there is autoplay
+            Window.alert('Cards played are invalid !')
+        self.__fakePlay()
+
+    def removeFromSelection(self, card):
+        self.selected.remove(card)
+        if len(self.selected) == 6:
+            ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show()
+
+    def tarotGameChooseContrat(self, xml_data):
+        """Called when the player has to select his contrat
+        @param xml_data: SàT xml representation of the form"""
+        body = XMLUI(self._parent.host, xml_data, flags=['NO_CANCEL'])
+        _dialog = GenericDialog(_('Please choose your contrat'), body, options=['NO_CLOSE'])
+        body.setCloseCb(_dialog.close)
+        _dialog.show()
+
+    def tarotGameShowCards(self, game_stage, cards, data):
+        """Display cards in the middle of the game (to show for e.g. chien ou poignée)"""
+        self.to_show = []
+        for suit, value in cards:
+            self.to_show.append(self.cards[(suit, value)])
+        self.updateToShow()
+        if game_stage == "chien" and data['attaquant'] == self.player_nick:
+            self.state = "wait_for_ecart"
+        else:
+            self.state = "chien"
+
+    def getPlayerLocation(self, nick):
+        """return player location (top,bottom,left or right)"""
+        for location in ['top', 'left', 'bottom', 'right']:
+            if getattr(self, '%s_nick' % location) == nick:
+                return location
+        log.error("This line should not be reached")
+
+    def tarotGameCardsPlayed(self, player, cards):
+        """A card has been played by player"""
+        if not len(cards):
+            log.warning("cards should not be empty")
+            return
+        if len(cards) > 1:
+            log.error("can't manage several cards played")
+        if self.to_show:
+            self.to_show = []
+            self.updateToShow()
+        suit, value = cards[0]
+        player_pos = self.getPlayerLocation(player)
+        player_panel = getattr(self, "inner_%s" % player_pos)
+
+        if player_panel.getWidget() != None:
+            #We have already cards on the table, we remove them
+            for pos in ['top', 'left', 'bottom', 'right']:
+                getattr(self, "inner_%s" % pos).setWidget(None)
+
+        card = self.cards[(suit, value)]
+        DOM.setElemAttribute(card.getElement(), "style", "")
+        player_panel.setWidget(card)
+
+    def tarotGameYourTurn(self):
+        """Called when we have to play :)"""
+        if self.state == "chien":
+            self.to_show = []
+            self.updateToShow()
+        self.state = "play"
+        self.__fakePlay()
+
+    def __fakePlay(self):
+        """Convenience method for stupid autoplay
+        /!\ don't forgot to comment any interactive dialog for invalid card"""
+        if self._autoplay == None:
+            return
+        if self._autoplay >= len(self.hand):
+            self._autoplay = 0
+        card = self.hand[self._autoplay]
+        self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)])
+        del self.hand[self._autoplay]
+        self.state = "wait"
+        self._autoplay += 1
+
+    def playCard(self, card):
+        self.hand.remove(card)
+        self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)])
+        self.state = "wait"
+        self.updateHand()
+
+    def tarotGameScore(self, xml_data, winners, loosers):
+        """Show score at the end of a round"""
+        if not winners and not loosers:
+            title = "Draw game"
+        else:
+            if self.player_nick in winners:
+                title = "You <b>win</b> !"
+            else:
+                title = "You <b>loose</b> :("
+        body = XMLUI(self._parent.host, xml_data, title=title, flags=['NO_CANCEL'])
+        _dialog = GenericDialog(title, body, options=['NO_CLOSE'])
+        body.setCloseCb(_dialog.close)
+        _dialog.show()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/constants.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,24 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a SAT frontend
+# Copyright (C) 2009, 2010, 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/>.
+
+from libervia.common import constants
+
+
+class Const(constants.Const):
+    """Add here the constants that are only used by the browser side."""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/contact.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,415 @@
+#!/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 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 import Window
+from pyjamas import DOM
+from __pyjamas__ import doc
+
+from jid import JID
+
+from base_panels import PopupMenuPanel
+from base_widget import DragLabel
+from panels import ChatPanel, MicroblogPanel, WebPanel, UniBoxPanel
+from html_tools import html_sanitize
+
+
+def setPresenceStyle(element, presence, base_style="contact"):
+    """
+    Set the CSS style of a contact's element according to its presence.
+    @param item: the UI element of the contact
+    @param presence: a value in ("", "chat", "away", "dnd", "xa").
+    @param base_style: the base name of the style to apply
+    """
+    if not hasattr(element, 'presence_style'):
+        element.presence_style = None
+    style = '%s-%s' % (base_style, presence or 'connected')
+    if style == element.presence_style:
+        return
+    if element.presence_style is not None:
+        element.removeStyleName(element.presence_style)
+    element.addStyleName(style)
+    element.presence_style = style
+
+
+class GroupLabel(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')
+        DragLabel.__init__(self, group, "GROUP")
+        ClickHandler.__init__(self)
+        self.addClickListener(self)
+
+    def onClick(self, sender):
+        self.host.getOrCreateLiberviaWidget(MicroblogPanel, self.group)
+
+
+class ContactLabel(DragLabel, HTML, ClickHandler):
+    def __init__(self, host, jid, name=None, handleClick=True):
+        HTML.__init__(self)
+        self.host = host
+        self.name = name or jid
+        self.waiting = False
+        self.jid = jid
+        self._fill()
+        self.setStyleName('contact')
+        DragLabel.__init__(self, jid, "CONTACT")
+        if handleClick:
+            ClickHandler.__init__(self)
+            self.addClickListener(self)
+
+    def _fill(self):
+        if self.waiting:
+            _wait_html = "<b>(*)</b>&nbsp;"
+        self.setHTML("%(wait)s%(name)s" % {'wait': _wait_html,
+                                           'name': 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._fill()
+
+    def onClick(self, sender):
+        self.host.getOrCreateLiberviaWidget(ChatPanel, self.jid)
+
+
+class GroupList(VerticalPanel):
+
+    def __init__(self, parent):
+        VerticalPanel.__init__(self)
+        self.setStyleName('groupList')
+        self._parent = parent
+
+    def add(self, group):
+        _item = GroupLabel(self._parent.host, group)
+        _item.addMouseListener(self._parent)
+        DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer")
+        index = 0
+        for group_ in [group.group for group in self.getChildren()]:
+            if group_ > group:
+                break
+            index += 1
+        VerticalPanel.insert(self, _item, index)
+
+    def remove(self, group):
+        for wid in self:
+            if isinstance(wid, GroupLabel) and wid.group == group:
+                VerticalPanel.remove(self, wid)
+
+
+class GenericContactList(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, handleClick=False):
+        VerticalPanel.__init__(self)
+        self.host = host
+        self.contacts = []
+        self.handleClick = handleClick
+
+    def add(self, jid, name=None, item_cb=None):
+        if jid in self.contacts:
+            return
+        index = 0
+        for contact_ in self.contacts:
+            if contact_ > jid:
+                break
+            index += 1
+        self.contacts.insert(index, jid)
+        _item = ContactLabel(self.host, jid, name, handleClick=self.handleClick)
+        DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer")
+        VerticalPanel.insert(self, _item, index)
+        if item_cb is not None:
+            item_cb(_item)
+
+    def remove(self, jid):
+        wid = self.getContactLabel(jid)
+        if not wid:
+            return
+        VerticalPanel.remove(self, wid)
+        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 getContactLabel(self, contact_jid):
+        """get contactList widget of a contact
+        @return: ContactLabel item if present, else None"""
+        for wid in self:
+            if isinstance(wid, ContactLabel) and wid.jid == contact_jid:
+                return wid
+        return None
+
+
+class ContactList(GenericContactList):
+    """The contact list that is displayed on the left side."""
+
+    def __init__(self, host):
+        GenericContactList.__init__(self, host, handleClick=True)
+        self.menu_entries = {"blog": {"title": "Public blog..."}}
+        self.context_menu = PopupMenuPanel(entries=self.menu_entries,
+                                           hide=self.contextMenuHide,
+                                           callback=self.contextMenuCallback,
+                                           vertical=False, style={"selected": "menu-selected"})
+
+    def contextMenuHide(self, sender, key):
+        """Return True if the item for that sender should be hidden."""
+        # TODO: enable the blogs of users that are on another server
+        return JID(sender.jid).domain != self.host._defaultDomain
+
+    def contextMenuCallback(self, sender, key):
+        if key == "blog":
+            # TODO: use the bare when all blogs can be retrieved
+            node = JID(sender.jid).node
+            web_panel = WebPanel(self.host, "/blog/%s" % node)
+            self.host.addTab("%s's blog" % node, web_panel)
+        else:
+            sender.onClick(sender)
+
+    def add(self, jid, name=None):
+        def item_cb(item):
+            self.context_menu.registerRightClickSender(item)
+        GenericContactList.add(self, jid, name, item_cb)
+
+    def setState(self, jid, type_, state):
+        """Change the appearance of the contact, according to the state
+        @param jid: jid which need to change state
+        @param type_: one of availability, messageWaiting
+        @param state:
+            - for messageWaiting type:
+                True if message are waiting
+            - for availability type:
+                'unavailable' if not connected, else presence like RFC6121 #4.7.2.1"""
+        _item = self.getContactLabel(jid)
+        if _item:
+            if type_ == 'availability':
+                setPresenceStyle(_item, state)
+            elif type_ == 'messageWaiting':
+                _item.setMessageWaiting(state)
+
+
+class ContactTitleLabel(DragLabel, Label, ClickHandler):
+    def __init__(self, host, text):
+        Label.__init__(self, text) #, Element=DOM.createElement('div')
+        self.host = host
+        self.setStyleName('contactTitle')
+        DragLabel.__init__(self, text, "CONTACT_TITLE")
+        ClickHandler.__init__(self)
+        self.addClickListener(self)
+
+    def onClick(self, sender):
+        self.host.getOrCreateLiberviaWidget(MicroblogPanel, None)
+
+
+class ContactPanel(SimplePanel):
+    """Manage the contacts and groups"""
+
+    def __init__(self, host):
+        SimplePanel.__init__(self)
+
+        self.scroll_panel = ScrollPanel()
+
+        self.host = host
+        self.groups = {}
+        self.connected = {}  # jid connected as key and their status
+
+        self.vPanel = VerticalPanel()
+        _title = ContactTitleLabel(host, 'Contacts')
+        DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer")
+
+        self._contact_list = ContactList(host)
+        self._contact_list.setStyleName('contactList')
+        self._groupList = GroupList(self)
+        self._groupList.setStyleName('groupList')
+
+        self.vPanel.add(_title)
+        self.vPanel.add(self._groupList)
+        self.vPanel.add(self._contact_list)
+        self.scroll_panel.add(self.vPanel)
+        self.add(self.scroll_panel)
+        self.setStyleName('contactBox')
+        Window.addWindowResizeListener(self)
+
+    def onWindowResized(self, width, height):
+        contact_panel_elt = self.getElement()
+        classname = 'widgetsPanel' if isinstance(self.getParent().getParent(), UniBoxPanel) else'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, attributes, groups):
+        """Add a contact to the panel if it doesn't exist, update it else
+        @param jid: jid userhost as unicode
+        @attributes: cf SàT Bridge API's newContact
+        @param groups: list of groups"""
+        _current_groups = self.getContactGroups(jid)
+        _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)
+            if not self.groups[group]:
+                # The group is now empty, we must remove it
+                del self.groups[group]
+                self._groupList.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 not group in self.groups.keys():
+                self.groups[group] = set()
+                self._groupList.add(group)
+                if self.host.uni_box:
+                    self.host.uni_box.addKey(_key % group)
+            self.groups[group].add(jid)
+
+        # We add the contact to contact list, it will check if contact already exists
+        self._contact_list.add(jid)
+
+    def removeContact(self, jid):
+        """Remove contacts from groups where he is and contact list"""
+        self.updateContact(jid, {}, [])  # we remove contact from every group
+        self._contact_list.remove(jid)
+
+    def setConnected(self, jid, resource, availability, priority, statuses):
+        """Set connection status
+        @param jid: JID userhost as unicode
+        """
+        if availability == 'unavailable':
+            if jid in self.connected:
+                if resource in self.connected[jid]:
+                    del self.connected[jid][resource]
+                if not self.connected[jid]:
+                    del self.connected[jid]
+        else:
+            if not jid in self.connected:
+                self.connected[jid] = {}
+            self.connected[jid][resource] = (availability, priority, statuses)
+
+        # check if the contact is connected with another resource, use the one with highest priority
+        if jid in self.connected:
+            max_resource = max_priority = None
+            for tmp_resource in self.connected[jid]:
+                if max_priority is None or self.connected[jid][tmp_resource][1] >= max_priority:
+                    max_resource = tmp_resource
+                    max_priority = self.connected[jid][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][max_resource][0]
+        if jid 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._contact_list.setState(jid, "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()
+
+    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._contact_list.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):
+        """Get groups where contact is
+       @param group: string of single group, or list of string
+       @param contact_jid: jid to test
+        """
+        result = set()
+        for group in self.groups:
+            if self.isContactInGroup(group, contact_jid):
+                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_label in self._contact_list:
+            if contact_jid == _contact_label.jid:
+                return True
+        return False
+
+    def getContacts(self):
+        return self._contact_list.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):
+            for contact in self._contact_list:
+                if contact.jid in self.groups[sender.group]:
+                    contact.addStyleName("selected")
+
+    def onMouseLeave(self, sender):
+        if isinstance(sender, GroupLabel):
+            for contact in self._contact_list:
+                if contact.jid in self.groups[sender.group]:
+                    contact.removeStyleName("selected")
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/contact_group.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,237 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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/>.
+
+from pyjamas.ui.FlexTable import FlexTable
+from pyjamas.ui.DockPanel import DockPanel
+from pyjamas.Timer import Timer
+from pyjamas.ui.Button import Button
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.DialogBox import DialogBox
+from pyjamas.ui import HasAlignment
+
+from dialog import ConfirmDialog, InfoDialog
+from list_manager import ListManager
+import dialog
+import contact
+
+
+class ContactGroupManager(ListManager):
+    """A manager for sub-panels to assign contacts to each group."""
+
+    def __init__(self, parent, keys_dict, contact_list, offsets, style):
+        ListManager.__init__(self, parent, keys_dict, contact_list, offsets, style)
+        self.registerPopupMenuPanel(entries={"Remove group": {}},
+                                    callback=lambda sender, key: Timer(5, lambda timer: self.removeContactKey(sender, key)))
+
+    def removeContactKey(self, sender, key):
+        key = sender.getText()
+
+        def confirm_cb(answer):
+            if answer:
+                ListManager.removeContactKey(self, key)
+                self._parent.removeKeyFromAddGroupPanel(key)
+
+        _dialog = ConfirmDialog(confirm_cb, text="Do you really want to delete the group '%s'?" % key)
+        _dialog.show()
+
+    def removeFromRemainingList(self, contacts):
+        ListManager.removeFromRemainingList(self, contacts)
+        self._parent.updateContactList(contacts=contacts)
+
+    def addToRemainingList(self, contacts, ignore_key=None):
+        ListManager.addToRemainingList(self, contacts, ignore_key)
+        self._parent.updateContactList(contacts=contacts)
+
+
+class ContactGroupEditor(DockPanel):
+    """Panel for the contact groups manager."""
+
+    def __init__(self, host, parent=None, onCloseCallback=None):
+        DockPanel.__init__(self)
+        self.host = host
+
+        # eventually display in a popup
+        if parent is None:
+            parent = DialogBox(autoHide=False, centered=True)
+            parent.setHTML("Manage contact groups")
+        self._parent = parent
+        self._on_close_callback = onCloseCallback
+        self.all_contacts = self.host.contact_panel.getContacts()
+
+        groups_list = self.host.contact_panel.groups.keys()
+        groups_list.sort()
+
+        self.add_group_panel = self.getAddGroupPanel(groups_list)
+        south_panel = self.getCloseSaveButtons()
+        center_panel = self.getContactGroupManager(groups_list)
+        east_panel = self.getContactList()
+
+        self.add(self.add_group_panel, DockPanel.CENTER)
+        self.add(east_panel, DockPanel.EAST)
+        self.add(center_panel, DockPanel.NORTH)
+        self.add(south_panel, DockPanel.SOUTH)
+
+        self.setCellHorizontalAlignment(center_panel, HasAlignment.ALIGN_LEFT)
+        self.setCellVerticalAlignment(center_panel, HasAlignment.ALIGN_TOP)
+        self.setCellHorizontalAlignment(east_panel, HasAlignment.ALIGN_RIGHT)
+        self.setCellVerticalAlignment(east_panel, HasAlignment.ALIGN_TOP)
+        self.setCellVerticalAlignment(self.add_group_panel, HasAlignment.ALIGN_BOTTOM)
+        self.setCellHorizontalAlignment(self.add_group_panel, HasAlignment.ALIGN_LEFT)
+        self.setCellVerticalAlignment(south_panel, HasAlignment.ALIGN_BOTTOM)
+        self.setCellHorizontalAlignment(south_panel, HasAlignment.ALIGN_CENTER)
+
+        # need to be done after the contact list has been initialized
+        self.groups.setContacts(self.host.contact_panel.groups)
+        self.toggleContacts(showAll=True)
+
+        # Hide the contacts list from the main panel to not confuse the user
+        self.restore_contact_panel = False
+        if self.host.contact_panel.getVisible():
+            self.restore_contact_panel = True
+            self.host.panel._contactsSwitch()
+
+        parent.add(self)
+        parent.setVisible(True)
+        if isinstance(parent, DialogBox):
+            parent.center()
+
+    def getContactGroupManager(self, groups_list):
+        """Set the list manager for the groups"""
+        flex_table = FlexTable(len(groups_list), 2)
+        flex_table.addStyleName('contactGroupEditor')
+        # overwrite the default style which has been set for rich text editor
+        style = {
+           "keyItem": "group",
+           "popupMenuItem": "popupMenuItem",
+           "removeButton": "contactGroupRemoveButton",
+           "buttonCell": "contactGroupButtonCell",
+           "keyPanel": "contactGroupPanel"
+        }
+        self.groups = ContactGroupManager(flex_table, groups_list, self.all_contacts, style=style)
+        self.groups.createWidgets()  # widgets are automatically added to FlexTable
+        # FIXME: clean that part which is dangerous
+        flex_table.updateContactList = self.updateContactList
+        flex_table.removeKeyFromAddGroupPanel = self.add_group_panel.groups.remove
+        return flex_table
+
+    def getAddGroupPanel(self, groups_list):
+        """Add the 'Add group' panel to the FlexTable"""
+
+        def add_group_cb(text):
+            self.groups.addContactKey(text)
+            self.add_group_panel.textbox.setFocus(True)
+
+        add_group_panel = dialog.AddGroupPanel(groups_list, add_group_cb)
+        add_group_panel.addStyleName("addContactGroupPanel")
+        return add_group_panel
+
+    def getCloseSaveButtons(self):
+        """Add the buttons to close the dialog / save the groups"""
+        buttons = HorizontalPanel()
+        buttons.addStyleName("marginAuto")
+        buttons.add(Button("Save", listener=self.closeAndSave))
+        buttons.add(Button("Cancel", listener=self.cancelWithoutSaving))
+        return buttons
+
+    def getContactList(self):
+        """Add the contact list to the DockPanel"""
+        self.toggle = Button("", self.toggleContacts)
+        self.toggle.addStyleName("toggleAssignedContacts")
+        self.contacts = contact.GenericContactList(self.host)
+        for contact_ in self.all_contacts:
+            self.contacts.add(contact_)
+        contact_panel = VerticalPanel()
+        contact_panel.add(self.toggle)
+        contact_panel.add(self.contacts)
+        return contact_panel
+
+    def toggleContacts(self, sender=None, showAll=None):
+        """Callback for the toggle button"""
+        if sender is None:
+            sender = self.toggle
+        sender.showAll = showAll if showAll is not None else not sender.showAll
+        if sender.showAll:
+            sender.setText("Hide assigned")
+        else:
+            sender.setText("Show assigned")
+        self.updateContactList(sender)
+
+    def updateContactList(self, sender=None, contacts=None):
+        """Update the contact list regarding the toggle button"""
+        if not hasattr(self, "toggle") or not hasattr(self.toggle, "showAll"):
+            return
+        sender = self.toggle
+        if contacts is not None:
+            if not isinstance(contacts, list):
+                contacts = [contacts]
+            for contact_ in contacts:
+                if contact_ not in self.all_contacts:
+                    contacts.remove(contact_)
+        else:
+            contacts = self.all_contacts
+        for contact_ in contacts:
+            if sender.showAll:
+                self.contacts.getContactLabel(contact_).setVisible(True)
+            else:
+                if contact_ in self.groups.remaining_list:
+                    self.contacts.getContactLabel(contact_).setVisible(True)
+                else:
+                    self.contacts.getContactLabel(contact_).setVisible(False)
+
+    def __close(self):
+        """Remove the widget from parent or close the popup."""
+        if isinstance(self._parent, DialogBox):
+            self._parent.hide()
+        self._parent.remove(self)
+        if self._on_close_callback is not None:
+            self._on_close_callback()
+        if self.restore_contact_panel:
+            self.host.panel._contactsSwitch()
+
+    def cancelWithoutSaving(self):
+        """Ask for confirmation before closing the dialog."""
+        def confirm_cb(answer):
+            if answer:
+                self.__close()
+
+        _dialog = ConfirmDialog(confirm_cb, text="Do you really want to cancel without saving?")
+        _dialog.show()
+
+    def closeAndSave(self):
+        """Call bridge methods to save the changes and close the dialog"""
+        map_ = {}
+        for contact_ in self.all_contacts:
+            map_[contact_] = set()
+        contacts = self.groups.getContacts()
+        for group in contacts.keys():
+            for contact_ in contacts[group]:
+                try:
+                    map_[contact_].add(group)
+                except KeyError:
+                    InfoDialog("Invalid contact",
+                           "The contact '%s' is not your contact list but it has been assigned to the group '%s'." % (contact_, group) +
+                           "Your changes could not be saved: please check your assignments and save again.", Width="400px").center()
+                    return
+        for contact_ in map_.keys():
+            groups = map_[contact_]
+            current_groups = self.host.contact_panel.getContactGroups(contact_)
+            if groups != current_groups:
+                self.host.bridge.call('updateContact', None, contact_, '', list(groups))
+        self.__close()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/dialog.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,547 @@
+#!/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/>.
+
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat_frontends.tools.misc import DEFAULT_MUC
+
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.Grid import Grid
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.PopupPanel import PopupPanel
+from pyjamas.ui.DialogBox import DialogBox
+from pyjamas.ui.ListBox import ListBox
+from pyjamas.ui.Button import Button
+from pyjamas.ui.TextBox import TextBox
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.RadioButton import RadioButton
+from pyjamas.ui import HasAlignment
+from pyjamas.ui.KeyboardListener import KEY_ESCAPE, KEY_ENTER
+from pyjamas.ui.MouseListener import MouseWheelHandler
+from pyjamas import Window
+
+from base_panels import ToggleStackPanel
+
+
+# List here the patterns that are not allowed in contact group names
+FORBIDDEN_PATTERNS_IN_GROUP = ()
+
+
+class RoomChooser(Grid):
+    """Select a room from the rooms you already joined, or create a new one"""
+
+    GENERATE_MUC = "<use random name>"
+
+    def __init__(self, host, default_room=DEFAULT_MUC):
+        Grid.__init__(self, 2, 2, Width='100%')
+        self.host = host
+
+        self.new_radio = RadioButton("room", "Discussion room:")
+        self.new_radio.setChecked(True)
+        self.box = TextBox(Width='95%')
+        self.box.setText(self.GENERATE_MUC if default_room == "" else default_room)
+        self.exist_radio = RadioButton("room", "Already joined:")
+        self.rooms_list = ListBox(Width='95%')
+
+        self.add(self.new_radio, 0, 0)
+        self.add(self.box, 0, 1)
+        self.add(self.exist_radio, 1, 0)
+        self.add(self.rooms_list, 1, 1)
+
+        self.box.addFocusListener(self)
+        self.rooms_list.addFocusListener(self)
+
+        self.exist_radio.setVisible(False)
+        self.rooms_list.setVisible(False)
+        self.setRooms()
+
+    def onFocus(self, sender):
+        if sender == self.rooms_list:
+            self.exist_radio.setChecked(True)
+        elif sender == self.box:
+            if self.box.getText() == self.GENERATE_MUC:
+                self.box.setText("")
+            self.new_radio.setChecked(True)
+
+    def onLostFocus(self, sender):
+        if sender == self.box:
+            if self.box.getText() == "":
+                self.box.setText(self.GENERATE_MUC)
+
+    def setRooms(self):
+        for room in self.host.room_list:
+            self.rooms_list.addItem(room.bare)
+        if len(self.host.room_list) > 0:
+            self.exist_radio.setVisible(True)
+            self.rooms_list.setVisible(True)
+            self.exist_radio.setChecked(True)
+
+    def getRoom(self):
+        if self.exist_radio.getChecked():
+            values = self.rooms_list.getSelectedValues()
+            return "" if values == [] else values[0]
+        value = self.box.getText()
+        return "" if value == self.GENERATE_MUC else value
+
+
+class ContactsChooser(VerticalPanel):
+    """Select one or several connected contacts"""
+
+    def __init__(self, host, nb_contact=None, ok_button=None):
+        """
+        @param host: SatWebFrontend instance
+        @param nb_contact: number of contacts that have to be selected, None for no limit
+        If a tuple is given instead of an integer, nb_contact[0] is the minimal and
+        nb_contact[1] is the maximal number of contacts to be chosen.
+        """
+        self.host = host
+        if isinstance(nb_contact, tuple):
+            if len(nb_contact) == 0:
+                nb_contact = None
+            elif len(nb_contact) == 1:
+                nb_contact = (nb_contact[0], nb_contact[0])
+        elif nb_contact is not None:
+            nb_contact = (nb_contact, nb_contact)
+        if nb_contact is None:
+            log.warning("Need to select as many contacts as you want")
+        else:
+            log.warning("Need to select between %d and %d contacts" % nb_contact)
+        self.nb_contact = nb_contact
+        self.ok_button = ok_button
+        VerticalPanel.__init__(self, Width='100%')
+        self.contacts_list = ListBox()
+        self.contacts_list.setMultipleSelect(True)
+        self.contacts_list.setWidth("95%")
+        self.contacts_list.addStyleName('contactsChooser')
+        self.contacts_list.addChangeListener(self.onChange)
+        self.add(self.contacts_list)
+        self.setContacts()
+        self.onChange()
+
+    def onChange(self, sender=None):
+        if self.ok_button is None:
+            return
+        if self.nb_contact:
+            selected = len(self.contacts_list.getSelectedValues(True))
+            if  selected >= self.nb_contact[0] and selected <= self.nb_contact[1]:
+                self.ok_button.setEnabled(True)
+            else:
+                self.ok_button.setEnabled(False)
+
+    def setContacts(self, selected=[]):
+        """Fill the list with the connected contacts
+        @param select: list of the contacts to select by default
+        """
+        self.contacts_list.clear()
+        contacts = self.host.contact_panel.getConnected(filter_muc=True)
+        self.contacts_list.setVisibleItemCount(10 if len(contacts) > 5 else 5)
+        self.contacts_list.addItem("")
+        for contact in contacts:
+            if contact not in [room.bare for room in self.host.room_list]:
+                self.contacts_list.addItem(contact)
+        self.contacts_list.setItemTextSelection(selected)
+
+    def getContacts(self):
+        return self.contacts_list.getSelectedValues(True)
+
+
+class RoomAndContactsChooser(DialogBox):
+    """Select a room and some users to invite in"""
+
+    def __init__(self, host, callback, nb_contact=None, ok_button="OK", title="Discussion groups",
+                 title_room="Join room", title_invite="Invite contacts", visible=(True, True)):
+        DialogBox.__init__(self, centered=True)
+        self.host = host
+        self.callback = callback
+        self.title_room = title_room
+        self.title_invite = title_invite
+
+        button_panel = HorizontalPanel()
+        button_panel.addStyleName("marginAuto")
+        ok_button = Button("OK", self.onOK)
+        button_panel.add(ok_button)
+        button_panel.add(Button("Cancel", self.onCancel))
+
+        self.room_panel = RoomChooser(host, "" if visible == (False, True) else DEFAULT_MUC)
+        self.contact_panel = ContactsChooser(host, nb_contact, ok_button)
+
+        self.stack_panel = ToggleStackPanel(Width="100%")
+        self.stack_panel.add(self.room_panel, visible=visible[0])
+        self.stack_panel.add(self.contact_panel, visible=visible[1])
+        self.stack_panel.addStackChangeListener(self)
+        self.onStackChanged(self.stack_panel, 0, visible[0])
+        self.onStackChanged(self.stack_panel, 1, visible[1])
+
+        main_panel = VerticalPanel()
+        main_panel.setStyleName("room-contact-chooser")
+        main_panel.add(self.stack_panel)
+        main_panel.add(button_panel)
+
+        self.setWidget(main_panel)
+        self.setHTML(title)
+        self.show()
+
+        # needed to update the contacts list when someone logged in/out
+        self.host.room_contacts_chooser = self
+
+    def getRoom(self, asSuffix=False):
+        room = self.room_panel.getRoom()
+        if asSuffix:
+            return room if room == "" else ": %s" % room
+        else:
+            return room
+
+    def getContacts(self, asSuffix=False):
+        contacts = self.contact_panel.getContacts()
+        if asSuffix:
+            return "" if contacts == [] else ": %s" % ", ".join(contacts)
+        else:
+            return contacts
+
+    def onStackChanged(self, sender, index, visible=None):
+        if visible is None:
+            visible = sender.getWidget(index).getVisible()
+        if index == 0:
+            sender.setStackText(0, self.title_room + ("" if visible else self.getRoom(True)))
+        elif index == 1:
+            sender.setStackText(1, self.title_invite + ("" if visible else self.getContacts(True)))
+
+    def resetContacts(self):
+        """Called when someone log in/out to update the list"""
+        self.contact_panel.setContacts(self.getContacts())
+
+    def onOK(self, sender):
+        room_jid = self.getRoom()
+        if room_jid != "" and "@" not in room_jid:
+            Window.alert('You must enter a room jid in the form room@chat.%s' % self.host._defaultDomain)
+            return
+        self.hide()
+        self.callback(room_jid, self.getContacts())
+
+    def onCancel(self, sender):
+        self.hide()
+
+    def hide(self):
+        self.host.room_contacts_chooser = None
+        DialogBox.hide(self, autoClosed=True)
+
+
+class GenericConfirmDialog(DialogBox):
+
+    def __init__(self, widgets, callback, title='Confirmation', prompt=None, **kwargs):
+        """
+        Dialog to confirm an action
+        @param widgets: widgets to attach
+        @param callback: method to call when a button is clicked
+        @param title: title of the dialog
+        @param prompt: textbox from which to retrieve the string value to be passed to the callback when
+        OK button is pressed. If None, OK button will return "True". Cancel button always returns "False".
+        """
+        self.callback = callback
+        DialogBox.__init__(self, centered=True, **kwargs)
+
+        content = VerticalPanel()
+        content.setWidth('100%')
+        for wid in widgets:
+            content.add(wid)
+            if wid == prompt:
+                wid.setWidth('100%')
+        button_panel = HorizontalPanel()
+        button_panel.addStyleName("marginAuto")
+        self.confirm_button = Button("OK", self.onConfirm)
+        button_panel.add(self.confirm_button)
+        self.cancel_button = Button("Cancel", self.onCancel)
+        button_panel.add(self.cancel_button)
+        content.add(button_panel)
+        self.setHTML(title)
+        self.setWidget(content)
+        self.prompt = prompt
+
+    def onConfirm(self, sender):
+        self.hide()
+        self.callback(self.prompt.getText() if self.prompt else True)
+
+    def onCancel(self, sender):
+        self.hide()
+        self.callback(False)
+
+    def show(self):
+        DialogBox.show(self)
+        if self.prompt:
+            self.prompt.setFocus(True)
+
+
+class ConfirmDialog(GenericConfirmDialog):
+
+    def __init__(self, callback, text='Are you sure ?', title='Confirmation', **kwargs):
+        GenericConfirmDialog.__init__(self, [HTML(text)], callback, title, **kwargs)
+
+
+class GenericDialog(DialogBox):
+    """Dialog which just show a widget and a close button"""
+
+    def __init__(self, title, main_widget, callback=None, options=None, **kwargs):
+        """Simple notice dialog box
+        @param title: HTML put in the header
+        @param main_widget: widget put in the body
+        @param callback: method to call on closing
+        @param options: one or more of the following options:
+                        - NO_CLOSE: don't add a close button"""
+        DialogBox.__init__(self, centered=True, **kwargs)
+        self.callback = callback
+        if not options:
+            options = []
+        _body = VerticalPanel()
+        _body.setSize('100%','100%')
+        _body.setSpacing(4)
+        _body.add(main_widget)
+        _body.setCellWidth(main_widget, '100%')
+        _body.setCellHeight(main_widget, '100%')
+        if not 'NO_CLOSE' in options:
+            _close_button = Button("Close", self.onClose)
+            _body.add(_close_button)
+            _body.setCellHorizontalAlignment(_close_button, HasAlignment.ALIGN_CENTER)
+        self.setHTML(title)
+        self.setWidget(_body)
+        self.panel.setSize('100%', '100%') #Need this hack to have correct size in Gecko & Webkit
+
+    def close(self):
+        """Same effect as clicking the close button"""
+        self.onClose(None)
+
+    def onClose(self, sender):
+        self.hide()
+        if self.callback:
+            self.callback()
+
+
+class InfoDialog(GenericDialog):
+
+    def __init__(self, title, body, callback=None, options=None, **kwargs):
+        GenericDialog.__init__(self, title, HTML(body), callback, options, **kwargs)
+
+
+class PromptDialog(GenericConfirmDialog):
+
+    def __init__(self, callback, text='', title='User input', **kwargs):
+        prompt = TextBox()
+        prompt.setText(text)
+        GenericConfirmDialog.__init__(self, [prompt], callback, title, prompt, **kwargs)
+
+
+class PopupPanelWrapper(PopupPanel):
+    """This wrapper catch Escape event to avoid request cancellation by Firefox"""
+
+    def onEventPreview(self, event):
+        if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE:
+            #needed to prevent request cancellation in Firefox
+            event.preventDefault()
+        return PopupPanel.onEventPreview(self, event)
+
+
+class ExtTextBox(TextBox):
+    """Extended TextBox"""
+
+    def __init__(self, *args, **kwargs):
+        if 'enter_cb' in kwargs:
+            self.enter_cb = kwargs['enter_cb']
+            del kwargs['enter_cb']
+        TextBox.__init__(self, *args, **kwargs)
+        self.addKeyboardListener(self)
+
+    def onKeyUp(self, sender, keycode, modifiers):
+        pass
+
+    def onKeyDown(self, sender, keycode, modifiers):
+        pass
+
+    def onKeyPress(self, sender, keycode, modifiers):
+        if self.enter_cb and keycode == KEY_ENTER:
+            self.enter_cb(self)
+
+
+class GroupSelector(DialogBox):
+
+    def __init__(self, top_widgets, initial_groups, selected_groups,
+                 ok_title="OK", ok_cb=None, cancel_cb=None):
+        DialogBox.__init__(self, centered=True)
+        main_panel = VerticalPanel()
+        self.ok_cb = ok_cb
+        self.cancel_cb = cancel_cb
+
+        for wid in top_widgets:
+            main_panel.add(wid)
+
+        main_panel.add(Label('Select in which groups your contact is:'))
+        self.list_box = ListBox()
+        self.list_box.setMultipleSelect(True)
+        self.list_box.setVisibleItemCount(5)
+        self.setAvailableGroups(initial_groups)
+        self.setGroupsSelected(selected_groups)
+        main_panel.add(self.list_box)
+
+        def cb(text):
+            self.list_box.addItem(text)
+            self.list_box.setItemSelected(self.list_box.getItemCount() - 1, "selected")
+
+        main_panel.add(AddGroupPanel(initial_groups, cb))
+
+        button_panel = HorizontalPanel()
+        button_panel.addStyleName("marginAuto")
+        button_panel.add(Button(ok_title, self.onOK))
+        button_panel.add(Button("Cancel", self.onCancel))
+        main_panel.add(button_panel)
+
+        self.setWidget(main_panel)
+
+    def getSelectedGroups(self):
+        """Return a list of selected groups"""
+        return self.list_box.getSelectedValues()
+
+    def setAvailableGroups(self, groups):
+        _groups = list(set(groups))
+        _groups.sort()
+        self.list_box.clear()
+        for group in _groups:
+            self.list_box.addItem(group)
+
+    def setGroupsSelected(self, selected_groups):
+        self.list_box.setItemTextSelection(selected_groups)
+
+    def onOK(self, sender):
+        self.hide()
+        if self.ok_cb:
+            self.ok_cb(self)
+
+    def onCancel(self, sender):
+        self.hide()
+        if self.cancel_cb:
+            self.cancel_cb(self)
+
+
+class AddGroupPanel(HorizontalPanel):
+    def __init__(self, groups, cb=None):
+        """
+        @param groups: list of the already existing groups
+        """
+        HorizontalPanel.__init__(self)
+        self.groups = groups
+        self.add(Label('Add group:'))
+        self.textbox = ExtTextBox(enter_cb=self.onGroupInput)
+        self.add(self.textbox)
+        self.add(Button("add", lambda sender: self.onGroupInput(self.textbox)))
+        self.cb = cb
+
+    def onGroupInput(self, sender):
+        text = sender.getText()
+        if text == "":
+            return
+        for group in self.groups:
+            if text == group:
+                Window.alert("The group '%s' already exists." % text)
+                return
+        for pattern in FORBIDDEN_PATTERNS_IN_GROUP:
+            if pattern in text:
+                Window.alert("The pattern '%s' is not allowed in group names." % pattern)
+                return
+        sender.setText('')
+        self.groups.append(text)
+        if self.cb is not None:
+            self.cb(text)
+
+
+class WheelTextBox(TextBox, MouseWheelHandler):
+
+    def __init__(self, *args, **kwargs):
+        TextBox.__init__(self, *args, **kwargs)
+        MouseWheelHandler.__init__(self)
+
+
+class IntSetter(HorizontalPanel):
+    """This class show a bar with button to set an int value"""
+
+    def __init__(self, label, value=0, value_max=None, visible_len=3):
+        """initialize the intSetter
+        @param label: text shown in front of the setter
+        @param value: initial value
+        @param value_max: limit value, None or 0 for unlimited"""
+        HorizontalPanel.__init__(self)
+        self.value = value
+        self.value_max = value_max
+        _label = Label(label)
+        self.add(_label)
+        self.setCellWidth(_label, "100%")
+        minus_button = Button("-", self.onMinus)
+        self.box = WheelTextBox()
+        self.box.setVisibleLength(visible_len)
+        self.box.setText(str(value))
+        self.box.addInputListener(self)
+        self.box.addMouseWheelListener(self)
+        plus_button = Button("+", self.onPlus)
+        self.add(minus_button)
+        self.add(self.box)
+        self.add(plus_button)
+        self.valueChangedListener = []
+
+    def addValueChangeListener(self, listener):
+        self.valueChangedListener.append(listener)
+
+    def removeValueChangeListener(self, listener):
+        if listener in self.valueChangedListener:
+            self.valueChangedListener.remove(listener)
+
+    def _callListeners(self):
+        for listener in self.valueChangedListener:
+            listener(self.value)
+
+    def setValue(self, value):
+        """Change the value and fire valueChange listeners"""
+        self.value = value
+        self.box.setText(str(value))
+        self._callListeners()
+
+    def onMinus(self, sender, step=1):
+        self.value = max(0, self.value - step)
+        self.box.setText(str(self.value))
+        self._callListeners()
+
+    def onPlus(self, sender, step=1):
+        self.value += step
+        if self.value_max:
+            self.value = min(self.value, self.value_max)
+        self.box.setText(str(self.value))
+        self._callListeners()
+
+    def onInput(self, sender):
+        """Accept only valid integer && normalize print (no leading 0)"""
+        try:
+            self.value = int(self.box.getText()) if self.box.getText() else 0
+        except ValueError:
+            pass
+        if self.value_max:
+            self.value = min(self.value, self.value_max)
+        self.box.setText(str(self.value))
+        self._callListeners()
+
+    def onMouseWheel(self, sender, velocity):
+        if velocity > 0:
+            self.onMinus(sender, 10)
+        else:
+            self.onPlus(sender, 10)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/file_tools.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,148 @@
+#!/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/>.
+
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from pyjamas.ui.FileUpload import FileUpload
+from pyjamas.ui.FormPanel import FormPanel
+from pyjamas import Window
+from pyjamas import DOM
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.Button import Button
+from pyjamas.ui.Label import Label
+
+
+class FilterFileUpload(FileUpload):
+
+    def __init__(self, name, max_size, types=None):
+        """
+        @param name: the input element name and id
+        @param max_size: maximum file size in MB
+        @param types: allowed types as a list of couples (x, y, z):
+        - x: MIME content type e.g. "audio/ogg"
+        - y: file extension e.g. "*.ogg"
+        - z: description for the user e.g. "Ogg Vorbis Audio"
+        If types is None, all file format are accepted
+        """
+        FileUpload.__init__(self)
+        self.setName(name)
+        while DOM.getElementById(name):
+            name = "%s_" % name
+        self.setID(name)
+        self._id = name
+        self.max_size = max_size
+        self.types = types
+
+    def getFileInfo(self):
+        from __pyjamas__ import JS
+        JS("var file = top.document.getElementById(this._id).files[0]; return [file.size, file.type]")
+
+    def check(self):
+        if self.getFilename() == "":
+            return False
+        (size, filetype) = self.getFileInfo()
+        if self.types and filetype not in [x for (x, y, z) in self.types]:
+            types = []
+            for type_ in ["- %s (%s)" % (z, y) for (x, y, z) in self.types]:
+                if type_ not in types:
+                    types.append(type_)
+            Window.alert('This file type is not accepted.\nAccepted file types are:\n\n%s' % "\n".join(types))
+            return False
+        if size > self.max_size * pow(2, 20):
+            Window.alert('This file is too big!\nMaximum file size: %d MB.' % self.max_size)
+            return False
+        return True
+
+
+class FileUploadPanel(FormPanel):
+
+    def __init__(self, action_url, input_id, max_size, texts=None, close_cb=None):
+        """Build a form panel to upload a file.
+        @param action_url: the form action URL
+        @param input_id: the input element name and id
+        @param max_size: maximum file size in MB
+        @param texts: a dict to ovewrite the default textual values
+        @param close_cb: the close button callback method
+        """
+        FormPanel.__init__(self)
+        self.texts = {'ok_button': 'Upload file',
+                     'cancel_button': 'Cancel',
+                     'body': 'Please select a file.',
+                     'submitting': '<strong>Submitting, please wait...</strong>',
+                     'errback': "Your file has been rejected...",
+                     'body_errback': 'Please select another file.',
+                     'callback': "Your file has been accepted!"}
+        if isinstance(texts, dict):
+            self.texts.update(texts)
+        self.close_cb = close_cb
+        self.setEncoding(FormPanel.ENCODING_MULTIPART)
+        self.setMethod(FormPanel.METHOD_POST)
+        self.setAction(action_url)
+        self.vPanel = VerticalPanel()
+        self.message = HTML(self.texts['body'])
+        self.vPanel.add(self.message)
+
+        hPanel = HorizontalPanel()
+        hPanel.setSpacing(5)
+        hPanel.setStyleName('marginAuto')
+        self.file_upload = FilterFileUpload(input_id, max_size)
+        self.vPanel.add(self.file_upload)
+
+        self.upload_btn = Button(self.texts['ok_button'], getattr(self, "onSubmitBtnClick"))
+        hPanel.add(self.upload_btn)
+        hPanel.add(Button(self.texts['cancel_button'], getattr(self, "onCloseBtnClick")))
+
+        self.status = Label()
+        hPanel.add(self.status)
+
+        self.vPanel.add(hPanel)
+
+        self.add(self.vPanel)
+        self.addFormHandler(self)
+
+    def setCloseCb(self, close_cb):
+        self.close_cb = close_cb
+
+    def onCloseBtnClick(self):
+        if self.close_cb:
+            self.close_cb()
+        else:
+            log.warning("no close method defined")
+
+    def onSubmitBtnClick(self):
+        if not self.file_upload.check():
+            return
+        self.message.setHTML(self.texts['submitting'])
+        self.upload_btn.setEnabled(False)
+        self.submit()
+
+    def onSubmit(self, event):
+        pass
+
+    def onSubmitComplete(self, event):
+        result = event.getResults()
+        if result != "OK":
+            Window.alert(self.texts['errback'])
+            self.message.setHTML(self.texts['body_errback'])
+            self.upload_btn.setEnabled(True)
+        else:
+            Window.alert(self.texts['callback'])
+            self.close_cb()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/html_tools.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,47 @@
+#!/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/>.
+
+from sat_frontends.tools import xmltools
+
+from nativedom import NativeDOM
+import re
+
+dom = NativeDOM()
+
+
+def html_sanitize(html):
+    """Naive sanitization of HTML"""
+    return html.replace('<', '&lt;').replace('>', '&gt;')
+
+
+def html_strip(html):
+    """Strip leading/trailing white spaces, HTML line breaks and &nbsp; sequences."""
+    cleaned = re.sub(r"^(<br/?>|&nbsp;|\s)+", "", html)
+    cleaned = re.sub(r"(<br/?>|&nbsp;|\s)+$", "", cleaned)
+    return cleaned
+
+
+def inlineRoot(xhtml):
+    """ make root element inline """
+    doc = dom.parseString(xhtml)
+    return xmltools.inlineRoot(doc)
+
+
+def convertNewLinesToXHTML(text):
+    return text.replace('\n', '<br/>')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/jid.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,54 @@
+#!/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/>.
+
+
+class JID(object):
+    """This class help manage JID (Node@Domaine/Resource)"""
+
+    def __init__(self, jid):
+        self.__raw = str(jid)
+        self.__parse()
+
+    def __parse(self):
+        """find node domaine and resource"""
+        node_end = self.__raw.find('@')
+        if node_end < 0:
+            node_end = 0
+        domain_end = self.__raw.find('/')
+        if domain_end < 1:
+            domain_end = len(self.__raw)
+        self.node = self.__raw[:node_end]
+        self.domain = self.__raw[(node_end + 1) if node_end else 0:domain_end]
+        self.resource = self.__raw[domain_end + 1:]
+        if not node_end:
+            self.bare = self.__raw
+        else:
+            self.bare = self.node + '@' + self.domain
+
+    def __str__(self):
+        return self.__raw.__str__()
+
+    def is_valid(self):
+        """return True if the jid is xmpp compliant"""
+        #FIXME: always return True for the moment
+        return True
+
+    def __eq__(self, other):
+        """Redefine equality operator to implement the naturally expected test"""
+        return self.node == other.node and self.domain == other.domain and self.resource == other.resource
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/libervia_main.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,903 @@
+#!/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
+
+### logging configuration ###
+import logging
+logging.configure()
+from sat.core.log import getLogger
+log = getLogger(__name__)
+###
+
+from sat_frontends.tools.misc import InputHistory
+from sat_frontends.tools.strings import getURLParams
+from sat.core.i18n import _
+
+from pyjamas.ui.RootPanel import RootPanel
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.KeyboardListener import KEY_ESCAPE
+from pyjamas.Timer import Timer
+from pyjamas import Window, DOM
+from pyjamas.JSONService import JSONProxy
+
+from register import RegisterBox
+from contact import ContactPanel
+from base_widget import WidgetsPanel
+from panels import MicroblogItem
+import panels, dialog
+from jid import JID
+from xmlui import XMLUI
+from html_tools import html_sanitize
+from notification import Notification
+
+from constants import Const as C
+
+
+MAX_MBLOG_CACHE = 500  # Max microblog entries kept in memories
+
+# Set to true to not create a new LiberviaWidget when a similar one
+# already exist (i.e. a chat panel with the same target). Instead
+# the existing widget will be eventually removed from its parent
+# and added to new WidgetsPanel, or replaced to the expected
+# position if the previous and the new parent are the same.
+REUSE_EXISTING_LIBERVIA_WIDGETS = True
+
+
+class LiberviaJsonProxy(JSONProxy):
+    def __init__(self, *args, **kwargs):
+        JSONProxy.__init__(self, *args, **kwargs)
+        self.handler = self
+        self.cb = {}
+        self.eb = {}
+
+    def call(self, method, cb, *args):
+        _id = self.callMethod(method, args)
+        if cb:
+            if isinstance(cb, tuple):
+                if len(cb) != 2:
+                    log.error("tuple syntax for bridge.call is (callback, errback), aborting")
+                    return
+                if cb[0] is not None:
+                    self.cb[_id] = cb[0]
+                self.eb[_id] = cb[1]
+            else:
+                self.cb[_id] = cb
+
+    def onRemoteResponse(self, response, request_info):
+        if request_info.id in self.cb:
+            _cb = self.cb[request_info.id]
+            # if isinstance(_cb, tuple):
+            #     #we have arguments attached to the callback
+            #     #we send them after the answer
+            #     callback, args = _cb
+            #     callback(response, *args)
+            # else:
+            #     #No additional argument, we call directly the callback
+            _cb(response)
+            del self.cb[request_info.id]
+            if request_info.id in self.eb:
+                del self.eb[request_info.id]
+
+    def onRemoteError(self, code, errobj, request_info):
+        """def dump(obj):
+            print "\n\nDUMPING %s\n\n" % obj
+            for i in dir(obj):
+                print "%s: %s" % (i, getattr(obj,i))"""
+        if request_info.id in self.eb:
+            _eb = self.eb[request_info.id]
+            _eb((code, errobj))
+            del self.cb[request_info.id]
+            del self.eb[request_info.id]
+        else:
+            if code != 0:
+                log.error("Internal server error")
+                """for o in code, error, request_info:
+                    dump(o)"""
+            else:
+                if isinstance(errobj['message'], dict):
+                    log.error("Error %s: %s" % (errobj['message']['faultCode'], errobj['message']['faultString']))
+                else:
+                    log.error("%s" % errobj['message'])
+
+
+class RegisterCall(LiberviaJsonProxy):
+    def __init__(self):
+        LiberviaJsonProxy.__init__(self, "/register_api",
+                        ["isRegistered", "isConnected", "connect", "registerParams", "getMenus"])
+
+
+class BridgeCall(LiberviaJsonProxy):
+    def __init__(self):
+        LiberviaJsonProxy.__init__(self, "/json_api",
+                        ["getContacts", "addContact", "sendMessage", "sendMblog", "sendMblogComment",
+                         "getLastMblogs", "getMassiveLastMblogs", "getMblogComments", "getProfileJid",
+                         "getHistory", "getPresenceStatuses", "joinMUC", "mucLeave", "getRoomsJoined",
+                         "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady",
+                         "tarotGamePlayCards", "launchRadioCollective", "getMblogs", "getMblogsWithComments",
+                         "getWaitingSub", "subscription", "delContact", "updateContact", "getCard",
+                         "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction",
+                         "disconnect", "chatStateComposing", "getNewAccountDomain", "confirmationAnswer",
+                         "syntaxConvert", "getAccountDialogUI",
+                        ])
+
+
+class BridgeSignals(LiberviaJsonProxy):
+    RETRY_BASE_DELAY = 1000
+
+    def __init__(self, host):
+        self.host = host
+        self.retry_delay = self.RETRY_BASE_DELAY
+        LiberviaJsonProxy.__init__(self, "/json_signal_api",
+                        ["getSignals"])
+
+    def onRemoteResponse(self, response, request_info):
+        self.retry_delay = self.RETRY_BASE_DELAY
+        LiberviaJsonProxy.onRemoteResponse(self, response, request_info)
+
+    def onRemoteError(self, code, errobj, request_info):
+        if errobj['message'] == 'Empty Response':
+            Window.getLocation().reload()  # XXX: reset page in case of session ended.
+                                           # FIXME: Should be done more properly without hard reload
+        LiberviaJsonProxy.onRemoteError(self, code, errobj, request_info)
+        #we now try to reconnect
+        if isinstance(errobj['message'], dict) and errobj['message']['faultCode'] == 0:
+            Window.alert('You are not allowed to connect to server')
+        else:
+            def _timerCb(timer):
+                self.host.bridge_signals.call('getSignals', self.host._getSignalsCB)
+            Timer(notify=_timerCb).schedule(self.retry_delay)
+            self.retry_delay *= 2
+
+
+class SatWebFrontend(InputHistory):
+    def onModuleLoad(self):
+        log.info("============ onModuleLoad ==============")
+        panels.ChatPanel.registerClass()
+        panels.MicroblogPanel.registerClass()
+        self.whoami = None
+        self._selected_listeners = set()
+        self.bridge = BridgeCall()
+        self.bridge_signals = BridgeSignals(self)
+        self.uni_box = None
+        self.status_panel = HTML('<br />')
+        self.contact_panel = ContactPanel(self)
+        self.panel = panels.MainPanel(self)
+        self.discuss_panel = self.panel.discuss_panel
+        self.tab_panel = self.panel.tab_panel
+        self.tab_panel.addTabListener(self)
+        self.libervia_widgets = set()  # keep track of all actives LiberviaWidgets
+        self.room_list = []  # list of rooms
+        self.mblog_cache = []  # used to keep our own blog entries in memory, to show them in new mblog panel
+        self.avatars_cache = {}  # keep track of jid's avatar hash (key=jid, value=file)
+        self._register_box = None
+        RootPanel().add(self.panel)
+        self.notification = Notification()
+        DOM.addEventPreview(self)
+        self._register = RegisterCall()
+        self._register.call('getMenus', self.panel.menu.createMenus)
+        self._register.call('registerParams', None)
+        self._register.call('isRegistered', self._isRegisteredCB)
+        self.initialised = False
+        self.init_cache = []  # used to cache events until initialisation is done
+        # define here the parameters that have an incidende to UI refresh
+        self.params_ui = {"unibox": {"name": C.ENABLE_UNIBOX_PARAM,
+                                     "category": C.ENABLE_UNIBOX_KEY,
+                                     "cast": lambda value: value == 'true',
+                                     "value": None
+                                     }
+                          }
+
+    def addSelectedListener(self, callback):
+        self._selected_listeners.add(callback)
+
+    def getSelected(self):
+        wid = self.tab_panel.getCurrentPanel()
+        if not isinstance(wid, WidgetsPanel):
+            log.error("Tab widget is not a WidgetsPanel, can't get selected widget")
+            return None
+        return wid.selected
+
+    def setSelected(self, widget):
+        """Define the selected widget"""
+        widgets_panel = self.tab_panel.getCurrentPanel()
+        if not isinstance(widgets_panel, WidgetsPanel):
+            return
+
+        selected = widgets_panel.selected
+
+        if selected == widget:
+            return
+
+        if selected:
+            selected.removeStyleName('selected_widget')
+
+        widgets_panel.selected = widget
+
+        if widget:
+            widgets_panel.selected.addStyleName('selected_widget')
+
+        for callback in self._selected_listeners:
+            callback(widget)
+
+    def resize(self):
+        """Resize elements"""
+        Window.onResize()
+
+    def onBeforeTabSelected(self, sender, tab_index):
+        return True
+
+    def onTabSelected(self, sender, tab_index):
+        selected = self.getSelected()
+        for callback in self._selected_listeners:
+            callback(selected)
+
+    def onEventPreview(self, event):
+        if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE:
+            #needed to prevent request cancellation in Firefox
+            event.preventDefault()
+        return True
+
+    def getAvatar(self, jid_str):
+        """Return avatar of a jid if in cache, else ask for it"""
+        def dataReceived(result):
+            if 'avatar' in result:
+                self._entityDataUpdatedCb(jid_str, 'avatar', result['avatar'])
+            else:
+                self.bridge.call("getCard", None, jid_str)
+
+        def avatarError(error_data):
+            # The jid is maybe not in our roster, we ask for the VCard
+            self.bridge.call("getCard", None, jid_str)
+
+        if jid_str not in self.avatars_cache:
+            self.bridge.call('getEntityData', (dataReceived, avatarError), jid_str, ['avatar'])
+            self.avatars_cache[jid_str] = "/media/icons/tango/emotes/64/face-plain.png"
+        return self.avatars_cache[jid_str]
+
+    def registerWidget(self, wid):
+        log.debug("Registering %s" % wid.getDebugName())
+        self.libervia_widgets.add(wid)
+
+    def unregisterWidget(self, wid):
+        try:
+            self.libervia_widgets.remove(wid)
+        except KeyError:
+            log.warning('trying to remove a non registered Widget: %s' % wid.getDebugName())
+
+    def refresh(self):
+        """Refresh the general display."""
+        self.panel.refresh()
+        if self.params_ui['unibox']['value']:
+            self.uni_box = self.panel.unibox_panel.unibox
+        else:
+            self.uni_box = None
+        for lib_wid in self.libervia_widgets:
+            lib_wid.refresh()
+        self.resize()
+
+    def addTab(self, label, wid, select=True):
+        """Create a new tab and eventually add a widget in
+        @param label: label of the tab
+        @param wid: LiberviaWidget to add
+        @param select: True to select the added tab
+        """
+        widgets_panel = WidgetsPanel(self)
+        self.tab_panel.add(widgets_panel, label)
+        widgets_panel.addWidget(wid)
+        if select:
+            self.tab_panel.selectTab(self.tab_panel.getWidgetCount() - 1)
+        return widgets_panel
+
+    def addWidget(self, wid, tab_index=None):
+        """ Add a widget at the bottom of the current or specified tab
+        @param wid: LiberviaWidget to add
+        @param tab_index: index of the tab to add the widget to"""
+        if tab_index is None or tab_index < 0 or tab_index >= self.tab_panel.getWidgetCount():
+            panel = self.tab_panel.getCurrentPanel()
+        else:
+            panel = self.tab_panel.tabBar.getTabWidget(tab_index)
+        panel.addWidget(wid)
+
+    def displayNotification(self, title, body):
+        self.notification.notify(title, body)
+
+    def _isRegisteredCB(self, result):
+        registered, warning = result
+        if not registered:
+            self._register_box = RegisterBox(self.logged)
+            self._register_box.centerBox()
+            self._register_box.show()
+            if warning:
+                dialog.InfoDialog(_('Security warning'), warning).show()
+            self._tryAutoConnect(skip_validation=not not warning)
+        else:
+            self._register.call('isConnected', self._isConnectedCB)
+
+    def _isConnectedCB(self, connected):
+        if not connected:
+            self._register.call('connect', lambda x: self.logged())
+        else:
+            self.logged()
+
+    def logged(self):
+        if self._register_box:
+            self._register_box.hide()
+            del self._register_box  # don't work if self._register_box is None
+
+        # display the real presence status panel
+        self.panel.header.remove(self.status_panel)
+        self.status_panel = panels.PresenceStatusPanel(self)
+        self.panel.header.add(self.status_panel)
+
+        #it's time to fill the page
+        self.bridge.call('getContacts', self._getContactsCB)
+        self.bridge.call('getParamsUI', self._getParamsUICB)
+        self.bridge_signals.call('getSignals', self._getSignalsCB)
+        #We want to know our own jid
+        self.bridge.call('getProfileJid', self._getProfileJidCB)
+
+        def domain_cb(value):
+            self._defaultDomain = value
+            log.info("new account domain: %s" % value)
+
+        def domain_eb(value):
+            self._defaultDomain = "libervia.org"
+
+        self.bridge.call("getNewAccountDomain", (domain_cb, domain_eb))
+        self.discuss_panel.addWidget(panels.MicroblogPanel(self, []))
+
+        # get ui params and refresh the display
+        count = 0  # used to do something similar to DeferredList
+
+        def params_ui_cb(param, value=None):
+            count += 1
+            refresh = count == len(self.params_ui)
+            self._paramUpdate(param['name'], value, param['category'], refresh)
+        for param in self.params_ui:
+            self.bridge.call('asyncGetParamA', lambda value: params_ui_cb(self.params_ui[param], value),
+                             self.params_ui[param]['name'], self.params_ui[param]['category'])
+
+    def _tryAutoConnect(self, skip_validation=False):
+        """This method retrieve the eventual URL parameters to auto-connect the user.
+        @param skip_validation: if True, set the form values but do not validate it
+        """
+        params = getURLParams(Window.getLocation().getSearch())
+        if "login" in params:
+            self._register_box._form.login_box.setText(params["login"])
+            self._register_box._form.login_pass_box.setFocus(True)
+            if "passwd" in params:
+                # try to connect
+                self._register_box._form.login_pass_box.setText(params["passwd"])
+                if not skip_validation:
+                    self._register_box._form.onLogin(None)
+                return True
+            else:
+                # this would eventually set the browser saved password
+                Timer(5, lambda: self._register_box._form.login_pass_box.setFocus(True))
+
+    def _actionCb(self, data):
+        if not data:
+            # action was a one shot, nothing to do
+            pass
+        elif "xmlui" in data:
+            ui = XMLUI(self, xml_data = data['xmlui'])
+            options = ['NO_CLOSE'] if ui.type == 'form' else []
+            _dialog = dialog.GenericDialog(ui.title, ui, options=options)
+            ui.setCloseCb(_dialog.close)
+            _dialog.show()
+        else:
+            dialog.InfoDialog("Error",
+                              "Unmanaged action result", Width="400px").center()
+
+    def _actionEb(self, err_data):
+        err_code, err_obj = err_data
+        dialog.InfoDialog("Error",
+                          str(err_obj), Width="400px").center()
+
+    def launchAction(self, callback_id, data):
+        """ Launch a dynamic action
+        @param callback_id: id of the action to launch
+        @param data: data needed only for certain actions
+
+        """
+        if data is None:
+            data = {}
+        self.bridge.call('launchAction', (self._actionCb, self._actionEb), callback_id, data)
+
+    def _getContactsCB(self, contacts_data):
+        for contact in contacts_data:
+            jid, attributes, groups = contact
+            self._newContactCb(jid, attributes, groups)
+
+    def _getSignalsCB(self, signal_data):
+        self.bridge_signals.call('getSignals', self._getSignalsCB)
+        log.debug("Got signal ==> name: %s, params: %s" % (signal_data[0], signal_data[1]))
+        name, args = signal_data
+        if name == 'personalEvent':
+            self._personalEventCb(*args)
+        elif name == 'newMessage':
+            self._newMessageCb(*args)
+        elif name == 'presenceUpdate':
+            self._presenceUpdateCb(*args)
+        elif name == 'paramUpdate':
+            self._paramUpdate(*args)
+        elif name == 'roomJoined':
+            self._roomJoinedCb(*args)
+        elif name == 'roomLeft':
+            self._roomLeftCb(*args)
+        elif name == 'roomUserJoined':
+            self._roomUserJoinedCb(*args)
+        elif name == 'roomUserLeft':
+            self._roomUserLeftCb(*args)
+        elif name == 'roomUserChangedNick':
+            self._roomUserChangedNickCb(*args)
+        elif name == 'askConfirmation':
+            self._askConfirmation(*args)
+        elif name == 'newAlert':
+            self._newAlert(*args)
+        elif name == 'tarotGamePlayers':
+            self._tarotGameStartedCb(True, *args)
+        elif name == 'tarotGameStarted':
+            self._tarotGameStartedCb(False, *args)
+        elif name == 'tarotGameNew' or \
+             name == 'tarotGameChooseContrat' or \
+             name == 'tarotGameShowCards' or \
+             name == 'tarotGameInvalidCards' or \
+             name == 'tarotGameCardsPlayed' or \
+             name == 'tarotGameYourTurn' or \
+             name == 'tarotGameScore':
+            self._tarotGameGenericCb(name, args[0], args[1:])
+        elif name == 'radiocolPlayers':
+            self._radioColStartedCb(True, *args)
+        elif name == 'radiocolStarted':
+            self._radioColStartedCb(False, *args)
+        elif name == 'radiocolPreload':
+            self._radioColGenericCb(name, args[0], args[1:])
+        elif name == 'radiocolPlay':
+            self._radioColGenericCb(name, args[0], args[1:])
+        elif name == 'radiocolNoUpload':
+            self._radioColGenericCb(name, args[0], args[1:])
+        elif name == 'radiocolUploadOk':
+            self._radioColGenericCb(name, args[0], args[1:])
+        elif name == 'radiocolSongRejected':
+            self._radioColGenericCb(name, args[0], args[1:])
+        elif name == 'subscribe':
+            self._subscribeCb(*args)
+        elif name == 'contactDeleted':
+            self._contactDeletedCb(*args)
+        elif name == 'newContact':
+            self._newContactCb(*args)
+        elif name == 'entityDataUpdated':
+            self._entityDataUpdatedCb(*args)
+        elif name == 'chatStateReceived':
+            self._chatStateReceivedCb(*args)
+
+    def _getParamsUICB(self, xmlui):
+        """Hide the parameters item if there's nothing to display"""
+        if not xmlui:
+            self.panel.menu.removeItemParams()
+
+    def _ownBlogsFills(self, mblogs):
+        #put our own microblogs in cache, then fill all panels with them
+        for publisher in mblogs:
+            for mblog in mblogs[publisher]:
+                if not mblog.has_key('content'):
+                    log.warning("No content found in microblog [%s]" % mblog)
+                    continue
+                if mblog.has_key('groups'):
+                    _groups = set(mblog['groups'].split() if mblog['groups'] else [])
+                else:
+                    _groups = None
+                mblog_entry = MicroblogItem(mblog)
+                self.mblog_cache.append((_groups, mblog_entry))
+
+        if len(self.mblog_cache) > MAX_MBLOG_CACHE:
+            del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)]
+        for lib_wid in self.libervia_widgets:
+            if isinstance(lib_wid, panels.MicroblogPanel):
+                self.FillMicroblogPanel(lib_wid)
+        self.initialised = True # initialisation phase is finished here
+        for event_data in self.init_cache: # so we have to send all the cached events
+            self._personalEventCb(*event_data)
+        del self.init_cache
+
+    def _getProfileJidCB(self, jid):
+        self.whoami = JID(jid)
+        #we can now ask our status
+        self.bridge.call('getPresenceStatuses', self._getPresenceStatusesCb)
+        #the rooms where we are
+        self.bridge.call('getRoomsJoined', self._getRoomsJoinedCb)
+        #and if there is any subscription request waiting for us
+        self.bridge.call('getWaitingSub', self._getWaitingSubCb)
+        #we fill the panels already here
+        for lib_wid in self.libervia_widgets:
+            if isinstance(lib_wid, panels.MicroblogPanel):
+                if lib_wid.accept_all():
+                    self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'ALL', [], 10)
+                else:
+                    self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'GROUP', lib_wid.accepted_groups, 10)
+
+        #we ask for our own microblogs:
+        self.bridge.call('getMassiveLastMblogs', self._ownBlogsFills, 'JID', [self.whoami.bare], 10)
+
+    ## Signals callbacks ##
+
+    def _personalEventCb(self, sender, event_type, data):
+        if not self.initialised:
+            self.init_cache.append((sender, event_type, data))
+            return
+        sender = JID(sender).bare
+        if event_type == "MICROBLOG":
+            if not 'content' in data:
+                log.warning("No content found in microblog data")
+                return
+            if 'groups' in data:
+                _groups = set(data['groups'].split() if data['groups'] else [])
+            else:
+                _groups = None
+            mblog_entry = MicroblogItem(data)
+
+            for lib_wid in self.libervia_widgets:
+                if isinstance(lib_wid, panels.MicroblogPanel):
+                    self.addBlogEntry(lib_wid, sender, _groups, mblog_entry)
+
+            if sender == self.whoami.bare:
+                found = False
+                for index in xrange(0, len(self.mblog_cache)):
+                    entry = self.mblog_cache[index]
+                    if entry[1].id == mblog_entry.id:
+                        # replace existing entry
+                        self.mblog_cache.remove(entry)
+                        self.mblog_cache.insert(index, (_groups, mblog_entry))
+                        found = True
+                        break
+                if not found:
+                    self.mblog_cache.append((_groups, mblog_entry))
+                    if len(self.mblog_cache) > MAX_MBLOG_CACHE:
+                        del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)]
+        elif event_type == 'MICROBLOG_DELETE':
+            for lib_wid in self.libervia_widgets:
+                if isinstance(lib_wid, panels.MicroblogPanel):
+                    lib_wid.removeEntry(data['type'], data['id'])
+            log.debug("%s %s %s" % (self.whoami.bare, sender, data['type']))
+
+            if sender == self.whoami.bare and data['type'] == 'main_item':
+                for index in xrange(0, len(self.mblog_cache)):
+                    entry = self.mblog_cache[index]
+                    if entry[1].id == data['id']:
+                        self.mblog_cache.remove(entry)
+                        break
+
+    def addBlogEntry(self, mblog_panel, sender, _groups, mblog_entry):
+        """Check if an entry can go in MicroblogPanel and add to it
+        @param mblog_panel: MicroblogPanel instance
+        @param sender: jid of the entry sender
+        @param _groups: groups which can receive this entry
+        @param mblog_entry: MicroblogItem instance"""
+        if mblog_entry.type == "comment" or mblog_panel.isJidAccepted(sender) or (_groups == None and self.whoami and sender == self.whoami.bare) \
+           or (_groups and _groups.intersection(mblog_panel.accepted_groups)):
+            mblog_panel.addEntry(mblog_entry)
+
+    def FillMicroblogPanel(self, mblog_panel):
+        """Fill a microblog panel with entries in cache
+        @param mblog_panel: MicroblogPanel instance
+        """
+        #XXX: only our own entries are cached
+        for cache_entry in self.mblog_cache:
+            _groups, mblog_entry = cache_entry
+            self.addBlogEntry(mblog_panel, self.whoami.bare, *cache_entry)
+
+    def getEntityMBlog(self, entity):
+        log.info("geting mblog for entity [%s]" % (entity,))
+        for lib_wid in self.libervia_widgets:
+            if isinstance(lib_wid, panels.MicroblogPanel):
+                if lib_wid.isJidAccepted(entity):
+                    self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'JID', [entity], 10)
+
+    def getLiberviaWidget(self, class_, entity, ignoreOtherTabs=True):
+        """Get the corresponding panel if it exists.
+        @param class_: class of the panel (ChatPanel, MicroblogPanel...)
+        @param entity: polymorphic parameter, see class_.matchEntity.
+        @param ignoreOtherTabs: if True, the widgets that are not
+        contained by the currently selected tab will be ignored
+        @return: the existing widget that has been found or None."""
+        selected_tab = self.tab_panel.getCurrentPanel()
+        for lib_wid in self.libervia_widgets:
+            parent = lib_wid.getWidgetsPanel(verbose=False)
+            if parent is None or (ignoreOtherTabs and parent != selected_tab):
+                # do not return a widget that is not in the currently selected tab
+                continue
+            if isinstance(lib_wid, class_):
+                try:
+                    if lib_wid.matchEntity(entity):
+                        log.debug("existing widget found: %s" % lib_wid.getDebugName())
+                        return lib_wid
+                except AttributeError as e:
+                    e.stack_list()
+                    return None
+        return None
+
+    def getOrCreateLiberviaWidget(self, class_, entity, select=True, new_tab=None):
+        """Get the matching LiberviaWidget if it exists, or create a new one.
+        @param class_: class of the panel (ChatPanel, MicroblogPanel...)
+        @param entity: polymorphic parameter, see class_.matchEntity.
+        @param select: if True, select the widget that has been found or created
+        @param new_tab: if not None, a widget which is created is created in
+        a new tab. In that case new_tab is a unicode to label that new tab.
+        If new_tab is not None and a widget is found, no tab is created.
+        @return: the newly created wigdet if REUSE_EXISTING_LIBERVIA_WIDGETS
+         is set to False or if the widget has not been found, the existing
+         widget that has been found otherwise."""
+        lib_wid = None
+        tab = None
+        if REUSE_EXISTING_LIBERVIA_WIDGETS:
+            lib_wid = self.getLiberviaWidget(class_, entity, new_tab is None)
+        if lib_wid is None:  # create a new widget
+            lib_wid = class_.createPanel(self, entity[0] if isinstance(entity, tuple) else entity)
+            if new_tab is None:
+                self.addWidget(lib_wid)
+            else:
+                tab = self.addTab(new_tab, lib_wid, False)
+        else:  # reuse existing widget
+            tab = lib_wid.getWidgetsPanel(verbose=False)
+            if new_tab is None:
+                if tab is not None:
+                    tab.removeWidget(lib_wid)
+                self.addWidget(lib_wid)
+        if select:
+            if new_tab is not None:
+                self.tab_panel.selectTab(tab)
+            # must be done after the widget is added,
+            # for example to scroll to the bottom
+            self.setSelected(lib_wid)
+            lib_wid.refresh()
+        return lib_wid
+
+    def _newMessageCb(self, from_jid, msg, msg_type, to_jid, extra):
+        _from = JID(from_jid)
+        _to = JID(to_jid)
+        other = _to if _from.bare == self.whoami.bare else _from
+        lib_wid = self.getLiberviaWidget(panels.ChatPanel, other, ignoreOtherTabs=False)
+        self.displayNotification(_from, msg)
+        if lib_wid is not None:
+            lib_wid.printMessage(_from, msg, extra)
+        else:
+            # The message has not been shown, we must indicate it
+            self.contact_panel.setContactMessageWaiting(other.bare, True)
+
+    def _presenceUpdateCb(self, entity, show, priority, statuses):
+        entity_jid = JID(entity)
+        if self.whoami and self.whoami == entity_jid:  # XXX: QnD way to get our presence/status
+            self.status_panel.setPresence(show)
+            if statuses:
+                self.status_panel.setStatus(statuses.values()[0])
+        else:
+            self.contact_panel.setConnected(entity_jid.bare, entity_jid.resource, show, priority, statuses)
+
+    def _roomJoinedCb(self, room_jid, room_nicks, user_nick):
+        _target = JID(room_jid)
+        if _target not in self.room_list:
+            self.room_list.append(_target)
+        chat_panel = panels.ChatPanel(self, _target, type_='group')
+        chat_panel.setUserNick(user_nick)
+        if _target.node.startswith('sat_tarot_'): #XXX: it's not really beautiful, but it works :)
+            self.addTab("Tarot", chat_panel)
+        elif _target.node.startswith('sat_radiocol_'):
+            self.addTab("Radio collective", chat_panel)
+        else:
+            self.addTab(_target.node, chat_panel)
+        chat_panel.setPresents(room_nicks)
+        chat_panel.historyPrint()
+        chat_panel.refresh()
+
+    def _roomLeftCb(self, room_jid, room_nicks, user_nick):
+        # FIXME: room_list contains JID instances so why MUST we do
+        # 'remove(room_jid)' and not 'remove(JID(room_jid))' ????!!
+        # This looks like a pyjamas bug --> check/report
+        try:
+            self.room_list.remove(room_jid)
+        except KeyError:
+            pass
+
+    def _roomUserJoinedCb(self, room_jid_s, user_nick, user_data):
+        for lib_wid in self.libervia_widgets:
+            if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
+                lib_wid.userJoined(user_nick, user_data)
+
+    def _roomUserLeftCb(self, room_jid_s, user_nick, user_data):
+        for lib_wid in self.libervia_widgets:
+            if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
+                lib_wid.userLeft(user_nick, user_data)
+
+    def _roomUserChangedNickCb(self, room_jid_s, old_nick, new_nick):
+        """Called when an user joined a MUC room"""
+        for lib_wid in self.libervia_widgets:
+            if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
+                lib_wid.changeUserNick(old_nick, new_nick)
+
+    def _tarotGameStartedCb(self, waiting, room_jid_s, referee, players):
+        for lib_wid in self.libervia_widgets:
+            if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
+                lib_wid.startGame("Tarot", waiting, referee, players)
+
+    def _tarotGameGenericCb(self, event_name, room_jid_s, args):
+        for lib_wid in self.libervia_widgets:
+            if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
+                getattr(lib_wid.getGame("Tarot"), event_name)(*args)
+
+    def _radioColStartedCb(self, waiting, room_jid_s, referee, players, queue_data):
+        for lib_wid in self.libervia_widgets:
+            if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
+                lib_wid.startGame("RadioCol", waiting, referee, players, queue_data)
+
+    def _radioColGenericCb(self, event_name, room_jid_s, args):
+        for lib_wid in self.libervia_widgets:
+            if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s:
+                getattr(lib_wid.getGame("RadioCol"), event_name)(*args)
+
+    def _getPresenceStatusesCb(self, presence_data):
+        for entity in presence_data:
+            for resource in presence_data[entity]:
+                args = presence_data[entity][resource]
+                self._presenceUpdateCb("%s/%s" % (entity, resource), *args)
+
+    def _getRoomsJoinedCb(self, room_data):
+        for room in room_data:
+            self._roomJoinedCb(*room)
+
+    def _getWaitingSubCb(self, waiting_sub):
+        for sub in waiting_sub:
+            self._subscribeCb(waiting_sub[sub], sub)
+
+    def _subscribeCb(self, sub_type, entity):
+        if sub_type == 'subscribed':
+            dialog.InfoDialog('Subscription confirmation', 'The contact <b>%s</b> has added you to his/her contact list' % html_sanitize(entity)).show()
+            self.getEntityMBlog(entity)
+
+        elif sub_type == 'unsubscribed':
+            dialog.InfoDialog('Subscription refusal', 'The contact <b>%s</b> has refused to add you in his/her contact list' % html_sanitize(entity)).show()
+            #TODO: remove microblogs from panels
+
+        elif sub_type == 'subscribe':
+            #The user want to subscribe to our presence
+            _dialog = None
+            msg = HTML('The contact <b>%s</b> want to add you in his/her contact list, do you accept ?' % html_sanitize(entity))
+
+            def ok_cb(ignore):
+                self.bridge.call('subscription', None, "subscribed", entity, '', _dialog.getSelectedGroups())
+            def cancel_cb(ignore):
+                self.bridge.call('subscription', None, "unsubscribed", entity, '', '')
+
+            _dialog = dialog.GroupSelector([msg], self.contact_panel.getGroups(), [], "Add", ok_cb, cancel_cb)
+            _dialog.setHTML('<b>Add contact request</b>')
+            _dialog.show()
+
+    def _contactDeletedCb(self, entity):
+        self.contact_panel.removeContact(entity)
+
+    def _newContactCb(self, contact, attributes, groups):
+        self.contact_panel.updateContact(contact, attributes, groups)
+
+    def _entityDataUpdatedCb(self, entity_jid_s, key, value):
+        if key == "avatar":
+            avatar = '/avatars/%s' % value
+
+            self.avatars_cache[entity_jid_s] = avatar
+
+            for lib_wid in self.libervia_widgets:
+                if isinstance(lib_wid, panels.MicroblogPanel):
+                    if lib_wid.isJidAccepted(entity_jid_s) or (self.whoami and entity_jid_s == self.whoami.bare):
+                        lib_wid.updateValue('avatar', entity_jid_s, avatar)
+
+    def _chatStateReceivedCb(self, from_jid_s, state):
+        """Callback when a new chat state is received.
+        @param from_jid_s: JID of the contact who sent his state, or '@ALL@'
+        @param state: new state (string)
+        """
+        if from_jid_s == '@ALL@':
+            target = '@ALL@'
+            nick = C.ALL_OCCUPANTS
+        else:
+            from_jid = JID(from_jid_s)
+            target = from_jid.bare
+            nick = from_jid.resource
+
+        for lib_wid in self.libervia_widgets:
+            if isinstance(lib_wid, panels.ChatPanel):
+                if target == '@ALL' or target == lib_wid.target.bare:
+                    if lib_wid.type == 'one2one':
+                        lib_wid.setState(state)
+                    elif lib_wid.type == 'group':
+                        lib_wid.setState(state, nick=nick)
+
+    def _askConfirmation(self, confirmation_id, confirmation_type, data):
+        answer_data = {}
+
+        def confirm_cb(result):
+            self.bridge.call('confirmationAnswer', None, confirmation_id, result, answer_data)
+
+        if confirmation_type == "YES/NO":
+            dialog.ConfirmDialog(confirm_cb, text=data["message"], title=data["title"]).show()
+
+    def _newAlert(self, message, title, alert_type):
+        dialog.InfoDialog(title, message).show()
+
+    def _paramUpdate(self, name, value, category, refresh=True):
+        """This is called when the paramUpdate signal is received, but also
+        during initialization when the UI parameters values are retrieved.
+        @param refresh: set to True to refresh the general UI
+        """
+        for param in self.params_ui:
+            if name == self.params_ui[param]['name']:
+                self.params_ui[param]['value'] = self.params_ui[param]['cast'](value)
+                if refresh:
+                    self.refresh()
+                break
+
+    def sendError(self, errorData):
+        dialog.InfoDialog("Error while sending message",
+                          "Your message can't be sent", Width="400px").center()
+        log.error("sendError: %s" % str(errorData))
+
+    def send(self, targets, text, extra={}):
+        """Send a message to any target type.
+        @param targets: list of tuples (type, entities, addr) with:
+        - type in ("PUBLIC", "GROUP", "COMMENT", "STATUS" , "groupchat" , "chat")
+        - entities could be a JID, a list groups, a node hash... depending the target
+        - addr in ("To", "Cc", "Bcc") - ignore case
+        @param text: the message content
+        @param extra: options
+        """
+        # FIXME: too many magic strings, we should use constants instead
+        addresses = []
+        for target in targets:
+            type_, entities, addr = target[0], target[1], 'to' if len(target) < 3 else target[2].lower()
+            if type_ in ("PUBLIC", "GROUP"):
+                self.bridge.call("sendMblog", None, type_, entities if type_ == "GROUP" else None, text, extra)
+            elif type_ == "COMMENT":
+                self.bridge.call("sendMblogComment", None, entities, text, extra)
+            elif type_ == "STATUS":
+                self.bridge.call('setStatus', None, self.status_panel.presence, text)
+            elif type_ in ("groupchat", "chat"):
+                addresses.append((addr, entities))
+            else:
+                log.error("Unknown target type")
+        if addresses:
+            if len(addresses) == 1 and addresses[0][0] == 'to':
+                self.bridge.call('sendMessage', (None, self.sendError), addresses[0][1], text, '', type_, extra)
+            else:
+                extra.update({'address': '\n'.join([('%s:%s' % entry) for entry in addresses])})
+                self.bridge.call('sendMessage', (None, self.sendError), self.whoami.domain, text, '', type_, extra)
+
+    def showWarning(self, type_=None, msg=None):
+        """Display a popup information message, e.g. to notify the recipient of a message being composed.
+        If type_ is None, a popup being currently displayed will be hidden.
+        @type_: a type determining the CSS style to be applied (see WarningPopup.showWarning)
+        @msg: message to be displayed
+        """
+        if not hasattr(self, "warning_popup"):
+            self.warning_popup = panels.WarningPopup()
+        self.warning_popup.showWarning(type_, msg)
+
+
+if __name__ == '__main__':
+    app = SatWebFrontend()
+    app.onModuleLoad()
+    pyjd.run()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/list_manager.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,608 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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/>.
+
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from pyjamas.ui.Grid import Grid
+from pyjamas.ui.Button import Button
+from pyjamas.ui.ListBox import ListBox
+from pyjamas.ui.FlowPanel import FlowPanel
+from pyjamas.ui.AutoComplete import AutoCompleteTextBox
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.DialogBox import DialogBox
+from pyjamas.ui.KeyboardListener import KEY_ENTER
+from pyjamas.ui.MouseListener import MouseHandler
+from pyjamas.ui.FocusListener import FocusHandler
+from pyjamas.ui.DropWidget import DropWidget
+from pyjamas.Timer import Timer
+from pyjamas import DOM
+
+from base_panels import PopupMenuPanel
+from base_widget import DragLabel
+
+# HTML content for the removal button (image or text)
+REMOVE_BUTTON = '<span class="recipientRemoveIcon">x</span>'
+
+# Item to be considered for an empty list box selection.
+# Could be whatever which doesn't look like a JID or a group name.
+EMPTY_SELECTION_ITEM = ""
+
+
+class ListManager():
+    """A manager for sub-panels to assign elements to lists."""
+
+    def __init__(self, parent, keys_dict={}, contact_list=[], offsets={}, style={}):
+        """
+        @param parent: FlexTable parent widget for the manager
+        @param keys_dict: dict with the contact keys mapped to data
+        @param contact_list: list of string (the contact JID userhosts)
+        @param offsets: dict to set widget positions offset within parent
+        - "x_first": the x offset for the first widget's row on the grid
+        - "x": the x offset for all widgets rows, except the first one if "x_first" is defined
+        - "y": the y offset for all widgets columns on the grid
+        """
+        self._parent = parent
+        if isinstance(keys_dict, set) or isinstance(keys_dict, list):
+            tmp = {}
+            for key in keys_dict:
+                tmp[key] = {}
+            keys_dict = tmp
+        self.__keys_dict = keys_dict
+        if isinstance(contact_list, set):
+            contact_list = list(contact_list)
+        self.__list = contact_list
+        self.__list.sort()
+        # store the list of contacts that are not assigned yet
+        self.__remaining_list = []
+        self.__remaining_list.extend(self.__list)
+        # mark a change to sort the list before it's used
+        self.__remaining_list_sorted = True
+
+        self.offsets = {"x_first": 0, "x": 0, "y": 0}
+        if "x" in offsets and not "x_first" in offsets:
+            offsets["x_first"] = offsets["x"]
+        self.offsets.update(offsets)
+
+        self.style = {
+           "keyItem": "recipientTypeItem",
+           "popupMenuItem": "recipientTypeItem",
+           "buttonCell": "recipientButtonCell",
+           "dragoverPanel": "dragover-recipientPanel",
+           "keyPanel": "recipientPanel",
+           "textBox": "recipientTextBox",
+           "textBox-invalid": "recipientTextBox-invalid",
+           "removeButton": "recipientRemoveButton",
+        }
+        self.style.update(style)
+
+    def createWidgets(self, title_format="%s"):
+        """Fill the parent grid with all the widgets (some may be hidden during the initialization)."""
+        self.__children = {}
+        for key in self.__keys_dict:
+            self.addContactKey(key, title_format=title_format)
+
+    def addContactKey(self, key, dict_={}, title_format="%s"):
+        if key not in self.__keys_dict:
+            self.__keys_dict[key] = dict_
+        # copy the key to its associated sub-map
+        self.__keys_dict[key]["title"] = key
+        self._addChild(self.__keys_dict[key], title_format)
+
+    def removeContactKey(self, key):
+        """Remove a list panel and all its associated data."""
+        contacts = self.__children[key]["panel"].getContacts()
+        (y, x) = self._parent.getIndex(self.__children[key]["button"])
+        self._parent.removeRow(y)
+        del self.__children[key]
+        del self.__keys_dict[key]
+        self.addToRemainingList(contacts)
+
+    def _addChild(self, entry, title_format):
+        """Add a button and FlowPanel for the corresponding map entry."""
+        button = Button(title_format % entry["title"])
+        button.setStyleName(self.style["keyItem"])
+        if hasattr(entry, "desc"):
+            button.setTitle(entry["desc"])
+        if not "optional" in entry:
+            entry["optional"] = False
+        button.setVisible(not entry["optional"])
+        y = len(self.__children) + self.offsets["y"]
+        x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"]
+
+        self._parent.insertRow(y)
+        self._parent.setWidget(y, x, button)
+        self._parent.getCellFormatter().setStyleName(y, x, self.style["buttonCell"])
+
+        _child = ListPanel(self, entry, self.style)
+        self._parent.setWidget(y, x + 1, _child)
+
+        self.__children[entry["title"]] = {}
+        self.__children[entry["title"]]["button"] = button
+        self.__children[entry["title"]]["panel"] = _child
+
+        if hasattr(self, "popup_menu"):
+            # this is done if self.registerPopupMenuPanel has been called yet
+            self.popup_menu.registerClickSender(button)
+
+    def _refresh(self, visible=True):
+        """Set visible the sub-panels that are non optional or non empty, hide the rest."""
+        for key in self.__children:
+            self.setContactPanelVisible(key, False)
+        if not visible:
+            return
+        _map = self.getContacts()
+        for key in _map:
+            if len(_map[key]) > 0 or not self.__keys_dict[key]["optional"]:
+                self.setContactPanelVisible(key, True)
+
+    def setVisible(self, visible):
+        self._refresh(visible)
+
+    def setContactPanelVisible(self, key, visible=True, sender=None):
+        """Do not remove the "sender" param as it is needed for the context menu."""
+        self.__children[key]["button"].setVisible(visible)
+        self.__children[key]["panel"].setVisible(visible)
+
+    @property
+    def list(self):
+        """Return the full list of potential contacts."""
+        return self.__list
+
+    @property
+    def keys(self):
+        return self.__keys_dict.keys()
+
+    @property
+    def keys_dict(self):
+        return self.__keys_dict
+
+    @property
+    def remaining_list(self):
+        """Return the contacts that have not been selected yet."""
+        if not self.__remaining_list_sorted:
+            self.__remaining_list_sorted = True
+            self.__remaining_list.sort()
+        return self.__remaining_list
+
+    def setRemainingListUnsorted(self):
+        """Mark a change (deletion) so the list will be sorted before it's used."""
+        self.__remaining_list_sorted = False
+
+    def removeFromRemainingList(self, contacts):
+        """Remove contacts after they have been added to a sub-panel."""
+        if not isinstance(contacts, list):
+            contacts = [contacts]
+        for contact_ in contacts:
+            if contact_ in self.__remaining_list:
+                self.__remaining_list.remove(contact_)
+
+    def addToRemainingList(self, contacts, ignore_key=None):
+        """Add contacts after they have been removed from a sub-panel."""
+        if not isinstance(contacts, list):
+            contacts = [contacts]
+        assigned_contacts = set()
+        assigned_map = self.getContacts()
+        for key_ in assigned_map.keys():
+            if ignore_key is not None and key_ == ignore_key:
+                continue
+            assigned_contacts.update(assigned_map[key_])
+        for contact_ in contacts:
+            if contact_ not in self.__list or contact_ in self.__remaining_list:
+                continue
+            if contact_ in assigned_contacts:
+                continue  # the contact is assigned somewhere else
+            self.__remaining_list.append(contact_)
+            self.setRemainingListUnsorted()
+
+    def setContacts(self, _map={}):
+        """Set the contacts for each contact key."""
+        for key in self.__keys_dict:
+            if key in _map:
+                self.__children[key]["panel"].setContacts(_map[key])
+            else:
+                self.__children[key]["panel"].setContacts([])
+        self._refresh()
+
+    def getContacts(self):
+        """Get the contacts for all the lists.
+        @return: a mapping between keys and contact lists."""
+        _map = {}
+        for key in self.__children:
+            _map[key] = self.__children[key]["panel"].getContacts()
+        return _map
+
+    @property
+    def target_drop_cell(self):
+        """@return: the panel where something has been dropped."""
+        return self._target_drop_cell
+
+    def setTargetDropCell(self, target_drop_cell):
+        """@param: target_drop_cell: the panel where something has been dropped."""
+        self._target_drop_cell = target_drop_cell
+
+    def registerPopupMenuPanel(self, entries, hide, callback):
+        "Register a popup menu panel that will be bound to all contact keys elements."
+        self.popup_menu = PopupMenuPanel(entries=entries, hide=hide, callback=callback, style={"item": self.style["popupMenuItem"]})
+
+
+class DragAutoCompleteTextBox(AutoCompleteTextBox, DragLabel, MouseHandler, FocusHandler):
+    """A draggable AutoCompleteTextBox which is used for representing a contact.
+    This class is NOT generic because of the onDragEnd method which call methods
+    from ListPanel. It's probably not reusable for another scenario.
+    """
+
+    def __init__(self, parent, event_cbs, style):
+        AutoCompleteTextBox.__init__(self)
+        DragLabel.__init__(self, '', 'CONTACT_TEXTBOX')  # The group prefix "@" is already in text so we use only the "CONTACT_TEXTBOX" type
+        self._parent = parent
+        self.event_cbs = event_cbs
+        self.style = style
+        self.addMouseListener(self)
+        self.addFocusListener(self)
+        self.addChangeListener(self)
+        self.addStyleName(style["textBox"])
+        self.reset()
+
+    def reset(self):
+        self.setText("")
+        self.setValid()
+
+    def setValid(self, valid=True):
+        if self.getText() == "":
+            valid = True
+        if valid:
+            self.removeStyleName(self.style["textBox-invalid"])
+        else:
+            self.addStyleName(self.style["textBox-invalid"])
+        self.valid = valid
+
+    def onDragStart(self, event):
+        self._text = self.getText()
+        DragLabel.onDragStart(self, event)
+        self._parent.setTargetDropCell(None)
+        self.setSelectionRange(len(self.getText()), 0)
+
+    def onDragEnd(self, event):
+        target = self._parent.target_drop_cell  # parent or another ListPanel
+        if self.getText() == "" or target is None:
+            return
+        self.event_cbs["drop"](self, target)
+
+    def setRemoveButton(self):
+
+        def remove_cb(sender):
+            """Callback for the button to remove this contact."""
+            self._parent.remove(self)
+            self._parent.remove(self.remove_btn)
+            self.event_cbs["remove"](self)
+
+        self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False)
+        self.remove_btn.setStyleName(self.style["removeButton"])
+        self._parent.add(self.remove_btn)
+
+    def removeOrReset(self):
+        if hasattr(self, "remove_btn"):
+            self.remove_btn.click()
+        else:
+            self.reset()
+
+    def onMouseMove(self, sender):
+        """Mouse enters the area of a DragAutoCompleteTextBox."""
+        if hasattr(sender, "remove_btn"):
+            sender.remove_btn.setVisible(True)
+
+    def onMouseLeave(self, sender):
+        """Mouse leaves the area of a DragAutoCompleteTextBox."""
+        if hasattr(sender, "remove_btn"):
+            Timer(1500, lambda timer: sender.remove_btn.setVisible(False))
+
+    def onFocus(self, sender):
+        sender.setSelectionRange(0, len(self.getText()))
+        self.event_cbs["focus"](sender)
+
+    def validate(self):
+        self.setSelectionRange(len(self.getText()), 0)
+        self.event_cbs["validate"](self)
+
+    def onChange(self, sender):
+        """The textbox or list selection is changed"""
+        if isinstance(sender, ListBox):
+            AutoCompleteTextBox.onChange(self, sender)
+        self.validate()
+
+    def onClick(self, sender):
+        """The list is clicked"""
+        AutoCompleteTextBox.onClick(self, sender)
+        self.validate()
+
+    def onKeyUp(self, sender, keycode, modifiers):
+        """Listen for ENTER key stroke"""
+        AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers)
+        if keycode == KEY_ENTER:
+            self.validate()
+
+
+class DropCell(DropWidget):
+    """A cell where you can drop widgets. This class is NOT generic because of
+    onDrop which uses methods from ListPanel. It has been created to
+    separate the drag and drop methods from the others and add a bit of
+    lisibility, but it's probably not reusable for another scenario.
+    """
+
+    def __init__(self, drop_cbs):
+        DropWidget.__init__(self)
+        self.drop_cbs = drop_cbs
+
+    def onDragEnter(self, event):
+        self.addStyleName(self.style["dragoverPanel"])
+        DOM.eventPreventDefault(event)
+
+    def onDragLeave(self, event):
+        if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop()\
+            or event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1\
+            or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1:
+            # We check that we are inside widget's box, and we don't remove the style in this case because
+            # if the mouse is over a widget inside the DropWidget, we don't want the style to be removed
+            self.removeStyleName(self.style["dragoverPanel"])
+
+    def onDragOver(self, event):
+        DOM.eventPreventDefault(event)
+
+    def onDrop(self, event):
+        DOM.eventPreventDefault(event)
+        dt = event.dataTransfer
+        # 'text', 'text/plain', and 'Text' are equivalent.
+        item, item_type = dt.getData("text/plain").split('\n')  # Workaround for webkit, only text/plain seems to be managed
+        if item_type and item_type[-1] == '\0':  # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
+            item_type = item_type[:-1]           # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
+        if item_type in self.drop_cbs.keys():
+            self.drop_cbs[item_type](self, item)
+        self.removeStyleName(self.style["dragoverPanel"])
+
+
+VALID = 1
+INVALID = 2
+DELETE = 3
+
+
+class ListPanel(FlowPanel, DropCell):
+    """Sub-panel used for each contact key. Beware that pyjamas.ui.FlowPanel
+    is not fully implemented yet and can not be used with pyjamas.ui.Label."""
+
+    def __init__(self, parent, entry, style={}):
+        """Initialization with a button and a DragAutoCompleteTextBox."""
+        FlowPanel.__init__(self, Visible=(False if entry["optional"] else True))
+        drop_cbs = {"GROUP": lambda panel, item: self.addContact("@%s" % item),
+                    "CONTACT": lambda panel, item: self.addContact(item),
+                    "CONTACT_TITLE": lambda panel, item: self.addContact('@@'),
+                    "CONTACT_TEXTBOX": lambda panel, item: self.setTargetDropCell(panel)
+                    }
+        DropCell.__init__(self, drop_cbs)
+        self.style = style
+        self.addStyleName(self.style["keyPanel"])
+        self._parent = parent
+        self.key = entry["title"]
+        self._addTextBox()
+
+    def _addTextBox(self, switchPrevious=False):
+        """Add a text box to the last position. If switchPrevious is True, simulate
+        an insertion before the current last textbox by copying the text and valid state.
+        @return: the created textbox or the previous one if switchPrevious is True.
+        """
+        if hasattr(self, "_last_textbox"):
+            if self._last_textbox.getText() == "":
+                return
+            self._last_textbox.setRemoveButton()
+        else:
+            switchPrevious = False
+
+        def focus_cb(sender):
+            if sender != self._last_textbox:
+                # save the current value before it's being modified
+                self._parent.addToRemainingList(sender.getText(), ignore_key=self.key)
+            sender.setCompletionItems(self._parent.remaining_list)
+
+        def remove_cb(sender):
+            """Callback for the button to remove this contact."""
+            self._parent.addToRemainingList(sender.getText())
+            self._parent.setRemainingListUnsorted()
+            self._last_textbox.setFocus(True)
+
+        def drop_cb(sender, target):
+            """Callback when the textbox is drag-n-dropped."""
+            parent = sender._parent
+            if target != parent and target.addContact(sender.getText()):
+                sender.removeOrReset()
+            else:
+                parent._parent.removeFromRemainingList(sender.getText())
+
+        events_cbs = {"focus": focus_cb, "validate": self.addContact, "remove": remove_cb, "drop": drop_cb}
+        textbox = DragAutoCompleteTextBox(self, events_cbs, self.style)
+        self.add(textbox)
+        if switchPrevious:
+            textbox.setText(self._last_textbox.getText())
+            textbox.setValid(self._last_textbox.valid)
+            self._last_textbox.reset()
+            previous = self._last_textbox
+        self._last_textbox = textbox
+        return previous if switchPrevious else textbox
+
+    def _checkContact(self, contact, modify):
+        """
+        @param contact: the contact to check
+        @param modify: True if the contact is being modified
+        @return:
+        - VALID if the contact is valid
+        - INVALID if the contact is not valid but can be displayed
+        - DELETE if the contact should not be displayed at all
+        """
+        def countItemInList(list_, item):
+            """For some reason the built-in count function doesn't work..."""
+            count = 0
+            for elem in list_:
+                if elem == item:
+                    count += 1
+            return count
+        if contact is None or contact == "":
+            return DELETE
+        if countItemInList(self.getContacts(), contact) > (1 if modify else 0):
+            return DELETE
+        return VALID if contact in self._parent.list else INVALID
+
+    def addContact(self, contact, sender=None):
+        """The first parameter type is checked, so it is also possible to call addContact(sender).
+        If contact is not defined, sender.getText() is used. If sender is not defined, contact will
+        be written to the last textbox and a new textbox is added afterward.
+        @param contact: unicode
+        @param sender: DragAutoCompleteTextBox instance
+        """
+        if isinstance(contact, DragAutoCompleteTextBox):
+            sender = contact
+            contact = sender.getText()
+        valid = self._checkContact(contact, sender is not None)
+        if sender is None:
+            # method has been called to modify but to add a contact
+            if valid == VALID:
+                # eventually insert before the last textbox if it's not empty
+                sender = self._addTextBox(True) if self._last_textbox.getText() != "" else self._last_textbox
+                sender.setText(contact)
+        else:
+            sender.setValid(valid == VALID)
+        if valid != VALID:
+            if sender is not None and valid == DELETE:
+                sender.removeOrReset()
+            return False
+        if sender == self._last_textbox:
+            self._addTextBox()
+        try:
+            sender.setVisibleLength(len(contact))
+        except:
+            # IndexSizeError: Index or size is negative or greater than the allowed amount
+            log.warning("FIXME: len(%s) returns %d... javascript bug?" % (contact, len(contact)))
+        self._parent.removeFromRemainingList(contact)
+        self._last_textbox.setFocus(True)
+        return True
+
+    def emptyContacts(self):
+        """Empty the list of contacts."""
+        for child in self.getChildren():
+            if hasattr(child, "remove_btn"):
+                child.remove_btn.click()
+
+    def setContacts(self, tab):
+        """Set the contacts."""
+        self.emptyContacts()
+        if isinstance(tab, set):
+            tab = list(tab)
+        tab.sort()
+        for contact in tab:
+            self.addContact(contact)
+
+    def getContacts(self):
+        """Get the contacts
+        @return: an array of string"""
+        tab = []
+        for widget in self.getChildren():
+            if isinstance(widget, DragAutoCompleteTextBox):
+                # not to be mixed with EMPTY_SELECTION_ITEM
+                if widget.getText() != "":
+                    tab.append(widget.getText())
+        return tab
+
+    @property
+    def target_drop_cell(self):
+        """@return: the panel where something has been dropped."""
+        return self._parent.target_drop_cell
+
+    def setTargetDropCell(self, target_drop_cell):
+        """
+        XXX: Property setter here would not make it, you need a proper method!
+        @param target_drop_cell: the panel where something has been dropped."""
+        self._parent.setTargetDropCell(target_drop_cell)
+
+
+class ContactChooserPanel(DialogBox):
+    """Display the contacts chooser dialog. This has been implemented while
+    prototyping and is currently not used. Left for an eventual later use.
+    Replaced by the popup menu which allows to add a panel for Cc or Bcc.
+    """
+
+    def __init__(self, manager, **kwargs):
+        """Display a listbox for each contact key"""
+        DialogBox.__init__(self, autoHide=False, centered=True, **kwargs)
+        self.setHTML("Select contacts")
+        self.manager = manager
+        self.listboxes = {}
+        self.contacts = manager.getContacts()
+
+        container = VerticalPanel(Visible=True)
+        container.addStyleName("marginAuto")
+
+        grid = Grid(2, len(self.manager.keys_dict))
+        index = -1
+        for key in self.manager.keys_dict:
+            index += 1
+            grid.add(Label("%s:" % self.manager.keys_dict[key]["desc"]), 0, index)
+            listbox = ListBox()
+            listbox.setMultipleSelect(True)
+            listbox.setVisibleItemCount(15)
+            listbox.addItem(EMPTY_SELECTION_ITEM)
+            for element in manager.list:
+                listbox.addItem(element)
+            self.listboxes[key] = listbox
+            grid.add(listbox, 1, index)
+        self._reset()
+
+        buttons = HorizontalPanel()
+        buttons.addStyleName("marginAuto")
+        btn_close = Button("Cancel", self.hide)
+        buttons.add(btn_close)
+        btn_reset = Button("Reset", self._reset)
+        buttons.add(btn_reset)
+        btn_ok = Button("OK", self._validate)
+        buttons.add(btn_ok)
+
+        container.add(grid)
+        container.add(buttons)
+
+        self.add(container)
+        self.center()
+
+    def _reset(self):
+        """Reset the selections."""
+        for key in self.manager.keys_dict:
+            listbox = self.listboxes[key]
+            for i in xrange(0, listbox.getItemCount()):
+                if listbox.getItemText(i) in self.contacts[key]:
+                    listbox.setItemSelected(i, "selected")
+                else:
+                    listbox.setItemSelected(i, "")
+
+    def _validate(self):
+        """Sets back the selected contacts to the good sub-panels."""
+        _map = {}
+        for key in self.manager.keys_dict:
+            selections = self.listboxes[key].getSelectedItemText()
+            if EMPTY_SELECTION_ITEM in selections:
+                selections.remove(EMPTY_SELECTION_ITEM)
+            _map[key] = selections
+        self.manager.setContacts(_map)
+        self.hide()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/logging.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,50 @@
+#!/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/>.
+"""This module configure logs for Libervia browser side"""
+
+from __pyjamas__ import console
+from constants import Const as C
+from sat.core import log  # XXX: we don't use core.log_config here to avoid the impossible imports in pyjamas
+
+
+class LiberviaLogger(log.Logger):
+
+    def out(self, message, level=None):
+        if level == C.LOG_LVL_DEBUG:
+            console.debug(message)
+        elif level == C.LOG_LVL_INFO:
+            console.info(message)
+        elif level == C.LOG_LVL_WARNING:
+            console.warn(message)
+        else:
+            console.error(message)
+
+
+def configure():
+    fmt = '[%(name)s] %(message)s'
+    log.configure(C.LOG_BACKEND_CUSTOM,
+                  logger_class = LiberviaLogger,
+                  level = C.LOG_LVL_DEBUG,
+                  fmt = fmt,
+                  output = None,
+                  logger = None,
+                  colors = False,
+                  force_colors = False)
+    # FIXME: workaround for Pyjamas, need to be removed when Pyjamas is fixed
+    LiberviaLogger.fmt = fmt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/menu.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,271 @@
+#!/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.core.i18n import _
+
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.MenuBar import MenuBar
+from pyjamas.ui.MenuItem import MenuItem
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.Frame import Frame
+from pyjamas import Window
+
+from jid import JID
+
+from file_tools import FileUploadPanel
+from xmlui import XMLUI
+import panels
+import dialog
+from contact_group import ContactGroupEditor
+
+
+class MenuCmd:
+
+    def __init__(self, object_, handler):
+        self._object = object_
+        self._handler = handler
+
+    def execute(self):
+        handler = getattr(self._object, self._handler)
+        handler()
+
+
+class PluginMenuCmd:
+
+    def __init__(self, host, action_id):
+        self.host = host
+        self.action_id = action_id
+
+    def execute(self):
+        self.host.launchAction(self.action_id, None)
+
+
+class LiberviaMenuBar(MenuBar):
+
+    def __init__(self):
+        MenuBar.__init__(self, vertical=False)
+        self.setStyleName('gwt-MenuBar-horizontal') # XXX: workaround for the Pyjamas' class name fix (it's now "gwt-MenuBar gwt-MenuBar-horizontal")
+                                                    # TODO: properly adapt CSS to the new class name
+
+    def doItemAction(self, item, fireCommand):
+        MenuBar.doItemAction(self, item, fireCommand)
+        if item == self.items[-1] and self.popup:
+            self.popup.setPopupPosition(Window.getClientWidth() -
+                                        self.popup.getOffsetWidth() - 22,
+                                        self.getAbsoluteTop() +
+                                        self.getOffsetHeight() - 1)
+            self.popup.addStyleName('menuLastPopup')
+
+
+class AvatarUpload(FileUploadPanel):
+    def __init__(self):
+        texts = {'ok_button': 'Upload avatar',
+                 'body': 'Please select an image to show as your avatar...<br>Your picture must be a square and will be resized to 64x64 pixels if necessary.',
+                 'errback': "Can't open image... did you actually submit an image?",
+                 'body_errback': 'Please select another image file.',
+                 'callback': "Your new profile picture has been set!"}
+        FileUploadPanel.__init__(self, 'upload_avatar', 'avatar_path', 2, texts)
+
+
+class Menu(SimplePanel):
+
+    def __init__(self, host):
+        self.host = host
+        SimplePanel.__init__(self)
+        self.setStyleName('menuContainer')
+
+    def createMenus(self, add_menus):
+        _item_tpl = "<img src='media/icons/menu/%s_menu_red.png' />%s"
+        menus_dict = {}
+        menus_order = []
+
+        def addMenu(menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd):
+            """ add a menu to menu_dict """
+            log.info("addMenu: %s %s %s %s %s" % (menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd))
+            try:
+                menu_bar = menus_dict[menu_name]
+            except KeyError:
+                menu_bar = menus_dict[menu_name] = MenuBar(vertical=True)
+                menus_order.append((menu_name, menu_name_i18n, icon))
+            if item_name_i18n and menu_cmd:
+                menu_bar.addItem(item_name_i18n, menu_cmd)
+
+        addMenu("General", _("General"), _("Web widget"), 'home', MenuCmd(self, "onWebWidget"))
+        addMenu("General", _("General"), _("Disconnect"), 'home', MenuCmd(self, "onDisconnect"))
+        addMenu("Contacts", _("Contacts"), None, 'social', None)
+        addMenu("Groups", _("Groups"), _("Discussion"), 'social', MenuCmd(self, "onJoinRoom"))
+        addMenu("Groups", _("Groups"), _("Collective radio"), 'social', MenuCmd(self, "onCollectiveRadio"))
+        addMenu("Games", _("Games"), _("Tarot"), 'games', MenuCmd(self, "onTarotGame"))
+        addMenu("Games", _("Games"), _("Xiangqi"), 'games', MenuCmd(self, "onXiangqiGame"))
+
+        # additional menus
+        for action_id, type_, path, path_i18n in add_menus:
+            if not path:
+                log.warning("skipping menu without path")
+                continue
+            if len(path) != len(path_i18n):
+                log.error("inconsistency between menu paths")
+                continue
+            menu_name = path[0]
+            menu_name_i18n = path_i18n[0]
+            item_name = path[1:]
+            if not item_name:
+                log.warning("skipping menu with a path of lenght 1 [%s]" % path[0])
+                continue
+            item_name_i18n = ' | '.join(path_i18n[1:])
+            addMenu(menu_name, menu_name_i18n, item_name_i18n, 'plugins', PluginMenuCmd(self.host, action_id))
+
+        # menu items that should be displayed after the automatically added ones
+        addMenu("Contacts", _("Contacts"), _("Manage groups"), 'social', MenuCmd(self, "onManageContactGroups"))
+
+        menus_order.append(None)  # we add separator
+
+        addMenu("Help", _("Help"), _("Social contract"), 'help', MenuCmd(self, "onSocialContract"))
+        addMenu("Help", _("Help"), _("About"), 'help', MenuCmd(self, "onAbout"))
+        addMenu("Settings", _("Settings"), _("Account"), 'settings', MenuCmd(self, "onAccount"))
+        addMenu("Settings", _("Settings"), _("Parameters"), 'settings', MenuCmd(self, "onParameters"))
+
+        # XXX: temporary, will change when a full profile will be managed in SàT
+        addMenu("Settings", _("Settings"), _("Upload avatar"), 'settings', MenuCmd(self, "onAvatarUpload"))
+
+        menubar = LiberviaMenuBar()
+
+        for menu_data in menus_order:
+            if menu_data is None:
+                _separator = MenuItem('', None)
+                _separator.setStyleName('menuSeparator')
+                menubar.addItem(_separator, None)
+            else:
+                menu_name, menu_name_i18n, icon = menu_data
+                menubar.addItem(MenuItem(_item_tpl % (icon, menu_name_i18n), True, menus_dict[menu_name]))
+
+        self.add(menubar)
+
+    #General menu
+    def onWebWidget(self):
+        web_panel = panels.WebPanel(self.host, "http://www.goffi.org")
+        self.host.addWidget(web_panel)
+        self.host.setSelected(web_panel)
+
+    def onDisconnect(self):
+        def confirm_cb(answer):
+            if answer:
+                log.info("disconnection")
+                self.host.bridge.call('disconnect', None)
+        _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to disconnect ?")
+        _dialog.show()
+
+    def onSocialContract(self):
+        _frame = Frame('contrat_social.html')
+        _frame.setStyleName('infoFrame')
+        _dialog = dialog.GenericDialog("Contrat Social", _frame)
+        _dialog.setSize('80%', '80%')
+        _dialog.show()
+
+    def onAbout(self):
+        _about = HTML("""<b>Libervia</b>, a Salut &agrave; Toi project<br />
+<br />
+You can contact the author at <a href="mailto:goffi@goffi.org">goffi@goffi.org</a><br />
+Blog available (mainly in french) at <a href="http://www.goffi.org" target="_blank">http://www.goffi.org</a><br />
+Project page: <a href="http://sat.goffi.org"target="_blank">http://sat.goffi.org</a><br />
+<br />
+Any help welcome :)
+<p style='font-size:small;text-align:center'>This project is dedicated to Roger Poisson</p>
+""")
+        _dialog = dialog.GenericDialog("About", _about)
+        _dialog.show()
+
+    #Contact menu
+    def onManageContactGroups(self):
+        """Open the contact groups manager."""
+
+        def onCloseCallback():
+            pass
+
+        ContactGroupEditor(self.host, None, onCloseCallback)
+
+    #Group menu
+    def onJoinRoom(self):
+
+        def invite(room_jid, contacts):
+            for contact in contacts:
+                self.host.bridge.call('inviteMUC', None, contact, room_jid)
+
+        def join(room_jid, contacts):
+            if self.host.whoami:
+                nick = self.host.whoami.node
+                if room_jid not in [room.bare for room in self.host.room_list]:
+                    self.host.bridge.call('joinMUC', lambda room_jid: invite(room_jid, contacts), room_jid, nick)
+                else:
+                    self.host.getOrCreateLiberviaWidget(panels.ChatPanel, (room_jid, "group"), True, JID(room_jid).bare)
+                    invite(room_jid, contacts)
+
+        dialog.RoomAndContactsChooser(self.host, join, ok_button="Join", visible=(True, False))
+
+    def onCollectiveRadio(self):
+        def callback(room_jid, contacts):
+            self.host.bridge.call('launchRadioCollective', None, contacts, room_jid)
+        dialog.RoomAndContactsChooser(self.host, callback, ok_button="Choose", title="Collective Radio", visible=(False, True))
+
+    #Game menu
+    def onTarotGame(self):
+        def onPlayersSelected(room_jid, other_players):
+            self.host.bridge.call('launchTarotGame', None, other_players, room_jid)
+        dialog.RoomAndContactsChooser(self.host, onPlayersSelected, 3, title="Tarot", title_invite="Please select 3 other players", visible=(False, True))
+
+    def onXiangqiGame(self):
+        Window.alert("A Xiangqi game is planed, but not available yet")
+
+    #Settings menu
+
+    def onAccount(self):
+        def gotUI(xmlui):
+            if not xmlui:
+                return
+            body = XMLUI(self.host, xmlui)
+            _dialog = dialog.GenericDialog("Manage your XMPP account", body, options=['NO_CLOSE'])
+            body.setCloseCb(_dialog.close)
+            _dialog.show()
+        self.host.bridge.call('getAccountDialogUI', gotUI)
+
+    def onParameters(self):
+        def gotParams(xmlui):
+            if not xmlui:
+                return
+            body = XMLUI(self.host, xmlui)
+            _dialog = dialog.GenericDialog("Parameters", body, options=['NO_CLOSE'])
+            body.setCloseCb(_dialog.close)
+            _dialog.setSize('80%', '80%')
+            _dialog.show()
+        self.host.bridge.call('getParamsUI', gotParams)
+
+    def removeItemParams(self):
+        """Remove the Parameters item from the Settings menu bar."""
+        self.menu_settings.removeItem(self.item_params)
+
+    def onAvatarUpload(self):
+        body = AvatarUpload()
+        _dialog = dialog.GenericDialog("Avatar upload", body, options=['NO_CLOSE'])
+        body.setCloseCb(_dialog.close)
+        _dialog.setWidth('40%')
+        _dialog.show()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/nativedom.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,108 @@
+#!/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/>.
+
+"""
+This class provide basic DOM parsing based on native javascript parser
+__init__ code comes from Tim Down at http://stackoverflow.com/a/8412989
+"""
+
+from __pyjamas__ import JS
+
+
+class Node():
+
+    def __init__(self, js_node):
+        self._node = js_node
+
+    def _jsNodesList2List(self, js_nodes_list):
+        ret=[]
+        for i in range(len(js_nodes_list)):
+            #ret.append(Element(js_nodes_list.item(i)))
+            ret.append(self.__class__(js_nodes_list.item(i))) # XXX: Ugly, but used to word around a Pyjamas's bug
+        return ret
+
+    @property
+    def nodeName(self):
+        return self._node.nodeName
+
+    @property
+    def wholeText(self):
+        return self._node.wholeText
+
+    @property
+    def childNodes(self):
+        return self._jsNodesList2List(self._node.childNodes)
+
+    def getAttribute(self, attr):
+        return self._node.getAttribute(attr)
+
+    def setAttribute(self, attr, value):
+        return self._node.setAttribute(attr, value)
+
+    def hasAttribute(self, attr):
+        return self._node.hasAttribute(attr)
+
+    def toxml(self):
+        return JS("""this._node.outerHTML || new XMLSerializer().serializeToString(this._node);""")
+
+
+class Element(Node):
+
+    def __init__(self, js_node):
+        Node.__init__(self, js_node)
+
+    def getElementsByTagName(self, tagName):
+        return self._jsNodesList2List(self._node.getElementsByTagName(tagName))
+
+
+class Document(Node):
+
+    def __init__(self, js_document):
+        Node.__init__(self, js_document)
+
+    @property
+    def documentElement(self):
+        return Element(self._node.documentElement)
+
+
+class NativeDOM:
+
+    def __init__(self):
+        JS("""
+
+        if (typeof window.DOMParser != "undefined") {
+            this.parseXml = function(xmlStr) {
+                return ( new window.DOMParser() ).parseFromString(xmlStr, "text/xml");
+            };
+        } else if (typeof window.ActiveXObject != "undefined" &&
+               new window.ActiveXObject("Microsoft.XMLDOM")) {
+            this.parseXml = function(xmlStr) {
+                var xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM");
+                xmlDoc.async = "false";
+                xmlDoc.loadXML(xmlStr);
+                return xmlDoc;
+            };
+        } else {
+            throw new Error("No XML parser found");
+        }
+        """)
+
+    def parseString(self, xml):
+        return Document(self.parseXml(xml))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/notification.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,122 @@
+from __pyjamas__ import JS, wnd
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat.core.i18n import _
+
+from pyjamas import Window
+from pyjamas.Timer import Timer
+
+import dialog
+
+TIMER_DELAY = 5000
+
+
+class Notification(object):
+    """
+    If the browser supports it, the user allowed it to and the tab is in the
+    background, send desktop notifications on messages.
+
+    Requires both Web Notifications and Page Visibility API.
+    """
+
+    def __init__(self):
+        self.enabled = False
+        user_agent = None
+        notif_permission = None
+        JS("""
+        if (!('hidden' in document))
+            document.hidden = false;
+
+        user_agent = navigator.userAgent
+
+        if (!('Notification' in window))
+            return;
+
+        notif_permission = Notification.permission
+
+        if (Notification.permission === 'granted')
+            this.enabled = true;
+
+        else if (Notification.permission === 'default') {
+            Notification.requestPermission(function(permission){
+                if (permission !== 'granted')
+                    return;
+
+                self.enabled = true; //need to use self instead of this
+            });
+        }
+        """)
+
+        if "Chrome" in user_agent and notif_permission not in ['granted', 'denied']:
+            self.user_agent = user_agent
+            self._installChromiumWorkaround()
+
+        wnd().onfocus = self.onFocus
+        # wnd().onblur = self.onBlur
+        self._notif_count = 0
+        self._orig_title = Window.getTitle()
+
+    def _installChromiumWorkaround(self):
+        # XXX: Workaround for Chromium behaviour, it's doens't manage requestPermission on onLoad event
+        # see https://code.google.com/p/chromium/issues/detail?id=274284
+        # FIXME: need to be removed if Chromium behaviour changes
+        try:
+            version_full = [s for s in self.user_agent.split() if "Chrome" in s][0].split('/')[1]
+            version = int(version_full.split('.')[0])
+        except (IndexError, ValueError):
+            log.warning("Can't find Chromium version")
+            version = 0
+        log.info("Chromium version: %d" % (version,))
+        if version < 22:
+            log.info("Notification use the old prefixed version or are unmanaged")
+            return
+        if version < 32:
+            dialog.InfoDialog(_("Notifications activation for Chromium"), _('You need to activate notifications manually for your Chromium version.<br/>To activate notifications, click on the favicon on the left of the address bar')).show()
+            return
+
+        log.info("==> Installing Chromium notifications request workaround <==")
+        self._old_click = wnd().onclick
+        wnd().onclick = self._chromiumWorkaround
+
+    def _chromiumWorkaround(self):
+        log.info("Activating workaround")
+        JS("""
+            Notification.requestPermission(function(permission){
+                if (permission !== 'granted')
+                    return;
+                self.enabled = true; //need to use self instead of this
+            });
+        """)
+        wnd().onclick = self._old_click
+
+    def onFocus(self):
+        Window.setTitle(self._orig_title)
+        self._notif_count = 0
+
+    # def onBlur(self):
+    #     pass
+
+    def isHidden(self):
+        JS("""return document.hidden;""")
+
+    def _notify(self, title, body, icon):
+        if not self.enabled:
+            return
+        notification = None
+        JS("""
+           notification = new Notification(title, {body: body, icon: icon});
+           // Probably won’t work, but it doesn’t hurt to try.
+           notification.addEventListener('click', function() {
+               window.focus();
+           });
+           """)
+        notification.onshow = lambda: Timer(TIMER_DELAY, lambda timer: notification.close())
+
+    def highlightTab(self):
+        self._notif_count += 1
+        Window.setTitle("%s (%d)" % (self._orig_title, self._notif_count))
+
+    def notify(self, title, body, icon='/media/icons/apps/48/sat.png'):
+        if self.isHidden():
+            self._notify(title, body, icon)
+            self.highlightTab()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/panels.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,1409 @@
+#!/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.tools.strings import addURLToText
+from sat_frontends.tools.games import SYMBOLS
+from sat.core.i18n import _
+
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.HTMLPanel import HTMLPanel
+from pyjamas.ui.Frame import Frame
+from pyjamas.ui.TextArea import TextArea
+from pyjamas.ui.Label import Label
+from pyjamas.ui.Button import Button
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.Image import Image
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui.FlowPanel import FlowPanel
+from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN, KeyboardHandler
+from pyjamas.ui.MouseListener import MouseHandler
+from pyjamas.ui.FocusListener import FocusHandler
+from pyjamas.Timer import Timer
+from pyjamas import DOM
+from pyjamas import Window
+from __pyjamas__ import doc
+
+from datetime import datetime
+from time import time
+
+from jid import JID
+from html_tools import html_sanitize
+from base_panels import ChatText, OccupantsList, PopupMenuPanel, BaseTextEditor, LightTextEditor, HTMLTextEditor
+from card_game import CardPanel
+from radiocol import RadioColPanel
+from menu import Menu
+import dialog
+import base_widget
+import richtext
+import contact
+from constants import Const as C
+from plugin_xep_0085 import ChatStateMachine
+
+
+# TODO: at some point we should decide which behaviors to keep and remove these two constants
+TOGGLE_EDITION_USE_ICON = False  # set to True to use an icon inside the "toggle syntax" button
+NEW_MESSAGE_USE_BUTTON = False  # set to True to display the "New message" button instead of an empty entry
+
+
+class UniBoxPanel(HorizontalPanel):
+    """Panel containing the UniBox"""
+
+    def __init__(self, host):
+        HorizontalPanel.__init__(self)
+        self.host = host
+        self.setStyleName('uniBoxPanel')
+        self.unibox = None
+
+    def refresh(self):
+        """Enable or disable this panel. Contained widgets are created when necessary."""
+        enable = self.host.params_ui['unibox']['value']
+        self.setVisible(enable)
+        if enable and not self.unibox:
+            self.button = Button('<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>')
+            self.button.setTitle('Open the rich text editor')
+            self.button.addStyleName('uniBoxButton')
+            self.add(self.button)
+            self.unibox = UniBox(self.host)
+            self.add(self.unibox)
+            self.setCellWidth(self.unibox, '100%')
+            self.button.addClickListener(self.openRichMessageEditor)
+            self.unibox.addKey("@@: ")
+            self.unibox.onSelectedChange(self.host.getSelected())
+
+    def openRichMessageEditor(self):
+        """Open the rich text editor."""
+        self.button.setVisible(False)
+        self.unibox.setVisible(False)
+        self.setCellWidth(self.unibox, '0px')
+        self.host.panel._contactsMove(self)
+
+        def afterEditCb():
+            Window.removeWindowResizeListener(self)
+            self.host.panel._contactsMove(self.host.panel._hpanel)
+            self.setCellWidth(self.unibox, '100%')
+            self.button.setVisible(True)
+            self.unibox.setVisible(True)
+            self.host.resize()
+
+        richtext.RichMessageEditor.getOrCreate(self.host, self, afterEditCb)
+        Window.addWindowResizeListener(self)
+        self.host.resize()
+
+    def onWindowResized(self, width, height):
+        right = self.host.panel.menu.getAbsoluteLeft() + self.host.panel.menu.getOffsetWidth()
+        left = self.host.panel._contacts.getAbsoluteLeft() + self.host.panel._contacts.getOffsetWidth()
+        ideal_width = right - left - 40
+        self.host.richtext.setWidth("%spx" % ideal_width)
+
+
+class MessageBox(TextArea):
+    """A basic text area for entering messages"""
+
+    def __init__(self, host):
+        TextArea.__init__(self)
+        self.host = host
+        self.__size = (0, 0)
+        self.setStyleName('messageBox')
+        self.addKeyboardListener(self)
+        MouseHandler.__init__(self)
+        self.addMouseListener(self)
+        self._selected_cache = None
+
+    def onBrowserEvent(self, event):
+        # XXX: woraroung a pyjamas bug: self.currentEvent is not set
+        #     so the TextBox's cancelKey doens't work. This is a workaround
+        #     FIXME: fix the bug upstream
+        self.currentEvent = event
+        TextArea.onBrowserEvent(self, event)
+
+    def onKeyPress(self, sender, keycode, modifiers):
+        _txt = self.getText()
+
+        def history_cb(text):
+            self.setText(text)
+            Timer(5, lambda timer: self.setCursorPos(len(text)))
+
+        if keycode == KEY_ENTER:
+            if _txt:
+                self._selected_cache.onTextEntered(_txt)
+                self.host._updateInputHistory(_txt)
+            self.setText('')
+            sender.cancelKey()
+        elif keycode == KEY_UP:
+            self.host._updateInputHistory(_txt, -1, history_cb)
+        elif keycode == KEY_DOWN:
+            self.host._updateInputHistory(_txt, +1, history_cb)
+        else:
+            self.__onComposing()
+
+    def __onComposing(self):
+        """Callback when the user is composing a text."""
+        if hasattr(self._selected_cache, "target"):
+            self._selected_cache.state_machine._onEvent("composing")
+
+    def onMouseUp(self, sender, x, y):
+        size = (self.getOffsetWidth(), self.getOffsetHeight())
+        if size != self.__size:
+            self.__size = size
+            self.host.resize()
+
+    def onSelectedChange(self, selected):
+        self._selected_cache = selected
+
+
+class UniBox(MessageBox, MouseHandler): #AutoCompleteTextBox):
+    """This text box is used as a main typing point, for message, microblog, etc"""
+
+    def __init__(self, host):
+        MessageBox.__init__(self, host)
+        #AutoCompleteTextBox.__init__(self)
+        self.setStyleName('uniBox')
+        host.addSelectedListener(self.onSelectedChange)
+
+    def addKey(self, key):
+        return
+        #self.getCompletionItems().completions.append(key)
+
+    def removeKey(self, key):
+        return
+        # TODO: investigate why AutoCompleteTextBox doesn't work here,
+        # maybe it can work on a TextBox but no TextArea. Remove addKey
+        # and removeKey methods if they don't serve anymore.
+        try:
+            self.getCompletionItems().completions.remove(key)
+        except KeyError:
+            log.warning("trying to remove an unknown key")
+
+    def _getTarget(self, txt):
+        """ Say who will receive the messsage
+        @return: a tuple (selected, target_type, target info) with:
+            - target_hook: None if we use the selected widget, (msg, data) if we have a hook (e.g. "@@: " for a public blog), where msg is the parsed message (i.e. without the "hook key: "@@: bla" become ("bla", None))
+            - target_type: one of PUBLIC, GROUP, ONE2ONE, STATUS, MISC
+            - msg: HTML message which will appear in the privacy warning banner """
+        target = self._selected_cache
+
+        def getSelectedOrStatus():
+            if target and target.isSelectable():
+                _type, msg = target.getWarningData()
+                target_hook = None  # we use the selected widget, not a hook
+            else:
+                _type, msg = "STATUS", "This will be your new status message"
+                target_hook = (txt, None)
+            return (target_hook, _type, msg)
+
+        if not txt.startswith('@'):
+            target_hook, _type, msg = getSelectedOrStatus()
+        elif txt.startswith('@@: '):
+            _type = "PUBLIC"
+            msg = MicroblogPanel.warning_msg_public
+            target_hook = (txt[4:], None)
+        elif txt.startswith('@'):
+            _end = txt.find(': ')
+            if _end == -1:
+                target_hook, _type, msg = getSelectedOrStatus()
+            else:
+                group = txt[1:_end]  # only one target group is managed for the moment
+                if not group or not group in self.host.contact_panel.getGroups():
+                    # the group doesn't exists, we ignore the key
+                    group = None
+                    target_hook, _type, msg = getSelectedOrStatus()
+                else:
+                    _type = "GROUP"
+                    msg = MicroblogPanel.warning_msg_group % group
+                    target_hook = (txt[_end + 2:], group)
+        else:
+            log.error("Unknown target")
+            target_hook, _type, msg = getSelectedOrStatus()
+
+        return (target_hook, _type, msg)
+
+    def onKeyPress(self, sender, keycode, modifiers):
+        _txt = self.getText()
+        target_hook, type_, msg = self._getTarget(_txt)
+
+        if keycode == KEY_ENTER:
+            if _txt:
+                if target_hook:
+                    parsed_txt, data = target_hook
+                    self.host.send([(type_, data)], parsed_txt)
+                    self.host._updateInputHistory(_txt)
+                    self.setText('')
+            self.host.showWarning(None, None)
+        else:
+            self.host.showWarning(type_, msg)
+        MessageBox.onKeyPress(self, sender, keycode, modifiers)
+
+    def getTargetAndData(self):
+        """For external use, to get information about the (hypothetical) message
+        that would be sent if we press Enter right now in the unibox.
+        @return a tuple (target, data) with:
+          - data: what would be the content of the message (body)
+          - target: JID, group with the prefix "@" or the public entity "@@"
+        """
+        _txt = self.getText()
+        target_hook, _type, _msg = self._getTarget(_txt)
+        if target_hook:
+            data, target = target_hook
+            if target is None:
+                return target_hook
+            return (data, "@%s" % (target if target != "" else "@"))
+        if isinstance(self._selected_cache, MicroblogPanel):
+            groups = self._selected_cache.accepted_groups
+            target = "@%s" % (groups[0] if len(groups) > 0 else "@")
+            if len(groups) > 1:
+                Window.alert("Sole the first group of the selected panel is taken in consideration: '%s'" % groups[0])
+        elif isinstance(self._selected_cache, ChatPanel):
+            target = self._selected_cache.target
+        else:
+            target = None
+        return (_txt, target)
+
+    def onWidgetClosed(self, lib_wid):
+        """Called when a libervia widget is closed"""
+        if self._selected_cache == lib_wid:
+            self.onSelectedChange(None)
+
+    """def complete(self):
+
+        #self.visible=False #XXX: self.visible is not unset in pyjamas when ENTER is pressed and a completion is done
+        #XXX: fixed directly on pyjamas, if the patch is accepted, no need to walk around this
+        return AutoCompleteTextBox.complete(self)"""
+
+
+class WarningPopup():
+
+    def __init__(self):
+        self._popup = None
+        self._timer = Timer(notify=self._timeCb)
+
+    def showWarning(self, type_=None, msg=None, duration=2000):
+        """Display a popup information message, e.g. to notify the recipient of a message being composed.
+        If type_ is None, a popup being currently displayed will be hidden.
+        @type_: a type determining the CSS style to be applied (see __showWarning)
+        @msg: message to be displayed
+        """
+        if type_ is None:
+            self.__removeWarning()
+            return
+        if not self._popup:
+            self.__showWarning(type_, msg)
+        elif (type_, msg) != self._popup.target_data:
+            self._timeCb(None)  # we remove the popup
+            self.__showWarning(type_, msg)
+
+        self._timer.schedule(duration)
+
+    def __showWarning(self, type_, msg):
+        """Display a popup information message, e.g. to notify the recipient of a message being composed.
+        @type_: a type determining the CSS style to be applied. For now the defined styles are
+        "NONE" (will do nothing), "PUBLIC", "GROUP", "STATUS" and "ONE2ONE".
+        @msg: message to be displayed
+        """
+        if type_ == "NONE":
+            return
+        if not msg:
+            log.warning("no msg set uniBox warning")
+            return
+        if type_ == "PUBLIC":
+            style = "targetPublic"
+        elif type_ == "GROUP":
+            style = "targetGroup"
+        elif type_ == "STATUS":
+            style = "targetStatus"
+        elif type_ == "ONE2ONE":
+            style = "targetOne2One"
+        else:
+            log.error("unknown message type")
+            return
+        contents = HTML(msg)
+
+        self._popup = dialog.PopupPanelWrapper(autoHide=False, modal=False)
+        self._popup.target_data = (type_, msg)
+        self._popup.add(contents)
+        self._popup.setStyleName("warningPopup")
+        if style:
+            self._popup.addStyleName(style)
+
+        left = 0
+        top = 0  # max(0, self.getAbsoluteTop() - contents.getOffsetHeight() - 2)
+        self._popup.setPopupPosition(left, top)
+        self._popup.show()
+
+    def _timeCb(self, timer):
+        if self._popup:
+            self._popup.hide()
+            del self._popup
+            self._popup = None
+
+    def __removeWarning(self):
+        """Remove the popup"""
+        self._timeCb(None)
+
+
+class MicroblogItem():
+    # XXX: should be moved in a separated module
+
+    def __init__(self, data):
+        self.id = data['id']
+        self.type = data.get('type', 'main_item')
+        self.empty = data.get('new', False)
+        self.title = data.get('title', '')
+        self.title_xhtml = data.get('title_xhtml', '')
+        self.content = data.get('content', '')
+        self.content_xhtml = data.get('content_xhtml', '')
+        self.author = data['author']
+        self.updated = float(data.get('updated', 0))  # XXX: int doesn't work here
+        self.published = float(data.get('published', self.updated))  # XXX: int doesn't work here
+        self.service = data.get('service', '')
+        self.node = data.get('node', '')
+        self.comments = data.get('comments', False)
+        self.comments_service = data.get('comments_service', '')
+        self.comments_node = data.get('comments_node', '')
+
+
+class MicroblogEntry(SimplePanel, ClickHandler, FocusHandler, KeyboardHandler):
+
+    def __init__(self, blog_panel, data):
+        """
+        @param blog_panel: the parent panel
+        @param data: dict containing the blog item data, or a MicroblogItem instance.
+        """
+        self._base_item = data if isinstance(data, MicroblogItem) else MicroblogItem(data)
+        for attr in ['id', 'type', 'empty', 'title', 'title_xhtml', 'content', 'content_xhtml',
+                     'author', 'updated', 'published', 'comments', 'service', 'node',
+                     'comments_service', 'comments_node']:
+            getter = lambda attr: lambda inst: getattr(inst._base_item, attr)
+            setter = lambda attr: lambda inst, value: setattr(inst._base_item, attr, value)
+            setattr(MicroblogEntry, attr, property(getter(attr), setter(attr)))
+
+        SimplePanel.__init__(self)
+        self._blog_panel = blog_panel
+
+        self.panel = FlowPanel()
+        self.panel.setStyleName('mb_entry')
+
+        self.header = HTMLPanel('')
+        self.panel.add(self.header)
+
+        self.entry_actions = VerticalPanel()
+        self.entry_actions.setStyleName('mb_entry_actions')
+        self.panel.add(self.entry_actions)
+
+        entry_avatar = SimplePanel()
+        entry_avatar.setStyleName('mb_entry_avatar')
+        self.avatar = Image(self._blog_panel.host.getAvatar(self.author))
+        entry_avatar.add(self.avatar)
+        self.panel.add(entry_avatar)
+
+        if TOGGLE_EDITION_USE_ICON:
+            self.entry_dialog = HorizontalPanel()
+        else:
+            self.entry_dialog = VerticalPanel()
+        self.entry_dialog.setStyleName('mb_entry_dialog')
+        self.panel.add(self.entry_dialog)
+
+        self.add(self.panel)
+        ClickHandler.__init__(self)
+        self.addClickListener(self)
+
+        self.__pub_data = (self.service, self.node, self.id)
+        self.__setContent()
+
+    def __setContent(self):
+        """Actually set the entry content (header, icons, bubble...)"""
+        self.delete_label = self.update_label = self.comment_label = None
+        self.bubble = self._current_comment = None
+        self.__setHeader()
+        self.__setBubble()
+        self.__setIcons()
+
+    def __setHeader(self):
+        """Set the entry header"""
+        if self.empty:
+            return
+        update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated)
+        self.header.setHTML("""<div class='mb_entry_header'>
+                                   <span class='mb_entry_author'>%(author)s</span> on
+                                   <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s
+                               </div>""" % {'author': html_sanitize(self.author),
+                                            'published': datetime.fromtimestamp(self.published),
+                                            'updated': update_text if self.published != self.updated else ''
+                                            }
+                            )
+
+    def __setIcons(self):
+        """Set the entry icons (delete, update, comment)"""
+        if self.empty:
+            return
+
+        def addIcon(label, title):
+            label = Label(label)
+            label.setTitle(title)
+            label.addClickListener(self)
+            self.entry_actions.add(label)
+            return label
+
+        if self.comments:
+            self.comment_label = addIcon(u"↶", "Comment this message")
+            self.comment_label.setStyleName('mb_entry_action_larger')
+        is_publisher = self.author == self._blog_panel.host.whoami.bare
+        if is_publisher:
+            self.update_label = addIcon(u"✍", "Edit this message")
+        if is_publisher or str(self.node).endswith(self._blog_panel.host.whoami.bare):
+            self.delete_label = addIcon(u"✗", "Delete this message")
+
+    def updateAvatar(self, new_avatar):
+        """Change the avatar of the entry
+        @param new_avatar: path to the new image"""
+        self.avatar.setUrl(new_avatar)
+
+    def onClick(self, sender):
+        if sender == self:
+            try:  # prevent re-selection of the main entry after a comment has been focused
+                if self.__ignoreNextEvent:
+                    self.__ignoreNextEvent = False
+                    return
+            except AttributeError:
+                pass
+            self._blog_panel.setSelectedEntry(self)
+        elif sender == self.delete_label:
+            self._delete()
+        elif sender == self.update_label:
+            self.edit(True)
+        elif sender == self.comment_label:
+            self.__ignoreNextEvent = True
+            self._comment()
+
+    def __modifiedCb(self, content):
+        """Send the new content to the backend
+        @return: False to restore the original content if a deletion has been cancelled
+        """
+        if not content['text']:  # previous content has been emptied
+            self._delete(True)
+            return False
+        extra = {'published': str(self.published)}
+        if isinstance(self.bubble, richtext.RichTextEditor):
+            # TODO: if the user change his parameters after the message edition started,
+            # the message syntax could be different then the current syntax: pass the
+            # message syntax in extra for the frontend to use it instead of current syntax.
+            extra.update({'content_rich': content['text'], 'title': content['title']})
+        if self.empty:
+            if self.type == 'main_item':
+                self._blog_panel.host.bridge.call('sendMblog', None, None, self._blog_panel.accepted_groups, content['text'], extra)
+            else:
+                self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, content['text'], extra)
+        else:
+            self._blog_panel.host.bridge.call('updateMblog', None, self.__pub_data, self.comments, content['text'], extra)
+        return True
+
+    def __afterEditCb(self, content):
+        """Remove the entry if it was an empty one (used for creating a new blog post).
+        Data for the actual new blog post will be received from the bridge"""
+        if self.empty:
+            self._blog_panel.removeEntry(self.type, self.id)
+            if self.type == 'main_item':  # restore the "New message" button
+                self._blog_panel.refresh()
+            else:  # allow to create a new comment
+                self._parent_entry._current_comment = None
+        self.entry_dialog.setWidth('auto')
+        try:
+            self.toggle_syntax_button.removeFromParent()
+        except TypeError:
+            pass
+
+    def __setBubble(self, edit=False):
+        """Set the bubble displaying the initial content."""
+        content = {'text': self.content_xhtml if self.content_xhtml else self.content,
+                   'title': self.title_xhtml if self.title_xhtml else self.title}
+        if self.content_xhtml:
+            content.update({'syntax': C.SYNTAX_XHTML})
+            if self.author != self._blog_panel.host.whoami.bare:
+                options = ['read_only']
+            else:
+                options = [] if self.empty else ['update_msg']
+            self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options)
+        else:  # assume raw text message have no title
+            self.bubble = LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True})
+        self.bubble.addStyleName("bubble")
+        try:
+            self.toggle_syntax_button.removeFromParent()
+        except TypeError:
+            pass
+        self.entry_dialog.add(self.bubble)
+        self.edit(edit)
+        self.bubble.addEditListener(self.__showWarning)
+
+    def __showWarning(self, sender, keycode):
+        if keycode == KEY_ENTER:
+            self._blog_panel.host.showWarning(None, None)
+        else:
+            self._blog_panel.host.showWarning(*self._blog_panel.getWarningData(self.type == 'comment'))
+
+    def _delete(self, empty=False):
+        """Ask confirmation for deletion.
+        @return: False if the deletion has been cancelled."""
+        def confirm_cb(answer):
+            if answer:
+                self._blog_panel.host.bridge.call('deleteMblog', None, self.__pub_data, self.comments)
+            else:  # restore the text if it has been emptied during the edition
+                self.bubble.setContent(self.bubble._original_content)
+
+        if self.empty:
+            text = _("New ") + (_("message") if self.comments else _("comment")) + _(" without body has been cancelled.")
+            dialog.InfoDialog(_("Information"), text).show()
+            return
+        text = ""
+        if empty:
+            text = (_("Message") if self.comments else _("Comment")) + _(" body has been emptied.<br/>")
+        target = _('message and all its comments') if self.comments else _('comment')
+        text += _("Do you really want to delete this %s?") % target
+        dialog.ConfirmDialog(confirm_cb, text=text).show()
+
+    def _comment(self):
+        """Add an empty entry for a new comment"""
+        if self._current_comment:
+            self._current_comment.bubble.setFocus(True)
+            self._blog_panel.setSelectedEntry(self._current_comment)
+            return
+        data = {'id': str(time()),
+                'new': True,
+                'type': 'comment',
+                'author': self._blog_panel.host.whoami.bare,
+                'service': self.comments_service,
+                'node': self.comments_node
+                }
+        entry = self._blog_panel.addEntry(data)
+        if entry is None:
+            log.info("The entry of id %s can not be commented" % self.id)
+            return
+        entry._parent_entry = self
+        self._current_comment = entry
+        self.edit(True, entry)
+        self._blog_panel.setSelectedEntry(entry)
+
+    def edit(self, edit, entry=None):
+        """Toggle the bubble between display and edit mode
+        @edit: boolean value
+        @entry: MicroblogEntry instance, or None to use self
+        """
+        if entry is None:
+            entry = self
+        try:
+            entry.toggle_syntax_button.removeFromParent()
+        except TypeError:
+            pass
+        entry.bubble.edit(edit)
+        if edit:
+            if isinstance(entry.bubble, richtext.RichTextEditor):
+                image = '<a class="richTextIcon">A</a>'
+                html = '<a style="color: blue;">raw text</a>'
+                title = _('Switch to raw text edition')
+            else:
+                image = '<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>'
+                html = '<a style="color: blue;">rich text</a>'
+                title = _('Switch to rich text edition')
+            if TOGGLE_EDITION_USE_ICON:
+                entry.entry_dialog.setWidth('80%')
+                entry.toggle_syntax_button = Button(image, entry.toggleContentSyntax)
+                entry.toggle_syntax_button.setTitle(title)
+                entry.entry_dialog.add(entry.toggle_syntax_button)
+            else:
+                entry.toggle_syntax_button = HTML(html)
+                entry.toggle_syntax_button.addClickListener(entry.toggleContentSyntax)
+                entry.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax')
+                entry.entry_dialog.add(entry.toggle_syntax_button)
+                entry.toggle_syntax_button.setStyleAttribute('top', '-20px')  # XXX: need to force CSS
+                entry.toggle_syntax_button.setStyleAttribute('left', '-20px')
+
+    def toggleContentSyntax(self):
+        """Toggle the editor between raw and rich text"""
+        original_content = self.bubble.getOriginalContent()
+        rich = not isinstance(self.bubble, richtext.RichTextEditor)
+        if rich:
+            original_content['syntax'] = C.SYNTAX_XHTML
+
+        def setBubble(text):
+            self.content = text
+            self.content_xhtml = text if rich else ''
+            self.content_title = self.content_title_xhtml = ''
+            self.bubble.removeFromParent()
+            self.__setBubble(True)
+            self.bubble.setOriginalContent(original_content)
+            if rich:
+                self.bubble.setDisplayContent()  # needed in case the edition is aborted, to not end with an empty bubble
+
+        text = self.bubble.getContent()['text']
+        if not text:
+            setBubble(' ')  # something different than empty string is needed to initialize the rich text editor
+            return
+        if not rich:
+            def confirm_cb(answer):
+                if answer:
+                    self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT)
+            dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show()
+        else:
+            self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML)
+
+
+class MicroblogPanel(base_widget.LiberviaWidget):
+    warning_msg_public = "This message will be PUBLIC and everybody will be able to see it, even people you don't know"
+    warning_msg_group = "This message will be published for all the people of the group <span class='warningTarget'>%s</span>"
+
+    def __init__(self, host, accepted_groups):
+        """Panel used to show microblog
+        @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts
+        """
+        base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True)
+        self.setAcceptedGroup(accepted_groups)
+        self.host = host
+        self.entries = {}
+        self.comments = {}
+        self.selected_entry = None
+        self.vpanel = VerticalPanel()
+        self.vpanel.setStyleName('microblogPanel')
+        self.setWidget(self.vpanel)
+
+    def refresh(self):
+        """Refresh the display of this widget. If the unibox is disabled,
+        display the 'New message' button or an empty bubble on top of the panel"""
+        if hasattr(self, 'new_button'):
+            self.new_button.setVisible(self.host.uni_box is None)
+            return
+        if self.host.uni_box is None:
+            def addBox():
+                if hasattr(self, 'new_button'):
+                    self.new_button.setVisible(False)
+                data = {'id': str(time()),
+                        'new': True,
+                        'author': self.host.whoami.bare,
+                        }
+                entry = self.addEntry(data)
+                entry.edit(True)
+            if NEW_MESSAGE_USE_BUTTON:
+                self.new_button = Button("New message", listener=addBox)
+                self.new_button.setStyleName("microblogNewButton")
+                self.vpanel.insert(self.new_button, 0)
+            else:
+                addBox()
+
+    @classmethod
+    def registerClass(cls):
+        base_widget.LiberviaWidget.addDropKey("GROUP", cls.createPanel)
+        base_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", cls.createMetaPanel)
+
+    @classmethod
+    def createPanel(cls, host, item):
+        """Generic panel creation for one, several or all groups (meta).
+        @parem host: the SatWebFrontend instance
+        @param item: single group as a string, list of groups
+         (as an array) or None (for the meta group = "all groups")
+        @return: the created MicroblogPanel
+        """
+        _items = item if isinstance(item, list) else ([] if item is None else [item])
+        _type = 'ALL' if _items == [] else 'GROUP'
+        # XXX: pyjamas doesn't support use of cls directly
+        _new_panel = MicroblogPanel(host, _items)
+        host.FillMicroblogPanel(_new_panel)
+        host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, _type, _items, 10)
+        host.setSelected(_new_panel)
+        _new_panel.refresh()
+        return _new_panel
+
+    @classmethod
+    def createMetaPanel(cls, host, item):
+        """Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group"""
+        return MicroblogPanel.createPanel(host, None)
+
+    @property
+    def accepted_groups(self):
+        return self._accepted_groups
+
+    def matchEntity(self, entity):
+        """
+        @param entity: single group as a string, list of groups
+        (as an array) or None (for the meta group = "all groups")
+        @return: True if self matches the given entity
+        """
+        entity = entity if isinstance(entity, list) else ([] if entity is None else [entity])
+        entity.sort()  # sort() do not return the sorted list: do it here, not on the "return" line
+        return self.accepted_groups == entity
+
+    def getWarningData(self, comment=None):
+        """
+        @param comment: True if the composed message is a comment. If None, consider we are
+        composing from the unibox and guess the message type from self.selected_entry
+        @return: a couple (type, msg) for calling self.host.showWarning"""
+        if comment is None:  # composing from the unibox
+            if self.selected_entry and not self.selected_entry.comments:
+                log.error("an item without comment is selected")
+                return ("NONE", None)
+            comment = self.selected_entry is not None
+        if comment:
+            return ("PUBLIC", "This is a <span class='warningTarget'>comment</span> and keep the initial post visibility, so it is potentialy public")
+        elif not self._accepted_groups:
+            # we have a meta MicroblogPanel, we publish publicly
+            return ("PUBLIC", self.warning_msg_public)
+        else:
+            # we only accept one group at the moment
+            # FIXME: manage several groups
+            return ("GROUP", self.warning_msg_group % self._accepted_groups[0])
+
+    def onTextEntered(self, text):
+        if self.selected_entry:
+            # we are entering a comment
+            comments_url = self.selected_entry.comments
+            if not comments_url:
+                raise Exception("ERROR: the comments URL is empty")
+            target = ("COMMENT", comments_url)
+        elif not self._accepted_groups:
+            # we are entering a public microblog
+            target = ("PUBLIC", None)
+        else:
+            # we are entering a microblog restricted to a group
+            # FIXME: manage several groups
+            target = ("GROUP", self._accepted_groups[0])
+        self.host.send([target], text)
+
+    def accept_all(self):
+        return not self._accepted_groups  # we accept every microblog only if we are not filtering by groups
+
+    def getEntries(self):
+        """Ask all the entries for the currenly accepted groups,
+        and fill the panel"""
+
+    def massiveInsert(self, mblogs):
+        """Insert several microblogs at once
+        @param mblogs: dictionary of microblogs, as the result of getMassiveLastGroupBlogs
+        """
+        log.debug("Massive insertion of %d microblogs" % len(mblogs))
+        for publisher in mblogs:
+            log.debug("adding blogs for [%s]" % publisher)
+            for mblog in mblogs[publisher]:
+                if not "content" in mblog:
+                    log.warning("No content found in microblog [%s]", mblog)
+                    continue
+                self.addEntry(mblog)
+
+    def mblogsInsert(self, mblogs):
+        """ Insert several microblogs at once
+        @param mblogs: list of microblogs
+        """
+        for mblog in mblogs:
+            if not "content" in mblog:
+                log.warning("No content found in microblog [%s]", mblog)
+                continue
+            self.addEntry(mblog)
+
+    def _chronoInsert(self, vpanel, entry, reverse=True):
+        """ Insert an entry in chronological order
+        @param vpanel: VerticalPanel instance
+        @param entry: MicroblogEntry
+        @param reverse: more recent entry on top if True, chronological order else"""
+        if entry.empty:
+            entry.published = time()
+        # we look for the right index to insert our entry:
+        # if reversed, we insert the entry above the first entry
+        # in the past
+        idx = 0
+
+        for child in vpanel.children:
+            if not isinstance(child, MicroblogEntry):
+                idx += 1
+                continue
+            if reverse:
+                if child.published < entry.published:
+                    break
+            else:
+                if child.published > entry.published:
+                    break
+            idx += 1
+
+        vpanel.insert(entry, idx)
+
+    def addEntry(self, data):
+        """Add an entry to the panel
+        @param data: dict containing the item data
+        @return: the added entry, or None
+        """
+        _entry = MicroblogEntry(self, data)
+        if _entry.type == "comment":
+            comments_hash = (_entry.service, _entry.node)
+            if not comments_hash in self.comments:
+                # The comments node is not known in this panel
+                return None
+            parent = self.comments[comments_hash]
+            parent_idx = self.vpanel.getWidgetIndex(parent)
+            # we find or create the panel where the comment must be inserted
+            try:
+                sub_panel = self.vpanel.getWidget(parent_idx + 1)
+            except IndexError:
+                sub_panel = None
+            if not sub_panel or not isinstance(sub_panel, VerticalPanel):
+                sub_panel = VerticalPanel()
+                sub_panel.setStyleName('microblogPanel')
+                sub_panel.addStyleName('subPanel')
+                self.vpanel.insert(sub_panel, parent_idx + 1)
+            for idx in xrange(0, len(sub_panel.getChildren())):
+                comment = sub_panel.getIndexedChild(idx)
+                if comment.id == _entry.id:
+                    # update an existing comment
+                    sub_panel.remove(comment)
+                    sub_panel.insert(_entry, idx)
+                    return _entry
+            # we want comments to be inserted in chronological order
+            self._chronoInsert(sub_panel, _entry, reverse=False)
+            return _entry
+
+        if _entry.id in self.entries:  # update
+            idx = self.vpanel.getWidgetIndex(self.entries[_entry.id])
+            self.vpanel.remove(self.entries[_entry.id])
+            self.vpanel.insert(_entry, idx)
+        else:  # new entry
+            self._chronoInsert(self.vpanel, _entry)
+        self.entries[_entry.id] = _entry
+
+        if _entry.comments:
+            # entry has comments, we keep the comments service/node as a reference
+            comments_hash = (_entry.comments_service, _entry.comments_node)
+            self.comments[comments_hash] = _entry
+            self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node)
+
+        return _entry
+
+    def removeEntry(self, type_, id_):
+        """Remove an entry from the panel
+        @param type_: entry type ('main_item' or 'comment')
+        @param id_: entry id
+        """
+        for child in self.vpanel.getChildren():
+            if isinstance(child, MicroblogEntry) and type_ == 'main_item':
+                if child.id == id_:
+                    main_idx = self.vpanel.getWidgetIndex(child)
+                    try:
+                        sub_panel = self.vpanel.getWidget(main_idx + 1)
+                        if isinstance(sub_panel, VerticalPanel):
+                            sub_panel.removeFromParent()
+                    except IndexError:
+                        pass
+                    child.removeFromParent()
+                    self.selected_entry = None
+                    break
+            elif isinstance(child, VerticalPanel) and type_ == 'comment':
+                for comment in child.getChildren():
+                    if comment.id == id_:
+                        comment.removeFromParent()
+                        self.selected_entry = None
+                        break
+
+    def setSelectedEntry(self, entry):
+        try:
+            self.vpanel.getParent().ensureVisible(entry)  # scroll to the clicked entry
+        except AttributeError:
+            log.warning("FIXME: MicroblogPanel.vpanel should be wrapped in a ScrollPanel!")
+        removeStyle = lambda entry: entry.removeStyleName('selected_entry')
+        if not self.host.uni_box or not entry.comments:
+            entry.addStyleName('selected_entry')  # blink the clicked entry
+            clicked_entry = entry  # entry may be None when the timer is done
+            Timer(500, lambda timer: removeStyle(clicked_entry))
+        if not self.host.uni_box:
+            return  # unibox is disabled
+        # from here the previous behavior (toggle main item selection) is conserved
+        entry = entry if entry.comments else None
+        if self.selected_entry == entry:
+            entry = None
+        if self.selected_entry:
+            removeStyle(self.selected_entry)
+        if entry:
+            log.debug("microblog entry selected (author=%s)" % entry.author)
+            entry.addStyleName('selected_entry')
+        self.selected_entry = entry
+
+    def updateValue(self, type_, jid, value):
+        """Update a jid value in entries
+        @param type_: one of 'avatar', 'nick'
+        @param jid: jid concerned
+        @param value: new value"""
+        def updateVPanel(vpanel):
+            for child in vpanel.children:
+                if isinstance(child, MicroblogEntry) and child.author == jid:
+                    child.updateAvatar(value)
+                elif isinstance(child, VerticalPanel):
+                    updateVPanel(child)
+        if type_ == 'avatar':
+            updateVPanel(self.vpanel)
+
+    def setAcceptedGroup(self, group):
+        """Add one or more group(s) which can be displayed in this panel.
+        Prevent from duplicate values and keep the list sorted.
+        @param group: string of the group, or list of string
+        """
+        if not hasattr(self, "_accepted_groups"):
+            self._accepted_groups = []
+        groups = group if isinstance(group, list) else [group]
+        for _group in groups:
+            if _group not in self._accepted_groups:
+                self._accepted_groups.append(_group)
+        self._accepted_groups.sort()
+
+    def isJidAccepted(self, jid):
+        """Tell if a jid is actepted and shown in this panel
+        @param jid: jid
+        @return: True if the jid is accepted"""
+        if self.accept_all():
+            return True
+        for group in self._accepted_groups:
+            if self.host.contact_panel.isContactInGroup(group, jid):
+                return True
+        return False
+
+
+class StatusPanel(HTMLTextEditor):
+
+    EMPTY_STATUS = '&lt;click to set a status&gt;'
+
+    def __init__(self, host, status=''):
+        self.host = host
+        modifiedCb = lambda content: self.host.bridge.call('setStatus', None, self.host.status_panel.presence, content['text']) or True
+        HTMLTextEditor.__init__(self, {'text': status}, modifiedCb, options={'no_xhtml': True, 'listen_focus': True, 'listen_click': True})
+        self.edit(False)
+        self.setStyleName('statusPanel')
+
+    @property
+    def status(self):
+        return self._original_content['text']
+
+    def __cleanContent(self, content):
+        status = content['text']
+        if status == self.EMPTY_STATUS or status in C.PRESENCE.values():
+            content['text'] = ''
+        return content
+
+    def getContent(self):
+        return self.__cleanContent(HTMLTextEditor.getContent(self))
+
+    def setContent(self, content):
+        content = self.__cleanContent(content)
+        BaseTextEditor.setContent(self, content)
+
+    def setDisplayContent(self):
+        status = self._original_content['text']
+        try:
+            presence = self.host.status_panel.presence
+        except AttributeError:  # during initialization
+            presence = None
+        if not status:
+            if presence and presence in C.PRESENCE:
+                status = C.PRESENCE[presence]
+            else:
+                status = self.EMPTY_STATUS
+        self.display.setHTML(addURLToText(status))
+
+
+class PresenceStatusPanel(HorizontalPanel, ClickHandler):
+
+    def __init__(self, host, presence="", status=""):
+        self.host = host
+        HorizontalPanel.__init__(self, Width='100%')
+        self.presence_button = Label(u"◉")
+        self.presence_button.setStyleName("presence-button")
+        self.status_panel = StatusPanel(host, status=status)
+        self.setPresence(presence)
+        entries = {}
+        for value in C.PRESENCE.keys():
+            entries.update({C.PRESENCE[value]: {"value": value}})
+
+        def callback(sender, key):
+            self.setPresence(entries[key]["value"])  # order matters
+            self.host.send([("STATUS", None)], self.status_panel.status)
+
+        self.presence_list = PopupMenuPanel(entries, callback=callback, style={"menu": "gwt-ListBox"})
+        self.presence_list.registerClickSender(self.presence_button)
+
+        panel = HorizontalPanel()
+        panel.add(self.presence_button)
+        panel.add(self.status_panel)
+        panel.setCellVerticalAlignment(self.presence_button, 'baseline')
+        panel.setCellVerticalAlignment(self.status_panel, 'baseline')
+        panel.setStyleName("marginAuto")
+        self.add(panel)
+
+        self.status_panel.edit(False)
+
+        ClickHandler.__init__(self)
+        self.addClickListener(self)
+
+    @property
+    def presence(self):
+        return self._presence
+
+    @property
+    def status(self):
+        return self.status_panel._original_content['text']
+
+    def setPresence(self, presence):
+        self._presence = presence
+        contact.setPresenceStyle(self.presence_button, self._presence)
+
+    def setStatus(self, status):
+        self.status_panel.setContent({'text': status})
+        self.status_panel.setDisplayContent()
+
+    def onClick(self, sender):
+        # As status is the default target of uniBar, we don't want to select anything if click on it
+        self.host.setSelected(None)
+
+
+class ChatPanel(base_widget.LiberviaWidget):
+
+    def __init__(self, host, target, type_='one2one'):
+        """Panel used for conversation (one 2 one or group chat)
+        @param host: SatWebFrontend instance
+        @param target: entity (JID) with who we have a conversation (contact's jid for one 2 one chat, or MUC room)
+        @param type: one2one for simple conversation, group for MUC"""
+        base_widget.LiberviaWidget.__init__(self, host, title=target.bare, selectable=True)
+        self.vpanel = VerticalPanel()
+        self.vpanel.setSize('100%', '100%')
+        self.type = type_
+        self.nick = None
+        if not target:
+            log.error("Empty target !")
+            return
+        self.target = target
+        self.__body = AbsolutePanel()
+        self.__body.setStyleName('chatPanel_body')
+        chat_area = HorizontalPanel()
+        chat_area.setStyleName('chatArea')
+        if type_ == 'group':
+            self.occupants_list = OccupantsList()
+            chat_area.add(self.occupants_list)
+        self.__body.add(chat_area)
+        self.content = AbsolutePanel()
+        self.content.setStyleName('chatContent')
+        self.content_scroll = base_widget.ScrollPanelWrapper(self.content)
+        chat_area.add(self.content_scroll)
+        chat_area.setCellWidth(self.content_scroll, '100%')
+        self.vpanel.add(self.__body)
+        self.vpanel.setCellHeight(self.__body, '100%')
+        self.addStyleName('chatPanel')
+        self.setWidget(self.vpanel)
+        self.state_machine = ChatStateMachine(self.host, str(self.target))
+        self._state = None
+
+    @classmethod
+    def registerClass(cls):
+        base_widget.LiberviaWidget.addDropKey("CONTACT", cls.createPanel)
+
+    @classmethod
+    def createPanel(cls, host, item):
+        _contact = item if isinstance(item, JID) else JID(item)
+        host.contact_panel.setContactMessageWaiting(_contact.bare, False)
+        _new_panel = ChatPanel(host, _contact)  # XXX: pyjamas doesn't seems to support creating with cls directly
+        _new_panel.historyPrint()
+        host.setSelected(_new_panel)
+        _new_panel.refresh()
+        return _new_panel
+
+    def refresh(self):
+        """Refresh the display of this widget. If the unibox is disabled,
+        add a message box at the bottom of the panel"""
+        self.host.contact_panel.setContactMessageWaiting(self.target.bare, False)
+        self.content_scroll.scrollToBottom()
+
+        enable_box = self.host.uni_box is None
+        if hasattr(self, 'message_box'):
+            self.message_box.setVisible(enable_box)
+            return
+        if enable_box:
+            self.message_box = MessageBox(self.host)
+            self.message_box.onSelectedChange(self)
+            self.vpanel.add(self.message_box)
+
+    def matchEntity(self, entity):
+        """
+        @param entity: target jid as a string or JID instance.
+        Could also be a couple with a type in the second element.
+        @return: True if self matches the given entity
+        """
+        if isinstance(entity, tuple):
+            entity, type_ = entity if len(entity) > 1 else (entity[0], self.type)
+        else:
+            type_ = self.type
+        entity = entity if isinstance(entity, JID) else JID(entity)
+        try:
+            return self.target.bare == entity.bare and self.type == type_
+        except AttributeError as e:
+            e.include_traceback()
+            return False
+
+    def getWarningData(self):
+        if self.type not in ["one2one", "group"]:
+            raise Exception("Unmanaged type !")
+        if self.type == "one2one":
+            msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % self.target
+        elif self.type == "group":
+            msg = "This message will be sent to all the participants of the multi-user room <span class='warningTarget'>%s</span>" % self.target
+        return ("ONE2ONE" if self.type == "one2one" else "GROUP", msg)
+
+    def onTextEntered(self, text):
+        self.host.send([("groupchat" if self.type == 'group' else "chat", str(self.target))], text)
+        self.state_machine._onEvent("active")
+
+    def onQuit(self):
+        base_widget.LiberviaWidget.onQuit(self)
+        if self.type == 'group':
+            self.host.bridge.call('mucLeave', None, self.target.bare)
+
+    def setUserNick(self, nick):
+        """Set the nick of the user, usefull for e.g. change the color of the user"""
+        self.nick = nick
+
+    def setPresents(self, nicks):
+        """Set the users presents in this room
+        @param occupants: list of nicks (string)"""
+        self.occupants_list.clear()
+        for nick in nicks:
+            self.occupants_list.addOccupant(nick)
+
+    def userJoined(self, nick, data):
+        self.occupants_list.addOccupant(nick)
+        self.printInfo("=> %s has joined the room" % nick)
+
+    def userLeft(self, nick, data):
+        self.occupants_list.removeOccupant(nick)
+        self.printInfo("<= %s has left the room" % nick)
+
+    def changeUserNick(self, old_nick, new_nick):
+        assert(self.type == "group")
+        self.occupants_list.removeOccupant(old_nick)
+        self.occupants_list.addOccupant(new_nick)
+        self.printInfo(_("%(old_nick)s is now known as %(new_nick)s") % {'old_nick': old_nick, 'new_nick': new_nick})
+
+    def historyPrint(self, size=20):
+        """Print the initial history"""
+        def getHistoryCB(history):
+            # display day change
+            day_format = "%A, %d %b %Y"
+            previous_day = datetime.now().strftime(day_format)
+            for line in history:
+                timestamp, from_jid, to_jid, message, mess_type, extra = line
+                message_day = datetime.fromtimestamp(float(timestamp or time())).strftime(day_format)
+                if previous_day != message_day:
+                    self.printInfo("* " + message_day)
+                    previous_day = message_day
+                self.printMessage(from_jid, message, extra, timestamp)
+        self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, self.target.bare, size, True)
+
+    def printInfo(self, msg, type_='normal', link_cb=None):
+        """Print general info
+        @param msg: message to print
+        @param type_: one of:
+            "normal": general info like "toto has joined the room"
+            "link": general info that is clickable like "click here to join the main room"
+            "me": "/me" information like "/me clenches his fist" ==> "toto clenches his fist"
+        @param link_cb: method to call when the info is clicked, ignored if type_ is not 'link'
+        """
+        _wid = HTML(msg) if type_ == 'link' else Label(msg)
+        if type_ == 'normal':
+            _wid.setStyleName('chatTextInfo')
+        elif type_ == 'link':
+            _wid.setStyleName('chatTextInfo-link')
+            if link_cb:
+                _wid.addClickListener(link_cb)
+        elif type_ == 'me':
+            _wid.setStyleName('chatTextMe')
+        else:
+            _wid.setStyleName('chatTextInfo')
+        self.content.add(_wid)
+
+    def printMessage(self, from_jid, msg, extra, timestamp=None):
+        """Print message in chat window. Must be implemented by child class"""
+        _jid = JID(from_jid)
+        nick = _jid.node if self.type == 'one2one' else _jid.resource
+        mymess = _jid.resource == self.nick if self.type == "group" else _jid.bare == self.host.whoami.bare  # mymess = True if message comes from local user
+        if msg.startswith('/me '):
+            self.printInfo('* %s %s' % (nick, msg[4:]), type_='me')
+            return
+        self.content.add(ChatText(timestamp, nick, mymess, msg, extra.get('xhtml')))
+        self.content_scroll.scrollToBottom()
+
+    def startGame(self, game_type, waiting, referee, players, *args):
+        """Configure the chat window to start a game"""
+        classes = {"Tarot": CardPanel, "RadioCol": RadioColPanel}
+        if game_type not in classes.keys():
+            return  # unknown game
+        attr = game_type.lower()
+        self.occupants_list.updateSpecials(players, SYMBOLS[attr])
+        if waiting or not self.nick in players:
+            return  # waiting for player or not playing
+        attr = "%s_panel" % attr
+        if hasattr(self, attr):
+            return
+        log.info("%s Game Started \o/" % game_type)
+        panel = classes[game_type](self, referee, self.nick, players, *args)
+        setattr(self, attr, panel)
+        self.vpanel.insert(panel, 0)
+        self.vpanel.setCellHeight(panel, panel.getHeight())
+
+    def getGame(self, game_type):
+        """Return class managing the game type"""
+        # TODO: check that the game is launched, and manage errors
+        if game_type == "Tarot":
+            return self.tarot_panel
+        elif game_type == "RadioCol":
+            return self.radiocol_panel
+
+    def setState(self, state, nick=None):
+        """Set the chat state (XEP-0085) of the contact. Leave nick to None
+        to set the state for a one2one conversation, or give a nickname or
+        C.ALL_OCCUPANTS to set the state of a participant within a MUC.
+        @param state: the new chat state
+        @param nick: None for one2one, the MUC user nick or ALL_OCCUPANTS
+        """
+        if nick:
+            assert(self.type == 'group')
+            occupants = self.occupants_list.occupants_list.keys() if nick == C.ALL_OCCUPANTS else [nick]
+            for occupant in occupants:
+                self.occupants_list.occupants_list[occupant].setState(state)
+        else:
+            assert(self.type == 'one2one')
+            self._state = state
+            self.refreshTitle()
+        self.state_machine.started = not not state  # start to send "composing" state from now
+
+    def refreshTitle(self):
+        """Refresh the title of this ChatPanel dialog"""
+        if self._state:
+            self.setTitle(self.target.bare + " (" + self._state + ")")
+        else:
+            self.setTitle(self.target.bare)
+
+
+class WebPanel(base_widget.LiberviaWidget):
+    """ (mini)browser like widget """
+
+    def __init__(self, host, url=None):
+        """
+        @param host: SatWebFrontend instance
+        """
+        base_widget.LiberviaWidget.__init__(self, host)
+        self._vpanel = VerticalPanel()
+        self._vpanel.setSize('100%', '100%')
+        self._url = dialog.ExtTextBox(enter_cb=self.onUrlClick)
+        self._url.setText(url or "")
+        self._url.setWidth('100%')
+        hpanel = HorizontalPanel()
+        hpanel.add(self._url)
+        btn = Button("Go", self.onUrlClick)
+        hpanel.setCellWidth(self._url, "100%")
+        #self.setCellWidth(btn, "10%")
+        hpanel.add(self._url)
+        hpanel.add(btn)
+        self._vpanel.add(hpanel)
+        self._vpanel.setCellHeight(hpanel, '20px')
+        self._frame = Frame(url or "")
+        self._frame.setSize('100%', '100%')
+        DOM.setStyleAttribute(self._frame.getElement(), "position", "relative")
+        self._vpanel.add(self._frame)
+        self.setWidget(self._vpanel)
+
+    def onUrlClick(self, sender):
+        self._frame.setUrl(self._url.getText())
+
+
+class MainPanel(AbsolutePanel):
+
+    def __init__(self, host):
+        self.host = host
+        AbsolutePanel.__init__(self)
+
+        # menu
+        self.menu = Menu(host)
+
+        # unibox
+        self.unibox_panel = UniBoxPanel(host)
+        self.unibox_panel.setVisible(False)
+
+        # contacts
+        self._contacts = HorizontalPanel()
+        self._contacts.addStyleName('globalLeftArea')
+        self.contacts_switch = Button(u'«', self._contactsSwitch)
+        self.contacts_switch.addStyleName('contactsSwitch')
+        self._contacts.add(self.contacts_switch)
+        self._contacts.add(self.host.contact_panel)
+
+        # tabs
+        self.tab_panel = base_widget.MainTabPanel(host)
+        self.discuss_panel = base_widget.WidgetsPanel(self.host, locked=True)
+        self.tab_panel.add(self.discuss_panel, "Discussions")
+        self.tab_panel.selectTab(0)
+
+        self.header = AbsolutePanel()
+        self.header.add(self.menu)
+        self.header.add(self.unibox_panel)
+        self.header.add(self.host.status_panel)
+        self.header.setStyleName('header')
+        self.add(self.header)
+
+        self._hpanel = HorizontalPanel()
+        self._hpanel.add(self._contacts)
+        self._hpanel.add(self.tab_panel)
+        self.add(self._hpanel)
+
+        self.setWidth("100%")
+        Window.addWindowResizeListener(self)
+
+    def _contactsSwitch(self, btn=None):
+        """ (Un)hide contacts panel """
+        if btn is None:
+            btn = self.contacts_switch
+        cpanel = self.host.contact_panel
+        cpanel.setVisible(not cpanel.getVisible())
+        btn.setText(u"«" if cpanel.getVisible() else u"»")
+        self.host.resize()
+
+    def _contactsMove(self, parent):
+        """Move the contacts container (containing the contact list and
+        the "hide/show" button) to another parent, but always as the
+        first child position (insert at index 0).
+        """
+        if self._contacts.getParent():
+            if self._contacts.getParent() == parent:
+                return
+            self._contacts.removeFromParent()
+        parent.insert(self._contacts, 0)
+
+    def onWindowResized(self, width, height):
+        _elts = doc().getElementsByClassName('gwt-TabBar')
+        if not _elts.length:
+            tab_bar_h = 0
+        else:
+            tab_bar_h = _elts.item(0).offsetHeight
+        ideal_height = Window.getClientHeight() - tab_bar_h
+        self.setHeight("%s%s" % (ideal_height, "px"))
+
+    def refresh(self):
+        """Refresh the main panel"""
+        self.unibox_panel.refresh()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/plugin_xep_0085.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,82 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# SAT plugin for Chat State Notifications Protocol (xep-0085)
+# Copyright (C) 2013, 2014 Adrien Cossa (souliane@mailoo.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/>.
+
+from pyjamas.Timer import Timer
+
+
+# Copy of the map from sat/src/plugins/plugin_xep_0085
+TRANSITIONS = {"active": {"next_state": "inactive", "delay": 120},
+               "inactive": {"next_state": "gone", "delay": 480},
+               "gone": {"next_state": "", "delay": 0},
+               "composing": {"next_state": "paused", "delay": 30},
+               "paused": {"next_state": "inactive", "delay": 450}
+               }
+
+
+class ChatStateMachine:
+    """This is an adapted version of the ChatStateMachine from sat/src/plugins/plugin_xep_0085
+    which manage a timer on the web browser and keep it synchronized with the timer that runs
+    on the backend. This is only needed to avoid calling the bridge method chatStateComposing
+    too often ; accuracy is not needed so we can ignore the delay of the communication between
+    the web browser and the backend (the timer on the web browser always starts a bit before).
+    /!\ Keep this file up to date if you modify the one in the sat plugins directory.
+    """
+    def __init__(self, host, target_s):
+
+        self.host = host
+        self.target_s = target_s
+        self.started = False
+        self.state = None
+        self.timer = None
+
+    def _onEvent(self, state):
+        """Pyjamas callback takes no extra argument so we need this trick"""
+        # Here we should check the value of the parameter "Send chat state notifications"
+        # but this costs two messages. It's even better to call chatStateComposing
+        # with a doubt, it will be checked by the back-end anyway before sending
+        # the actual notifications to the other client.
+        if state == "composing" and not self.started:
+            return
+        self.started = True
+        self.next_state = state
+        self.__onEvent(None)
+
+    def __onEvent(self, timer):
+        # print "on event %s" % self.next_state
+        state = self.next_state
+        self.next_state = ""
+        if state != self.state and state == "composing":
+            self.host.bridge.call('chatStateComposing', None, self.target_s)
+        self.state = state
+        if not self.timer is None:
+            self.timer.cancel()
+
+        if not state in TRANSITIONS:
+            return
+        if not "next_state" in TRANSITIONS[state]:
+            return
+        if not "delay" in TRANSITIONS[state]:
+            return
+        next_state = TRANSITIONS[state]["next_state"]
+        delay = TRANSITIONS[state]["delay"]
+        if next_state == "" or delay < 0:
+            return
+        self.next_state = next_state
+        # pyjamas timer in milliseconds
+        self.timer = Timer(delay * 1000, self.__onEvent)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/public/contrat_social.html	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,110 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html><head>
+  
+  <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type">
+  <title>Salut  Toi: Contrat Social</title>
+
+  
+</head><body>
+Le projet  Salut  Toi  est n d'un besoin de protection de nos
+liberts, de notre vie prive et de notre indpendance. Il se veut
+garant des droits et liberts qu'un utilisateur a vis  vis de ses
+propres informations, des informations numriques sur sa vie ou celles
+de ses connaissances, des donnes qu'il manipule; et se veut galement
+un point de contact humain, ne se substituant pas aux rapports rels,
+mais au contraire les facilitant.<br>
+
+Salut  Toi lutte et luttera toujours contre toute forme de main mise
+sur les technologies par des intrts privs. Le rseau global doit
+appartenir  tous, et tre un point d'expression et de libert pour
+l'Humanit.<br>
+
+<br>
+
+ ce titre,  Salut  Toi  et ceux qui y participent se basent sur un
+contrat social, un engagement vis  vis de ceux qui l'utilisent. Ce
+contrat consiste en les points suivants:<br>
+
+<ul>
+
+  <li>nous plaons la <span style="font-style: italic;">Libert</span> en tte de nos priorits: libert de
+l'utilisateur, libert vis  vis de ses donnes. Pour cela,  Salut 
+Toi  est un logiciel Libre - condition essentielle -, et son
+infrastructure se base galement sur des logiciels Libres, c'est  dire
+des logiciels qui respectent ces 4 liberts fondamentales
+    <ul>
+
+    <li>la libert d'excuter le programme, pour tous les usages,</li>
+  
+    </ul>
+    <ul>
+
+    <li>la libert d'tudier le fonctionnement du programme et de
+l'adapter  ses besoins,</li>
+  
+    </ul>
+    <ul>
+
+    <li>la libert de redistribuer des copies du programme,</li>
+  
+    </ul>
+    <ul>
+
+    <li>la libert d'amliorer le programme et de distribuer ces
+amliorations au public.<br>
+</li>
+  
+    </ul>
+</li>
+  
+  
+  
+  
+
+Vous avez ainsi la possibilit d'installer votre propre version de 
+Salut  Toi  sur votre propre machine, d'en vrifier - et de
+comprendre - ainsi son fonctionnement, de l'adapter  vos besoins, d'en
+faire profiter vos amis.
+
+  <li>Les informations vous concernant vous appartiennent, et nous
+n'aurons pas la prtention - et l'indcence ! - de considrer le
+contenu que vous produisez ou faites circuler via  Salut  Toi  comme
+nous appartenant. De mme, nous nous engageons  ne jamais faire de
+profit en revendant vos informations personnelles.</li>
+  <li>Nous incitons fortement  la <span style="text-decoration: underline;">dcentralisation gnralise</span>. 
+Salut  Toi  tant bas sur un protocole dcentralis (XMPP), il l'est
+lui-mme par nature. La dcentralisation est essentielle pour une
+meilleure protection de vos informations, une meilleure rsistance  la
+censure ou aux pannes, et pour viter les drives autoritaires.</li>
+  <li>Luttant contre les tentatives de contrle priv et les abus
+commerciaux du rseau global, et afin de garder notre indpendance,
+nous nous refusons  toute forme de publicit: vous ne verrez <span style="font-weight: bold;">jamais</span>
+de forme de rclame commerciale de notre fait.</li>
+  <li>L'<span style="font-style: italic;">galit</span> des utilisateurs est essentielle pour nous, nous
+refusons toute forme de discrimination, que ce soit pour une zone
+gographique, une catgorie de la population, ou tout autre raison.</li>
+  <li>Nous ferons tout notre possible pour lutter contre toute
+tentative de censure. Le rseau global doit tre un moyen d'expression
+pour tous.</li>
+  <li>Nous refusons toute ide d'autorit absolue en ce qui concerne
+les dcisions prises pour  Salut  Toi  et son fonctionnement, et le
+choix de la dcentralisation et l'utilisation de logiciel Libre permet
+de lutter contre toute forme de hirarchie.</li>
+  
+  <li>L'ide de <span style="font-style: italic;">Fraternit</span> est essentielle, aussi:
+    <ul>
+      <li>nous ferons notre
+possible pour aider les utilisateurs, quel que soit leur niveau</li>
+      <li>de mme, des efforts seront fait quant 
+l'accessibilit aux personnes victimes d'un handicap</li>
+      <li> Salut  Toi ,
+XMPP, et les technologies utilises facilitent les changes
+lectroniques, mais nous dsirons mettre l'accent sur les rencontres
+relles et humaines: nous favoriserons toujours le rel sur le virtuel.</li>
+    </ul>
+</li>
+  
+  
+</ul>
+
+</body></html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/public/libervia.css	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,1580 @@
+/*
+Libervia: a Salut à Toi frontend
+Copyright (C) 2011, 2012, 2013, 2014  Jérôme Poisson <goffi@goffi.org>
+Copyright (C) 2011  Adrien Vigneron <adrienvigneron@mailoo.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/>.
+*/
+
+
+/*
+ * CSS Reset: see http://pyjs.org/wiki/csshellandhowtodealwithit/
+ */
+
+/* reset/default styles */
+
+html, body, div, span, applet, object, iframe,
+p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, font, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center, dl, dt, dd, li,
+fieldset, form, label, legend, table, caption,
+tbody, tfoot, thead, tr, th, td {
+    margin: 0;
+    padding: 0;
+    border: 0;
+    outline: 0;
+    font-size: 100%;
+    vertical-align: baseline;
+    background: transparent;
+    color: #444;
+}
+
+/* styles for displaying rich text - START */
+h1, h2, h3, h4, h5, h6 {
+    margin: 0;
+    padding: 0;
+    border: 0;
+    outline: 0;
+    vertical-align: baseline;
+    background: transparent;
+    color: #444;
+    border-bottom: 1px solid rgb(170, 170, 170);
+    margin-bottom: 0.6em;
+}
+ol, ul {
+    margin: 0;
+    border: 0;
+    outline: 0;
+    font-size: 100%;
+    vertical-align: baseline;
+    background: transparent;
+    color: #444;
+}
+a:link {
+    color: blue;
+}
+.bubble p {
+    margin: 0.4em 0em;
+}
+.bubble img {
+    /* /!\ setting a max-width percentage value affects the toolbar icons */
+    max-width: 600px;
+}
+
+/* styles for displaying rich text - END */
+
+blockquote, q { quotes: none; }
+
+blockquote:before, blockquote:after,
+q:before, q:after {
+    content: '';
+    content: none;
+}
+
+:focus { outline: 0; }
+ins { text-decoration: none; }
+del { text-decoration: line-through; }
+
+table {
+    border-collapse: collapse;
+    border-spacing: 0;
+}
+
+/* pyjamas iframe hide */
+iframe { position: absolute; }
+
+
+html, body {
+    width: 100%;
+    height: 100%;
+    min-height: 100%;
+
+}
+
+body {
+    line-height: 1em;
+    font-size: 1em;
+    overflow: auto;
+
+}
+
+.scrollpanel {
+   margin-bottom: -10000px;
+
+}
+
+.iescrollpanelfix {
+   position: relative;
+   top: 100%;
+   margin-bottom: -10000px;
+
+}
+
+/* undo part of the above (non-IE) */
+html>body .iescrollpanelfix { position: static; }
+
+/* CSS Reset END */
+
+body {
+    background-color: #fff;
+    font: normal 0.8em/1.5em Arial, Helvetica, sans-serif;
+}
+
+.header {
+    background-color: #eee;
+    border-bottom: 1px solid #ddd;
+}
+
+/* Misc Pyjamas stuff */
+
+.menuContainer {
+    margin: 0 32px 0 20px;   
+}
+
+.gwt-MenuBar,.gwt-MenuBar-horizontal {
+  /*background-color: #01FF78;
+  border: 1px solid #87B3FF;
+  cursor: default;*/
+    width: 100%;
+    height: 28px;
+    margin: 0;
+    padding: 5px 5px 0 5px;    
+    line-height: 100%;
+    box-shadow: 0px 1px 4px #000;
+    -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
+    -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
+    border: 1px solid #ddd;
+    border-radius: 0 0 1em 1em;
+    -webkit-border-radius: 0 0 1em 1em;
+    -moz-border-radius: 0 0 1em 1em;
+    background-color: #222;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’444444′, endColorstr=’#222222’);
+    background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222));
+    background: -moz-linear-gradient(top, #444444, #222222);
+    background-image: -o-linear-gradient(#444444,#222222);
+    display: inline-block;
+}
+
+.gwt-MenuBar-horizontal .gwt-MenuItem {
+    text-decoration: none;    
+    font-weight: bold;
+    height: 100%;
+    color: #e7e5e5;
+    padding: 3px 15px;
+    /*display: block;*/
+    border-radius: 1em 1em 1em 1em;
+    -webkit-border-radius: 1em 1em 1em 1em;
+    -moz-border-radius: 1em 1em 1em 1em;
+    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
+    -webkit-transition: color 0.2s linear; 
+    -moz-transition: color 0.2s linear; 
+    -o-transition: color 0.2s linear;
+}
+
+.gwt-MenuItem img {
+    padding-right: 2px;
+}
+
+.gwt-MenuBar-horizontal .gwt-MenuItem-selected {
+     background-color: #eee;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#eee’, endColorstr=’#aaa′);
+    background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa));
+    background: -moz-linear-gradient(top, #eee, #aaa);
+    background-image: -o-linear-gradient(#eee,#aaa);
+    color: #444;
+    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);    
+    cursor: pointer;
+}
+
+.menuSeparator {
+    width: 100%;
+}
+
+.menuSeparator.gwt-MenuItem-selected {
+    border: 0;
+    background: inherit;
+    cursor: default;
+}
+
+.gwt-MenuBar {
+    background-color: #fff;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#fff’, endColorstr=’#ccc’);
+    background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc));
+    background: -moz-linear-gradient(top, #fff, #ccc);
+    background-image: -o-linear-gradient(#fff,#ccc);
+    /*display: none;*/
+    height: 100%;
+    min-width: 148px;
+    margin: 0;
+    padding: 0;
+    /*min-width: 148px;
+    top: 28px;*/
+    border: solid 1px #aaa;
+    -webkit-border-radius: 0 0 10px 10px;
+    -moz-border-radius: 0 0 10px 10px;
+    border-radius: 0 0 10px 10px;
+    -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
+    -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
+    box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
+}
+
+.gwt-MenuBar table {
+    width: 100%;
+    display: inline-table;
+}
+
+.gwt-MenuBar .gwt-MenuItem {
+    padding: 8px 15px;
+}
+
+
+.gwt-MenuBar .gwt-MenuItem-selected {
+    background: #cf2828 !important;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828’, endColorstr=’#981a1a’) !important;
+    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)) !important;
+    background: -moz-linear-gradient(top, #cf2828, #981a1a) !important;
+    background-image: -o-linear-gradient(#cf2828,#981a1a) !important;
+    color: #fff !important;
+    -webkit-border-radius: 0 0 0 0;
+    -moz-border-radius: 0 0 0 0;
+    border-radius: 0 0 0 0;
+    text-shadow: 0 1px 1px rgba(0, 0, 0, .1);
+    transition: color 0.2s linear;
+    -webkit-transition: color 0.2s linear; 
+    -moz-transition: color 0.2s linear; 
+    -o-transition: color 0.2s linear; 
+    cursor: pointer;
+}
+
+/*.menuLastPopup div tr:first-child td{
+    border-radius: 0 0 9px 9px !important; 
+    -webkit-border-radius: 0 0 9px 9px !important; 
+    -moz-border-radius: 0 0 9px 9px !important; 
+}*/
+
+.gwt-MenuBar tr:last-child td {
+    border-radius: 0 0 9px 9px !important;
+    -webkit-border-radius: 0 0 9px 9px !important;
+    -moz-border-radius: 0 0 9px 9px !important;
+}
+
+
+.menuLastPopup .gwt-MenuBar {
+    border-top-right-radius: 9px 9px 9px 9px;
+    -webkit-border-top-right-radius: 9px 9px 9px 9px;
+    -moz-border-top-right-radius: 9px 9px 9px 9px;
+}
+
+.gwt-AutoCompleteTextBox {
+  width: 80%;
+  border: 1px solid #87B3FF;
+  margin-top: 20px;
+}
+.gwt-DialogBox {
+  padding: 10px;
+  border: 1px solid #aaa;
+  background-color: #fff;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#fff’, endColorstr=’#ccc’);
+  background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc));
+  background: -moz-linear-gradient(top, #fff, #ccc);
+  background-image: -o-linear-gradient(#fff,#ccc);
+  border-radius: 9px 9px 9px 9px; 
+  -webkit-border-radius: 9px 9px 9px 9px; 
+  -moz-border-radius: 9px 9px 9px 9px;
+  box-shadow: 0px 1px 4px #000;
+  -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
+  -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); 
+}
+
+.gwt-DialogBox .Caption {
+  height: 20px;
+  font-size: 1.3em !important;
+  background-color: #cf2828;
+  background: #cf2828 !important;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828’, endColorstr=’#981a1a’);
+  background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)) !important;
+  background: -moz-linear-gradient(top, #cf2828, #981a1a) !important;
+  background-image: -o-linear-gradient(#cf2828,#981a1a);
+  color: #fff;
+  padding: 3px 3px 4px 3px;
+  margin: -10px;
+  margin-bottom: 5px;
+  font-weight: bold;
+  cursor: default;
+  text-align: center;
+  border-radius: 7px 7px 0 0; 
+  -webkit-border-radius: 7px 7px 0 0; 
+  -moz-border-radius: 7px 7px 0 0;
+}
+
+/*DIALOG: button, listbox, textbox, label */
+
+.gwt-DialogBox .gwt-button {
+    background-color: #ccc;
+    border-radius: 5px 5px 5px 5px;
+    -webkit-border-radius: 5px 5px 5px 5px;
+    -moz-border-radius: 5px 5px 5px 5px;
+    box-shadow: 0px 1px 4px #000;
+    -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6);
+    -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6);
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#444&#888;, endColorstr=’#222’); 
+    background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222)); 
+    background: -moz-linear-gradient(top, #444, #222); 
+    background-image: -o-linear-gradient(#444,#222);
+	 text-shadow: 1px 1px 1px rgba(0,0,0,0.2);
+	 padding: 3px 5px 3px 5px;
+	 margin: 10px 5px 10px 5px;
+	 color: #fff;
+	 font-weight: bold;
+	 font-size: 1em;
+	 border: none;
+	 -webkit-transition: color 0.2s linear; 
+    -moz-transition: color 0.2s linear; 
+    -o-transition: color 0.2s linear;
+}
+
+.gwt-DialogBox .gwt-button:hover {
+	background-color: #cf2828;
+	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828&#888;, endColorstr=’#981a1a’); 
+   background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); 
+   background: -moz-linear-gradient(top, #cf2828, #981a1a); 
+   background-image: -o-linear-gradient(#cf2828,#981a1a);
+   color: #fff;
+   text-shadow: 1px 1px 1px rgba(0,0,0,0.25);  
+}
+
+.gwt-DialogBox .gwt-TextBox {
+	 background-color: #fff;
+    border-radius: 5px 5px 5px 5px;
+    -webkit-border-radius: 5px 5px 5px 5px;
+    -moz-border-radius: 5px 5px 5px 5px;
+    box-shadow:inset 0px 1px 4px #000;
+    -webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.6);
+    -moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.6);
+	 padding: 3px 5px 3px 5px;
+	 margin: 10px 5px 10px 5px;
+	 color: #444;
+	 font-size: 1em;
+	 border: none;
+}
+
+.gwt-DialogBox .gwt-ListBox {
+    overflow: auto;
+    width: 100%;
+    background-color: #fff;
+    border-radius: 5px 5px 5px 5px;
+    -webkit-border-radius: 5px 5px 5px 5px;
+    -moz-border-radius: 5px 5px 5px 5px;
+    box-shadow:inset 0px 1px 4px #000;
+    -webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.6);
+    -moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.6);
+	 padding: 3px 5px 3px 5px;
+	 margin: 10px 5px 10px 5px;
+	 color: #444;
+	 font-size: 1em;
+	 border: none;
+}
+
+.gwt-DialogBox .gwt-Label {
+	margin-top: 13px;
+}
+
+/* Custom Dialogs */
+
+.formWarning { /* used when a form is not valid and must be corrected before submission */
+    font-weight: bold;
+    color: red !important;
+}
+
+.contactsChooser {
+    text-align: center;
+    margin:auto;
+    cursor: pointer;
+}
+
+.infoDialogBody {
+    width: 100%;
+    height: 100%
+}
+/* Contact List */
+
+div.contactBox {
+    width: 100%;
+	/* We want the contact panel to not use all the available height when displayed
+	   in the unibox panel (grey part), because the dialogs panels (white part) should
+	   still be visible. The setting max-height: fit-content would be appropriate here
+	   but it doesn't work with firefox 24.0. TODO: check if the current setting works
+	   with other browsers... the panel should of course not be displayed on 100px
+	   but exactly fit the contacts box.
+     */
+	max-height: 100px;
+}
+
+.contactTitle {
+    color: #cf2828;
+    font-size: 1.7em;
+    text-indent: 5px;
+    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+    width: 200px;
+    height: 30px; 
+}
+
+.contactsSwitch {
+    /* Button used to switch contacts panel */
+    background: none;
+    border: 0;
+    padding: 0;
+    font-size: large;
+}
+
+.groupList {
+    width: 100%;    
+}
+
+.groupList tr:first-child td {
+    padding-top: 10px;
+}
+
+.group {
+    padding: 2px 15px;
+    margin: 5px;
+    display: inline-block;
+    text-decoration: none;     
+    font-weight: bold; 
+    color: #e7e5e5;
+    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); 
+    border-radius: 1em 1em 1em 1em; 
+    -webkit-border-radius: 1em 1em 1em 1em; 
+    -moz-border-radius: 1em 1em 1em 1em;
+    background-color: #eee; 
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#eee’, endColorstr=’#aaa&#888;); 
+    background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); 
+    background: -moz-linear-gradient(top, #eee, #aaa); 
+    background-image: -o-linear-gradient(#eee,#aaa);
+    color: #444;
+    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
+    box-shadow: 0px 1px 1px #000;
+    -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6);
+    -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6);
+}
+
+div.group:hover {
+    color: #fff;
+    text-shadow: 0 1px 0 rgba(0, 0, 0, 0.6);
+    background-color: #cf2828;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828&#888;, endColorstr=’#981a1a’); 
+    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); 
+    background: -moz-linear-gradient(top, #cf2828, #981a1a); 
+    background-image: -o-linear-gradient(#cf2828,#981a1a);
+    -webkit-transition: color 0.1s linear; 
+   -moz-transition: color 0.1s linear; 
+   -o-transition: color 0.1s linear;  
+}
+.contact {
+    font-size: 1em;
+    margin-top: 3px;
+    padding: 3px 10px 3px 10px;
+}
+
+.contact-menu-selected {
+    font-size: 1em;
+    margin-top: 3px;
+    padding: 3px 10px 3px 10px;
+    border-radius: 5px;
+	background-color: rgb(175, 175, 175);
+}
+
+/* START - contact presence status */
+.contact-connected {
+    color: #3c7e0c;
+    font-weight: bold;
+}
+.contact-unavailable {
+}
+.contact-chat {
+    color: #3c7e0c;
+    font-weight: bold;
+}
+.contact-away {
+    color: brown;
+    font-weight: bold;
+}
+.contact-dnd {
+    color: red;
+    font-weight: bold;
+}
+.contact-xa {
+    color: red;
+    font-weight: bold;
+}
+/* END - contact presence status */
+
+.selected {
+    color: #fff;
+    background-color: #cf2828;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828&#888;, endColorstr=’#981a1a’); 
+    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); 
+    background: -moz-linear-gradient(top, #cf2828, #981a1a); 
+    background-image: -o-linear-gradient(#cf2828,#981a1a);
+    border-radius: 1em 1em 1em 1em; 
+    -webkit-border-radius: 1em 1em 1em 1em; 
+    -moz-border-radius: 1em 1em 1em 1em;
+    -webkit-transition: color 0.2s linear; 
+    -moz-transition: color 0.2s linear; 
+    -o-transition: color 0.2s linear;
+}
+
+.messageBox {
+    width: 100%;
+    padding: 5px;
+    border: 1px solid #bbb;
+    color: #444;
+    background: #fff url('media/libervia/unibox_2.png') top bottom no-repeat;
+    box-shadow:inset 0 0 10px #ddd;
+    -webkit-box-shadow:inset 0 0 10px #ddd;
+    -moz-box-shadow:inset 0 0 10px #ddd;
+    border-radius: 0px 0px 10px 10px;
+    height: 25px;
+    margin: 0px;
+}
+
+/* UniBox & Status */
+
+.uniBoxPanel {
+    margin: 15px 22px 0 22px;
+}
+
+.uniBox {
+    width: 100%;
+    height: 45px;
+    padding: 5px;
+    border: 1px solid #bbb;
+    color: #444;
+    background: #fff url('media/libervia/unibox_2.png') top right no-repeat;    
+    box-shadow:inset 0 0 10px #ddd;
+    -webkit-box-shadow:inset 0 0 10px #ddd; 
+    -moz-box-shadow:inset 0 0 10px #ddd;
+}
+
+.uniBoxButton {
+    width:30px;
+    height:45px;
+}
+
+.statusPanel {
+    margin: auto;
+    text-align: center;
+    width: 100%;
+    padding: 5px 0px;
+    text-shadow: 0 -1px 1px rgba(255,255,255,0.25);
+    font-size: 1.2em;
+    background-color: #eee;
+    font-style: italic;
+    font-weight: bold;
+    color: #666;
+    cursor: pointer;
+}
+
+.presence-button {
+	font-size: x-large;
+	padding-right: 5px;
+	cursor: pointer;
+}
+
+/* RegisterBox */
+
+.registerPanel_main button {
+    margin: 0;
+    padding: 0;
+    border: 0;
+}
+
+.registerPanel_main div, .registerPanel_main button {
+    color: #fff;
+    text-decoration: none;
+}
+
+.registerPanel_main{
+    height: 100%;
+    border: 5px solid #222;
+    box-shadow: 0px 1px 4px #000;
+    -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+    -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+}
+
+.registerPanel_tabs .gwt-Label {
+    margin: 15px 7.5px 0px 7.5px;
+    cursor: pointer;
+    font-size: larger;
+}
+
+.registerPanel_tabs .gwt-TabBarItem div {
+    color: #444;
+    padding: 5px 7.5px;
+    border-radius: 5px 5px 0px 5px;
+    box-shadow: inset 0px 0px 2px 1px #9F2828;
+}
+
+.registerPanel_tabs .gwt-TabBarItem div:hover {
+    color: #fff;
+    box-shadow: inset 0px 0px 2px 2px #9F2828;
+}
+
+.registerPanel_tabs .gwt-TabBarItem-selected div {
+    color: #fff;
+    box-shadow: inset 0px 0px 2px 2px #9F2828;
+}
+
+.registerPanel_tabs .gwt-TabBarRest {
+    border-bottom: 1px #3F1818 dashed;
+}
+
+.registerPanel_right_side {
+   background: #111 url('media/libervia/register_right.png');
+   height: 100%;
+   width: 100%;
+}
+.registerPanel_content {
+   margin-left: 50px;
+   margin-top: 30px;
+}
+
+.registerPanel_content div {
+   font-size: 1em;
+   margin-left: 10px;
+   margin-top: 15px;
+   font-style: bold;
+   color: #888;
+}
+
+.registerPanel_content input {
+   height: 25px;
+   line-height: 25px;
+   width: 200px;
+   text-indent: 11px;
+
+   background: #000;
+   color: #aaa;
+   border: 1px solid #222;
+   border-radius: 15px 15px 15px 15px;
+   -webkit-border-radius: 15px 15px 15px 15px;
+   -moz-border-radius: 15px 15px 15px 15px;
+}
+
+.registerPanel_content input:focus {
+   border: 1px solid #444;
+}
+
+
+.registerPanel_content .button, .registerPanel_content .button:visited {
+   background: #222 url('media/libervia/gradient.png') repeat-x;
+   display: inline-block;
+   text-decoration: none;
+   border-radius: 6px 6px 6px 6px;
+   -moz-border-radius: 6px 6px 6px 6px;
+   -webkit-border-radius: 6px 6px 6px 6px;
+   -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.6);
+   -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.6);
+   border-bottom: 1px solid rgba(0,0,0,0.25);
+   cursor: pointer;
+   margin-top: 30px;
+}
+
+/* Fix for Opera */
+.button, .button:visited {
+    border-radius: 6px 6px 6px 6px !important;
+}
+
+.registerPanel_content .button:hover { background-color: #111; color: #fff; }
+.registerPanel_content .button:active    { top: 1px; }
+.registerPanel_content .button, .registerPanel_content .button:visited { font-size: 1em; font-weight: bold; line-height: 1; text-shadow: 0 -1px 1px rgba(0,0,0,0.25); padding: 7px 10px 8px; }
+.registerPanel_content .red.button, .registerPanel_content .red.button:visited { background-color: #000; }
+.registerPanel_content .red.button:hover { background-color: #bc0000; }
+
+/* Widgets */
+
+.widgetsPanel td {
+    vertical-align: top;
+}
+
+.widgetsPanel > div > table {
+    border-collapse: separate !important;
+    border-spacing: 7px;
+}
+
+.widgetHeader {
+    margin: auto;
+    height: 25px;
+    /*border: 1px solid #ddd;*/
+    border-radius: 10px 10px 0 0; 
+    -webkit-border-radius: 10px 10px 0 0; 
+    -moz-border-radius: 10px 10px 0 0; 
+    background-color: #222;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#444&#888;, endColorstr=’#222’); 
+    background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222)); 
+    background: -moz-linear-gradient(top, #444, #222); 
+    background-image: -o-linear-gradient(#444,#222);
+}
+
+.widgetHeader_title {
+    color: #fff;
+    font-weight: bold;
+    text-align: left;
+    text-indent: 15px;
+    margin-top: 4px;
+}
+
+.widgetHeader_buttonsWrapper {
+    position: absolute;
+    top: 0;
+    height: 100%;
+    width: 100%;
+}
+
+.widgetHeader_buttonGroup {
+    float: right;
+}
+
+.widgetHeader_buttonGroup img {
+   background-color: transparent;
+    width: 25px;
+    height: 20px;
+    padding-top: 2px;
+    padding-bottom: 3px;
+    border-left: 1px solid #666;
+    border-top: 0;
+    border-radius: 0 10px 0 0; 
+    -webkit-border-radius: 0 10px 0 0; 
+    -moz-border-radius: 0 10px 0 0; 
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#555&#888;, endColorstr=’#333’); 
+    background: -webkit-gradient(linear, left top, left bottom, from(#555), to(#333)); 
+    background: -moz-linear-gradient(top, #555, #333); 
+    background-image: -o-linear-gradient(#555,#333);
+}
+
+.widgetHeader_closeButton {
+    border-radius: 0 10px 0 0 !important; 
+    -webkit-border-radius: 0 10px 0 0 !important; 
+    -moz-border-radius: 0 10px 0 0 !important;
+}
+
+.widgetHeader_settingButton {
+    border-radius: 0 0 0 0 !important; 
+    -webkit-border-radius: 0 0 0 0 !important; 
+    -moz-border-radius: 0 0 0 0 !important;          
+}
+
+.widgetHeader_buttonGroup img:hover {
+    background-color: #cf2828;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828&#888;, endColorstr=’#981a1a’); 
+    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); 
+    background: -moz-linear-gradient(top, #cf2828, #981a1a); 
+    background-image: -o-linear-gradient(#cf2828,#981a1a);
+}
+
+.widgetBody {
+    border-radius: 0 0 10px 10px; 
+    -webkit-border-radius: 0 0 10px 10px; 
+    -moz-border-radius: 0 0 10px 10px;
+    background-color: #fff;  
+    min-width: 200px;
+    min-height: 150px;
+    box-shadow:inset 0px 0 1px #444;
+    -webkit-box-shadow:inset 0 0 1px #444;
+    -moz-box-shadow:inset 0 0 1px #444;
+}
+
+/* BorderWidgets */
+
+.bottomBorderWidget {
+    height: 10px !important;
+}
+
+.leftBorderWidget, .rightBorderWidget {
+    width: 10px !important;
+}
+
+/* Microblog */
+
+.microblogPanel {
+/*    margin: auto;
+    width: 95% !important;*/
+    width: 100%;
+}
+
+.microblogNewButton {
+    width: 100%;
+    height: 35px;
+}
+
+.subPanel {
+}
+
+.subpanel .mb_entry {
+    padding-left: 65px;
+}
+
+.mb_entry {
+	min-height: 64px;
+}
+
+.mb_entry_header
+{
+    cursor: pointer;
+}
+
+.selected_widget .selected_entry .mb_entry_header
+{
+    background: #cf2828;
+}
+
+.mb_entry_author {
+    font-weight: bold;
+    padding-left: 5px;
+}
+
+.mb_entry_avatar {
+	float: left;
+}
+
+.mb_entry_avatar img {
+	width: 48px;
+	height: 48px;
+	padding: 8px;
+}
+
+.mb_entry_dialog {
+	float: left;
+	min-height: 54px;
+	padding: 5px 20px 5px 20px;
+    border-collapse: separate;  # for the bubble queue since the entry dialog is now a HorizontalPanel
+}
+
+.bubble {
+    position: relative;
+    padding: 15px;
+    margin: 2px;
+    -webkit-border-radius:10px;
+    -moz-border-radius:10px;
+    border-radius:10px;
+    background: #EDEDED;
+    border-color: #C1C1C1;
+    border-width: 1px;
+    border-style: solid;
+    display: block;
+    border-collapse: separate;
+    min-height: 15px;  # for the bubble queue to be aligned when the bubble is empty
+}
+
+.bubble:after {
+    background: transparent url('media/libervia/bubble_after.png') top right no-repeat;
+    border: none;	
+    content: "";
+    position: absolute;
+    bottom: auto;
+    left: -20px;
+    top: 16px;
+    display: block;
+    height: 20;
+    width: 20;
+}
+
+.bubble textarea{
+    width: 100%;
+}
+
+.mb_entry_timestamp {
+    font-style: italic;
+}
+
+.mb_entry_actions {
+    float: right;
+    margin: 5px;
+    cursor: pointer;
+    font-size: large;
+}
+
+.mb_entry_action_larger {
+    font-size: x-large;
+}
+
+.mb_entry_toggle_syntax {
+    cursor: pointer; 
+    text-align: right;
+    display: block;
+    position: relative;
+    top: -20px:
+    left: -20px;
+}
+
+/* Chat & MUC Room */
+
+.chatPanel {
+    height: 100%;
+    width: 100%;
+}
+
+.chatPanel_body {
+    height: 100%;
+    width: 100%;
+}
+
+.chatContent {
+    overflow: auto;
+    padding: 5px 15px 5px 15px;
+}
+
+.chatText {
+  margin-top: 7px;
+}
+
+.chatTextInfo {
+    font-weight: bold;
+    font-style: italic;
+}
+
+.chatTextInfo-link {
+    font-weight: bold;
+    font-style: italic;
+	cursor: pointer;
+	display: inline;
+}
+
+.chatArea {
+    height:100%;
+    width:100%;
+}
+
+.chat_text_timestamp {
+    font-style: italic;
+    margin-right: -4px;
+    padding: 1px 3px 1px 3px;
+    -moz-border-radius: 15px 0 0 15px;
+    -webkit-border-radius: 15px 0 0 15px;
+    border-radius: 15px 0 0 15px;
+    background-color: #eee;
+    color: #888;
+    border: 1px solid #ddd;
+    border-right: none;
+}
+
+.chat_text_nick {
+    font-weight: bold;
+    padding: 1px 3px 1px 3px;
+    -moz-border-radius: 0 15px 15px 0;
+    -webkit-border-radius: 10 15px 15px 0;
+    border-radius: 0 15px 15px 0;
+    background-color: #eee;
+    color: #b01e1e;
+    border: 1px solid #ddd;
+    border-left: none;
+}
+
+.chat_text_msg {
+    white-space: pre-wrap;
+}
+
+.chat_text_mymess {
+    color: #006600;
+}
+
+.occupant {
+    margin-top: 10px;
+    margin-right: 4px;
+    min-width: 120px;
+    padding: 5px 15px 5px 15px;
+    font-weight: bold;
+    background-color: #eee;
+    border: 1px solid #ddd;
+    white-space: nowrap;
+}
+
+.occupantsList {
+    border-right: 2px dotted #ddd;
+    margin-left: 5px;
+    margin-right: 10px;
+    height: 100%;
+}
+
+/* Games */
+
+.cardPanel {
+    background: #02FE03;
+    margin: 0 auto;
+}
+
+.cardGamePlayerNick {
+    font-weight: bold;
+}
+
+/* Radiocol */
+
+.radiocolPanel {
+
+}
+
+.radiocol_metadata_lbl {
+    font-weight: bold;
+    padding-right: 5px;
+}
+
+.radiocol_next_song {
+    margin-right: 5px;
+    font-style:italic;
+}
+
+.radiocol_status {
+    margin-left: 10px;
+    margin-right: 10px;
+    font-weight: bold;
+	color: black;
+}
+
+.radiocol_upload_status_ok {
+    margin-left: 10px;
+    margin-right: 10px;
+    font-weight: bold;
+	color: #28F215;
+}
+
+.radiocol_upload_status_ko {
+    margin-left: 10px;
+    margin-right: 10px;
+    font-weight: bold;
+	color: #B80000;
+}
+
+/* Drag and drop */
+
+.dragover {
+    background: #cf2828 !important;
+    border-radius: 1em 1em 1em 1em !important;
+    -webkit-border-radius: 1em 1em 1em 1em !important;
+    -moz-border-radius: 1em 1em 1em 1em !important;
+}
+
+.dragover .widgetHeader, .dragover .widgetBody, .dragover .widgetBody span, .dragover .widgetHeader img {
+    background: #cf2828 !important;
+}
+
+.dragover.widgetHeader {
+    border-radius: 1em 1em 0 0 !important;
+    -webkit-border-radius: 1em 1em 0 0 !important;
+    -moz-border-radius: 1em 1em 0 0 !important;
+}
+
+.dragover.widgetBody {
+    border-radius: 0 0 1em 1em !important;
+    -webkit-border-radius: 0 0 1em 1em !important;
+    -moz-border-radius: 0 0 1em 1em !important;
+}
+
+/* Warning message */
+
+.warningPopup {
+	font-size: 1em;
+    width: 100%;
+    height: 26px;
+    text-align: center;
+    padding: 5px 0;
+    border-bottom: 1px solid #444;
+    /*background-color: #fff;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’fff′, endColorstr=’#ccc’);
+    background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc));
+    background: -moz-linear-gradient(top, #fff, #ccc);
+    background-image: -o-linear-gradient(#fff,#ccc); */
+
+}
+
+.warningTarget {
+    font-weight: bold;
+   
+}
+
+.targetPublic {
+    background-color: red; /*#cf2828;*/
+    /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828′, endColorstr=’#981a1a’);
+    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a));
+    background: -moz-linear-gradient(top, #cf2828, #981a1a);
+    background-image: -o-linear-gradient(#cf2828,#981a1a); */
+}
+
+.targetGroup {
+    background-color: #00FFFB;
+    /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’68ba0f′, endColorstr=’#40700d’);
+    background: -webkit-gradient(linear, left top, left bottom, from(#68ba0f), to(#40700d));
+    background: -moz-linear-gradient(top, #68ba0f, #40700d);
+    background-image: -o-linear-gradient(#68ba0f,#40700d); */
+}
+
+.targetOne2One {
+    background-color: #66FF00;
+    /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’444444′, endColorstr=’#222222’);
+    background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222));
+    background: -moz-linear-gradient(top, #444444, #222222);
+    background-image: -o-linear-gradient(#444444,#222222);*/ 
+}
+
+.targetStatus {
+    background-color: #fff;
+    /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’fff′, endColorstr=’#ccc’);
+    background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc));
+    background: -moz-linear-gradient(top, #fff, #ccc);
+    background-image: -o-linear-gradient(#fff,#ccc); */
+}
+
+/* Tab panel */
+
+.liberviaTabPanel {
+}
+
+.gwt-TabPanel {
+}
+
+.gwt-TabPanelBottom {
+  height: 100%;
+}
+
+.gwt-TabBar {
+  font-weight: bold;
+  text-decoration: none;
+  border-bottom: 3px solid #a01c1c;  
+}
+
+.mainTabPanel .gwt-TabBar {
+  z-index: 10;
+  position: fixed;
+  bottom: 0;
+  left: 0;
+}
+
+.gwt-TabBar .gwt-TabBarFirst {
+  height: 100%;
+}
+
+.gwt-TabBar .gwt-TabBarRest {
+}
+
+.liberviaTabPanel .gwt-TabBar {;
+}
+
+.liberviaTabPanel .gwt-TabBar .gwt-TabBarItem {
+  cursor: pointer;
+  margin-right: 5px;
+}
+
+.liberviaTabPanel .gwt-TabBarItem div {
+	color: #fff;
+}
+
+.liberviaTabPanel .gwt-TabBarItem {
+  color: #444 !important;
+  background-color: #222;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#444′, endColorstr=’#222’);
+  background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222));
+  background: -moz-linear-gradient(top, #444, #222);
+  background-image: -o-linear-gradient(#444,#222);
+  box-shadow: 0px 1px 4px #000;
+  -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+  -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+  padding: 4px 15px 4px 15px;
+  border-radius: 1em 1em 0 0;
+  -webkit-border-radius: 1em 1em 0 0;
+  -moz-border-radius: 1em 1em 0 0;
+  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
+}
+
+.liberviaTabPanel .gwt-TabBarItem-selected {
+  color: #fff;
+  background-color: #cf2828;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828′, endColorstr=’#981a1a’);
+  background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a));
+  background: -moz-linear-gradient(top, #cf2828, #981a1a);
+  background-image: -o-linear-gradient(#cf2828,#981a1a);
+  box-shadow: 0px 1px 4px #000;
+  -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+  -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+  padding: 4px 15px 4px 15px;
+  border-radius: 1em 1em 0 0;
+  -webkit-border-radius: 1em 1em 0 0;
+  -moz-border-radius: 1em 1em 0 0;
+  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
+}
+
+.liberviaTabPanel div.gwt-TabBarItem:hover {
+  color: #fff;
+  background-color: #cf2828;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828′, endColorstr=’#981a1a’);
+  background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a));
+  background: -moz-linear-gradient(top, #cf2828, #981a1a);
+  background-image: -o-linear-gradient(#cf2828,#981a1a);
+  box-shadow: 0px 1px 4px #000;
+  -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+  -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+  padding: 4px 15px 4px 15px;
+  border-radius: 1em 1em 0 0;
+  -webkit-border-radius: 1em 1em 0 0;
+  -moz-border-radius: 1em 1em 0 0;
+  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); 
+}
+
+.liberviaTabPanel .gwt-TabBar .gwt-TabBarItem-selected {
+  cursor: default;
+}
+
+.globalLeftArea {
+    margin-top: 9px;
+}
+
+
+/* Misc */
+
+.selected_widget .widgetHeader  {
+    /* this property is set when a widget is the current target of the uniBox
+     * (messages entered in unibox will be sent to this widget)
+     */
+    background-color: #cf2828;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828&#888;, endColorstr=’#981a1a’); 
+    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); 
+    background: -moz-linear-gradient(top, #cf2828, #981a1a); 
+    background-image: -o-linear-gradient(#cf2828,#981a1a);
+}
+
+.infoFrame {
+    position: relative;
+    width: 100%;
+    height: 100%;
+}
+
+.marginAuto {
+    margin: auto;
+}
+
+.transparent {
+    opacity: 0;
+}
+
+/* URLs */
+
+a.url {
+    color: blue;
+    text-decoration: none
+}
+
+a:hover.url {
+    text-decoration: underline
+}
+
+/* Rich Text/Message Editor */
+
+.richTextEditor {
+}
+
+.richTextEditor tbody {
+    width: 100%;
+    display: table;
+}
+
+.richMessageEditor {
+    width: 100%;
+    margin: 9px 18px;
+}
+
+.richTextTitle {
+    margin-bottom: 5px;
+}
+
+.richTextTitle textarea {
+    height: 23px;
+    width: 99%;
+    margin: auto;
+    display: block;
+}
+
+.richTextToolbar {
+    white-space: nowrap;
+    width: 100%;
+}
+
+.richTextArea {
+    width: 100%;
+}
+
+.richMessageArea {
+    width: 100%;
+    height: 250px;
+}
+
+.richTextWysiwyg {
+    min-height: 50px;
+    background-color: white;
+    border: 1px solid #a0a0a0;
+    border-radius: 5px;
+    display: block;
+    font-size: larger;
+    white-space: pre;
+}
+
+.richTextSyntaxLabel {
+	text-align: right;
+	margin: 14px 0px 0px 14px;
+	font-size: 12px;
+}
+
+.richTextToolButton {
+	cursor: pointer;
+    width:26px;
+    height:26px;
+    vertical-align: middle;
+    margin: 2px 1px;
+    border-radius: 5px 5px 5px 5px;
+    -webkit-border-radius: 5px 5px 5px 5px;
+    -moz-border-radius: 5px 5px 5px 5px;
+    box-shadow: 0px 1px 4px #000;
+    -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
+    -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
+	border: none;
+	-webkit-transition: color 0.2s linear; 
+    -moz-transition: color 0.2s linear; 
+    -o-transition: color 0.2s linear;
+}
+
+.richTextIcon {
+    width:16px;
+    height:16px;
+    vertical-align: middle;
+}
+
+/* Recipients panel */
+
+.recipientButtonCell {
+	width:55px;
+}
+
+.recipientTypeMenu {
+}
+
+.recipientTypeItem {
+	cursor: pointer;
+    border-radius: 5px;
+    width: 50px;
+}
+
+.recipientPanel {
+}
+
+.recipientTextBox {
+	cursor: pointer;
+    width: auto;
+    border-radius: 5px 5px 5px 5px;
+    -webkit-border-radius: 5px 5px 5px 5px;
+    -moz-border-radius: 5px 5px 5px 5px;
+    box-shadow: inset 0px 1px 4px rgba(135, 179, 255, 0.6);
+    -webkit-box-shadow:inset 0 1px 4px rgba(135, 179, 255, 0.6);
+    -moz-box-shadow:inset 0 1px 4px rgba(135, 179, 255, 0.6);
+    padding: 2px 1px;
+    margin: 0px;
+    color: #444;
+    font-size: 1em;
+}
+
+.recipientTextBox-invalid {
+    box-shadow: inset 0px 1px 4px rgba(255, 0, 0, 0.6);
+    -webkit-box-shadow:inset 0 1px 4px rgba(255, 0, 0, 0.6);
+    -moz-box-shadow:inset 0 1px 4px rgba(255, 0, 0, 0.6);
+    border: 1px solid rgb(255, 0, 0);
+}
+
+.recipientRemoveButton {
+	margin: 0px 10px 0px 0px;
+	padding: 0px;
+	border: 1px dashed red;
+    border-radius: 5px 5px 5px 5px;
+}
+
+.recipientRemoveIcon {
+	color: red;
+    width:15px;
+    height:15px;
+	vertical-align: baseline;
+}
+
+.dragover-recipientPanel {
+	border-radius: 5px;
+    background: none repeat scroll 0% 0% rgb(135, 179, 255);
+    border: 1px dashed rgb(35,79,255);
+}
+
+.recipientSpacer {
+    height: 15px;
+}
+
+/* Popup (context) menu */
+
+.popupMenuItem {
+    cursor: pointer;
+    border-radius: 5px;
+    width: 100%;
+}
+
+/* Contact group manager */
+
+.contactGroupEditor {
+	width: 800px;
+	max-width:800px;
+	min-width: 800px;
+	margin-top: 9px;
+	margin-left:18px;
+}
+
+.contactGroupRemoveButton {
+	margin: 0px 10px 0px 0px;
+	padding: 0px;
+	border: 1px dashed red;
+    border-radius: 5px 5px 5px 5px;
+}
+
+.addContactGroupPanel {
+	
+}
+
+.contactGroupPanel {
+	vertical-align:middle;
+}
+
+.toggleAssignedContacts {
+    white-space: nowrap;
+}
+
+.contactGroupButtonCell {
+    vertical-align: baseline;
+    width: 55px;
+    white-space: nowrap;
+}
+
+/* Room and contacts chooser */
+
+.room-contact-chooser {
+	width:380px;
+}
+
+/* StackPanel */
+
+.gwt-StackPanel {
+}
+
+.gwt-StackPanel .gwt-StackPanelItem {
+    background-color: #222;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’444444′, endColorstr=’#222222’);
+    background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222));
+    background: -moz-linear-gradient(top, #444444, #222222);
+    background-image: -o-linear-gradient(#444444,#222222);
+    text-decoration: none;    
+    font-weight: bold;
+    height: 100%;
+    color: #e7e5e5;
+    padding: 3px 15px;
+    /*display: block;*/
+    border-radius: 1em 1em 1em 1em;
+    -webkit-border-radius: 1em 1em 1em 1em;
+    -moz-border-radius: 1em 1em 1em 1em;
+    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
+    -webkit-transition: color 0.2s linear; 
+    -moz-transition: color 0.2s linear; 
+    -o-transition: color 0.2s linear;
+}
+
+.gwt-StackPanel .gwt-StackPanelItem:hover {
+    background-color: #eee;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#eee’, endColorstr=’#aaa′);
+    background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa));
+    background: -moz-linear-gradient(top, #eee, #aaa);
+    background-image: -o-linear-gradient(#eee,#aaa);
+    color: #444;
+    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);    
+    cursor: pointer;
+}
+
+.gwt-StackPanel .gwt-StackPanelItem-selected {
+    background-color: #eee;
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#eee’, endColorstr=’#aaa′);
+    background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa));
+    background: -moz-linear-gradient(top, #eee, #aaa);
+    background-image: -o-linear-gradient(#eee,#aaa);
+    color: #444;
+    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);    
+    cursor: pointer;
+}
+
+/* Caption Panel */
+
+.gwt-CaptionPanel {
+    overflow: auto;
+    background-color: #fff;
+    border-radius: 5px 5px 5px 5px;
+    -webkit-border-radius: 5px 5px 5px 5px;
+    -moz-border-radius: 5px 5px 5px 5px;
+	 padding: 3px 5px 3px 5px;
+	 margin: 10px 5px 10px 5px;
+	 color: #444;
+	 font-size: 1em;
+	 border: solid 1px gray;
+}
+
+/* Radio buttons */
+
+.gwt-RadioButton {
+	white-space: nowrap;
+}
+
+[contenteditable="true"] {
+}
+
+/* XMLUI styles */
+
+.AdvancedListSelectable tr{
+    cursor: pointer;
+}
+
+.AdvancedListSelectable tr:hover{
+    background: none repeat scroll 0 0 #EE0000;
+}
+
+.line hr {
+
+}
+
+.dot hr {
+    height: 0px;
+    border-top: 1px dotted;
+    border-bottom: 0px;
+}
+
+.dash hr {
+    height: 0px;
+    border-top: 1px dashed;
+    border-bottom: 0px;
+}
+
+.plain hr {
+    height: 10px;
+    color: black;
+    background-color: black;
+}
+
+.blank hr {
+    border: 0px;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/public/libervia.html	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,32 @@
+<!--
+Libervia: a Salut à Toi frontend
+Copyright (C) 2011  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/>.
+-->
+
+<html>
+<head profile="http://www.w3.org/2005/10/profile">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<meta name="pygwt:module" content="libervia_main">
+<link rel='stylesheet' href='libervia.css'>
+<link rel="icon" type="image/png" href="sat_logo_16.png">
+
+<title>Libervia</title>
+</head>
+<body bgcolor="white">
+<script language="javascript" src="bootstrap.js"></script>
+<iframe id='__pygwt_historyFrame' style='display:none;width:0;height:0;border:0'></iframe>
+</body>
+</html>
Binary file src/browser/public/sat_logo_16.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/radiocol.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,322 @@
+#!/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.tools.misc import DEFAULT_MUC
+from sat.core.i18n import _
+
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.FlexTable import FlexTable
+from pyjamas.ui.FormPanel import FormPanel
+from pyjamas.ui.Label import Label
+from pyjamas.ui.Button import Button
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui.Hidden import Hidden
+from pyjamas.ui.CaptionPanel import CaptionPanel
+from pyjamas.media.Audio import Audio
+from pyjamas import Window
+from pyjamas.Timer import Timer
+
+from html_tools import html_sanitize
+from file_tools import FilterFileUpload
+
+
+class MetadataPanel(FlexTable):
+
+    def __init__(self):
+        FlexTable.__init__(self)
+        title_lbl = Label("title:")
+        title_lbl.setStyleName('radiocol_metadata_lbl')
+        artist_lbl = Label("artist:")
+        artist_lbl.setStyleName('radiocol_metadata_lbl')
+        album_lbl = Label("album:")
+        album_lbl.setStyleName('radiocol_metadata_lbl')
+        self.title = Label("")
+        self.title.setStyleName('radiocol_metadata')
+        self.artist = Label("")
+        self.artist.setStyleName('radiocol_metadata')
+        self.album = Label("")
+        self.album.setStyleName('radiocol_metadata')
+        self.setWidget(0, 0, title_lbl)
+        self.setWidget(1, 0, artist_lbl)
+        self.setWidget(2, 0, album_lbl)
+        self.setWidget(0, 1, self.title)
+        self.setWidget(1, 1, self.artist)
+        self.setWidget(2, 1, self.album)
+        self.setStyleName("radiocol_metadata_pnl")
+
+    def setTitle(self, title):
+        self.title.setText(title)
+
+    def setArtist(self, artist):
+        self.artist.setText(artist)
+
+    def setAlbum(self, album):
+        self.album.setText(album)
+
+
+class ControlPanel(FormPanel):
+    """Panel used to show controls to add a song, or vote for the current one"""
+
+    def __init__(self, parent):
+        FormPanel.__init__(self)
+        self.setEncoding(FormPanel.ENCODING_MULTIPART)
+        self.setMethod(FormPanel.METHOD_POST)
+        self.setAction("upload_radiocol")
+        self.timer_on = False
+        self._parent = parent
+        vPanel = VerticalPanel()
+
+        types = [('audio/ogg', '*.ogg', 'Ogg Vorbis Audio'),
+                 ('video/ogg', '*.ogv', 'Ogg Vorbis Video'),
+                 ('application/ogg', '*.ogx', 'Ogg Vorbis Multiplex'),
+                 ('audio/mpeg', '*.mp3', 'MPEG-Layer 3'),
+                 ('audio/mp3', '*.mp3', 'MPEG-Layer 3'),
+                 ]
+        self.file_upload = FilterFileUpload("song", 10, types)
+        vPanel.add(self.file_upload)
+
+        hPanel = HorizontalPanel()
+        self.upload_btn = Button("Upload song", getattr(self, "onBtnClick"))
+        hPanel.add(self.upload_btn)
+        self.status = Label()
+        self.updateStatus()
+        hPanel.add(self.status)
+        #We need to know the filename and the referee
+        self.filename_field = Hidden('filename', '')
+        hPanel.add(self.filename_field)
+        referee_field = Hidden('referee', self._parent.referee)
+        hPanel.add(self.filename_field)
+        hPanel.add(referee_field)
+        vPanel.add(hPanel)
+
+        self.add(vPanel)
+        self.addFormHandler(self)
+
+    def updateStatus(self):
+        if self.timer_on:
+            return
+        # TODO: the status should be different if a song is being played or not
+        queue = self._parent.getQueueSize()
+        queue_data = self._parent.queue_data
+        if queue < queue_data[0]:
+            left = queue_data[0] - queue
+            self.status.setText("[we need %d more song%s]" % (left, "s" if left > 1 else ""))
+        elif queue < queue_data[1]:
+            left = queue_data[1] - queue
+            self.status.setText("[%d available spot%s]" % (left, "s" if left > 1 else ""))
+        elif queue >= queue_data[1]:
+                self.status.setText("[The queue is currently full]")
+        self.status.setStyleName('radiocol_status')
+
+    def onBtnClick(self):
+        if self.file_upload.check():
+            self.status.setText('[Submitting, please wait...]')
+            self.filename_field.setValue(self.file_upload.getFilename())
+            if self.file_upload.getFilename().lower().endswith('.mp3'):
+                self._parent._parent.host.showWarning('STATUS', 'For a better support, it is recommended to submit Ogg Vorbis file instead of MP3. You can convert your files easily, ask for help if needed!', 5000)
+            self.submit()
+            self.file_upload.setFilename("")
+
+    def onSubmit(self, event):
+        pass
+
+    def blockUpload(self):
+        self.file_upload.setVisible(False)
+        self.upload_btn.setEnabled(False)
+
+    def unblockUpload(self):
+        self.file_upload.setVisible(True)
+        self.upload_btn.setEnabled(True)
+
+    def setTemporaryStatus(self, text, style):
+        self.status.setText(text)
+        self.status.setStyleName('radiocol_upload_status_%s' % style)
+        self.timer_on = True
+
+        def cb(timer):
+            self.timer_on = False
+            self.updateStatus()
+
+        Timer(5000, cb)
+
+    def onSubmitComplete(self, event):
+        result = event.getResults()
+        if result == "OK":
+            # the song can still be rejected (not readable, full queue...)
+            self.setTemporaryStatus('[Your song has been submitted to the radio]', "ok")
+        elif result == "KO":
+            self.setTemporaryStatus('[Something went wrong during your song upload]', "ko")
+            self._parent.radiocolSongRejected(_("The uploaded file has been rejected, only Ogg Vorbis and MP3 songs are accepted."))
+            # TODO: would be great to re-use the original Exception class and message
+            # but it is lost in the middle of the traceback and encapsulated within
+            # a DBusException instance --> extract the data from the traceback?
+        else:
+            Window.alert('Submit error: %s' % result)
+            self.status.setText('')
+
+
+class Player(Audio):
+
+    def __init__(self, player_id, metadata_panel):
+        Audio.__init__(self)
+        self._id = player_id
+        self.metadata = metadata_panel
+        self.timestamp = ""
+        self.title = ""
+        self.artist = ""
+        self.album = ""
+        self.filename = None
+        self.played = False  # True when the song is playing/has played, becomes False on preload
+        self.setAutobuffer(True)
+        self.setAutoplay(False)
+        self.setVisible(False)
+
+
+    def preload(self, timestamp, filename, title, artist, album):
+        """preload the song but doesn't play it"""
+        self.timestamp = timestamp
+        self.filename = filename
+        self.title = title
+        self.artist = artist
+        self.album = album
+        self.played = False
+        self.setSrc("radiocol/%s" % html_sanitize(filename))
+        log.debug("preloading %s in %s" % (title, self._id))
+
+    def play(self, play=True):
+        """Play or pause the song
+        @param play: set to True to play or to False to pause
+        """
+        if play:
+            self.played = True
+            self.metadata.setTitle(self.title)
+            self.metadata.setArtist(self.artist)
+            self.metadata.setAlbum(self.album)
+            Audio.play(self)
+        else:
+            self.pause()
+
+
+class RadioColPanel(HorizontalPanel, ClickHandler):
+
+    def __init__(self, parent, referee, player_nick, players, queue_data):
+        """
+        @param parent
+        @param referee
+        @param player_nick
+        @param players
+        @param queue_data: list of integers (queue to start, queue limit)
+        """
+        # We need to set it here and not in the CSS :(
+        HorizontalPanel.__init__(self, Height="90px")
+        ClickHandler.__init__(self)
+        self._parent = parent
+        self.referee = referee
+        self.queue_data = queue_data
+        self.setStyleName("radiocolPanel")
+
+        # Now we set up the layout
+        self.metadata_panel = MetadataPanel()
+        self.add(CaptionPanel("Now playing", self.metadata_panel))
+        self.playlist_panel = VerticalPanel()
+        self.add(CaptionPanel("Songs queue", self.playlist_panel))
+        self.control_panel = ControlPanel(self)
+        self.add(CaptionPanel("Controls", self.control_panel))
+
+        self.next_songs = []
+        self.players = [Player("player_%d" % i, self.metadata_panel) for i in xrange(queue_data[1] + 1)]
+        self.current_player = None
+        for player in self.players:
+            self.add(player)
+        self.addClickListener(self)
+
+        help_msg = """Accepted file formats: Ogg Vorbis (recommended), MP3.<br />
+        Please do not submit files that are protected by copyright.<br />
+        Click <a style="color: red;">here</a> if you need some support :)"""
+        link_cb = lambda: self._parent.host.bridge.call('joinMUC', None, DEFAULT_MUC, self._parent.nick)
+        self._parent.printInfo(help_msg, type_='link', link_cb=link_cb)
+
+    def pushNextSong(self, title):
+        """Add a song to the left panel's next songs queue"""
+        next_song = Label(title)
+        next_song.setStyleName("radiocol_next_song")
+        self.next_songs.append(next_song)
+        self.playlist_panel.append(next_song)
+        self.control_panel.updateStatus()
+
+    def popNextSong(self):
+        """Remove the first song of next songs list
+        should be called when the song is played"""
+        #FIXME: should check that the song we remove is the one we play
+        next_song = self.next_songs.pop(0)
+        self.playlist_panel.remove(next_song)
+        self.control_panel.updateStatus()
+
+    def getQueueSize(self):
+        return len(self.playlist_panel.getChildren())
+
+    def radiocolCheckPreload(self, timestamp):
+        for player in self.players:
+            if player.timestamp == timestamp:
+                return False
+        return True
+
+    def radiocolPreload(self, timestamp, filename, title, artist, album, sender):
+        if not self.radiocolCheckPreload(timestamp):
+            return  # song already preloaded
+        preloaded = False
+        for player in self.players:
+            if not player.filename or \
+               (player.played and player != self.current_player):
+                #if player has no file loaded, or it has already played its song
+                #we use it to preload the next one
+                player.preload(timestamp, filename, title, artist, album)
+                preloaded = True
+                break
+        if not preloaded:
+            log.warning("Can't preload song, we are getting too many songs to preload, we shouldn't have more than %d at once" % self.queue_data[1])
+        else:
+            self.pushNextSong(title)
+            self._parent.printInfo(_('%(user)s uploaded %(artist)s - %(title)s') % {'user': sender, 'artist': artist, 'title': title})
+
+    def radiocolPlay(self, filename):
+        found = False
+        for player in self.players:
+            if not found and player.filename == filename:
+                player.play()
+                self.popNextSong()
+                self.current_player = player
+                found = True
+            else:
+                player.play(False)  # in case the previous player was not sync
+        if not found:
+            log.error("Song not found in queue, can't play it. This should not happen")
+
+    def radiocolNoUpload(self):
+        self.control_panel.blockUpload()
+
+    def radiocolUploadOk(self):
+        self.control_panel.unblockUpload()
+
+    def radiocolSongRejected(self, reason):
+        Window.alert("Song rejected: %s" % reason)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/register.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,252 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
+# Copyright (C) 2011, 2012  Adrien Vigneron <adrienvigneron@mailoo.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/>.
+
+#This page manage subscription and new account creation
+
+import pyjd # this is dummy in pyjs
+from sat.core.i18n import _
+
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.TabPanel import TabPanel
+from pyjamas.ui.TabBar import TabBar
+from pyjamas.ui.PasswordTextBox import PasswordTextBox
+from pyjamas.ui.TextBox import TextBox
+from pyjamas.ui.FormPanel import FormPanel
+from pyjamas.ui.Button import Button
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.PopupPanel import PopupPanel
+from pyjamas.ui.Image import Image
+from pyjamas.ui.Hidden import Hidden
+from pyjamas import Window
+from pyjamas.ui.KeyboardListener import KEY_ENTER
+from pyjamas.Timer import Timer
+
+import re
+
+from constants import Const as C
+
+
+class RegisterPanel(FormPanel):
+
+    def __init__(self, callback):
+        """
+        @param callback: method to call if login successful
+        """
+        FormPanel.__init__(self)
+        self.setSize('600px', '350px')
+        self.callback = callback
+        self.setMethod(FormPanel.METHOD_POST)
+        main_panel = HorizontalPanel()
+        main_panel.setStyleName('registerPanel_main')
+        left_side = Image("media/libervia/register_left.png")
+        main_panel.add(left_side)
+
+        ##TabPanel##
+        tab_bar = TabBar()
+        tab_bar.setStyleName('registerPanel_tabs')
+        self.right_side = TabPanel(tab_bar)
+        self.right_side.setStyleName('registerPanel_right_side')
+        main_panel.add(self.right_side)
+        main_panel.setCellWidth(self.right_side, '100%')
+
+        ##Login tab##
+        login_tab = SimplePanel()
+        login_tab.setStyleName('registerPanel_content')
+        login_vpanel = VerticalPanel()
+        login_tab.setWidget(login_vpanel)
+
+        self.login_warning_msg = Label('')
+        self.login_warning_msg.setVisible(False)
+        self.login_warning_msg.setStyleName('formWarning')
+        login_vpanel.add(self.login_warning_msg)
+
+        login_label = Label('Login:')
+        self.login_box = TextBox()
+        self.login_box.setName("login")
+        self.login_box.addKeyboardListener(self)
+        login_pass_label = Label('Password:')
+        self.login_pass_box = PasswordTextBox()
+        self.login_pass_box.setName("login_password")
+        self.login_pass_box.addKeyboardListener(self)
+
+        login_vpanel.add(login_label)
+        login_vpanel.add(self.login_box)
+        login_vpanel.add(login_pass_label)
+        login_vpanel.add(self.login_pass_box)
+        login_but = Button("Log in", getattr(self, "onLogin"))
+        login_but.setStyleName('button')
+        login_but.addStyleName('red')
+        login_vpanel.add(login_but)
+
+        #The hidden submit_type field
+        self.submit_type = Hidden('submit_type')
+        login_vpanel.add(self.submit_type)
+
+        ##Register tab##
+        register_tab = SimplePanel()
+        register_tab.setStyleName('registerPanel_content')
+        register_vpanel = VerticalPanel()
+        register_tab.setWidget(register_vpanel)
+
+        self.register_warning_msg = HTML('')
+        self.register_warning_msg.setVisible(False)
+        self.register_warning_msg.setStyleName('formWarning')
+        register_vpanel.add(self.register_warning_msg)
+
+        register_login_label = Label('Login:')
+        self.register_login_box = TextBox()
+        self.register_login_box.setName("register_login")
+        self.register_login_box.addKeyboardListener(self)
+        email_label = Label('E-mail:')
+        self.email_box = TextBox()
+        self.email_box.setName("email")
+        self.email_box.addKeyboardListener(self)
+        register_pass_label = Label('Password:')
+        self.register_pass_box = PasswordTextBox()
+        self.register_pass_box.setName("register_password")
+        self.register_pass_box.addKeyboardListener(self)
+        register_vpanel.add(register_login_label)
+        register_vpanel.add(self.register_login_box)
+        register_vpanel.add(email_label)
+        register_vpanel.add(self.email_box)
+        register_vpanel.add(register_pass_label)
+        register_vpanel.add(self.register_pass_box)
+
+        register_but = Button("Register", getattr(self, "onRegister"))
+        register_but.setStyleName('button')
+        register_but.addStyleName('red')
+        register_vpanel.add(register_but)
+
+        self.right_side.add(login_tab, 'Login')
+        self.right_side.add(register_tab, 'Register')
+        self.right_side.addTabListener(self)
+        self.right_side.selectTab(1)
+        login_tab.setWidth(None)
+        register_tab.setWidth(None)
+
+        self.add(main_panel)
+        self.addFormHandler(self)
+        self.setAction('register_api/login')
+
+    def onBeforeTabSelected(self, sender, tabIndex):
+        return True
+
+    def onTabSelected(self, sender, tabIndex):
+        if tabIndex == 0:
+            self.login_box.setFocus(True)
+        elif tabIndex == 1:
+            self.register_login_box.setFocus(True)
+
+    def onKeyPress(self, sender, keycode, modifiers):
+        if keycode == KEY_ENTER:
+            # Browsers offer an auto-completion feature to any
+            # text box, but the selected value is not set when
+            # the widget looses the focus. Using a timer with
+            # any delay value > 0 would do the trick.
+            if sender == self.login_box:
+                Timer(5, lambda timer: self.login_pass_box.setFocus(True))
+            elif sender == self.login_pass_box:
+                self.onLogin(None)
+            elif sender == self.register_login_box:
+                Timer(5, lambda timer: self.email_box.setFocus(True))
+            elif sender == self.email_box:
+                Timer(5, lambda timer: self.register_pass_box.setFocus(True))
+            elif sender == self.register_pass_box:
+                self.onRegister(None)
+
+    def onKeyUp(self, sender, keycode, modifiers):
+        pass
+
+    def onKeyDown(self, sender, keycode, modifiers):
+        pass
+
+    def onLogin(self, button):
+        if not re.match(r'^[a-z0-9_-]+$', self.login_box.getText(), re.IGNORECASE):
+            self.login_warning_msg.setText('Invalid login, valid characters are a-z A-Z 0-9 _ -')
+            self.login_warning_msg.setVisible(True)
+        else:
+            self.submit_type.setValue('login')
+            self.submit()
+
+    def onRegister(self, button):
+        if not re.match(r'^[a-z0-9_-]+$', self.register_login_box.getText(), re.IGNORECASE):
+            self.register_warning_msg.setHTML(_('Invalid login, valid characters<br>are a-z A-Z 0-9 _ -'))
+            self.register_warning_msg.setVisible(True)
+        elif not re.match(r'^.+@.+\..+', self.email_box.getText(), re.IGNORECASE):
+            self.register_warning_msg.setHTML(_('Invalid email address'))
+            self.register_warning_msg.setVisible(True)
+        elif len(self.register_pass_box.getText()) < C.PASSWORD_MIN_LENGTH:
+            self.register_warning_msg.setHTML(_('Your password must contain<br>at least %d characters') % C.PASSWORD_MIN_LENGTH)
+            self.register_warning_msg.setVisible(True)
+        else:
+            self.register_warning_msg.setVisible(False)
+            self.submit_type.setValue('register')
+            self.submit()
+
+    def onSubmit(self, event):
+        pass
+
+    def onSubmitComplete(self, event):
+        result = event.getResults()
+        if result == "AUTH ERROR":
+            Window.alert('Your login and/or password is incorrect. Please try again')
+        elif result == "LOGGED":
+            self.callback()
+        elif result == "SESSION_ACTIVE":
+            Window.alert('Session already active, this should not happen, please contact the author to fix it')
+        elif result == "ALREADY EXISTS":
+            self.register_warning_msg.setHTML('This login already exists,<br>please choose another one')
+            self.register_warning_msg.setVisible(True)
+        elif result == "INTERNAL":
+            self.register_warning_msg.setHTML('SERVER ERROR: something went wrong during registration process, please contact the server administrator')
+            self.register_warning_msg.setVisible(True)
+        elif result == "REGISTRATION":
+            self.login_warning_msg.setVisible(False)
+            self.register_warning_msg.setVisible(False)
+            self.login_box.setText(self.register_login_box.getText())
+            self.login_pass_box.setText('')
+            self.register_login_box.setText('')
+            self.register_pass_box.setText('')
+            self.email_box.setText('')
+            self.right_side.selectTab(0)
+            self.login_pass_box.setFocus(True)
+            Window.alert('An email has been sent to you with your login informations\nPlease remember that this is ONLY A TECHNICAL DEMO')
+        else:
+            Window.alert('Submit error: %s' % result)
+
+
+class RegisterBox(PopupPanel):
+
+    def __init__(self, callback, *args, **kwargs):
+        PopupPanel.__init__(self, *args, **kwargs)
+        self._form = RegisterPanel(callback)
+        self.setWidget(self._form)
+
+    def onWindowResized(self, width, height):
+        super(RegisterBox, self).onWindowResized(width, height)
+        self.centerBox()
+
+    def show(self):
+        super(RegisterBox, self).show()
+        self.centerBox()
+        self._form.login_box.setFocus(True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/richtext.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,536 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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/>.
+
+from sat_frontends.tools import composition
+from sat.core.i18n import _
+
+from pyjamas.ui.TextArea import TextArea
+from pyjamas.ui.Button import Button
+from pyjamas.ui.CheckBox import CheckBox
+from pyjamas.ui.DialogBox import DialogBox
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.FlexTable import FlexTable
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas import Window
+from pyjamas.ui.KeyboardListener import KeyboardHandler
+from __pyjamas__ import doc
+
+from constants import Const as C
+from dialog import ConfirmDialog, InfoDialog
+from base_panels import TitlePanel, BaseTextEditor, HTMLTextEditor
+from list_manager import ListManager
+from html_tools import html_sanitize
+import panels
+
+
+class RichTextEditor(BaseTextEditor, FlexTable):
+    """Panel for the rich text editor."""
+
+    def __init__(self, host, content=None, modifiedCb=None, afterEditCb=None, options=None, style=None):
+        """
+        @param host: the SatWebFrontend instance
+        @param content: dict with at least a 'text' key
+        @param modifiedCb: method to be called when the text has been modified
+        @param afterEditCb: method to be called when the edition is done
+        @param options: list of UI options (see self.readOptions)
+        """
+        self.host = host
+        self._debug = False  # TODO: don't forget to set  it False before commit
+        self.wysiwyg = False
+        self.__readOptions(options)
+        self.style = {'main': 'richTextEditor',
+                      'title': 'richTextTitle',
+                      'toolbar': 'richTextToolbar',
+                      'textarea': 'richTextArea'}
+        if isinstance(style, dict):
+            self.style.update(style)
+        self._prepareUI()
+        BaseTextEditor.__init__(self, content, None, modifiedCb, afterEditCb)
+
+    def __readOptions(self, options):
+        """Set the internal flags according to the given options."""
+        if options is None:
+            options = []
+        self.read_only = 'read_only' in options
+        self.update_msg = 'update_msg' in options
+        self.no_title = 'no_title' in options or self.read_only
+        self.no_command = 'no_command' in options or self.read_only
+
+    def _prepareUI(self, y_offset=0):
+        """Prepare the UI to host title panel, toolbar, text area...
+        @param y_offset: Y offset to start from (extra rows on top)"""
+        if not self.read_only:
+            self.title_offset = y_offset
+            self.toolbar_offset = self.title_offset + (0 if self.no_title else 1)
+            self.content_offset = self.toolbar_offset + (len(composition.RICH_SYNTAXES) if self._debug else 1)
+            self.command_offset = self.content_offset + 1
+        else:
+            self.title_offset = self.toolbar_offset = self.content_offset = y_offset
+            self.command_offset = self.content_offset + 1
+        FlexTable.__init__(self, self.command_offset + (0 if self.no_command else 1), 2)
+        self.addStyleName(self.style['main'])
+
+    def addEditListener(self, listener):
+        """Add a method to be called whenever the text is edited.
+        @param listener: method taking two arguments: sender, keycode"""
+        BaseTextEditor.addEditListener(self, listener)
+        if hasattr(self, 'display'):
+            self.display.addEditListener(listener)
+
+    def refresh(self, edit=None):
+        """Refresh the UI for edition/display mode
+        @param edit: set to True to display the edition mode"""
+        if edit is None:
+            edit = hasattr(self, 'textarea') and self.textarea.getVisible()
+
+        for widget in ['title_panel', 'command']:
+            if hasattr(self, widget):
+                getattr(self, widget).setVisible(edit)
+
+        if hasattr(self, 'toolbar'):
+            self.toolbar.setVisible(False)
+        if not hasattr(self, 'display'):
+            self.display = HTMLTextEditor(options={'enhance_display': False, 'listen_keyboard': False})  # for display mode
+            for listener in self.edit_listeners:
+                self.display.addEditListener(listener)
+        if not self.read_only and not hasattr(self, 'textarea'):
+            self.textarea = EditTextArea(self)  # for edition mode
+            self.textarea.addStyleName(self.style['textarea'])
+
+        self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2)
+        if edit and not self.wysiwyg:
+            self.textarea.setWidth('100%')  # CSS width doesn't do it, don't know why
+            self.setWidget(self.content_offset, 0, self.textarea)
+        else:
+            self.setWidget(self.content_offset, 0, self.display)
+        if not edit:
+            return
+
+        if not self.no_title and not hasattr(self, 'title_panel'):
+            self.title_panel = TitlePanel()
+            self.title_panel.addStyleName(self.style['title'])
+            self.getFlexCellFormatter().setColSpan(self.title_offset, 0, 2)
+            self.setWidget(self.title_offset, 0, self.title_panel)
+
+        if not self.no_command and not hasattr(self, 'command'):
+            self.command = HorizontalPanel()
+            self.command.addStyleName("marginAuto")
+            self.command.add(Button("Cancel", lambda: self.edit(True, True)))
+            self.command.add(Button("Update" if self.update_msg else "Send message", lambda: self.edit(False)))
+            self.getFlexCellFormatter().setColSpan(self.command_offset, 0, 2)
+            self.setWidget(self.command_offset, 0, self.command)
+
+    def setToolBar(self, syntax):
+        """This method is called asynchronously after the parameter
+        holding the rich text syntax is retrieved. It is called at
+        each call of self.edit(True) because the user may
+        have change his setting since the last time."""
+        if syntax is None or syntax not in composition.RICH_SYNTAXES.keys():
+            syntax = composition.RICH_SYNTAXES.keys()[0]
+        if hasattr(self, "toolbar") and self.toolbar.syntax == syntax:
+            self.toolbar.setVisible(True)
+            return
+        count = 0
+        for syntax in composition.RICH_SYNTAXES.keys() if self._debug else [syntax]:
+            self.toolbar = HorizontalPanel()
+            self.toolbar.syntax = syntax
+            self.toolbar.addStyleName(self.style['toolbar'])
+            for key in composition.RICH_SYNTAXES[syntax].keys():
+                self.addToolbarButton(syntax, key)
+            self.wysiwyg_button = CheckBox(_('WYSIWYG edition'))
+            wysiywgCb = lambda sender: self.setWysiwyg(sender.getChecked())
+            self.wysiwyg_button.addClickListener(wysiywgCb)
+            self.toolbar.add(self.wysiwyg_button)
+            self.syntax_label = Label(_("Syntax: %s") % syntax)
+            self.syntax_label.addStyleName("richTextSyntaxLabel")
+            self.toolbar.add(self.syntax_label)
+            self.toolbar.setCellWidth(self.syntax_label, "100%")
+            self.getFlexCellFormatter().setColSpan(self.toolbar_offset + count, 0, 2)
+            self.setWidget(self.toolbar_offset + count, 0, self.toolbar)
+            count += 1
+
+    def setWysiwyg(self, wysiwyg, init=False):
+        """Toggle the edition mode between rich content syntax and wysiwyg.
+        @param wysiwyg: boolean value
+        @param init: set to True to re-init without switching the widgets."""
+        def setWysiwyg():
+            self.wysiwyg = wysiwyg
+            try:
+                self.wysiwyg_button.setChecked(wysiwyg)
+            except TypeError:
+                pass
+            try:
+                if wysiwyg:
+                    self.syntax_label.addStyleName('transparent')
+                else:
+                    self.syntax_label.removeStyleName('transparent')
+            except TypeError:
+                pass
+            if not wysiwyg:
+                self.display.removeStyleName('richTextWysiwyg')
+
+        if init:
+            setWysiwyg()
+            return
+
+        self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2)
+        if wysiwyg:
+            def syntaxConvertCb(text):
+                self.display.setContent({'text': text})
+                self.textarea.removeFromParent()  # XXX: force as it is not always done...
+                self.setWidget(self.content_offset, 0, self.display)
+                self.display.addStyleName('richTextWysiwyg')
+                self.display.edit(True)
+            content = self.getContent()
+            if content['text'] and content['syntax'] != C.SYNTAX_XHTML:
+                self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax'], C.SYNTAX_XHTML)
+            else:
+                syntaxConvertCb(content['text'])
+        else:
+            syntaxConvertCb = lambda text: self.textarea.setText(text)
+            text = self.display.getContent()['text']
+            if text and self.toolbar.syntax != C.SYNTAX_XHTML:
+                self.host.bridge.call('syntaxConvert', syntaxConvertCb, text)
+            else:
+                syntaxConvertCb(text)
+            self.setWidget(self.content_offset, 0, self.textarea)
+            self.textarea.setWidth('100%')  # CSS width doesn't do it, don't know why
+
+        setWysiwyg()  # do it in the end because it affects self.getContent
+
+    def addToolbarButton(self, syntax, key):
+        """Add a button with the defined parameters."""
+        button = Button('<img src="%s" class="richTextIcon" />' %
+                        composition.RICH_BUTTONS[key]["icon"])
+        button.setTitle(composition.RICH_BUTTONS[key]["tip"])
+        button.addStyleName('richTextToolButton')
+        self.toolbar.add(button)
+
+        def buttonCb():
+            """Generic callback for a toolbar button."""
+            text = self.textarea.getText()
+            cursor_pos = self.textarea.getCursorPos()
+            selection_length = self.textarea.getSelectionLength()
+            data = composition.RICH_SYNTAXES[syntax][key]
+            if selection_length == 0:
+                middle_text = data[1]
+            else:
+                middle_text = text[cursor_pos:cursor_pos + selection_length]
+            self.textarea.setText(text[:cursor_pos]
+                                  + data[0]
+                                  + middle_text
+                                  + data[2]
+                                  + text[cursor_pos + selection_length:])
+            self.textarea.setCursorPos(cursor_pos + len(data[0]) + len(middle_text))
+            self.textarea.setFocus(True)
+            self.textarea.onKeyDown()
+
+        def wysiwygCb():
+            """Callback for a toolbar button while wysiwyg mode is enabled."""
+            data = composition.COMMANDS[key]
+
+            def execCommand(command, arg):
+                self.display.setFocus(True)
+                doc().execCommand(command, False, arg.strip() if arg else '')
+            # use Window.prompt instead of dialog.PromptDialog to not loose the focus
+            prompt = lambda command, text: execCommand(command, Window.prompt(text))
+            if isinstance(data, tuple) or isinstance(data, list):
+                if data[1]:
+                    prompt(data[0], data[1])
+                else:
+                    execCommand(data[0], data[2])
+            else:
+                execCommand(data, False, '')
+            self.textarea.onKeyDown()
+
+        button.addClickListener(lambda: wysiwygCb() if self.wysiwyg else buttonCb())
+
+    def getContent(self):
+        assert(hasattr(self, 'textarea'))
+        assert(hasattr(self, 'toolbar'))
+        if self.wysiwyg:
+            content = {'text': self.display.getContent()['text'], 'syntax': C.SYNTAX_XHTML}
+        else:
+            content = {'text': self.strproc(self.textarea.getText()), 'syntax': self.toolbar.syntax}
+        if hasattr(self, 'title_panel'):
+            content.update({'title': self.strproc(self.title_panel.getText())})
+        return content
+
+    def edit(self, edit=False, abort=False, sync=False):
+        """
+        Remark: the editor must be visible before you call this method.
+        @param edit: set to True to edit the content or False to only display it
+        @param abort: set to True to cancel the edition and loose the changes.
+        If edit and abort are both True, self.abortEdition can be used to ask for a
+        confirmation. When edit is False and abort is True, abortion is actually done.
+        @param sync: set to True to cancel the edition after the content has been saved somewhere else
+        """
+        if not (edit and abort):
+            self.refresh(edit)  # not when we are asking for a confirmation
+        BaseTextEditor.edit(self, edit, abort, sync)  # after the UI has been refreshed
+        if (edit and abort):
+            return  # self.abortEdition is called by BaseTextEditor.edit
+        self.setWysiwyg(False, init=True)  # after BaseTextEditor (it affects self.getContent)
+        if sync:
+            return
+        # the following must NOT be done at each UI refresh!
+        content = self._original_content
+        if edit:
+            def getParamCb(syntax):
+                # set the editable text in the current user-selected syntax
+                def syntaxConvertCb(text=None):
+                    if text is not None:
+                        # Important: this also update self._original_content
+                        content.update({'text': text})
+                    content.update({'syntax': syntax})
+                    self.textarea.setText(content['text'])
+                    if hasattr(self, 'title_panel') and 'title' in content:
+                        self.title_panel.setText(content['title'])
+                        self.title_panel.setStackVisible(0, content['title'] != '')
+                    self.setToolBar(syntax)
+                if content['text'] and content['syntax'] != syntax:
+                    self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax'])
+                else:
+                    syntaxConvertCb()
+            self.host.bridge.call('asyncGetParamA', getParamCb, composition.PARAM_NAME_SYNTAX, composition.PARAM_KEY_COMPOSITION)
+        else:
+            if not self.initialized:
+                # set the display text in XHTML only during init because a new MicroblogEntry instance is created after each modification
+                self.setDisplayContent()
+            self.display.edit(False)
+
+    def setDisplayContent(self):
+        """Set the content of the HTMLTextEditor which is used for display/wysiwyg"""
+        content = self._original_content
+        text = content['text']
+        if 'title' in content and content['title']:
+            text = '<h1>%s</h1>%s' % (html_sanitize(content['title']), content['text'])
+        self.display.setContent({'text': text})
+
+    def setFocus(self, focus):
+        self.textarea.setFocus(focus)
+
+    def abortEdition(self, content):
+        """Ask for confirmation before closing the dialog."""
+        def confirm_cb(answer):
+            if answer:
+                self.edit(False, True)
+        _dialog = ConfirmDialog(confirm_cb, text="Do you really want to %s?" % ("cancel your changes" if self.update_msg else "cancel this message"))
+        _dialog.cancel_button.setText(_("No"))
+        _dialog.show()
+
+
+class RichMessageEditor(RichTextEditor):
+    """Use the rich text editor for sending messages with extended addressing.
+    Recipient panels are on top and data may be synchronized from/to the unibox."""
+
+    @classmethod
+    def getOrCreate(cls, host, parent=None, callback=None):
+        """Get or create the message editor associated to that host.
+        Add it to parent if parent is not None, otherwise display it
+        in a popup dialog.
+        @param host: the host
+        @param parent: parent panel (or None to display in a popup).
+        @return: the RichTextEditor instance if parent is not None,
+        otherwise a popup DialogBox containing the RichTextEditor.
+        """
+        if not hasattr(host, 'richtext'):
+            modifiedCb = lambda content: True
+
+            def afterEditCb(content):
+                if hasattr(host.richtext, 'popup'):
+                    host.richtext.popup.hide()
+                else:
+                    host.richtext.setVisible(False)
+                callback()
+            options = ['no_title']
+            style = {'main': 'richMessageEditor', 'textarea': 'richMessageArea'}
+            host.richtext = RichMessageEditor(host, None, modifiedCb, afterEditCb, options, style)
+
+        def add(widget, parent):
+            if widget.getParent() is not None:
+                if widget.getParent() != parent:
+                    widget.removeFromParent()
+                    parent.add(widget)
+            else:
+                parent.add(widget)
+            widget.setVisible(True)
+            widget.initialized = False  # fake a new creation
+            widget.edit(True)
+
+        if parent is None:
+            if not hasattr(host.richtext, 'popup'):
+                host.richtext.popup = DialogBox(autoHide=False, centered=True)
+                host.richtext.popup.setHTML("Compose your message")
+                host.richtext.popup.add(host.richtext)
+            add(host.richtext, host.richtext.popup)
+            host.richtext.popup.center()
+        else:
+            add(host.richtext, parent)
+        return host.richtext.popup if parent is None else host.richtext
+
+    def _prepareUI(self, y_offset=0):
+        """Prepare the UI to host recipients panel, toolbar, text area...
+        @param y_offset: Y offset to start from (extra rows on top)"""
+        self.recipient_offset = y_offset
+        self.recipient_spacer_offset = self.recipient_offset + len(composition.RECIPIENT_TYPES)
+        RichTextEditor._prepareUI(self, self.recipient_spacer_offset + 1)
+
+    def refresh(self, edit=None):
+        """Refresh the UI between edition/display mode
+        @param edit: set to True to display the edition mode"""
+        if edit is None:
+            edit = hasattr(self, 'textarea') and self.textarea.getVisible()
+        RichTextEditor.refresh(self, edit)
+
+        for widget in ['recipient', 'recipient_spacer']:
+            if hasattr(self, widget):
+                getattr(self, widget).setVisible(edit)
+
+        if not edit:
+            return
+
+        if not hasattr(self, 'recipient'):
+            # recipient types sub-panels are automatically added by the manager
+            self.recipient = RecipientManager(self, self.recipient_offset)
+            self.recipient.createWidgets(title_format="%s: ")
+            self.recipient_spacer = HTML('')
+            self.recipient_spacer.setStyleName('recipientSpacer')
+            self.getFlexCellFormatter().setColSpan(self.recipient_spacer_offset, 0, 2)
+            self.setWidget(self.recipient_spacer_offset, 0, self.recipient_spacer)
+
+        if not hasattr(self, 'sync_button'):
+            self.sync_button = Button("Back to quick box", lambda: self.edit(True, sync=True))
+            self.command.insert(self.sync_button, 1)
+
+    def syncToEditor(self):
+        """Synchronize from unibox."""
+        def setContent(target, data):
+            if hasattr(self, 'recipient'):
+                self.recipient.setContacts({"To": [target]} if target else {})
+            self.setContent({'text': data if data else '', 'syntax': ''})
+            self.textarea.setText(data if data else '')
+        data, target = self.host.uni_box.getTargetAndData() if self.host.uni_box else (None, None)
+        setContent(target, data)
+
+    def __syncToUniBox(self, recipients=None, emptyText=False):
+        """Synchronize to unibox if a maximum of one recipient is set.
+        @return True if the sync could be done, False otherwise"""
+        if not self.host.uni_box:
+            return
+        setText = lambda: self.host.uni_box.setText("" if emptyText else self.getContent()['text'])
+        if not hasattr(self, 'recipient'):
+            setText()
+            return True
+        if recipients is None:
+            recipients = self.recipient.getContacts()
+        target = ""
+        # we could eventually allow more in the future
+        allowed = 1
+        for key in recipients:
+            count = len(recipients[key])
+            if count == 0:
+                continue
+            allowed -= count
+            if allowed < 0:
+                return False
+            # TODO: change this if later more then one recipients are allowed
+            target = recipients[key][0]
+        setText()
+        if target == "":
+            return True
+        if target.startswith("@"):
+            _class = panels.MicroblogPanel
+            target = None if target == "@@" else target[1:]
+        else:
+            _class = panels.ChatPanel
+        self.host.getOrCreateLiberviaWidget(_class, target)
+        return True
+
+    def syncFromEditor(self, content):
+        """Synchronize to unibox and close the dialog afterward. Display
+        a message and leave the dialog open if the sync was not possible."""
+        if self.__syncToUniBox():
+            self._afterEditCb(content)
+            return
+        InfoDialog("Too many recipients",
+                   "A message with more than one direct recipient (To)," +
+                   " or with any special recipient (Cc or Bcc), could not be" +
+                   " stored in the quick box.\n\nPlease finish your composing" +
+                   " in the rich text editor, and send your message directly" +
+                   " from here.", Width="400px").center()
+
+    def edit(self, edit=True, abort=False, sync=False):
+        if not edit and not abort and not sync:  # force sending message even when the text has not been modified
+            if not self.__sendMessage():  # message has not been sent (missing information), do nothing
+                return
+        RichTextEditor.edit(self, edit, abort, sync)
+
+    def __sendMessage(self):
+        """Send the message."""
+        recipients = self.recipient.getContacts()
+        targets = []
+        for addr in recipients:
+            for recipient in recipients[addr]:
+                if recipient.startswith("@"):
+                    targets.append(("PUBLIC", None, addr) if recipient == "@@" else ("GROUP", recipient[1:], addr))
+                else:
+                    targets.append(("chat", recipient, addr))
+        # check that we actually have a message target and data
+        content = self.getContent()
+        if content['text'] == "" or len(targets) == 0:
+            InfoDialog("Missing information",
+                       "Some information are missing and the message hasn't been sent.", Width="400px").center()
+            return None
+        self.__syncToUniBox(recipients, emptyText=True)
+        extra = {'content_rich': content['text']}
+        if hasattr(self, 'title_panel'):
+            extra.update({'title': content['title']})
+        self.host.send(targets, content['text'], extra=extra)
+        return True
+
+
+class RecipientManager(ListManager):
+    """A manager for sub-panels to set the recipients for each recipient type."""
+
+    def __init__(self, parent, y_offset=0):
+        # TODO: be sure we also display empty groups and disconnected contacts + their groups
+        # store the full list of potential recipients (groups and contacts)
+        list_ = []
+        list_.append("@@")
+        list_.extend("@%s" % group for group in parent.host.contact_panel.getGroups())
+        list_.extend(contact for contact in parent.host.contact_panel.getContacts())
+        ListManager.__init__(self, parent, composition.RECIPIENT_TYPES, list_, {'y': y_offset})
+
+        self.registerPopupMenuPanel(entries=composition.RECIPIENT_TYPES,
+                                    hide=lambda sender, key: self.__children[key]["panel"].isVisible(),
+                                    callback=self.setContactPanelVisible)
+
+
+class EditTextArea(TextArea, KeyboardHandler):
+    def __init__(self, _parent):
+        TextArea.__init__(self)
+        self._parent = _parent
+        KeyboardHandler.__init__(self)
+        self.addKeyboardListener(self)
+
+    def onKeyDown(self, sender=None, keycode=None, modifiers=None):
+        for listener in self._parent.edit_listeners:
+            listener(self, keycode)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/xmlui.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,420 @@
+#!/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/>.
+
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat_frontends.tools import xmlui
+
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.TabPanel import TabPanel
+from pyjamas.ui.Grid import Grid
+from pyjamas.ui.Label import Label
+from pyjamas.ui.TextBox import TextBox
+from pyjamas.ui.PasswordTextBox import PasswordTextBox
+from pyjamas.ui.TextArea import TextArea
+from pyjamas.ui.CheckBox import CheckBox
+from pyjamas.ui.ListBox import ListBox
+from pyjamas.ui.Button import Button
+from pyjamas.ui.HTML import HTML
+
+from nativedom import NativeDOM
+
+
+class EmptyWidget(xmlui.EmptyWidget, Label):
+
+    def __init__(self, parent):
+        Label.__init__(self, '')
+
+
+class TextWidget(xmlui.TextWidget, Label):
+
+    def __init__(self, parent, value):
+        Label.__init__(self, value)
+
+
+class LabelWidget(xmlui.LabelWidget, TextWidget):
+
+    def __init__(self, parent, value):
+        TextWidget.__init__(self, parent, value+": ")
+
+
+class JidWidget(xmlui.JidWidget, TextWidget):
+
+    def __init__(self, parent, value):
+        TextWidget.__init__(self, parent, value)
+
+
+class DividerWidget(xmlui.DividerWidget, HTML):
+
+    def __init__(self, parent, style='line'):
+        """Add a divider
+
+        @param parent
+        @param style (string): one of:
+                               - line: a simple line
+                               - dot: a line of dots
+                               - dash: a line of dashes
+                               - plain: a full thick line
+                               - blank: a blank line/space
+        """
+        HTML.__init__(self, "<hr/>")
+        self.addStyleName(style)
+
+
+class StringWidget(xmlui.StringWidget, TextBox):
+
+    def __init__(self, parent, value):
+        TextBox.__init__(self)
+        self.setText(value)
+
+    def _xmluiSetValue(self, value):
+        self.setText(value)
+
+    def _xmluiGetValue(self):
+        return self.getText()
+
+    def _xmluiOnChange(self, callback):
+        self.addChangeListener(callback)
+
+
+class PasswordWidget(xmlui.PasswordWidget, PasswordTextBox):
+
+    def __init__(self, parent, value):
+        PasswordTextBox.__init__(self)
+        self.setText(value)
+
+    def _xmluiSetValue(self, value):
+        self.setText(value)
+
+    def _xmluiGetValue(self):
+        return self.getText()
+
+    def _xmluiOnChange(self, callback):
+        self.addChangeListener(callback)
+
+
+class TextBoxWidget(xmlui.TextBoxWidget, TextArea):
+
+    def __init__(self, parent, value):
+        TextArea.__init__(self)
+        self.setText(value)
+
+    def _xmluiSetValue(self, value):
+        self.setText(value)
+
+    def _xmluiGetValue(self):
+        return self.getText()
+
+    def _xmluiOnChange(self, callback):
+        self.addChangeListener(callback)
+
+
+class BoolWidget(xmlui.BoolWidget, CheckBox):
+
+    def __init__(self, parent, state):
+        CheckBox.__init__(self)
+        self.setChecked(state)
+
+    def _xmluiSetValue(self, value):
+        self.setChecked(value == "true")
+
+    def _xmluiGetValue(self):
+        return "true" if self.isChecked() else "false"
+
+    def _xmluiOnChange(self, callback):
+        self.addClickListener(callback)
+
+
+class ButtonWidget(xmlui.ButtonWidget, Button):
+
+    def __init__(self, parent, value, click_callback):
+        Button.__init__(self, value, click_callback)
+
+    def _xmluiOnClick(self, callback):
+        self.addClickListener(callback)
+
+
+class ListWidget(xmlui.ListWidget, ListBox):
+
+    def __init__(self, parent, options, selected, flags):
+        ListBox.__init__(self)
+        multi_selection = 'single' not in flags
+        self.setMultipleSelect(multi_selection)
+        if multi_selection:
+            self.setVisibleItemCount(5)
+        for option in options:
+            self.addItem(option[1])
+        self._xmlui_attr_map = {label: value for value, label in options}
+        self._xmluiSelectValues(selected)
+
+    def _xmluiSelectValue(self, value):
+        """Select a value checking its item"""
+        try:
+            label = [label for label, _value in self._xmlui_attr_map.items() if _value == value][0]
+        except IndexError:
+            log.warning("Can't find value [%s] to select" % value)
+            return
+        self.selectItem(label)
+
+    def _xmluiSelectValues(self, values):
+        """Select multiple values, ignore the items"""
+        self.setValueSelection(values)
+
+    def _xmluiGetSelectedValues(self):
+        ret = []
+        for label in self.getSelectedItemText():
+            ret.append(self._xmlui_attr_map[label])
+        return ret
+
+    def _xmluiOnChange(self, callback):
+        self.addChangeListener(callback)
+
+    def _xmluiAddValues(self, values, select=True):
+        selected = self._xmluiGetSelectedValues()
+        for value in values:
+            if value not in self._xmlui_attr_map.values():
+                self.addItem(value)
+                self._xmlui_attr_map[value] = value
+            if value not in selected:
+                selected.append(value)
+        self._xmluiSelectValues(selected)
+
+
+class LiberviaContainer(object):
+
+    def _xmluiAppend(self, widget):
+        self.append(widget)
+
+
+class AdvancedListContainer(xmlui.AdvancedListContainer, Grid):
+
+    def __init__(self, parent, columns, selectable='no'):
+        Grid.__init__(self, 0, columns)
+        self.columns = columns
+        self.row = -1
+        self.col = 0
+        self._xmlui_rows_idx = []
+        self._xmlui_selectable = selectable != 'no'
+        self._xmlui_selected_row = None
+        self.addTableListener(self)
+        if self._xmlui_selectable:
+            self.addStyleName('AdvancedListSelectable')
+
+    def onCellClicked(self, grid, row, col):
+        if not self._xmlui_selectable:
+            return
+        self._xmlui_selected_row = row
+        try:
+            self._xmlui_select_cb(self)
+        except AttributeError:
+            log.warning("no select callback set")
+
+
+    def _xmluiAppend(self, widget):
+        self.setWidget(self.row, self.col, widget)
+        self.col += 1
+
+    def _xmluiAddRow(self, idx):
+        self.row += 1
+        self.col = 0
+        self._xmlui_rows_idx.insert(self.row, idx)
+        self.resizeRows(self.row+1)
+
+    def _xmluiGetSelectedWidgets(self):
+        return [self.getWidget(self._xmlui_selected_row, col) for col in range(self.columns)]
+
+    def _xmluiGetSelectedIndex(self):
+        try:
+            return self._xmlui_rows_idx[self._xmlui_selected_row]
+        except TypeError:
+            return None
+
+    def _xmluiOnSelect(self, callback):
+        self._xmlui_select_cb = callback
+
+
+class PairsContainer(xmlui.PairsContainer, Grid):
+
+    def __init__(self, parent):
+        Grid.__init__(self, 0, 0)
+        self.row = 0
+        self.col = 0
+
+    def _xmluiAppend(self, widget):
+        if self.col == 0:
+            self.resize(self.row+1, 2)
+        self.setWidget(self.row, self.col, widget)
+        self.col += 1
+        if self.col == 2:
+            self.row +=1
+            self.col = 0
+
+
+
+class TabsContainer(LiberviaContainer, xmlui.TabsContainer, TabPanel):
+
+    def __init__(self, parent):
+        TabPanel.__init__(self)
+        self.setStyleName('liberviaTabPanel')
+
+    def _xmluiAddTab(self, label):
+        tab_panel = VerticalContainer(self)
+        self.add(tab_panel, label)
+        if len(self.getChildren()) == 1:
+            self.selectTab(0)
+        return tab_panel
+
+
+class VerticalContainer(LiberviaContainer, xmlui.VerticalContainer, VerticalPanel):
+    __bases__ = (LiberviaContainer, xmlui.VerticalContainer, VerticalPanel)
+
+    def __init__(self, parent):
+        VerticalPanel.__init__(self)
+
+
+class WidgetFactory(object):
+    # XXX: __getattr__ doesn't work here for an unknown reason
+
+    def createVerticalContainer(self, *args, **kwargs):
+        instance = VerticalContainer(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createPairsContainer(self, *args, **kwargs):
+        instance = PairsContainer(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createTabsContainer(self, *args, **kwargs):
+        instance = TabsContainer(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createAdvancedListContainer(self, *args, **kwargs):
+        instance = AdvancedListContainer(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createEmptyWidget(self, *args, **kwargs):
+        instance = EmptyWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createTextWidget(self, *args, **kwargs):
+        instance = TextWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createLabelWidget(self, *args, **kwargs):
+        instance = LabelWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createJidWidget(self, *args, **kwargs):
+        instance = JidWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createDividerWidget(self, *args, **kwargs):
+        instance = DividerWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createStringWidget(self, *args, **kwargs):
+        instance = StringWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createPasswordWidget(self, *args, **kwargs):
+        instance = PasswordWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createTextBoxWidget(self, *args, **kwargs):
+        instance = TextBoxWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createBoolWidget(self, *args, **kwargs):
+        instance = BoolWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createButtonWidget(self, *args, **kwargs):
+        instance = ButtonWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+    def createListWidget(self, *args, **kwargs):
+        instance = ListWidget(*args, **kwargs)
+        instance._xmlui_main = self._xmlui_main
+        return instance
+
+
+    # def __getattr__(self, attr):
+    #     if attr.startswith("create"):
+    #         cls = globals()[attr[6:]]
+    #         cls._xmlui_main = self._xmlui_main
+    #         return cls
+
+
+class XMLUI(xmlui.XMLUI, VerticalPanel):
+    widget_factory = WidgetFactory()
+
+    def __init__(self, host, xml_data, title = None, flags = None):
+        self.widget_factory._xmlui_main = self
+        self.dom = NativeDOM()
+        dom_parse = lambda xml_data: self.dom.parseString(xml_data)
+        VerticalPanel.__init__(self)
+        self.setSize('100%', '100%')
+        xmlui.XMLUI.__init__(self, host, xml_data, title, flags, dom_parse)
+
+    def setCloseCb(self, close_cb):
+        self.close_cb = close_cb
+
+    def _xmluiClose(self):
+        if self.close_cb:
+            self.close_cb()
+        else:
+            log.warning("no close method defined")
+
+    def _xmluiLaunchAction(self, action_id, data):
+        self.host.launchAction(action_id, data)
+
+    def _xmluiSetParam(self, name, value, category):
+        self.host.bridge.call('setParam', None, name, value, category)
+
+    def constructUI(self, xml_data):
+        super(XMLUI, self).constructUI(xml_data)
+        self.add(self.main_cont)
+        self.setCellHeight(self.main_cont, '100%')
+        if self.type == 'form':
+            hpanel = HorizontalPanel()
+            hpanel.setStyleName('marginAuto')
+            hpanel.add(Button('Submit',self.onFormSubmitted))
+            if not 'NO_CANCEL' in self.flags:
+                hpanel.add(Button('Cancel',self.onFormCancelled))
+            self.add(hpanel)
+        elif self.type == 'param':
+            assert(isinstance(self.children[0][0],TabPanel))
+            hpanel = HorizontalPanel()
+            hpanel.add(Button('Save', self.onSaveParams))
+            hpanel.add(Button('Cancel', lambda ignore: self._xmluiClose()))
+            self.add(hpanel)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/common/constants.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,31 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a SAT frontend
+# Copyright (C) 2009, 2010, 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/>.
+
+from sat.core.i18n import D_
+from sat_frontends import constants
+
+
+class Const(constants.Const):
+
+    # Frontend parameters
+    ENABLE_UNIBOX_KEY = D_("Composition")
+    ENABLE_UNIBOX_PARAM = D_("Enable unibox")
+
+    # MISC
+    PASSWORD_MIN_LENGTH = 6  # for new account creation
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/libervia.sh	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,78 @@
+#!/bin/sh
+
+DEBUG=""
+PYTHON="python2"
+
+kill_process() {
+    # $1 is the file containing the PID to kill, $2 is the process name
+    if [ -f $1 ]; then
+        PID=`cat $1`
+        if ps -p $PID > /dev/null; then
+            echo "Terminating $2... "
+            kill -INT $PID
+        else
+            echo "No running process of ID $PID... removing PID file"
+            rm -f $1
+        fi
+    else
+        echo "$2 is probably not running (PID file doesn't exist)"
+    fi
+}
+
+#We use python to parse config files
+eval `"$PYTHON" << PYTHONEND
+
+from sat.core.constants import Const as C
+from sat.memory.memory import fixLocalDir
+from ConfigParser import SafeConfigParser
+from os.path import expanduser, join
+import sys
+
+fixLocalDir()  # XXX: tmp update code, will be removed in the future
+
+config = SafeConfigParser(defaults=C.DEFAULT_CONFIG)
+try:
+    config.read(C.CONFIG_FILES)
+except:
+    print ("echo \"/!\\ Can't read main config ! Please check the syntax\";")
+    print ("exit 1")
+    sys.exit()
+
+env=[]
+env.append("PID_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'pid_dir')),''))
+env.append("LOG_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'log_dir')),''))
+print ";".join(env)
+PYTHONEND
+`
+APP_NAME="Libervia"  # FIXME: the import from Python needs libervia module to be in PYTHONPATH
+APP_NAME_FILE="libervia"
+PID_FILE="$PID_DIR$APP_NAME_FILE.pid"
+LOG_FILE="$LOG_DIR$APP_NAME_FILE.log"
+
+# if there is one argument which is "stop", then we kill Libervia
+if [ $# -eq 1 ];then
+    if [ $1 = "stop" ];then
+        kill_process $PID_FILE "$APP_NAME"
+        exit 0
+    fi
+    if [ $1 = "debug" ];then
+        echo "Launching $APP_NAME in debug mode"
+        DEBUG="--debug"
+    fi
+fi
+
+DAEMON="n"
+MAIN_OPTIONS="-${DAEMON}o"
+DATA_DIR=".."
+
+#Don't change the next line
+AUTO_OPTIONS=""
+ADDITIONAL_OPTIONS="--pidfile $PID_FILE --logfile $LOG_FILE $AUTO_OPTIONS $DEBUG"
+
+log_dir=`dirname "$LOG_FILE"`
+if [ ! -d $log_dir ] ; then
+    mkdir $log_dir
+fi
+
+echo "Starting $APP_NAME..."
+twistd $MAIN_OPTIONS $ADDITIONAL_OPTIONS $APP_NAME_FILE -d $DATA_DIR
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/server/blog.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,226 @@
+#!/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/>.
+
+from sat.core.i18n import _
+from sat_frontends.tools.strings import addURLToText
+
+from twisted.internet import defer
+from twisted.web import server
+from twisted.web.resource import Resource
+from twisted.words.protocols.jabber.jid import JID
+from datetime import datetime
+import uuid
+import re
+
+from libervia.server.html_tools import sanitizeHtml
+from libervia.server.constants import Const as C
+
+
+class MicroBlog(Resource):
+    isLeaf = True
+
+    ERROR_TEMPLATE = """
+                <html>
+                <head profile="http://www.w3.org/2005/10/profile">
+                    <link rel="icon" type="image/png" href="%(root)ssat_logo_16.png">
+                    <title>MICROBLOG ERROR</title>
+                </head>
+                <body>
+                    <h1 style='text-align: center; color: red;'>%(message)s</h1>
+                </body>
+                </html>
+                """
+
+    def __init__(self, host):
+        self.host = host
+        Resource.__init__(self)
+
+    def render_GET(self, request):
+        if not request.postpath:
+            return MicroBlog.ERROR_TEMPLATE % {'root': '',
+                                               'message': "You must indicate a nickname"}
+        else:
+            prof_requested = request.postpath[0]
+            #TODO: char check: only use alphanumerical chars + some extra(_,-,...) here
+            prof_found = self.host.bridge.getProfileName(prof_requested)
+            if not prof_found or prof_found == 'libervia':
+                return MicroBlog.ERROR_TEMPLATE % {'root': '../' * len(request.postpath),
+                                                    'message': "Invalid nickname"}
+            else:
+                def got_jid(pub_jid_s):
+                    pub_jid = JID(pub_jid_s)
+                    d2 = defer.Deferred()
+                    item_id = None
+                    if len(request.postpath) > 1:
+                        if request.postpath[1] == 'atom.xml':  # return the atom feed
+                            d2.addCallbacks(self.render_atom_feed, self.render_error_blog, [request], None, [request, prof_found], None)
+                            self.host.bridge.getLastGroupBlogsAtom(pub_jid.userhost(), 10, 'libervia', d2.callback, d2.errback)
+                            return
+                        try:  # check if the given path is a valid UUID
+                            uuid.UUID(request.postpath[1])
+                            item_id = request.postpath[1]
+                        except ValueError:
+                            pass
+                    d2.addCallbacks(self.render_html_blog, self.render_error_blog, [request, prof_found], None, [request, prof_found], None)
+                    if item_id:  # display one message and its comments
+                        self.host.bridge.getGroupBlogsWithComments(pub_jid.userhost(), [item_id], 'libervia', d2.callback, d2.errback)
+                    else:  # display the last messages without comment
+                        self.host.bridge.getLastGroupBlogs(pub_jid.userhost(), 10, 'libervia', d2.callback, d2.errback)
+
+                d1 = defer.Deferred()
+                JID(self.host.bridge.asyncGetParamA('JabberID', 'Connection', 'value', C.SERVER_SECURITY_LIMIT, prof_found, callback=d1.callback, errback=d1.errback))
+                d1.addCallbacks(got_jid)
+
+                return server.NOT_DONE_YET
+
+    def render_html_blog(self, mblog_data, request, profile):
+        """Retrieve the user parameters before actually rendering the static blog
+        @param mblog_data: list of microblog data or list of couple (microblog data, list of microblog data)
+        @param request: HTTP request
+        @param profile
+        """
+        d_list = []
+        style = {}
+
+        def getCallback(param_name):
+            d = defer.Deferred()
+            d.addCallback(lambda value: style.update({param_name: value}))
+            d_list.append(d)
+            return d.callback
+
+        eb = lambda failure: self.render_error_blog(failure, request, profile)
+
+        for param_name in (C.STATIC_BLOG_PARAM_TITLE, C.STATIC_BLOG_PARAM_BANNER, C.STATIC_BLOG_PARAM_KEYWORDS, C.STATIC_BLOG_PARAM_DESCRIPTION):
+            self.host.bridge.asyncGetParamA(param_name, C.STATIC_BLOG_KEY, 'value', C.SERVER_SECURITY_LIMIT, profile, callback=getCallback(param_name), errback=eb)
+
+        cb = lambda dummy: self.__render_html_blog(mblog_data, style, request, profile)
+        defer.DeferredList(d_list).addCallback(cb)
+
+    def __render_html_blog(self, mblog_data, style, request, profile):
+        """Actually render the static blog. If mblog_data is a list of dict, we are missing
+        the comments items so we just display the main items. If mblog_data is a list of couple,
+        each couple is associating a main item data with the list of its comments, so we render all.
+        @param mblog_data: list of microblog data or list of couple (microblog data, list of microblog data)
+        @param style: dict defining the blog's rendering parameters
+        @param request: the HTTP request
+        @profile
+        """
+        if not isinstance(style, dict):
+            style = {}
+        user = sanitizeHtml(profile).encode('utf-8')
+        root_url = '../' * len(request.postpath)
+        base_url = root_url + 'blog/' + user
+
+        def getFromData(key):
+            return sanitizeHtml(style[key]).encode('utf-8') if key in style else ''
+
+        def getImageFromData(key, alt):
+            """regexp from http://answers.oreilly.com/topic/280-how-to-validate-urls-with-regular-expressions/"""
+            url = style[key].encode('utf-8') if key in style else ''
+            regexp = r"^(https?|ftp)://[a-z0-9-]+(\.[a-z0-9-]+)+(/[\w-]+)*/[\w-]+\.(gif|png|jpg)$"
+            return "<img src='%(url)s' alt='%(alt)s'/>" % {'alt': alt, 'url': url} if re.match(regexp, url) else alt
+
+        request.write("""
+            <html>
+            <head>
+                <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+                <meta name="keywords" content="%(keywords)s">
+                <meta name="description" content="%(description)s">
+                <link rel="alternate" type="application/atom+xml" href="%(base)s/atom.xml"/>
+                <link rel="stylesheet" type="text/css" href="%(root)scss/blog.css" />
+                <link rel="icon" type="image/png" href="%(root)ssat_logo_16.png">
+                <title>%(title)s</title>
+            </head>
+            <body>
+            <div class="mblog_title"><a href="%(base)s">%(banner_elt)s</a></div>
+            """ % {'base': base_url,
+                   'root': root_url,
+                   'user': user,
+                   'keywords': getFromData(C.STATIC_BLOG_PARAM_KEYWORDS),
+                   'description': getFromData(C.STATIC_BLOG_PARAM_DESCRIPTION),
+                   'title': getFromData(C.STATIC_BLOG_PARAM_TITLE) or "%s's microblog" % user,
+                   'banner_elt': getImageFromData(C.STATIC_BLOG_PARAM_BANNER, user)})
+        mblog_data = [(entry if isinstance(entry, tuple) else (entry, [])) for entry in mblog_data]
+        mblog_data = sorted(mblog_data, key=lambda entry: (-float(entry[0].get('published', 0))))
+        for entry in mblog_data:
+            self.__render_html_entry(entry[0], base_url, request)
+            comments = sorted(entry[1], key=lambda entry: (float(entry.get('published', 0))))
+            for comment in comments:
+                self.__render_html_entry(comment, base_url, request)
+        request.write('</body></html>')
+        request.finish()
+
+    def __render_html_entry(self, entry, base_url, request):
+        """Render one microblog entry.
+        @param entry: the microblog entry
+        @param base_url: the base url of the blog
+        @param request: the HTTP request
+        """
+        timestamp = float(entry.get('published', 0))
+        datetime_ = datetime.fromtimestamp(timestamp)
+        is_comment = entry['type'] == 'comment'
+        if is_comment:
+            author = (_("comment from %s") % entry['author']).encode('utf-8')
+            item_link = ''
+        else:
+            author = '&nbsp;'
+            item_link = ("%(base)s/%(item_id)s" % {'base': base_url, 'item_id': entry['id']}).encode('utf-8')
+
+        def getText(key):
+            if ('%s_xhtml' % key) in entry:
+                return entry['%s_xhtml' % key].encode('utf-8')
+            elif key in entry:
+                processor = addURLToText if key.startswith('content') else sanitizeHtml
+                return processor(entry[key]).encode('utf-8')
+            return ''
+
+        def addMainItemLink(elem):
+            if not item_link or not elem:
+                return elem
+            return """<a href="%(link)s" class="item_link">%(elem)s</a>""" % {'link': item_link, 'elem': elem}
+
+        header = addMainItemLink("""<div class="mblog_header">
+                                      <div class="mblog_metadata">
+                                        <div class="mblog_author">%(author)s</div>
+                                        <div class="mblog_timestamp">%(date)s</div>
+                                      </div>
+                                    </div>""" % {'author': author, 'date': datetime_})
+
+        title = addMainItemLink(getText('title'))
+        body = getText('content')
+        if title:  # insert the title within the body
+            body = """<h1>%(title)s</h1>\n%(body)s""" % {'title': title, 'body': body}
+
+        request.write("""<div class="mblog_entry %(extra_style)s">
+                           %(header)s
+                           <span class="mblog_content">%(content)s</span>
+                         </div>""" %
+                         {'extra_style': 'mblog_comment' if entry['type'] == 'comment' else '',
+                          'item_link': item_link,
+                          'header': header,
+                          'content': body})
+
+    def render_atom_feed(self, feed, request):
+        request.write(feed.encode('utf-8'))
+        request.finish()
+
+    def render_error_blog(self, error, request, profile):
+        request.write(MicroBlog.ERROR_TEMPLATE % {'root': '../' * len(request.postpath),
+                                                  'message': "Can't access requested data"})
+        request.finish()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/server/constants.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,42 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a SAT frontend
+# Copyright (C) 2009, 2010, 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/>.
+
+from libervia.common import constants
+
+
+class Const(constants.Const):
+
+    APP_NAME = 'Libervia'
+    SERVICE_PROFILE = 'libervia'  # the SàT profile that is used for exporting the service
+
+    TIMEOUT = 300  # Session's time out, after that the user will be disconnected
+    HTML_DIR = "html/"
+    SERVER_CSS_DIR = "server_css/"
+    MEDIA_DIR = "media/"
+    AVATARS_DIR = "avatars/"
+    CARDS_DIR = "games/cards/tarot"
+
+    ERRNUM_BRIDGE_ERRBACK = 0  # FIXME
+    ERRNUM_LIBERVIA = 0  # FIXME
+
+    # Security limit for Libervia (get/set params)
+    SECURITY_LIMIT = 5
+
+    # Security limit for Libervia server_side
+    SERVER_SECURITY_LIMIT = constants.Const.NO_SECURITY_LIMIT
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/server/html_tools.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,32 @@
+#!/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/>.
+
+def sanitizeHtml(text):
+    """Sanitize HTML by escaping everything"""
+    #this code comes from official python wiki: http://wiki.python.org/moin/EscapingHtml
+    html_escape_table = {
+        "&": "&amp;",
+        '"': "&quot;",
+        "'": "&apos;",
+        ">": "&gt;",
+        "<": "&lt;",
+        }
+
+    return "".join(html_escape_table.get(c,c) for c in text)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/server/server.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,1168 @@
+#!/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/>.
+
+from twisted.application import service
+from twisted.internet import glib2reactor
+glib2reactor.install()
+from twisted.internet import reactor, defer
+from twisted.web import server
+from twisted.web.static import File
+from twisted.web.resource import Resource, NoResource
+from twisted.web.util import Redirect, redirectTo
+from twisted.python.components import registerAdapter
+from twisted.python.failure import Failure
+from twisted.words.protocols.jabber.jid import JID
+
+from txjsonrpc.web import jsonrpc
+from txjsonrpc import jsonrpclib
+
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat_frontends.bridge.DBus import DBusBridgeFrontend, BridgeExceptionNoService
+from sat.core.i18n import _, D_
+from sat.tools.xml_tools import paramsXML2XMLUI
+
+import re
+import glob
+import os.path
+import sys
+import tempfile
+import shutil
+import uuid
+from zope.interface import Interface, Attribute, implements
+from xml.dom import minidom
+from httplib import HTTPS_PORT
+
+try:
+    import OpenSSL
+    from twisted.internet import ssl
+    ssl_available = True
+except:
+    ssl_available = False
+
+from libervia.server.constants import Const as C
+from libervia.server.blog import MicroBlog
+
+
+class ISATSession(Interface):
+    profile = Attribute("Sat profile")
+    jid = Attribute("JID associated with the profile")
+
+
+class SATSession(object):
+    implements(ISATSession)
+    def __init__(self, session):
+        self.profile = None
+        self.jid = None
+
+
+class LiberviaSession(server.Session):
+    sessionTimeout = C.TIMEOUT
+
+    def __init__(self, *args, **kwargs):
+        self.__lock = False
+        server.Session.__init__(self, *args, **kwargs)
+
+    def lock(self):
+        """Prevent session from expiring"""
+        self.__lock = True
+        self._expireCall.reset(sys.maxint)
+
+    def unlock(self):
+        """Allow session to expire again, and touch it"""
+        self.__lock = False
+        self.touch()
+
+    def touch(self):
+        if not self.__lock:
+            server.Session.touch(self)
+
+class ProtectedFile(File):
+    """A File class which doens't show directory listing"""
+
+    def directoryListing(self):
+        return NoResource()
+
+class SATActionIDHandler(object):
+    """Manage SàT action action_id lifecycle"""
+    ID_LIFETIME = 30 #after this time (in seconds), action_id will be suppressed and action result will be ignored
+
+    def __init__(self):
+        self.waiting_ids = {}
+
+    def waitForId(self, callback, action_id, profile, *args, **kwargs):
+        """Wait for an action result
+        @param callback: method to call when action gave a result back
+        @param action_id: action_id to wait for
+        @param profile: %(doc_profile)s
+        @param *args: additional argument to pass to callback
+        @param **kwargs: idem"""
+        action_tuple = (action_id, profile)
+        self.waiting_ids[action_tuple] = (callback, args, kwargs)
+        reactor.callLater(self.ID_LIFETIME, self.purgeID, action_tuple)
+
+    def purgeID(self, action_tuple):
+        """Called when an action_id has not be handled in time"""
+        if action_tuple in self.waiting_ids:
+            log.warning ("action of action_id %s [%s] has not been managed, action_id is now ignored" % action_tuple)
+            del self.waiting_ids[action_tuple]
+
+    def actionResultCb(self, answer_type, action_id, data, profile):
+        """Manage the actionResult signal"""
+        action_tuple = (action_id, profile)
+        if action_tuple in self.waiting_ids:
+            callback, args, kwargs = self.waiting_ids[action_tuple]
+            del self.waiting_ids[action_tuple]
+            callback(answer_type, action_id, data, *args, **kwargs)
+
+class JSONRPCMethodManager(jsonrpc.JSONRPC):
+
+    def __init__(self, sat_host):
+        jsonrpc.JSONRPC.__init__(self)
+        self.sat_host=sat_host
+
+    def asyncBridgeCall(self, method_name, *args, **kwargs):
+        """Call an asynchrone bridge method and return a deferred
+        @param method_name: name of the method as a unicode
+        @return: a deferred which trigger the result
+
+        """
+        d = defer.Deferred()
+
+        def _callback(*args):
+            if not args:
+                d.callback(None)
+            else:
+                if len(args) != 1:
+                    Exception("Multiple return arguments not supported")
+                d.callback(args[0])
+
+        def _errback(result):
+            d.errback(Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(result))))
+
+        kwargs["callback"] = _callback
+        kwargs["errback"] = _errback
+        getattr(self.sat_host.bridge, method_name)(*args, **kwargs)
+        return d
+
+
+class MethodHandler(JSONRPCMethodManager):
+
+    def __init__(self, sat_host):
+        JSONRPCMethodManager.__init__(self, sat_host)
+        self.authorized_params = None
+
+    def render(self, request):
+        self.session = request.getSession()
+        profile = ISATSession(self.session).profile
+        if not profile:
+            #user is not identified, we return a jsonrpc fault
+            parsed = jsonrpclib.loads(request.content.read())
+            fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia
+            return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))
+        return jsonrpc.JSONRPC.render(self, request)
+
+    def jsonrpc_getProfileJid(self):
+        """Return the jid of the profile"""
+        sat_session = ISATSession(self.session)
+        profile = sat_session.profile
+        sat_session.jid = JID(self.sat_host.bridge.getParamA("JabberID", "Connection", profile_key=profile))
+        return sat_session.jid.full()
+
+    def jsonrpc_disconnect(self):
+        """Disconnect the profile"""
+        sat_session = ISATSession(self.session)
+        profile = sat_session.profile
+        self.sat_host.bridge.disconnect(profile)
+
+    def jsonrpc_getContacts(self):
+        """Return all passed args."""
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.getContacts(profile)
+
+    def jsonrpc_addContact(self, entity, name, groups):
+        """Subscribe to contact presence, and add it to the given groups"""
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.addContact(entity, profile)
+        self.sat_host.bridge.updateContact(entity, name, groups, profile)
+
+    def jsonrpc_delContact(self, entity):
+        """Remove contact from contacts list"""
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.delContact(entity, profile)
+
+    def jsonrpc_updateContact(self, entity, name, groups):
+        """Update contact's roster item"""
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.updateContact(entity, name, groups, profile)
+
+    def jsonrpc_subscription(self, sub_type, entity, name, groups):
+        """Confirm (or infirm) subscription,
+        and setup user roster in case of subscription"""
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.subscription(sub_type, entity, profile)
+        if sub_type == 'subscribed':
+            self.sat_host.bridge.updateContact(entity, name, groups, profile)
+
+    def jsonrpc_getWaitingSub(self):
+        """Return list of room already joined by user"""
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.getWaitingSub(profile)
+
+    def jsonrpc_setStatus(self, presence, status):
+        """Change the presence and/or status
+        @param presence: value from ("", "chat", "away", "dnd", "xa")
+        @param status: any string to describe your status
+        """
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.setPresence('', presence, {'': status}, profile)
+
+
+    def jsonrpc_sendMessage(self, to_jid, msg, subject, type_, options={}):
+        """send message"""
+        profile = ISATSession(self.session).profile
+        return self.asyncBridgeCall("sendMessage", to_jid, msg, subject, type_, options, profile)
+
+    def jsonrpc_sendMblog(self, type_, dest, text, extra={}):
+        """ Send microblog message
+        @param type_: one of "PUBLIC", "GROUP"
+        @param dest: destinees (list of groups, ignored for "PUBLIC")
+        @param text: microblog's text
+        """
+        profile = ISATSession(self.session).profile
+        extra['allow_comments'] = 'True'
+
+        if not type_:  # auto-detect
+            type_ = "PUBLIC" if dest == [] else "GROUP"
+
+        if type_ in ("PUBLIC", "GROUP") and text:
+            if type_ == "PUBLIC":
+                #This text if for the public microblog
+                print "sending public blog"
+                return self.sat_host.bridge.sendGroupBlog("PUBLIC", [], text, extra, profile)
+            else:
+                print "sending group blog"
+                dest = dest if isinstance(dest, list) else [dest]
+                return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra, profile)
+        else:
+            raise Exception("Invalid data")
+
+    def jsonrpc_deleteMblog(self, pub_data, comments):
+        """Delete a microblog node
+        @param pub_data: a tuple (service, comment node identifier, item identifier)
+        @param comments: comments node identifier (for main item) or False
+        """
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile)
+
+    def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}):
+        """Modify a microblog node
+        @param pub_data: a tuple (service, comment node identifier, item identifier)
+        @param comments: comments node identifier (for main item) or False
+        @param message: new message
+        @param extra: dict which option name as key, which can be:
+            - allow_comments: True to accept an other level of comments, False else (default: False)
+            - rich: if present, contain rich text in currently selected syntax
+        """
+        profile = ISATSession(self.session).profile
+        if comments:
+            extra['allow_comments'] = 'True'
+        return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile)
+
+    def jsonrpc_sendMblogComment(self, node, text, extra={}):
+        """ Send microblog message
+        @param node: url of the comments node
+        @param text: comment
+        """
+        profile = ISATSession(self.session).profile
+        if node and text:
+            return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile)
+        else:
+            raise Exception("Invalid data")
+
+    def jsonrpc_getMblogs(self, publisher_jid, item_ids):
+        """Get specified microblogs posted by a contact
+        @param publisher_jid: jid of the publisher
+        @param item_ids: list of microblogs items IDs
+        @return list of microblog data (dict)"""
+        profile = ISATSession(self.session).profile
+        d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, profile)
+        return d
+
+    def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids):
+        """Get specified microblogs posted by a contact and their comments
+        @param publisher_jid: jid of the publisher
+        @param item_ids: list of microblogs items IDs
+        @return list of couple (microblog data, list of microblog data)"""
+        profile = ISATSession(self.session).profile
+        d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, profile)
+        return d
+
+    def jsonrpc_getLastMblogs(self, publisher_jid, max_item):
+        """Get last microblogs posted by a contact
+        @param publisher_jid: jid of the publisher
+        @param max_item: number of items to ask
+        @return list of microblog data (dict)"""
+        profile = ISATSession(self.session).profile
+        d = self.asyncBridgeCall("getLastGroupBlogs", publisher_jid, max_item, profile)
+        return d
+
+    def jsonrpc_getMassiveLastMblogs(self, publishers_type, publishers_list, max_item):
+        """Get lasts microblogs posted by several contacts at once
+        @param publishers_type: one of "ALL", "GROUP", "JID"
+        @param publishers_list: list of publishers type (empty list of all, list of groups or list of jids)
+        @param max_item: number of items to ask
+        @return: dictionary key=publisher's jid, value=list of microblog data (dict)"""
+        profile = ISATSession(self.session).profile
+        d = self.asyncBridgeCall("getMassiveLastGroupBlogs", publishers_type, publishers_list, max_item, profile)
+        self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers_list, profile)
+        return d
+
+    def jsonrpc_getMblogComments(self, service, node):
+        """Get all comments of given node
+        @param service: jid of the service hosting the node
+        @param node: comments node
+        """
+        profile = ISATSession(self.session).profile
+        d = self.asyncBridgeCall("getGroupBlogComments", service, node, profile)
+        return d
+
+
+    def jsonrpc_getPresenceStatuses(self):
+        """Get Presence information for connected contacts"""
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.getPresenceStatuses(profile)
+
+    def jsonrpc_getHistory(self, from_jid, to_jid, size, between):
+        """Return history for the from_jid/to_jid couple"""
+        sat_session = ISATSession(self.session)
+        profile = sat_session.profile
+        sat_jid = sat_session.jid
+        if not sat_jid:
+            log.error("No jid saved for this profile")
+            return {}
+        if JID(from_jid).userhost() != sat_jid.userhost() and JID(to_jid).userhost() != sat_jid.userhost():
+            log.error("Trying to get history from a different jid, maybe a hack attempt ?")
+            return {}
+        d = self.asyncBridgeCall("getHistory", from_jid, to_jid, size, between, profile)
+        def show(result_dbus):
+            result = []
+            for line in result_dbus:
+                #XXX: we have to do this stupid thing because Python D-Bus use its own types instead of standard types
+                #     and txJsonRPC doesn't accept D-Bus types, resulting in a empty query
+                timestamp, from_jid, to_jid, message, mess_type, extra = line
+                result.append((float(timestamp), unicode(from_jid), unicode(to_jid), unicode(message), unicode(mess_type), dict(extra)))
+            return result
+        d.addCallback(show)
+        return d
+
+    def jsonrpc_joinMUC(self, room_jid, nick):
+        """Join a Multi-User Chat room
+        @room_jid: leave empty string to generate a unique name
+        """
+        profile = ISATSession(self.session).profile
+        try:
+            if room_jid != "":
+                room_jid = JID(room_jid).userhost()
+        except:
+            log.warning('Invalid room jid')
+            return
+        d = self.asyncBridgeCall("joinMUC", room_jid, nick, {}, profile)
+        return d
+
+    def jsonrpc_inviteMUC(self, contact_jid, room_jid):
+        """Invite a user to a Multi-User Chat room"""
+        profile = ISATSession(self.session).profile
+        try:
+            room_jid = JID(room_jid).userhost()
+        except:
+            log.warning('Invalid room jid')
+            return
+        room_id = room_jid.split("@")[0]
+        service = room_jid.split("@")[1]
+        self.sat_host.bridge.inviteMUC(contact_jid, service, room_id, {}, profile)
+
+    def jsonrpc_mucLeave(self, room_jid):
+        """Quit a Multi-User Chat room"""
+        profile = ISATSession(self.session).profile
+        try:
+            room_jid = JID(room_jid)
+        except:
+            log.warning('Invalid room jid')
+            return
+        self.sat_host.bridge.mucLeave(room_jid.userhost(), profile)
+
+    def jsonrpc_getRoomsJoined(self):
+        """Return list of room already joined by user"""
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.getRoomsJoined(profile)
+
+    def jsonrpc_launchTarotGame(self, other_players, room_jid=""):
+        """Create a room, invite the other players and start a Tarot game
+        @param room_jid: leave empty string to generate a unique room name
+        """
+        profile = ISATSession(self.session).profile
+        try:
+            if room_jid != "":
+                room_jid = JID(room_jid).userhost()
+        except:
+            log.warning('Invalid room jid')
+            return
+        self.sat_host.bridge.tarotGameLaunch(other_players, room_jid, profile)
+
+    def jsonrpc_getTarotCardsPaths(self):
+        """Give the path of all the tarot cards"""
+        _join = os.path.join
+        _media_dir = _join(self.sat_host.media_dir,'')
+        return map(lambda x: _join(C.MEDIA_DIR, x[len(_media_dir):]), glob.glob(_join(_media_dir, C.CARDS_DIR, '*_*.png')));
+
+    def jsonrpc_tarotGameReady(self, player, referee):
+        """Tell to the server that we are ready to start the game"""
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.tarotGameReady(player, referee, profile)
+
+    def jsonrpc_tarotGamePlayCards(self, player_nick, referee, cards):
+        """Tell to the server the cards we want to put on the table"""
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.tarotGamePlayCards(player_nick, referee, cards, profile)
+
+    def jsonrpc_launchRadioCollective(self, invited, room_jid=""):
+        """Create a room, invite people, and start a radio collective
+        @param room_jid: leave empty string to generate a unique room name
+        """
+        profile = ISATSession(self.session).profile
+        try:
+            if room_jid != "":
+                room_jid = JID(room_jid).userhost()
+        except:
+            log.warning('Invalid room jid')
+            return
+        self.sat_host.bridge.radiocolLaunch(invited, room_jid, profile)
+
+    def jsonrpc_getEntityData(self, jid, keys):
+        """Get cached data for an entit
+        @param jid: jid of contact from who we want data
+        @param keys: name of data we want (list)
+        @return: requested data"""
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.getEntityData(jid, keys, profile)
+
+    def jsonrpc_getCard(self, jid):
+        """Get VCard for entiry
+        @param jid: jid of contact from who we want data
+        @return: id to retrieve the profile"""
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.getCard(jid, profile)
+
+    def jsonrpc_getAccountDialogUI(self):
+        """Get the dialog for managing user account
+        @return: XML string of the XMLUI"""
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.getAccountDialogUI(profile)
+
+    def jsonrpc_getParamsUI(self):
+        """Return the parameters XML for profile"""
+        profile = ISATSession(self.session).profile
+        d = self.asyncBridgeCall("getParams", C.SECURITY_LIMIT, C.APP_NAME, profile)
+
+        def setAuthorizedParams(params_xml):
+            if self.authorized_params is None:
+                self.authorized_params = {}
+                for cat in minidom.parseString(params_xml.encode('utf-8')).getElementsByTagName("category"):
+                    params = cat.getElementsByTagName("param")
+                    params_list = [param.getAttribute("name") for param in params]
+                    self.authorized_params[cat.getAttribute("name")] = params_list
+            if self.authorized_params:
+                return params_xml
+            else:
+                return None
+
+        d.addCallback(setAuthorizedParams)
+
+        d.addCallback(lambda params_xml: paramsXML2XMLUI(params_xml) if params_xml else "")
+
+        return d
+
+    def jsonrpc_asyncGetParamA(self, param, category, attribute="value"):
+        """Return the parameter value for profile"""
+        profile = ISATSession(self.session).profile
+        d = self.asyncBridgeCall("asyncGetParamA", param, category, attribute, C.SECURITY_LIMIT, profile_key=profile)
+        return d
+
+    def jsonrpc_setParam(self, name, value, category):
+        profile = ISATSession(self.session).profile
+        if category in self.authorized_params and name in self.authorized_params[category]:
+            return self.sat_host.bridge.setParam(name, value, category, C.SECURITY_LIMIT, profile)
+        else:
+            log.warning("Trying to set parameter '%s' in category '%s' without authorization!!!"
+                    % (name, category))
+
+    def jsonrpc_launchAction(self, callback_id, data):
+        #FIXME: any action can be launched, this can be a huge security issue if callback_id can be guessed
+        #       a security system with authorised callback_id must be implemented, similar to the one for authorised params
+        profile = ISATSession(self.session).profile
+        d = self.asyncBridgeCall("launchAction", callback_id, data, profile)
+        return d
+
+    def jsonrpc_chatStateComposing(self, to_jid_s):
+        """Call the method to process a "composing" state.
+        @param to_jid_s: contact the user is composing to
+        """
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.chatStateComposing(to_jid_s, profile)
+
+    def jsonrpc_getNewAccountDomain(self):
+        """@return: the domain for new account creation"""
+        d = self.asyncBridgeCall("getNewAccountDomain")
+        return d
+
+    def jsonrpc_confirmationAnswer(self, confirmation_id, result, answer_data):
+        """Send the user's answer to any previous 'askConfirmation' signal"""
+        profile = ISATSession(self.session).profile
+        self.sat_host.bridge.confirmationAnswer(confirmation_id, result, answer_data, profile)
+
+    def jsonrpc_syntaxConvert(self, text, syntax_from=C.SYNTAX_XHTML, syntax_to=C.SYNTAX_CURRENT):
+        """ Convert a text between two syntaxes
+        @param text: text to convert
+        @param syntax_from: source syntax (e.g. "markdown")
+        @param syntax_to: dest syntax (e.g.: "XHTML")
+        @param safe: clean resulting XHTML to avoid malicious code if True (forced here)
+        @return: converted text """
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.syntaxConvert(text, syntax_from, syntax_to, True, profile)
+
+
+class Register(JSONRPCMethodManager):
+    """This class manage the registration procedure with SàT
+    It provide an api for the browser, check password and setup the web server"""
+
+    def __init__(self, sat_host):
+        JSONRPCMethodManager.__init__(self, sat_host)
+        self.profiles_waiting={}
+        self.request=None
+
+    def getWaitingRequest(self, profile):
+        """Tell if a profile is trying to log in"""
+        if self.profiles_waiting.has_key(profile):
+            return self.profiles_waiting[profile]
+        else:
+            return None
+
+    def render(self, request):
+        """
+        Render method with some hacks:
+           - if login is requested, try to login with form data
+           - except login, every method is jsonrpc
+           - user doesn't need to be authentified for explicitely listed methods, but must be for all others
+        """
+        if request.postpath == ['login']:
+            return self.loginOrRegister(request)
+        _session = request.getSession()
+        parsed = jsonrpclib.loads(request.content.read())
+        method = parsed.get("method")
+        if  method not in ['isRegistered', 'registerParams', 'getMenus']:
+            #if we don't call these methods, we need to be identified
+            profile = ISATSession(_session).profile
+            if not profile:
+                #user is not identified, we return a jsonrpc fault
+                fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia
+                return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))
+        self.request = request
+        return jsonrpc.JSONRPC.render(self, request)
+
+    def loginOrRegister(self, request):
+        """This method is called with the POST information from the registering form.
+
+        @param request: request of the register form
+        @return: a constant indicating the state:
+            - BAD REQUEST: something is wrong in the request (bad arguments)
+            - a return value from self._loginAccount or self._registerNewAccount
+        """
+        try:
+            submit_type = request.args['submit_type'][0]
+        except KeyError:
+            return "BAD REQUEST"
+
+        if submit_type == 'register':
+            return self._registerNewAccount(request)
+        elif submit_type == 'login':
+            return self._loginAccount(request)
+        return Exception('Unknown submit type')
+
+    def _loginAccount(self, request):
+        """Try to authenticate the user with the request information.
+        @param request: request of the register form
+        @return: a constant indicating the state:
+            - BAD REQUEST: something is wrong in the request (bad arguments)
+            - AUTH ERROR: either the profile (login) or the password is wrong
+            - ALREADY WAITING: a request has already been submitted for this profile
+            - server.NOT_DONE_YET: the profile is being processed, the return
+                value will be given by self._logged or self._logginError
+        """
+        try:
+            login_ = request.args['login'][0]
+            password_ = request.args['login_password'][0]
+        except KeyError:
+            return "BAD REQUEST"
+
+        if login_.startswith('@'):
+            raise Exception('No profile_key allowed')
+
+        profile_check = self.sat_host.bridge.getProfileName(login_)
+        if not profile_check or profile_check != login_ or not password_:
+            # profiles with empty passwords are restricted to local frontends
+            return "AUTH ERROR"
+
+        if login_ in self.profiles_waiting:
+            return "ALREADY WAITING"
+
+        def auth_eb(ignore=None):
+            self.__cleanWaiting(login_)
+            log.info("Profile %s doesn't exist or the submitted password is wrong" % login_)
+            request.write("AUTH ERROR")
+            request.finish()
+
+        self.profiles_waiting[login_] = request
+        d = self.asyncBridgeCall("asyncConnect", login_, password_)
+        d.addCallbacks(lambda connected: self._logged(login_, request) if connected else None, auth_eb)
+
+        return server.NOT_DONE_YET
+
+    def _registerNewAccount(self, request):
+        """Create a new account, or return error
+        @param request: request of the register form
+        @return: a constant indicating the state:
+            - BAD REQUEST: something is wrong in the request (bad arguments)
+            - REGISTRATION: new account has been successfully registered
+            - ALREADY EXISTS: the given profile already exists
+            - INTERNAL or 'Unknown error (...)'
+            - server.NOT_DONE_YET: the profile is being processed, the return
+                value will be given later (one of those previously described)
+        """
+        try:
+            profile = login = request.args['register_login'][0]
+            password = request.args['register_password'][0]
+            email = request.args['email'][0]
+        except KeyError:
+            return "BAD REQUEST"
+        if not re.match(r'^[a-z0-9_-]+$', login, re.IGNORECASE) or \
+           not re.match(r'^.+@.+\..+', email, re.IGNORECASE) or \
+           len(password) < C.PASSWORD_MIN_LENGTH:
+            return "BAD REQUEST"
+
+        def registered(result):
+            request.write('REGISTRATION')
+            request.finish()
+
+        def registeringError(failure):
+            reason = failure.value.faultString
+            if reason == "ConflictError":
+                request.write('ALREADY EXISTS')
+            elif reason == "InternalError":
+                request.write('INTERNAL')
+            else:
+                log.error('Unknown registering error: %s' % (reason,))
+                request.write('Unknown error (%s)' % reason)
+            request.finish()
+
+        d = self.asyncBridgeCall("registerSatAccount", email, password, profile)
+        d.addCallback(registered)
+        d.addErrback(registeringError)
+        return server.NOT_DONE_YET
+
+    def __cleanWaiting(self, login):
+        """Remove login from waiting queue"""
+        try:
+            del self.profiles_waiting[login]
+        except KeyError:
+            pass
+
+    def _logged(self, profile, request):
+        """Set everything when a user just logged in
+
+        @param profile
+        @param request
+        @return: a constant indicating the state:
+            - LOGGED
+            - SESSION_ACTIVE
+        """
+        self.__cleanWaiting(profile)
+        _session = request.getSession()
+        sat_session = ISATSession(_session)
+        if sat_session.profile:
+            log.error(('/!\\ Session has already a profile, this should NEVER happen!'))
+            request.write('SESSION_ACTIVE')
+            request.finish()
+            return
+        sat_session.profile = profile
+        self.sat_host.prof_connected.add(profile)
+
+        def onExpire():
+            log.info("Session expired (profile=%s)" % (profile,))
+            try:
+                #We purge the queue
+                del self.sat_host.signal_handler.queue[profile]
+            except KeyError:
+                pass
+            #and now we disconnect the profile
+            self.sat_host.bridge.disconnect(profile)
+
+        _session.notifyOnExpire(onExpire)
+
+        request.write('LOGGED')
+        request.finish()
+
+    def _logginError(self, login, request, error_type):
+        """Something went wrong during logging in
+        @return: error
+        """
+        self.__cleanWaiting(login)
+        return error_type
+
+    def jsonrpc_isConnected(self):
+        _session = self.request.getSession()
+        profile = ISATSession(_session).profile
+        return self.sat_host.bridge.isConnected(profile)
+
+    def jsonrpc_connect(self):
+        _session = self.request.getSession()
+        profile = ISATSession(_session).profile
+        if self.profiles_waiting.has_key(profile):
+            raise jsonrpclib.Fault(1,'Already waiting') #FIXME: define some standard error codes for libervia
+        self.profiles_waiting[profile] = self.request
+        self.sat_host.bridge.connect(profile)
+        return server.NOT_DONE_YET
+
+    def jsonrpc_isRegistered(self):
+        """
+        @return: a couple (registered, message) with:
+        - registered: True if the user is already registered, False otherwise
+        - message: a security warning message if registered is False *and* the connection is unsecure, None otherwise
+        """
+        _session = self.request.getSession()
+        profile = ISATSession(_session).profile
+        if bool(profile):
+            return (True, None)
+        return (False, self.__getSecurityWarning())
+
+    def jsonrpc_registerParams(self):
+        """Register the frontend specific parameters"""
+        params = """
+        <params>
+        <individual>
+        <category name="%(category_name)s" label="%(category_label)s">
+            <param name="%(param_name)s" label="%(param_label)s" value="false" type="bool" security="0"/>
+         </category>
+        </individual>
+        </params>
+        """ % {
+            'category_name': C.ENABLE_UNIBOX_KEY,
+            'category_label': _(C.ENABLE_UNIBOX_KEY),
+            'param_name': C.ENABLE_UNIBOX_PARAM,
+            'param_label': _(C.ENABLE_UNIBOX_PARAM)
+        }
+
+        self.sat_host.bridge.paramsRegisterApp(params, C.SECURITY_LIMIT, C.APP_NAME)
+
+    def jsonrpc_getMenus(self):
+        """Return the parameters XML for profile"""
+        # XXX: we put this method in Register because we get menus before being logged
+        return self.sat_host.bridge.getMenus('', C.SECURITY_LIMIT)
+
+    def __getSecurityWarning(self):
+        """@return: a security warning message, or None if the connection is secure"""
+        if self.request.URLPath().scheme == 'https' or not self.sat_host.security_warning:
+            return None
+        text = D_("You are about to connect to an unsecured service.")
+        if self.sat_host.connection_type == 'both':
+            new_port = (':%s' % self.sat_host.port_https_ext) if self.sat_host.port_https_ext != HTTPS_PORT else ''
+            url = "https://%s" % self.request.URLPath().netloc.replace(':%s' % self.sat_host.port, new_port)
+            text += D_('<br />Secure version of this website: <a href="%(url)s">%(url)s</a>') % {'url': url}
+        return text
+
+
+class SignalHandler(jsonrpc.JSONRPC):
+
+    def __init__(self, sat_host):
+        Resource.__init__(self)
+        self.register=None
+        self.sat_host=sat_host
+        self.signalDeferred = {}
+        self.queue = {}
+
+    def plugRegister(self, register):
+        self.register = register
+
+    def jsonrpc_getSignals(self):
+        """Keep the connection alive until a signal is received, then send it
+        @return: (signal, *signal_args)"""
+        _session = self.request.getSession()
+        profile = ISATSession(_session).profile
+        if profile in self.queue: #if we have signals to send in queue
+            if self.queue[profile]:
+                return self.queue[profile].pop(0)
+            else:
+                #the queue is empty, we delete the profile from queue
+                del self.queue[profile]
+        _session.lock() #we don't want the session to expire as long as this connection is active
+        def unlock(signal, profile):
+            _session.unlock()
+            try:
+                source_defer = self.signalDeferred[profile]
+                if source_defer.called and source_defer.result[0] == "disconnected":
+                    log.info(u"[%s] disconnected" % (profile,))
+                    _session.expire()
+            except IndexError:
+                log.error("Deferred result should be a tuple with fonction name first")
+
+        self.signalDeferred[profile] = defer.Deferred()
+        self.request.notifyFinish().addBoth(unlock, profile)
+        return self.signalDeferred[profile]
+
+    def getGenericCb(self, function_name):
+        """Return a generic function which send all params to signalDeferred.callback
+        function must have profile as last argument"""
+        def genericCb(*args):
+            profile = args[-1]
+            if not profile in self.sat_host.prof_connected:
+                return
+            if profile in self.signalDeferred:
+                self.signalDeferred[profile].callback((function_name,args[:-1]))
+                del self.signalDeferred[profile]
+            else:
+                if not self.queue.has_key(profile):
+                    self.queue[profile] = []
+                self.queue[profile].append((function_name, args[:-1]))
+        return genericCb
+
+    def connected(self, profile):
+        assert(self.register)  # register must be plugged
+        request = self.register.getWaitingRequest(profile)
+        if request:
+            self.register._logged(profile, request)
+
+    def disconnected(self, profile):
+        if not profile in self.sat_host.prof_connected:
+            log.error("'disconnected' signal received for a not connected profile")
+            return
+        self.sat_host.prof_connected.remove(profile)
+        if profile in self.signalDeferred:
+            self.signalDeferred[profile].callback(("disconnected",))
+            del self.signalDeferred[profile]
+        else:
+            if not self.queue.has_key(profile):
+                self.queue[profile] = []
+            self.queue[profile].append(("disconnected",))
+
+
+    def connectionError(self, error_type, profile):
+        assert(self.register) #register must be plugged
+        request = self.register.getWaitingRequest(profile)
+        if request: #The user is trying to log in
+            if error_type == "AUTH_ERROR":
+                _error_t = "AUTH ERROR"
+            else:
+                _error_t = "UNKNOWN"
+            self.register._logginError(profile, request, _error_t)
+
+    def render(self, request):
+        """
+        Render method wich reject access if user is not identified
+        """
+        _session = request.getSession()
+        parsed = jsonrpclib.loads(request.content.read())
+        profile = ISATSession(_session).profile
+        if not profile:
+            #user is not identified, we return a jsonrpc fault
+            fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia
+            return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))
+        self.request = request
+        return jsonrpc.JSONRPC.render(self, request)
+
+class UploadManager(Resource):
+    """This class manage the upload of a file
+    It redirect the stream to SàT core backend"""
+    isLeaf = True
+    NAME = 'path' #name use by the FileUpload
+
+    def __init__(self, sat_host):
+        self.sat_host=sat_host
+        self.upload_dir = tempfile.mkdtemp()
+        self.sat_host.addCleanup(shutil.rmtree, self.upload_dir)
+
+    def getTmpDir(self):
+        return self.upload_dir
+
+    def _getFileName(self, request):
+        """Generate unique filename for a file"""
+        raise NotImplementedError
+
+    def _fileWritten(self, request, filepath):
+        """Called once the file is actually written on disk
+        @param request: HTTP request object
+        @param filepath: full filepath on the server
+        @return: a tuple with the name of the async bridge method
+        to be called followed by its arguments.
+        """
+        raise NotImplementedError
+
+    def render(self, request):
+        """
+        Render method with some hacks:
+           - if login is requested, try to login with form data
+           - except login, every method is jsonrpc
+           - user doesn't need to be authentified for isRegistered, but must be for all other methods
+        """
+        filename = self._getFileName(request)
+        filepath = os.path.join(self.upload_dir, filename)
+        #FIXME: the uploaded file is fully loaded in memory at form parsing time so far
+        #       (see twisted.web.http.Request.requestReceived). A custom requestReceived should
+        #       be written in the futur. In addition, it is not yet possible to get progression informations
+        #       (see http://twistedmatrix.com/trac/ticket/288)
+
+        with open(filepath,'w') as f:
+            f.write(request.args[self.NAME][0])
+
+        def finish(d):
+            error = isinstance(d, Exception) or isinstance (d, Failure)
+            request.write('KO' if error else 'OK')
+            # TODO: would be great to re-use the original Exception class and message
+            # but it is lost in the middle of the backtrace and encapsulated within
+            # a DBusException instance --> extract the data from the backtrace?
+            request.finish()
+
+        d = JSONRPCMethodManager(self.sat_host).asyncBridgeCall(*self._fileWritten(request, filepath))
+        d.addCallbacks(lambda d: finish(d), lambda failure: finish(failure))
+        return server.NOT_DONE_YET
+
+
+class UploadManagerRadioCol(UploadManager):
+    NAME = 'song'
+
+    def _getFileName(self, request):
+        extension = os.path.splitext(request.args['filename'][0])[1]
+        return "%s%s" % (str(uuid.uuid4()), extension)  # XXX: chromium doesn't seem to play song without the .ogg extension, even with audio/ogg mime-type
+
+    def _fileWritten(self, request, filepath):
+        """Called once the file is actually written on disk
+        @param request: HTTP request object
+        @param filepath: full filepath on the server
+        @return: a tuple with the name of the async bridge method
+        to be called followed by its arguments.
+        """
+        profile = ISATSession(request.getSession()).profile
+        return ("radiocolSongAdded", request.args['referee'][0], filepath, profile)
+
+
+class UploadManagerAvatar(UploadManager):
+    NAME = 'avatar_path'
+
+    def _getFileName(self, request):
+        return str(uuid.uuid4())
+
+    def _fileWritten(self, request, filepath):
+        """Called once the file is actually written on disk
+        @param request: HTTP request object
+        @param filepath: full filepath on the server
+        @return: a tuple with the name of the async bridge method
+        to be called followed by its arguments.
+        """
+        profile = ISATSession(request.getSession()).profile
+        return ("setAvatar", filepath, profile)
+
+
+def coerceConnectionType(value):  # called from Libervia.OPT_PARAMETERS
+    allowed_values = ('http', 'https', 'both')
+    if value not in allowed_values:
+        raise ValueError("%(given)s not in %(expected)s" % {'given': value, 'expected': str(allowed_values)})
+    return value
+
+
+def coerceDataDir(value):  # called from Libervia.OPT_PARAMETERS
+    html = os.path.join(value, C.HTML_DIR)
+    if not os.path.isfile(os.path.join(html, 'libervia.html')):
+        raise ValueError("%s is not a Libervia's browser HTML directory" % os.path.realpath(html))
+    server_css = os.path.join(value, C.SERVER_CSS_DIR)
+    if not os.path.isfile(os.path.join(server_css, 'blog.css')):
+        raise ValueError("%s is not a Libervia's server data directory" % os.path.realpath(server_css))
+    return value
+
+
+class Libervia(service.Service):
+
+    DATA_DIR_DEFAULT = ''
+    OPT_PARAMETERS = [['connection_type', 't', 'https', "'http', 'https' or 'both' (to launch both servers).", coerceConnectionType],
+                      ['port', 'p', 8080, 'The port number to listen HTTP on.', int],
+                      ['port_https', 's', 8443, 'The port number to listen HTTPS on.', int],
+                      ['port_https_ext', 'e', 0, 'The external port number used for HTTPS (0 means port_https value).', int],
+                      ['ssl_certificate', 'c', 'libervia.pem', 'PEM certificate with both private and public parts.', str],
+                      ['redirect_to_https', 'r', 1, 'Automatically redirect from HTTP to HTTPS.', int],
+                      ['security_warning', 'w', 1, 'Warn user that he is about to connect on HTTP.', int],
+                      # FIXME: twistd bugs when printing 'à' on "Unknown command" error (works on normal command listing)
+                      ['passphrase', 'k', '', u"Passphrase for the SaT profile named '%s'" % C.SERVICE_PROFILE, str],
+                      ['data_dir', 'd', DATA_DIR_DEFAULT, u'Data directory for Libervia', coerceDataDir],
+                      ]
+
+    def __init__(self, *args, **kwargs):
+        if not kwargs:
+            # During the loading of the twisted plugins, we just need the default values.
+            # This part is not executed when the plugin is actually started.
+            for name, value in [(option[0], option[2]) for option in self.OPT_PARAMETERS]:
+                kwargs[name] = value
+        self.initialised = defer.Deferred()
+        self.connection_type = kwargs['connection_type']
+        self.port = kwargs['port']
+        self.port_https = kwargs['port_https']
+        self.port_https_ext = kwargs['port_https_ext']
+        if not self.port_https_ext:
+            self.port_https_ext = self.port_https
+        self.ssl_certificate = kwargs['ssl_certificate']
+        self.redirect_to_https = kwargs['redirect_to_https']
+        self.security_warning = kwargs['security_warning']
+        self.passphrase = kwargs['passphrase']
+        self.data_dir = kwargs['data_dir']
+        if self.data_dir == Libervia.DATA_DIR_DEFAULT:
+            coerceDataDir(self.data_dir)  # this is not done when using the default value
+        self.html_dir = os.path.join(self.data_dir, C.HTML_DIR)
+        self.server_css_dir = os.path.join(self.data_dir, C.SERVER_CSS_DIR)
+        self._cleanup = []
+        root = ProtectedFile(self.html_dir)
+        self.signal_handler = SignalHandler(self)
+        _register = Register(self)
+        _upload_radiocol = UploadManagerRadioCol(self)
+        _upload_avatar = UploadManagerAvatar(self)
+        self.signal_handler.plugRegister(_register)
+        self.sessions = {} #key = session value = user
+        self.prof_connected = set() #Profiles connected
+        self.action_handler = SATActionIDHandler()
+        ## bridge ##
+        try:
+            self.bridge=DBusBridgeFrontend()
+        except BridgeExceptionNoService:
+            print(u"Can't connect to SàT backend, are you sure it's launched ?")
+            sys.exit(1)
+        def backendReady(dummy):
+            self.bridge.register("connected", self.signal_handler.connected)
+            self.bridge.register("disconnected", self.signal_handler.disconnected)
+            self.bridge.register("connectionError", self.signal_handler.connectionError)
+            self.bridge.register("actionResult", self.action_handler.actionResultCb)
+            #core
+            for signal_name in ['presenceUpdate', 'newMessage', 'subscribe', 'contactDeleted', 'newContact', 'entityDataUpdated', 'askConfirmation', 'newAlert', 'paramUpdate']:
+                self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name))
+            #plugins
+            for signal_name in ['personalEvent', 'roomJoined', 'roomUserJoined', 'roomUserLeft', 'tarotGameStarted', 'tarotGameNew', 'tarotGameChooseContrat',
+                                'tarotGameShowCards', 'tarotGameInvalidCards', 'tarotGameCardsPlayed', 'tarotGameYourTurn', 'tarotGameScore', 'tarotGamePlayers',
+                                'radiocolStarted', 'radiocolPreload', 'radiocolPlay', 'radiocolNoUpload', 'radiocolUploadOk', 'radiocolSongRejected', 'radiocolPlayers',
+                                'roomLeft', 'roomUserChangedNick', 'chatStateReceived']:
+                self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name), "plugin")
+            self.media_dir = self.bridge.getConfig('', 'media_dir')
+            self.local_dir = self.bridge.getConfig('', 'local_dir')
+            root.putChild('', Redirect('libervia.html'))
+            root.putChild('json_signal_api', self.signal_handler)
+            root.putChild('json_api', MethodHandler(self))
+            root.putChild('register_api', _register)
+            root.putChild('upload_radiocol', _upload_radiocol)
+            root.putChild('upload_avatar', _upload_avatar)
+            root.putChild('blog', MicroBlog(self))
+            root.putChild('css', ProtectedFile(self.server_css_dir))
+            root.putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir))
+            root.putChild(os.path.dirname(C.AVATARS_DIR), ProtectedFile(os.path.join(self.local_dir, C.AVATARS_DIR)))
+            root.putChild('radiocol', ProtectedFile(_upload_radiocol.getTmpDir(), defaultType="audio/ogg"))  # We cheat for PoC because we know we are on the same host, so we use directly upload dir
+            self.site = server.Site(root)
+            self.site.sessionFactory = LiberviaSession
+
+        self.bridge.getReady(lambda: self.initialised.callback(None),
+                             lambda failure: self.initialised.errback(Exception(failure)))
+        self.initialised.addCallback(backendReady)
+        self.initialised.addErrback(lambda failure: log.error("Init error: %s" % failure))
+
+    def addCleanup(self, callback, *args, **kwargs):
+        """Add cleaning method to call when service is stopped
+        cleaning method will be called in reverse order of they insertion
+        @param callback: callable to call on service stop
+        @param *args: list of arguments of the callback
+        @param **kwargs: list of keyword arguments of the callback"""
+        self._cleanup.insert(0, (callback, args, kwargs))
+
+    def startService(self):
+        """Connect the profile for Libervia and start the HTTP(S) server(s)"""
+        def eb(e):
+            log.error(_("Connection failed: %s") % e)
+            self.stop()
+
+        def initOk(dummy):
+            if not self.bridge.isConnected(C.SERVICE_PROFILE):
+                self.bridge.asyncConnect(C.SERVICE_PROFILE, self.passphrase,
+                                         callback=self._startService, errback=eb)
+
+        self.initialised.addCallback(initOk)
+
+    def _startService(self, dummy):
+        """Actually start the HTTP(S) server(s) after the profile for Libervia is connected.
+        @raise IOError: the certificate file doesn't exist
+        @raise OpenSSL.crypto.Error: the certificate file is invalid
+        """
+        if self.connection_type in ('https', 'both'):
+            if not ssl_available:
+                raise(ImportError(_("Python module pyOpenSSL is not installed!")))
+            try:
+                with open(os.path.expanduser(self.ssl_certificate)) as keyAndCert:
+                    try:
+                        cert = ssl.PrivateCertificate.loadPEM(keyAndCert.read())
+                    except OpenSSL.crypto.Error as e:
+                        log.error(_("The file '%s' must contain both private and public parts of the certificate") % self.ssl_certificate)
+                        raise e
+            except IOError as e:
+                log.error(_("The file '%s' doesn't exist") % self.ssl_certificate)
+                raise e
+            reactor.listenSSL(self.port_https, self.site, cert.options())
+        if self.connection_type in ('http', 'both'):
+            if self.connection_type == 'both' and self.redirect_to_https:
+                reactor.listenTCP(self.port, server.Site(RedirectToHTTPS(self.port, self.port_https_ext)))
+            else:
+                reactor.listenTCP(self.port, self.site)
+
+    def stopService(self):
+        print "launching cleaning methods"
+        for callback, args, kwargs in self._cleanup:
+            callback(*args, **kwargs)
+        self.bridge.disconnect(C.SERVICE_PROFILE)
+
+    def run(self):
+        reactor.run()
+
+    def stop(self):
+        reactor.stop()
+
+
+class RedirectToHTTPS(Resource):
+
+    def __init__(self, old_port, new_port):
+        Resource.__init__(self)
+        self.isLeaf = True
+        self.old_port = old_port
+        self.new_port = new_port
+
+    def render(self, request):
+        netloc = request.URLPath().netloc.replace(':%s' % self.old_port, ':%s' % self.new_port)
+        url = "https://" + netloc + request.uri
+        return redirectTo(url, request)
+
+
+registerAdapter(SATSession, server.Session, ISATSession)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/twisted/plugins/libervia_server.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,92 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2013  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+
+# 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/>.
+
+from twisted.internet import defer
+if defer.Deferred.debug:
+    # if we are in debug mode, we want to use ipdb instead of pdb
+    try:
+        import ipdb
+        import pdb
+        pdb.set_trace = ipdb.set_trace
+        pdb.post_mortem = ipdb.post_mortem
+    except ImportError:
+        pass
+
+# XXX: We need to configure logs before any log method is used, so here is the best place.
+from libervia.server.constants import Const as C
+from sat.core import log_config
+log_config.satConfigure(C.LOG_BACKEND_TWISTED, C)
+
+from zope.interface import implements
+
+from twisted.python import usage
+from twisted.plugin import IPlugin
+from twisted.application.service import IServiceMaker
+
+from ConfigParser import SafeConfigParser, NoSectionError, NoOptionError
+try:
+    from libervia.server.server import Libervia
+    opt_params = Libervia.OPT_PARAMETERS
+except (ImportError, SystemExit):
+    # avoid raising an error when you call twisted and sat is not launched
+    opt_params = []
+
+
+class Options(usage.Options):
+
+    # optArgs is not really useful in our case, we need more than a flag
+    optParameters = opt_params
+
+    def __init__(self):
+        """You want to read SàT configuration file now in order to overwrite the hard-coded default values.
+        This is because the priority for the usage of the values is (from lowest to highest):
+        - hard-coded default values
+        - values from SàT configuration files
+        - values passed on the command line
+        If you do it later: after the command line options have been parsed, there's no good way to know
+        if the  options values are the hard-coded ones or if they have been passed on the command line.
+        """
+        config = SafeConfigParser()
+        config.read(C.CONFIG_FILES)
+        for index, param in list(enumerate(self.optParameters)):
+            # index is only used to not modify the loop variable "param"
+            name = param[0]
+            try:
+                value = config.get('libervia', name)
+                self.optParameters[index][2] = param[4](value)
+            except (NoSectionError, NoOptionError):
+                pass
+        usage.Options.__init__(self)
+
+
+class LiberviaMaker(object):
+    implements(IServiceMaker, IPlugin)
+
+    tapname = 'libervia'
+    # FIXME: twistd bugs when printing 'à' on "Unknown command" error (works on normal command listing)
+    description = u'The web frontend of Salut a Toi'
+    options = Options
+
+    def makeService(self, options):
+        return Libervia(**dict(options))  # get rid of the usage.Option overload
+
+
+# affectation to some variable is necessary for twisted introspection to work
+serviceMaker = LiberviaMaker()
+
--- a/twisted/plugins/libervia.py	Fri May 16 11:51:10 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,90 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2013  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
-
-# 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/>.
-
-from twisted.internet import defer
-if defer.Deferred.debug:
-    # if we are in debug mode, we want to use ipdb instead of pdb
-    try:
-        import ipdb
-        import pdb
-        pdb.set_trace = ipdb.set_trace
-        pdb.post_mortem = ipdb.post_mortem
-    except ImportError:
-        pass
-
-# XXX: We need to configure logs before any log method is used, so here is the best place.
-from constants import Const as C
-from sat.core import log_config
-log_config.satConfigure(C.LOG_BACKEND_TWISTED, C)
-
-from zope.interface import implements
-
-from twisted.python import usage
-from twisted.plugin import IPlugin
-from twisted.application.service import IServiceMaker
-
-from ConfigParser import SafeConfigParser, NoSectionError, NoOptionError
-from sat.core.constants import Const as C
-try:
-    from libervia_server import Libervia
-    opt_params = Libervia.OPT_PARAMETERS
-except (ImportError, SystemExit):
-    # avoid raising an error when you call twisted and sat is not launched
-    opt_params = []
-
-
-class Options(usage.Options):
-
-    # optArgs is not really useful in our case, we need more than a flag
-    optParameters = opt_params
-
-    def __init__(self):
-        """You want to read SàT configuration file now in order to overwrite the hard-coded default values.
-        This is because the priority for the usage of the values is (from lowest to highest):
-        - hard-coded default values
-        - values from SàT configuration files
-        - values passed on the command line
-        If you do it later: after the command line options have been parsed, there's no good way to know
-        if the  options values are the hard-coded ones or if they have been passed on the command line.
-        """
-        config = SafeConfigParser()
-        config.read(C.CONFIG_FILES)
-        for index, param in list(enumerate(self.optParameters)):
-            # index is only used to not modify the loop variable "param"
-            name = param[0]
-            try:
-                value = config.get('libervia', name)
-                self.optParameters[index][2] = param[4](value)
-            except (NoSectionError, NoOptionError):
-                pass
-        usage.Options.__init__(self)
-
-
-class LiberviaMaker(object):
-    implements(IServiceMaker, IPlugin)
-    tapname = 'libervia'
-    description = u'The web frontend of Salut à Toi'
-    options = Options
-
-    def makeService(self, options):
-        return Libervia(**dict(options))  # get rid of the usage.Option overload
-
-
-# affectation to some variable is necessary for twisted introspection to work
-serviceMaker = LiberviaMaker()