diff src/browser/sat_browser/panels.py @ 589:a5019e62c3e9 frontends_multi_profiles

browser side: big refactoring to base Libervia on QuickFrontend, first draft: /!\ not finished, partially working and highly instable - add collections module with an OrderedDict like class - SatWebFrontend inherit from QuickApp - general sat_frontends tools.jid module is used - bridge/json methods have moved to json module - UniBox is partially removed (should be totally removed before merge to trunk) - Signals are now register with the generic registerSignal method (which is called mainly in QuickFrontend) - the generic getOrCreateWidget method from QuickWidgetsManager is used instead of Libervia's specific methods - all Widget are now based more or less directly on QuickWidget - with the new QuickWidgetsManager.getWidgets method, it's no more necessary to check all widgets which are instance of a particular class - ChatPanel and related moved to chat module - MicroblogPanel and related moved to blog module - global and overcomplicated send method has been disabled: each class should manage its own sending - for consistency with other frontends, former ContactPanel has been renamed to ContactList and vice versa - for the same reason, ChatPanel has been renamed to Chat - for compatibility with QuickFrontend, a fake profile is used in several places, it is set to C.PROF_KEY_NONE (real profile is managed server side for obvious security reasons) - changed default url for web panel to SàT website, and contact address to generic SàT contact address - ContactList is based on QuickContactList, UI changes are done in update method - bride call (now json module) have been greatly improved, in particular call can be done in the same way as for other frontends (bridge.method_name(arg1, arg2, ..., callback=cb, errback=eb). Blocking method must be called like async methods due to javascript architecture - in bridge calls, a callback can now exists without errback - hard reload on BridgeSignals remote error has been disabled, a better option should be implemented - use of constants where that make sens, some style improvments - avatars are temporarily disabled - lot of code disabled, will be fixed or removed before merge - various other changes, check diff for more details server side: manage remote exception on getEntityData, removed getProfileJid call, added getWaitingConf, added getRoomsSubjects
author Goffi <goffi@goffi.org>
date Sat, 24 Jan 2015 01:45:39 +0100
parents bade589dbd5a
children 5983d6be4f07
line wrap: on
line diff
--- a/src/browser/sat_browser/panels.py	Thu Oct 23 16:56:36 2014 +0200
+++ b/src/browser/sat_browser/panels.py	Sat Jan 24 01:45:39 2015 +0100
@@ -22,102 +22,82 @@
 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.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN
 from pyjamas.ui.MouseListener import MouseHandler
-from pyjamas.ui.FocusListener import FocusHandler
+from pyjamas.ui.Frame import Frame
 from pyjamas.Timer import Timer
+from pyjamas import Window
 from pyjamas import DOM
-from pyjamas import Window
 from __pyjamas__ import doc
 
-from datetime import datetime
-from time import time
 
-import jid
-import html_tools
 import base_panels
 import base_menu
-import card_game
-import radiocol
 import menu
 import dialog
 import base_widget
-import richtext
-import contact
+import contact_list
 from constants import Const as C
-import plugin_xep_0085
-
-
-# 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
+from sat_frontends.quick_frontend import quick_widgets
 
 
-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.getCachedParam(C.COMPOSITION_KEY, C.ENABLE_UNIBOX_PARAM) == 'true'
-        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 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.getCachedParam(C.COMPOSITION_KEY, C.ENABLE_UNIBOX_PARAM) == 'true'
+#         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):
@@ -126,12 +106,11 @@
     def __init__(self, host):
         TextArea.__init__(self)
         self.host = host
-        self.__size = (0, 0)
+        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
@@ -149,8 +128,8 @@
 
         if keycode == KEY_ENTER:
             if _txt:
-                self._selected_cache.onTextEntered(_txt)
-                self.host._updateInputHistory(_txt)
+                self.host.selected_widget.onTextEntered(_txt)
+                self.host._updateInputHistory(_txt) # FIXME: why using a global variable ?
             self.setText('')
             sender.cancelKey()
         elif keycode == KEY_UP:
@@ -158,140 +137,140 @@
         elif keycode == KEY_DOWN:
             self.host._updateInputHistory(_txt, +1, history_cb)
         else:
-            self.__onComposing()
+            self._onComposing()
 
-    def __onComposing(self):
+    def _onComposing(self):
         """Callback when the user is composing a text."""
-        if hasattr(self._selected_cache, "target"):
-            self._selected_cache.state_machine._onEvent("composing")
+        self.host.selected_widget.state_machine._onEvent("composing")
 
     def onMouseUp(self, sender, x, y):
         size = (self.getOffsetWidth(), self.getOffsetHeight())
-        if size != self.__size:
-            self.__size = size
+        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 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')
+#         # FIXME
+#         # 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): # FIXME
+#         #     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():
@@ -303,21 +282,21 @@
     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)
+        @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)
+            self._showWarning(type_, msg)
         elif (type_, msg) != self._popup.target_data:
             self._timeCb(None)  # we remove the popup
-            self.__showWarning(type_, msg)
+            self._showWarning(type_, msg)
 
         self._timer.schedule(duration)
 
-    def __showWarning(self, type_, msg):
+    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".
@@ -364,641 +343,6 @@
         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_tools.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:
-            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._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 = base_panels.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, True)
-            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, True)
-
-    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 <b>PUBLIC</b> 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)
-            elif not self.getNewMainEntry():
-                addBox()
-
-    def getNewMainEntry(self):
-        """Get the new entry being edited, or None if it doesn't exists.
-
-        @return (MicroblogEntry): the new entry being edited.
-        """
-        try:
-            first = self.vpanel.children[0]
-        except IndexError:
-            return None
-        assert(first.type == 'main_item')
-        return first if first.empty else None
-
-    @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, item):
-        """
-        @param item: 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
-        """
-        groups = item if isinstance(item, list) else ([] if item is None else [item])
-        groups.sort()  # sort() do not return the sorted list: do it here, not on the "return" line
-        return self.accepted_groups == groups
-
-    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
-        """
-        count = sum([len(value) for value in mblogs.values()])
-        log.debug("Massive insertion of %d microblogs" % count)
-        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"""
-        assert(isinstance(reverse, bool))
-        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
-            condition_to_stop = child.empty or (child.published > entry.published)
-            if condition_to_stop != reverse:  # != is XOR
-                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 ensureVisible(self, entry):
-        """Scroll to an entry to ensure its visibility
-
-        @param entry (MicroblogEntry): the 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!")
-
-    def setSelectedEntry(self, entry, ensure_visible=False):
-        """Select an entry.
-
-        @param entry (MicroblogEntry): the entry to select
-        @param ensure_visible (boolean): if True, also scroll to the entry
-        """
-        if ensure_visible:
-            self.ensureVisible(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: clicked_entry.removeStyleName('selected_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:
-            self.selected_entry.removeStyleName('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_s):
-        """Tell if a jid is actepted and shown in this panel
-        @param jid_s: 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_s):
-                return True
-        return False
-
-
 class StatusPanel(base_panels.HTMLTextEditor):
 
     EMPTY_STATUS = '&lt;click to set a status&gt;'
@@ -1047,7 +391,7 @@
         base_widget.WidgetMenuBar.__init__(self, None, parent.host, styles=styles)
         self.button = self.addCategory(u"◉", u"◉", '')
         for presence, presence_i18n in C.PRESENCE.items():
-            html = u'<span class="%s">◉</span> %s' % (contact.buildPresenceStyle(presence), presence_i18n)
+            html = u'<span class="%s">◉</span> %s' % (contact_list.buildPresenceStyle(presence), presence_i18n)
             self.addMenuItem([u"◉", presence], [u"◉", html], '', base_menu.MenuCmd(self, 'changePresenceCb', presence), asHTML=True)
         self.parent_panel = parent
 
@@ -1094,7 +438,7 @@
 
     def setPresence(self, presence):
         self._presence = presence
-        contact.setPresenceStyle(self.menu.button, self._presence)
+        contact_list.setPresenceStyle(self.menu.button, self._presence)
 
     def setStatus(self, status):
         self.status_panel.setContent({'text': status})
@@ -1105,283 +449,20 @@
         self.host.setSelected(None)
 
 
-class ChatPanel(base_widget.LiberviaWidget, KeyboardHandler):
-
-    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.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"""
-        self.vpanel = VerticalPanel()
-        self.vpanel.setSize('100%', '100%')
-        self.nick = None
-        if not target:
-            log.error("Empty target !")
-            return
-        self.target = target
-        self.type = type_
-
-        # FIXME: temporary dirty initialization to display the OTR state
-        def header_info_cb(cb):
-            host.plugins['otr'].infoTextCallback(target, cb)
-        header_info = header_info_cb if (type_ == 'one2one' and 'otr' in host.plugins) else None
-
-        base_widget.LiberviaWidget.__init__(self, host, title=target.bare, info=header_info, selectable=True)
-        self.__body = AbsolutePanel()
-        self.__body.setStyleName('chatPanel_body')
-        chat_area = HorizontalPanel()
-        chat_area.setStyleName('chatArea')
-        if type_ == 'group':
-            self.occupants_list = base_panels.OccupantsList()
-            self.occupants_initialised = False
-            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 = plugin_xep_0085.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, type_='one2one'):
-        assert(item)
-        _contact = item if isinstance(item, jid.JID) else jid.JID(item)
-        host.contact_panel.setContactMessageWaiting(_contact.bare, False)
-        _new_panel = ChatPanel(host, _contact, type_)  # 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.message_box.addKeyboardListener(self)
-            self.vpanel.add(self.message_box)
-
-    def onKeyDown(self, sender, keycode, modifiers):
-        if keycode == KEY_ENTER:
-            self.host.showWarning(None, None)
-        else:
-            self.host.showWarning(*self.getWarningData())
-
-    def matchEntity(self, item, type_=None):
-        """
-        @param entity: target jid as a string or jid.JID instance.
-        @return: True if self matches the given entity
-        """
-        if type_ is None:
-            type_ = self.type
-        entity = item if isinstance(item, jid.JID) else jid.JID(item)
-        try:
-            return self.target.bare == entity.bare and self.type == type_
-        except AttributeError as e:
-            e.include_traceback()
-            return False
-
-    def addMenus(self, menu_bar):
-        """Add cached menus to the header.
-
-        @param menu_bar (GenericMenuBar): menu bar of the widget's header
-        """
-        if self.type == 'group':
-            menu_bar.addCachedMenus(C.MENU_ROOM, {'room_jid': self.target.bare})
-        elif self.type == 'one2one':
-            menu_bar.addCachedMenus(C.MENU_SINGLE, {'jid': self.target})
-
-    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)"""
-        for nick in nicks:
-            self.occupants_list.addOccupant(nick)
-        self.occupants_initialised = True
-
-    def userJoined(self, nick, data):
-        if self.occupants_list.getOccupantBox(nick):
-            return  # user is already displayed
-        self.occupants_list.addOccupant(nick)
-        if self.occupants_initialised:
-            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=C.HISTORY_LIMIT_DEFAULT):
-        """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_s, to_jid_s, 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(jid.JID(from_jid_s), 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" (will be sanitized)
-            "link": general info that is clickable like "click here to join the main room" (no sanitize done)
-            "me": "/me" information like "/me clenches his fist" ==> "toto clenches his fist" (will stay on one line)
-        @param link_cb: method to call when the info is clicked, ignored if type_ is not 'link'
-        """
-        if type_ == 'normal':
-            _wid = HTML(addURLToText(html_tools.XHTML2Text(msg)))
-            _wid.setStyleName('chatTextInfo')
-        elif type_ == 'link':
-            _wid = HTML(msg)
-            _wid.setStyleName('chatTextInfo-link')
-            if link_cb:
-                _wid.addClickListener(link_cb)
-        elif type_ == 'me':
-            _wid = Label(msg)
-            _wid.setStyleName('chatTextMe')
-        else:
-            raise ValueError("Unknown printInfo type %s" % type_)
-        self.content.add(_wid)
-        self.content_scroll.scrollToBottom()
-
-    def printMessage(self, from_jid, msg, extra, timestamp=None):
-        """Print message in chat window. Must be implemented by child class"""
-        nick = from_jid.node if self.type == 'one2one' else from_jid.resource
-        mymess = from_jid.resource == self.nick if self.type == "group" else from_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(base_panels.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": card_game.CardPanel, "RadioCol": 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: ignored for one2one, otherwise the MUC user nick or C.ALL_OCCUPANTS
-        """
-        if self.type == 'group':
-            assert(nick)
-            if nick == C.ALL_OCCUPANTS:
-                occupants = self.occupants_list.occupants_list.keys()
-            else:
-                occupants = [nick] if nick in self.occupants_list.occupants_list else []
-            for occupant in occupants:
-                self.occupants_list.occupants_list[occupant].setState(state)
-        else:
-            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)
-
-    def setConnected(self, jid_s, resource, availability, priority, statuses):
-        """Set connection status
-        @param jid_s (str): JID userhost as unicode
-        """
-        assert(jid_s == self.target.bare)
-        if self.type != 'group':
-            return
-        box = self.occupants_list.getOccupantBox(resource)
-        if box:
-            contact.setPresenceStyle(box, availability)
-
-
-class WebPanel(base_widget.LiberviaWidget):
+class WebPanel(quick_widgets.QuickWidget, base_widget.LiberviaWidget):
     """ (mini)browser like widget """
 
-    def __init__(self, host, url=None):
+    def __init__(self, host, target, profiles=None):
         """
         @param host: SatWebFrontend instance
+        @param target: url to open
         """
+        quick_widgets.QuickWidget.__init__(self, host, target, C.PROF_KEY_NONE)
         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.setText(target or "")
         self._url.setWidth('100%')
         hpanel = HorizontalPanel()
         hpanel.add(self._url)
@@ -1392,7 +473,7 @@
         hpanel.add(btn)
         self._vpanel.add(hpanel)
         self._vpanel.setCellHeight(hpanel, '20px')
-        self._frame = Frame(url or "")
+        self._frame = Frame(target or "")
         self._frame.setSize('100%', '100%')
         DOM.setStyleAttribute(self._frame.getElement(), "position", "relative")
         self._vpanel.add(self._frame)
@@ -1411,9 +492,9 @@
         # menu
         self.menu = menu.MainMenuPanel(host)
 
-        # unibox
-        self.unibox_panel = UniBoxPanel(host)
-        self.unibox_panel.setVisible(False)
+        # # unibox
+        # self.unibox_panel = UniBoxPanel(host)
+        # self.unibox_panel.setVisible(False)
 
         # contacts
         self._contacts = HorizontalPanel()
@@ -1421,7 +502,6 @@
         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)
@@ -1431,7 +511,7 @@
 
         self.header = AbsolutePanel()
         self.header.add(self.menu)
-        self.header.add(self.unibox_panel)
+        # self.header.add(self.unibox_panel)
         self.header.add(self.host.status_panel)
         self.header.setStyleName('header')
         self.add(self.header)
@@ -1444,13 +524,16 @@
         self.setWidth("100%")
         Window.addWindowResizeListener(self)
 
+    def addContactList(self, contact_list):
+        self._contacts.add(contact_list)
+
     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"»")
+        clist = self.host.contact_list
+        clist.setVisible(not clist.getVisible())
+        btn.setText(u"«" if clist.getVisible() else u"»")
         self.host.resize()
 
     def _contactsMove(self, parent):