changeset 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 e11e34ac0f67 c66f7227848e
files .hgignore src/browser/collections.py src/browser/libervia_main.py src/browser/public/libervia.css src/browser/sat_browser/base_menu.py src/browser/sat_browser/base_panels.py src/browser/sat_browser/base_widget.py src/browser/sat_browser/blog.py src/browser/sat_browser/chat.py src/browser/sat_browser/constants.py src/browser/sat_browser/contact.py src/browser/sat_browser/contact_group.py src/browser/sat_browser/contact_list.py src/browser/sat_browser/jid.py src/browser/sat_browser/json.py src/browser/sat_browser/menu.py src/browser/sat_browser/nativedom.py src/browser/sat_browser/panels.py src/browser/sat_browser/plugin_sec_otr.py src/browser/sat_browser/register.py src/browser/sat_browser/xmlui.py src/server/server.py
diffstat 22 files changed, 2778 insertions(+), 2397 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Thu Oct 23 16:56:36 2014 +0200
+++ b/.hgignore	Sat Jan 24 01:45:39 2015 +0100
@@ -7,7 +7,6 @@
 tags
 twistd.log
 twistd.pid
-output
 sat.egg-info
 *.un~
 dist
@@ -16,3 +15,4 @@
 build/
 Session.vim
 ctags_links/
+html/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/collections.py	Sat Jan 24 01:45:39 2015 +0100
@@ -0,0 +1,142 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 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 OrderedDict(object):
+    """Naive implementation of OrderedDict which is compatible with pyjamas"""
+
+    def __init__(self, *args, **kwargs):
+        self.__internal_dict = {}
+        self.__keys = [] # this list keep the keys in order
+        if args:
+            if len(args)>1:
+                raise TypeError("OrderedDict expected at most 1 arguments, got {}".format(len(args)))
+            if isinstance(args[0], (dict, OrderedDict)):
+                for key, value in args[0].iteritems():
+                    self[key] = value
+            for key, value in args[0]:
+                self[key] = value
+
+    def __setitem__(self, key, value):
+        self.__keys.append(key)
+        self.__internal_dict[key] = value
+
+    def __getitem__(self, key):
+        return self.__internal_dict[key]
+
+    def __delitem__(self, key):
+        del self.__internal_dict[key]
+        self.__keys.remove(key)
+
+    def clear(self):
+        self.__internal_dict.clear()
+        del self.__keys[:]
+
+    def copy(self):
+        return OrderedDict(self)
+
+    @classmethod
+    def fromkeys(cls, seq, value=None):
+        ret = OrderedDict()
+        for key in seq:
+            ret[key] = value
+        return ret
+
+    def get(self, key, default=None):
+        try:
+            return self.__internal_dict[key]
+        except KeyError:
+            return default
+
+    def has_key(self, key):
+        return key in self.__keys
+
+    def keys(self):
+        return self.__keys[:]
+
+    def iterkeys(self):
+        for key in self.__keys:
+            yield key
+
+    def items(self):
+        ret = []
+        for key in self.__keys:
+            ret.append((key, self.__internal_dict[key]))
+        return ret
+
+    def iteritems(self):
+        for key in self.__keys:
+            yield (key, self.__internal_dict[key])
+
+    def values(self):
+        ret = []
+        for key in self.__keys:
+            ret.append(self.__internal_dict[key])
+        return ret
+
+    def itervalues(self):
+        for key in self.__keys:
+            yield (self.__internal_dict[key])
+
+    def popitem(self, last=True):
+        try:
+            key = self.__keys.pop(-1 if last else 0)
+        except IndexError:
+            raise KeyError('dictionnary is empty')
+        value = self.__internal_dict.pop(key)
+        return((key, value))
+
+    def setdefault(self, key, default=None):
+        try:
+            return self.__internal_dict[key]
+        except KeyError:
+            self.__internal_dict[key] = default
+            return default
+
+    def update(self, *args, **kwargs):
+        if len(args) > 1:
+            raise TypeError('udpate expected at most 1 argument, got {}'.format(len(args)))
+        if args:
+            if hasattr(args[0], 'keys'):
+                for k in args[0]:
+                    self[k] = args[0][k]
+            else:
+                for (k, v) in args[0]:
+                    self[k] = v
+        for k, v in kwargs.items():
+            self[k] = v
+
+    def pop(self, *args):
+        if not args:
+            raise TypeError('pop expected at least 1 argument, got 0')
+        try:
+            self.__internal_dict.pop(args[0])
+        except KeyError:
+            if len(args) == 2:
+                return args[1]
+            raise KeyError(args[0])
+        self.__keys.remove(args[0])
+
+    def viewitems():
+        raise NotImplementedError
+
+    def viewkeys():
+        raise NotImplementedError
+
+    def viewvalues():
+        raise NotImplementedError
--- a/src/browser/libervia_main.py	Thu Oct 23 16:56:36 2014 +0200
+++ b/src/browser/libervia_main.py	Sat Jan 24 01:45:39 2015 +0100
@@ -17,7 +17,6 @@
 # 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 sat_browser import logging
@@ -26,8 +25,11 @@
 log = getLogger(__name__)
 ###
 
+from sat_frontends.quick_frontend.quick_app import QuickApp
+
 from sat_frontends.tools.misc import InputHistory
 from sat_frontends.tools import strings
+from sat_frontends.tools import jid
 from sat.core.i18n import _
 
 from pyjamas.ui.RootPanel import RootPanel
@@ -35,26 +37,30 @@
 from pyjamas.ui.KeyboardListener import KEY_ESCAPE
 from pyjamas.Timer import Timer
 from pyjamas import Window, DOM
-from pyjamas.JSONService import JSONProxy
 
+from sat_browser import json
 from sat_browser import register
-from sat_browser import contact
+from sat_browser.contact_list import ContactList
 from sat_browser import base_widget
 from sat_browser import panels
+from sat_browser import chat
+from sat_browser import blog
 from sat_browser import dialog
-from sat_browser import jid
 from sat_browser import xmlui
 from sat_browser import html_tools
 from sat_browser import notification
 
 from sat_browser.constants import Const as C
 
+
 try:
     # FIXME: import plugin dynamically
     from sat_browser import plugin_sec_otr
 except ImportError:
     pass
 
+unicode = lambda s: str(s)
+
 MAX_MBLOG_CACHE = 500  # Max microblog entries kept in memories
 
 # Set to true to not create a new LiberviaWidget when a similar one
@@ -65,138 +71,25 @@
 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", "asyncConnect", "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", "getLastResource"
-                        ])
-
-
-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):
+class SatWebFrontend(InputHistory, QuickApp):
     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.bridge_signals = json.BridgeSignals(self)
+        QuickApp.__init__(self, json.BridgeCall)
+        self.uni_box = None # FIXME: to be removed
         self.status_panel = HTML('<br />')
-        self.contact_panel = contact.ContactPanel(self)
+        # self.contact_panel = contact.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.Notification()
         DOM.addEventPreview(self)
         self.importPlugins()
-        self._register = RegisterCall()
+        self._register = json.RegisterCall()
         self._register.call('getMenus', self.gotMenus)
         self._register.call('registerParams', None)
         self._register.call('isRegistered', self._isRegisteredCB)
@@ -204,6 +97,36 @@
         self.init_cache = []  # used to cache events until initialisation is done
         self.cached_params = {}
 
+        #FIXME: to be removed (managed with cache and in quick_frontend
+        self.avatars_cache = {}  # keep track of jid's avatar hash (key=jid, value=file)
+        #FIXME: microblog cache should be managed directly in blog module
+        self.mblog_cache = []  # used to keep our own blog entries in memory, to show them in new mblog panel
+
+
+
+        # panels.ChatPanel.registerClass()
+        # panels.MicroblogPanel.registerClass()
+        # self._selected_listeners = set()
+        # # self.avatars_cache = {}  # keep track of jid's avatar hash (key=jid, value=file)
+
+    @property
+    def whoami(self):
+        # XXX: works because Libervia is mono-profile
+        #      if one day Libervia manage several profiles at once, this must be deleted
+        return self.profiles[C.PROF_KEY_NONE].whoami
+
+    @property
+    def contact_list(self):
+        return self.contact_lists[C.PROF_KEY_NONE]
+
+    def registerSignal(self, functionName, handler=None, iface="core", with_profile=True):
+        if handler is None:
+            callback = getattr(self, "{}{}".format(functionName, "Handler"))
+        else:
+            callback = handler
+
+        self.bridge_signals.register(functionName, callback, with_profile=with_profile)
+
     def importPlugins(self):
         self.plugins = {}
         inhibited_menus = []
@@ -219,8 +142,8 @@
 
         self.plugins['dummy_plugin'] = DummyPlugin()
 
-    def addSelectedListener(self, callback):
-        self._selected_listeners.add(callback)
+    # def addSelectedListener(self, callback):
+    #     self._selected_listeners.add(callback)
 
     def getSelected(self):
         wid = self.tab_panel.getCurrentPanel()
@@ -244,12 +167,14 @@
             selected.removeStyleName('selected_widget')
 
         widgets_panel.selected = widget
+        self.selected_widget = widget
 
         if widget:
             widgets_panel.selected.addStyleName('selected_widget')
 
-        for callback in self._selected_listeners:
-            callback(widget)
+        # FIXME:
+        # for callback in self._selected_listeners:
+        #     callback(widget)
 
     def resize(self):
         """Resize elements"""
@@ -260,8 +185,9 @@
 
     def onTabSelected(self, sender, tab_index):
         selected = self.getSelected()
-        for callback in self._selected_listeners:
-            callback(selected)
+        # FIXME:
+        # for callback in self._selected_listeners:
+        #     callback(selected)
 
     def onEventPreview(self, event):
         if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE:
@@ -269,26 +195,28 @@
             event.preventDefault()
         return True
 
-    def getAvatar(self, jid_str):
-        """Return avatar of a jid if in cache, else ask for it.
+    # FIXME: must not call _entityDataUpdatedCb by itself
+    #        should not get VCard, backend plugin must be fixed too
+    # def getAvatar(self, jid_str):
+    #     """Return avatar of a jid if in cache, else ask for it.
 
-        @param jid_str (str): JID of the contact
-        @return: the URL to the avatar (str)
-        """
-        def dataReceived(result):
-            if 'avatar' in result:
-                self._entityDataUpdatedCb(jid_str, 'avatar', result['avatar'])
-            else:
-                self.bridge.call("getCard", None, jid_str)
+    #     @param jid_str (str): JID of the contact
+    #     @return: the URL to the avatar (str)
+    #     """
+    #     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)
+    #     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] = C.DEFAULT_AVATAR
-        return self.avatars_cache[jid_str]
+    #     if jid_str not in self.avatars_cache:
+    #         self.bridge.call('getEntityData', (dataReceived, avatarError), jid_str, ['avatar'])
+    #         self.avatars_cache[jid_str] = C.DEFAULT_AVATAR
+    #     return self.avatars_cache[jid_str]
 
     def registerWidget(self, wid):
         log.debug("Registering %s" % wid.getDebugName())
@@ -303,10 +231,6 @@
     def refresh(self):
         """Refresh the general display."""
         self.panel.refresh()
-        if self.getCachedParam(C.COMPOSITION_KEY, C.ENABLE_UNIBOX_PARAM) == 'true':
-            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()
@@ -326,8 +250,10 @@
 
     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"""
+        @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:
@@ -351,7 +277,7 @@
                 menus_data.append((id_, path, path_i18n))
 
         self.menus = {}
-        inhibited = set()
+        inhibited = set() # FIXME
         extras = []
         for plugin in self.plugins.values():
             if hasattr(plugin, "inhibitMenus"):
@@ -390,12 +316,14 @@
         self.status_panel = panels.PresenceStatusPanel(self)
         self.panel.header.add(self.status_panel)
 
+        self.bridge_signals.call('getSignals', self.bridge_signals.signalHandler)
+
         #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)
+        # 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
@@ -404,18 +332,50 @@
         def domain_eb(value):
             self._defaultDomain = "libervia.org"
 
-        self.bridge.call("getNewAccountDomain", (domain_cb, domain_eb))
-        self.discuss_panel.addWidget(panels.MicroblogPanel(self, []))
+        self.bridge.getNewAccountDomain(callback=domain_cb, errback=domain_eb)
+        self.plug_profiles([C.PROF_KEY_NONE]) # XXX: None was used intitially, but pyjamas bug when using variable arguments and None is the only arg.
+        microblog_widget = self.widgets.getOrCreateWidget(blog.MicroblogPanel, (), profile=C.PROF_KEY_NONE)
+        self.setSelected(microblog_widget)
+        # self.discuss_panel.addWidget(panels.MicroblogPanel(self, []))
+
+        # # get cached params and refresh the display
+        # def param_cb(cat, name, count):
+        #     count[0] += 1
+        #     refresh = count[0] == len(C.CACHED_PARAMS)
+        #     return lambda value: self._paramUpdate(name, value, cat, refresh)
+
+        # count = [0]  # used to do something similar to DeferredList
+        # for cat, name in C.CACHED_PARAMS:
+        #     self.bridge.call('asyncGetParamA', param_cb(cat, name, count), name, cat)
 
-        # get cached params and refresh the display
-        def param_cb(cat, name, count):
-            count[0] += 1
-            refresh = count[0] == len(C.CACHED_PARAMS)
-            return lambda value: self._paramUpdate(name, value, cat, refresh)
+    def profilePlugged(self, profile):
+        #we fill the panels already here
+        for widget in self.widgets.getWidgets(blog.MicroblogPanel):
+            if widget.accept_all():
+                self.bridge.getMassiveLastMblogs('ALL', [], 10, profile=C.PROF_KEY_NONE, callback=widget.massiveInsert)
+            else:
+                self.bridge.getMassiveLastMblogs('GROUP', widget.accepted_groups, 10, profile=C.PROF_KEY_NONE, callback=widget.massiveInsert)
+
+        #we ask for our own microblogs:
+        self.bridge.getMassiveLastMblogs('JID', [unicode(self.whoami.bare)], 10, profile=C.PROF_KEY_NONE, callback=self._ownBlogsFills)
 
-        count = [0]  # used to do something similar to DeferredList
-        for cat, name in C.CACHED_PARAMS:
-            self.bridge.call('asyncGetParamA', param_cb(cat, name, count), name, cat)
+        # initialize plugins which waited for the connection to be done
+        for plugin in self.plugins.values():
+            if hasattr(plugin, 'profileConnected'):
+                plugin.profileConnected()
+
+    def addContactList(self, dummy):
+        contact_list = ContactList(self)
+        self.contact_lists[C.PROF_KEY_NONE] = contact_list
+        self.panel.addContactList(contact_list)
+        return contact_list
+
+    def newWidget(self, widget):
+        log.debug("newWidget: {}".format(widget))
+        self.addWidget(widget)
+
+    def setStatusOnline(self, online=True, show="", statuses={}, profile=C.PROF_KEY_NONE):
+        log.warning("setStatusOnline is not implemented, as session are for unique profile which is always online for now")
 
     def _tryAutoConnect(self, skip_validation=False):
         """This method retrieve the eventual URL parameters to auto-connect the user.
@@ -471,71 +431,6 @@
             jid, attributes, groups = contact_
             self._newContactCb(jid, attributes, groups)
 
-    def _getSignalsCB(self, signal_data):
-        self.bridge_signals.call('getSignals', self._getSignalsCB)
-        if len(signal_data) == 1:
-            signal_data.append([])
-        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, xml_ui):
         """Hide the parameters item if there's nothing to display"""
         if not xml_ui:
@@ -552,42 +447,45 @@
                     _groups = set(mblog['groups'].split() if mblog['groups'] else [])
                 else:
                     _groups = None
-                mblog_entry = panels.MicroblogItem(mblog)
+                mblog_entry = blog.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)
+        for widget in self.widgets.getWidgets(blog.MicroblogPanel):
+            self.FillMicroblogPanel(widget)
+
+        # FIXME
         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_s):
-        self.whoami = jid.JID(jid_s)
-        #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)
+        # FIXME
+        raise Exception("should not be here !")
+        # self.whoami = jid.JID(jid_s)
+        # #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)
+        # #we ask for our own microblogs:
+        # self.bridge.call('getMassiveLastMblogs', self._ownBlogsFills, 'JID', [self.whoami.bare], 10)
 
-        # initialize plugins which waited for the connection to be done
-        for plugin in self.plugins.values():
-            if hasattr(plugin, 'profileConnected'):
-                plugin.profileConnected()
+        # # initialize plugins which waited for the connection to be done
+        # for plugin in self.plugins.values():
+        #     if hasattr(plugin, 'profileConnected'):
+        #         plugin.profileConnected()
 
     ## Signals callbacks ##
 
@@ -604,11 +502,10 @@
                 _groups = set(data['groups'].split() if data['groups'] else [])
             else:
                 _groups = None
-            mblog_entry = panels.MicroblogItem(data)
+            mblog_entry = blog.MicroblogItem(data)
 
-            for lib_wid in self.libervia_widgets:
-                if isinstance(lib_wid, panels.MicroblogPanel):
-                    self.addBlogEntry(lib_wid, sender, _groups, mblog_entry)
+            for widget in self.widgets.getWidgets(blog.MicroblogPanel):
+                self.addBlogEntry(widget, sender, _groups, mblog_entry)
 
             if sender == self.whoami.bare:
                 found = False
@@ -625,9 +522,8 @@
                     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'])
+            for widget in self.widgets.getWidgets(blog.MicroblogPanel):
+                widget.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':
@@ -663,209 +559,209 @@
                 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): class of the panel (ChatPanel, MicroblogPanel...)
-        @param entity (dict): dictionnary to define the entity.
-        @param ignoreOtherTabs (bool): 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(expect=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.values())):  # XXX: passing **entity bugs!
-                        log.debug("existing widget found: %s" % lib_wid.getDebugName())
-                        return lib_wid
-                except AttributeError as e:
-                    e.stack_list()
-                    return None
-        return None
+    # def getLiberviaWidget(self, class_, entity, ignoreOtherTabs=True):
+    #     """Get the corresponding panel if it exists.
+    #     @param class_ (class): class of the panel (ChatPanel, MicroblogPanel...)
+    #     @param entity (dict): dictionnary to define the entity.
+    #     @param ignoreOtherTabs (bool): 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(expect=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.values())):  # XXX: passing **entity bugs!
+    #                     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): class of the panel (ChatPanel, MicroblogPanel...)
-        @param entity (dict): dictionnary to define the entity.
-        @param select (bool): if True, select the widget that has been found or created
-        @param new_tab (str): 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.values()))  # XXX: passing **entity bugs!
-            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(expect=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 getOrCreateLiberviaWidget(self, class_, entity, select=True, new_tab=None):
+    #     """Get the matching LiberviaWidget if it exists, or create a new one.
+    #     @param class_ (class): class of the panel (ChatPanel, MicroblogPanel...)
+    #     @param entity (dict): dictionnary to define the entity.
+    #     @param select (bool): if True, select the widget that has been found or created
+    #     @param new_tab (str): 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.values()))  # XXX: passing **entity bugs!
+    #         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(expect=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 getRoomWidget(self, target):
-        """Get the MUC widget for the given target.
+    # def getRoomWidget(self, target):
+    #     """Get the MUC widget for the given target.
 
-        @param target (jid.JID): BARE jid of the MUC
-        @return: panels.ChatPanel instance or None
-        """
-        entity = {'item': target, 'type_': 'group'}
-        if target.full() in self.room_list or target in self.room_list:  # as JID is a string-based class, we don't know what will please Pyjamas...
-            return self.getLiberviaWidget(panels.ChatPanel, entity, ignoreOtherTabs=False)
-        return None
+    #     @param target (jid.JID): BARE jid of the MUC
+    #     @return: panels.ChatPanel instance or None
+    #     """
+    #     entity = {'item': target, 'type_': 'group'}
+    #     if target.full() in self.room_list or target in self.room_list:  # as JID is a string-based class, we don't know what will please Pyjamas...
+    #         return self.getLiberviaWidget(panels.ChatPanel, entity, ignoreOtherTabs=False)
+    #     return None
 
-    def getOrCreateRoomWidget(self, target):
-        """Get the MUC widget for the given target, create it if necessary.
+    # def getOrCreateRoomWidget(self, target):
+    #     """Get the MUC widget for the given target, create it if necessary.
 
-        @param target (jid.JID): BARE jid of the MUC
-        @return: panels.ChatPanel instance
-        """
-        lib_wid = self.getRoomWidget(target)
-        if lib_wid:
-            return lib_wid
+    #     @param target (jid.JID): BARE jid of the MUC
+    #     @return: panels.ChatPanel instance
+    #     """
+    #     lib_wid = self.getRoomWidget(target)
+    #     if lib_wid:
+    #         return lib_wid
 
-        # XXX: target.node.startwith(...) raises an error "startswith is not a function"
-        # This happens when node a is property defined in the JID class
-        # FIXME: pyjamas doesn't handle the properties well
-        node = target.node
+    #     # XXX: target.node.startwith(...) raises an error "startswith is not a function"
+    #     # This happens when node a is property defined in the JID class
+    #     # FIXME: pyjamas doesn't handle the properties well
+    #     node = target.node
 
-        # XXX: it's not really beautiful, but it works :)
-        if node.startswith('sat_tarot_'):
-            tab_name = "Tarot"
-        elif node.startswith('sat_radiocol_'):
-            tab_name = "Radio collective"
-        else:
-            tab_name = target.node
+    #     # XXX: it's not really beautiful, but it works :)
+    #     if node.startswith('sat_tarot_'):
+    #         tab_name = "Tarot"
+    #     elif node.startswith('sat_radiocol_'):
+    #         tab_name = "Radio collective"
+    #     else:
+    #         tab_name = target.node
 
-        self.room_list.append(target)
-        entity = {'item': target, 'type_': 'group'}
-        return self.getOrCreateLiberviaWidget(panels.ChatPanel, entity, new_tab=tab_name)
+    #     self.room_list.append(target)
+    #     entity = {'item': target, 'type_': 'group'}
+    #     return self.getOrCreateLiberviaWidget(panels.ChatPanel, entity, new_tab=tab_name)
 
-    def _newMessageCb(self, from_jid_s, msg, msg_type, to_jid_s, extra):
-        from_jid = jid.JID(from_jid_s)
-        to_jid = jid.JID(to_jid_s)
-        for plugin in self.plugins.values():
-            if hasattr(plugin, 'messageReceivedTrigger'):
-                if not plugin.messageReceivedTrigger(from_jid, msg, msg_type, to_jid, extra):
-                    return  # plugin returned False to interrupt the process
-        self.newMessageCb(from_jid, msg, msg_type, to_jid, extra)
+    # def _newMessageCb(self, from_jid_s, msg, msg_type, to_jid_s, extra):
+    #     from_jid = jid.JID(from_jid_s)
+    #     to_jid = jid.JID(to_jid_s)
+    #     for plugin in self.plugins.values():
+    #         if hasattr(plugin, 'messageReceivedTrigger'):
+    #             if not plugin.messageReceivedTrigger(from_jid, msg, msg_type, to_jid, extra):
+    #                 return  # plugin returned False to interrupt the process
+    #     self.newMessageCb(from_jid, msg, msg_type, to_jid, extra)
 
-    def newMessageCb(self, from_jid, msg, msg_type, to_jid, extra):
-        other = to_jid if from_jid.bare == self.whoami.bare else from_jid
-        lib_wid = self.getLiberviaWidget(panels.ChatPanel, {'item': other}, ignoreOtherTabs=False)
-        self.displayNotification(from_jid, msg)
-        if msg_type == 'headline' and from_jid.full() == self._defaultDomain:
-            try:
-                assert extra['subject']  # subject is defined and not empty
-                title = extra['subject']
-            except (KeyError, AssertionError):
-                title = _('Announcement from %s') % from_jid.full()
-            msg = strings.addURLToText(html_tools.XHTML2Text(msg))
-            dialog.InfoDialog(title, msg).show()
-            return
-        if lib_wid is not None:
-            if msg_type == C.MESS_TYPE_INFO:
-                lib_wid.printInfo(msg)
-            else:
-                lib_wid.printMessage(from_jid, msg, extra)
-            if 'header_info' in extra:
-                lib_wid.setHeaderInfo(extra['header_info'])
-        else:
-            # FIXME: "info" message and header info will be lost here
-            if not self.contact_panel.isContactInRoster(other.bare):
-                self.contact_panel.updateContact(other.bare, {}, [C.GROUP_NOT_IN_ROSTER])
-            # The message has not been shown, we must indicate it
-            self.contact_panel.setContactMessageWaiting(other.bare, True)
+    # def newMessageCb(self, from_jid, msg, msg_type, to_jid, extra):
+    #     other = to_jid if from_jid.bare == self.whoami.bare else from_jid
+    #     lib_wid = self.getLiberviaWidget(panels.ChatPanel, {'item': other}, ignoreOtherTabs=False)
+    #     self.displayNotification(from_jid, msg)
+    #     if msg_type == 'headline' and from_jid.full() == self._defaultDomain:
+    #         try:
+    #             assert extra['subject']  # subject is defined and not empty
+    #             title = extra['subject']
+    #         except (KeyError, AssertionError):
+    #             title = _('Announcement from %s') % from_jid.full()
+    #         msg = strings.addURLToText(html_tools.XHTML2Text(msg))
+    #         dialog.InfoDialog(title, msg).show()
+    #         return
+    #     if lib_wid is not None:
+    #         if msg_type == C.MESS_TYPE_INFO:
+    #             lib_wid.printInfo(msg)
+    #         else:
+    #             lib_wid.printMessage(from_jid, msg, extra)
+    #         if 'header_info' in extra:
+    #             lib_wid.setHeaderInfo(extra['header_info'])
+    #     else:
+    #         # FIXME: "info" message and header info will be lost here
+    #         if not self.contact_panel.isContactInRoster(other.bare):
+    #             self.contact_panel.updateContact(other.bare, {}, [C.GROUP_NOT_IN_ROSTER])
+    #         # 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.JID(entity)
-        if self.whoami and self.whoami == entity_jid:  # XXX: QnD way to get our presence/status
-            assert(isinstance(self.status_panel, panels.PresenceStatusPanel))
-            self.status_panel.setPresence(show)  # pylint: disable=E1103
-            if statuses:
-                self.status_panel.setStatus(statuses.values()[0])  # pylint: disable=E1103
-        else:
-            bare_jid = entity_jid.bareJID()
-            if bare_jid.full() in self.room_list or bare_jid in self.room_list:  # as JID is a string-based class, we don't know what will please Pyjamas...
-                wid = self.getRoomWidget(bare_jid)
-            else:
-                wid = self.contact_panel
-                if show == 'unavailable':  # XXX: save some resources as for now we only need 'unavailable'
-                    for plugin in self.plugins.values():
-                        if hasattr(plugin, 'presenceReceivedTrigger'):
-                            plugin.presenceReceivedTrigger(entity_jid, show, priority, statuses)
-            if wid:
-                wid.setConnected(entity_jid.bare, entity_jid.resource, show, priority, statuses)
+    # def _presenceUpdateCb(self, entity, show, priority, statuses):
+    #     entity_jid = jid.JID(entity)
+    #     if self.whoami and self.whoami == entity_jid:  # XXX: QnD way to get our presence/status
+    #         assert(isinstance(self.status_panel, panels.PresenceStatusPanel))
+    #         self.status_panel.setPresence(show)  # pylint: disable=E1103
+    #         if statuses:
+    #             self.status_panel.setStatus(statuses.values()[0])  # pylint: disable=E1103
+    #     else:
+    #         bare_jid = entity_jid.bareJID()
+    #         if bare_jid.full() in self.room_list or bare_jid in self.room_list:  # as JID is a string-based class, we don't know what will please Pyjamas...
+    #             wid = self.getRoomWidget(bare_jid)
+    #         else:
+    #             wid = self.contact_panel
+    #             if show == 'unavailable':  # XXX: save some resources as for now we only need 'unavailable'
+    #                 for plugin in self.plugins.values():
+    #                     if hasattr(plugin, 'presenceReceivedTrigger'):
+    #                         plugin.presenceReceivedTrigger(entity_jid, show, priority, statuses)
+    #         if wid:
+    #             wid.setConnected(entity_jid.bare, entity_jid.resource, show, priority, statuses)
 
-    def _roomJoinedCb(self, room_jid_s, room_nicks, user_nick):
-        chat_panel = self.getOrCreateRoomWidget(jid.JID(room_jid_s))
-        chat_panel.setUserNick(user_nick)
-        chat_panel.setPresents(room_nicks)
-        chat_panel.refresh()
+    # def _roomJoinedCb(self, room_jid_s, room_nicks, user_nick):
+    #     chat_panel = self.getOrCreateRoomWidget(jid.JID(room_jid_s))
+    #     chat_panel.setUserNick(user_nick)
+    #     chat_panel.setPresents(room_nicks)
+    #     chat_panel.refresh()
 
-    def _roomLeftCb(self, room_jid_s, room_nicks, user_nick):
-        try:
-            del self.room_list[room_jid_s]
-        except KeyError:
-            try:  # as JID is a string-based class,  we don't know what will please Pyjamas...
-                del self.room_list[jid.JID(room_jid_s)]
-            except KeyError:
-                pass
+    # def _roomLeftCb(self, room_jid_s, room_nicks, user_nick):
+    #     try:
+    #         del self.room_list[room_jid_s]
+    #     except KeyError:
+    #         try:  # as JID is a string-based class,  we don't know what will please Pyjamas...
+    #             del self.room_list[jid.JID(room_jid_s)]
+    #         except KeyError:
+    #             pass
 
-    def _roomUserJoinedCb(self, room_jid_s, user_nick, user_data):
-        lib_wid = self.getOrCreateRoomWidget(jid.JID(room_jid_s))
-        if lib_wid:
-            lib_wid.userJoined(user_nick, user_data)
+    # def _roomUserJoinedCb(self, room_jid_s, user_nick, user_data):
+    #     lib_wid = self.getOrCreateRoomWidget(jid.JID(room_jid_s))
+    #     if lib_wid:
+    #         lib_wid.userJoined(user_nick, user_data)
 
-    def _roomUserLeftCb(self, room_jid_s, user_nick, user_data):
-        lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
-        if lib_wid:
-            lib_wid.userLeft(user_nick, user_data)
+    # def _roomUserLeftCb(self, room_jid_s, user_nick, user_data):
+    #     lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
+    #     if lib_wid:
+    #         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"""
-        lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
-        if lib_wid:
-            lib_wid.changeUserNick(old_nick, new_nick)
+    # def _roomUserChangedNickCb(self, room_jid_s, old_nick, new_nick):
+    #     """Called when an user joined a MUC room"""
+    #     lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
+    #     if lib_wid:
+    #         lib_wid.changeUserNick(old_nick, new_nick)
 
-    def _tarotGameStartedCb(self, waiting, room_jid_s, referee, players):
-        lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
-        if lib_wid:
-            lib_wid.startGame("Tarot", waiting, referee, players)
+    # def _tarotGameStartedCb(self, waiting, room_jid_s, referee, players):
+    #     lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
+    #     if lib_wid:
+    #         lib_wid.startGame("Tarot", waiting, referee, players)
 
-    def _tarotGameGenericCb(self, event_name, room_jid_s, args):
-        lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
-        if lib_wid:
-            getattr(lib_wid.getGame("Tarot"), event_name)(*args)
+    # def _tarotGameGenericCb(self, event_name, room_jid_s, args):
+    #     lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
+    #     if lib_wid:
+    #         getattr(lib_wid.getGame("Tarot"), event_name)(*args)
 
-    def _radioColStartedCb(self, waiting, room_jid_s, referee, players, queue_data):
-        lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
-        if lib_wid:
-            lib_wid.startGame("RadioCol", waiting, referee, players, queue_data)
+    # def _radioColStartedCb(self, waiting, room_jid_s, referee, players, queue_data):
+    #     lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
+    #     if lib_wid:
+    #         lib_wid.startGame("RadioCol", waiting, referee, players, queue_data)
 
-    def _radioColGenericCb(self, event_name, room_jid_s, args):
-        lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
-        if lib_wid:
-            getattr(lib_wid.getGame("RadioCol"), event_name)(*args)
+    # def _radioColGenericCb(self, event_name, room_jid_s, args):
+    #     lib_wid = self.getRoomWidget(jid.JID(room_jid_s))
+    #     if lib_wid:
+    #         getattr(lib_wid.getGame("RadioCol"), event_name)(*args)
 
     def _getPresenceStatusesCb(self, presence_data):
         for entity in presence_data:
@@ -923,19 +819,19 @@
                     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@':
-            for lib_wid in self.libervia_widgets:
-                if isinstance(lib_wid, panels.ChatPanel):
-                    lib_wid.setState(state, nick=C.ALL_OCCUPANTS)
-            return
-        from_jid = jid.JID(from_jid_s)
-        lib_wid = self.getLiberviaWidget(panels.ChatPanel, {'item': from_jid}, ignoreOtherTabs=False)
-        lib_wid.setState(state, nick=from_jid.resource)
+    # 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@':
+    #         for lib_wid in self.libervia_widgets:
+    #             if isinstance(lib_wid, panels.ChatPanel):
+    #                 lib_wid.setState(state, nick=C.ALL_OCCUPANTS)
+    #         return
+    #     from_jid = jid.JID(from_jid_s)
+    #     lib_wid = self.getLiberviaWidget(panels.ChatPanel, {'item': from_jid}, ignoreOtherTabs=False)
+    #     lib_wid.setState(state, nick=from_jid.resource)
 
     def _askConfirmation(self, confirmation_id, confirmation_type, data):
         answer_data = {}
@@ -974,41 +870,43 @@
                           "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":
-                assert(isinstance(self.status_panel, panels.PresenceStatusPanel))
-                self.bridge.call('setStatus', None, self.status_panel.presence, text)  # pylint: disable=E1103
-            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':
-                to_jid_s = addresses[0][1]
-                for plugin in self.plugins.values():
-                    if hasattr(plugin, 'sendMessageTrigger'):
-                        if not plugin.sendMessageTrigger(jid.JID(to_jid_s), text, type_, extra):
-                            return  # plugin returned False to interrupt the process
-                self.bridge.call('sendMessage', (None, self.sendError), to_jid_s, 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)
+    # FIXME: this method is fat too complicated and depend of widget type
+    #        must be refactored and moved to each widget instead
+    # 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":
+    #             assert(isinstance(self.status_panel, panels.PresenceStatusPanel))
+    #             self.bridge.call('setStatus', None, self.status_panel.presence, text)  # pylint: disable=E1103
+    #         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':
+    #             to_jid_s = addresses[0][1]
+    #             for plugin in self.plugins.values():
+    #                 if hasattr(plugin, 'sendMessageTrigger'):
+    #                     if not plugin.sendMessageTrigger(jid.JID(to_jid_s), text, type_, extra):
+    #                         return  # plugin returned False to interrupt the process
+    #             self.bridge.call('sendMessage', (None, self.sendError), to_jid_s, 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.
@@ -1024,4 +922,3 @@
 if __name__ == '__main__':
     app = SatWebFrontend()
     app.onModuleLoad()
-    pyjd.run()
--- a/src/browser/public/libervia.css	Thu Oct 23 16:56:36 2014 +0200
+++ b/src/browser/public/libervia.css	Sat Jan 24 01:45:39 2015 +0100
@@ -420,7 +420,7 @@
 }
 /* Contact List */
 
-div.contactPanel {
+div.contactList {
     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
@@ -449,11 +449,11 @@
     font-size: large;
 }
 
-.groupList {
+.groupPanel {
     width: 100%;    
 }
 
-.groupList tr:first-child td {
+.groupPanel tr:first-child td {
     padding-top: 10px;
 }
 
--- a/src/browser/sat_browser/base_menu.py	Thu Oct 23 16:56:36 2014 +0200
+++ b/src/browser/sat_browser/base_menu.py	Sat Jan 24 01:45:39 2015 +0100
@@ -54,6 +54,7 @@
         self.data = data
 
     def execute(self):
+        log.debug("execute %s" % self.callback)
         self.callback(self.data) if self.data else self.callback()
 
 
@@ -307,7 +308,7 @@
     def addMenuItem(self, path, path_i18n, types, menu_cmd, asHTML=False):
         return self.node.addMenuItem(path, path_i18n, types, menu_cmd, asHTML).item
 
-    def addCategory(self, path, path_i18n, types, menu_bar):
+    def addCategory(self, path, path_i18n, types, menu_bar=None):
         return self.node.addCategory(path, path_i18n, types, menu_bar).item
 
     def addItem(self, item, asHTML=None, popup=None):
--- a/src/browser/sat_browser/base_panels.py	Thu Oct 23 16:56:36 2014 +0200
+++ b/src/browser/sat_browser/base_panels.py	Sat Jan 24 01:45:39 2015 +0100
@@ -26,7 +26,6 @@
 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
@@ -39,29 +38,10 @@
 from pyjamas.ui.ClickListener import ClickHandler
 from pyjamas import DOM
 
-from datetime import datetime
-from time import time
-
 import html_tools
 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_tools.html_sanitize(nick),
-                            "msg_class": ' '.join(_msg_class),
-                            "msg": strings.addURLToText(html_tools.html_sanitize(msg)) if not xhtml else html_tools.inlineRoot(xhtml)}  # FIXME: images and external links must be removed according to preferences
-                           )
-        self.setStyleName('chatText')
-
-
 class Occupant(HTML):
     """Occupant of a MUC room"""
 
--- a/src/browser/sat_browser/base_widget.py	Thu Oct 23 16:56:36 2014 +0200
+++ b/src/browser/sat_browser/base_widget.py	Sat Jan 24 01:45:39 2015 +0100
@@ -177,12 +177,13 @@
             menu_styles.update(styles)
         base_menu.GenericMenuBar.__init__(self, host, vertical=vertical, styles=menu_styles)
 
-        if hasattr(parent, 'addMenus'):
-            # regroup all the dynamic menu categories in a sub-menu
-            sub_menu = WidgetSubMenuBar(host, vertical=True)
-            parent.addMenus(sub_menu)
-            if len(sub_menu.getCategories()) > 0:
-                self.addCategory('', '', 'plugins', sub_menu)
+        # FIXME
+        # if hasattr(parent, 'addMenus'):
+        #     # regroup all the dynamic menu categories in a sub-menu
+        #     sub_menu = WidgetSubMenuBar(host, vertical=True)
+        #     parent.addMenus(sub_menu)
+        #     if len(sub_menu.getCategories()) > 0:
+        #         self.addCategory('', '', 'plugins', sub_menu)
 
     @classmethod
     def getCategoryHTML(cls, menu_name_i18n, type_):
@@ -231,35 +232,37 @@
 
     def __init__(self, host, title='', info=None, selectable=False):
         """Init the widget
+
         @param host (SatWebFrontend): SatWebFrontend instance
         @param title (str): title shown in the header of the widget
         @param info (str, callable): info shown in the header of the widget
-        @param selectable (bool): True is widget can be selected by user"""
+        @param selectable (bool): 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._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')
         if info is not None:
             if isinstance(info, str):
-                self.__info = HTML(info)
+                self._info = HTML(info)
             else:  # the info will be set by a callback
-                assert(callable(info))
-                self.__info = HTML()
-                info(self.__info.setHTML)
-            self.__info.setStyleName('widgetHeader_info')
+                assert callable(info)
+                self._info = HTML()
+                info(self._info.setHTML)
+            self._info.setStyleName('widgetHeader_info')
         else:
-            self.__info = None
+            self._info = None
         self._close_listeners = []
-        header = WidgetHeader(self, host, self.__title, self.__info)
+        header = WidgetHeader(self, host, self._title, self._info)
         self.add(header)
         self.setSize('100%', '100%')
         self.addStyleName('widget')
-        if self.__selectable:
+        if self._selectable:
             self.addClickListener(self)
 
             def onClose(sender):
@@ -268,10 +271,10 @@
                     self.host.uni_box.onWidgetClosed(sender)
 
             self.addCloseListener(onClose)
-        self.host.registerWidget(self)
+        # self.host.registerWidget(self) # FIXME
 
     def getDebugName(self):
-        return "%s (%s)" % (self, self.__title.getText())
+        return "%s (%s)" % (self, self._title.getText())
 
     def getWidgetsPanel(self, expect=True):
         return self.getParent(WidgetsPanel, expect)
@@ -392,28 +395,28 @@
     def setTitle(self, text):
         """change the title in the header of the widget
         @param text: text of the new title"""
-        self.__title.setText(text)
+        self._title.setText(text)
 
     def setHeaderInfo(self, text):
         """change the info in the header of the widget
         @param text: text of the new title"""
         try:
-            self.__info.setHTML(text)
+            self._info.setHTML(text)
         except TypeError:
             log.error("LiberviaWidget.setInfo: info widget has not been initialized!")
 
     def isSelectable(self):
-        return self.__selectable
+        return self._selectable
 
     def setSelectable(self, selectable):
-        if not 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
+        self._selectable = selectable
 
     def getWarningData(self):
         """ Return exposition warning level when this widget is selected and something is sent to it
@@ -425,7 +428,7 @@
             - MISC
             - NONE
         """
-        if not self.__selectable:
+        if not self._selectable:
             log.error("getWarningLevel must not be called for an unselectable widget")
             raise Exception
         # TODO: cleaner warning types (more general constants)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/blog.py	Sat Jan 24 01:45:39 2015 +0100
@@ -0,0 +1,698 @@
+#!/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.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.HTMLPanel import HTMLPanel
+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, KeyboardHandler
+from pyjamas.ui.FocusListener import FocusHandler
+from pyjamas.Timer import Timer
+
+from datetime import datetime
+from time import time
+
+import html_tools
+import base_panels
+import dialog
+import base_widget
+import richtext
+from constants import Const as C
+from sat_frontends.quick_frontend import quick_widgets
+
+# 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 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')
+        # FIXME
+        self.avatar = Image(C.DEFAULT_AVATAR) # 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(quick_widgets.QuickWidget, 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>"
+    # FIXME: all the generic parts must be moved to quick_frontends
+
+    def __init__(self, host, accepted_groups, profiles=None):
+        """Panel used to show microblog
+
+        @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts
+        """
+        self.setAcceptedGroup(accepted_groups)
+        quick_widgets.QuickWidget.__init__(self, host, self.target, C.PROF_KEY_NONE)
+        base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True)
+        self.entries = {}
+        self.comments = {}
+        self.selected_entry = None
+        self.vpanel = VerticalPanel()
+        self.vpanel.setStyleName('microblogPanel')
+        self.setWidget(self.vpanel)
+
+    @property
+    def target(self):
+        return tuple(self.accepted_groups)
+
+    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 isinstance(group, basestring):
+            groups = [group]
+        else:
+            groups = list(group)
+        try:
+            self._accepted_groups.extend(groups)
+        except (AttributeError, TypeError): # XXX: should be AttributeError, but pyjamas bugs here
+            self._accepted_groups = groups
+        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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/chat.py	Sat Jan 24 01:45:39 2015 +0100
@@ -0,0 +1,364 @@
+#!/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.games import SYMBOLS
+from sat_frontends.tools import strings
+from sat_frontends.tools import jid
+from sat_frontends.quick_frontend import quick_widgets
+from sat_frontends.quick_frontend.quick_chat import QuickChat
+from sat.core.i18n import _
+
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler
+from pyjamas.ui.HTMLPanel import HTMLPanel
+
+from datetime import datetime
+from time import time
+
+import html_tools
+import base_panels
+import panels
+import card_game
+import radiocol
+import base_widget
+import contact_list
+from constants import Const as C
+import plugin_xep_0085
+
+
+class ChatText(HTMLPanel):
+
+    def __init__(self, nick, mymess, msg, extra):
+        try:
+            timestamp = float(extra['timestamp'])
+        except KeyError:
+            timestamp=None
+        xhtml = extra.get('xhtml')
+        _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_tools.html_sanitize(nick),
+                            "msg_class": ' '.join(_msg_class),
+                            "msg": strings.addURLToText(html_tools.html_sanitize(msg)) if not xhtml else html_tools.inlineRoot(xhtml)}  # FIXME: images and external links must be removed according to preferences
+                           )
+        self.setStyleName('chatText')
+
+
+class Chat(QuickChat, base_widget.LiberviaWidget, KeyboardHandler):
+
+    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
+        """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"""
+        QuickChat.__init__(self, host, target, type_, profiles=profiles)
+        self.vpanel = VerticalPanel()
+        self.vpanel.setSize('100%', '100%')
+
+        # 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_ == C.CHAT_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_ == C.CHAT_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
+        self.refresh()
+        if type_ == C.CHAT_ONE2ONE:
+            self.historyPrint(profile=self.profile)
+
+    @property
+    def target(self):
+        # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickChat
+        # FIXME: must remove this when either pyjamas is fixed, or we use an alternative
+        if self.type == C.CHAT_GROUP:
+            return self.current_target.bare
+        return self.current_target
+
+    @property
+    def profile(self):
+        # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickWidget
+        # FIXME: must remove this when either pyjamas is fixed, or we use an alternative
+        assert len(self.profiles) == 1 and not self.PROFILES_MULTIPLE and not self.PROFILES_ALLOW_NONE
+        return list(self.profiles)[0]
+
+    @classmethod
+    def registerClass(cls):
+        base_widget.LiberviaWidget.addDropKey("CONTACT", cls.createPanel)
+
+    @classmethod
+    def createPanel(cls, host, item, type_=C.CHAT_ONE2ONE):
+        assert(item)
+        _contact = item if isinstance(item, jid.JID) else jid.JID(item)
+        host.contact_panel.setContactMessageWaiting(_contact.bare, False)
+        _new_panel = Chat(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"""
+        # FIXME: must be checked
+        # 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)
+        elif enable_box:
+            self.message_box = panels.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 == C.CHAT_GROUP:
+            menu_bar.addCachedMenus(C.MENU_ROOM, {'room_jid': self.target.bare})
+        elif self.type == C.CHAT_ONE2ONE:
+            menu_bar.addCachedMenus(C.MENU_SINGLE, {'jid': self.target})
+
+    def getWarningData(self):
+        if self.type not in [C.CHAT_ONE2ONE, C.CHAT_GROUP]:
+            raise Exception("Unmanaged type !")
+        if self.type == C.CHAT_ONE2ONE:
+            msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % self.target
+        elif self.type == C.CHAT_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 == C.CHAT_ONE2ONE else "GROUP", msg)
+
+    def onTextEntered(self, text):
+        self.host.sendMessage(str(self.target),
+                              text,
+                              mess_type = C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT,
+                             errback=self.host.sendError,
+                             profile_key=C.PROF_KEY_NONE
+                             )
+        self.state_machine._onEvent("active")
+
+    def onQuit(self):
+        base_widget.LiberviaWidget.onQuit(self)
+        if self.type == C.CHAT_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 == C.CHAT_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', extra=None, 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 extra (dict): message data
+        @param link_cb: method to call when the info is clicked, ignored if type_ is not 'link'
+        """
+        if extra is None:
+            extra = {}
+        if type_ == 'normal':
+            _wid = HTML(strings.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=None, profile=C.PROF_KEY_NONE):
+        if extra is None:
+            extra = {}
+        try:
+            nick, mymess = QuickChat.printMessage(self, from_jid, msg, extra, profile)
+        except TypeError:
+            # None is returned, the message is managed
+            return
+        self.content.add(ChatText(nick, mymess, msg, extra))
+        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 == C.CHAT_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 != C.CHAT_GROUP:
+            return
+        box = self.occupants_list.getOccupantBox(resource)
+        if box:
+            contact_list.setPresenceStyle(box, availability)
+
+    def updateChatState(self, from_jid, state):
+        #TODO
+        pass
+
+quick_widgets.register(QuickChat, Chat)
--- a/src/browser/sat_browser/constants.py	Thu Oct 23 16:56:36 2014 +0200
+++ b/src/browser/sat_browser/constants.py	Sat Jan 24 01:45:39 2015 +0100
@@ -37,3 +37,4 @@
 
     # Empty avatar
     EMPTY_AVATAR = "/media/misc/empty_avatar"
+    WEB_PANEL_DEFAULT_URL = "http://salut-a-toi.org"
--- a/src/browser/sat_browser/contact.py	Thu Oct 23 16:56:36 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,538 +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.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.ClickListener import ClickHandler
-from pyjamas.ui.Label import Label
-from pyjamas.ui.HTML import HTML
-from pyjamas.ui.Image import Image
-from pyjamas import Window
-from pyjamas import DOM
-from __pyjamas__ import doc
-
-from constants import Const as C
-from jid import JID
-import base_widget
-import panels
-import html_tools
-
-
-def buildPresenceStyle(presence, base_style=None):
-    """Return the CSS classname to be used for displaying the given presence information.
-    @param presence (str): presence is a value in ('', 'chat', 'away', 'dnd', 'xa')
-    @param base_style (str): base classname
-    @return: str
-    """
-    if not base_style:
-        base_style = "contactLabel"
-    return '%s-%s' % (base_style, presence or 'connected')
-
-
-def setPresenceStyle(widget, presence, base_style=None):
-    """
-    Set the CSS style of a contact's element according to its presence.
-
-    @param widget (Widget): the UI element of the contact
-    @param presence (str): a value in ("", "chat", "away", "dnd", "xa").
-    @param base_style (str): the base name of the style to apply
-    """
-    if not hasattr(widget, 'presence_style'):
-        widget.presence_style = None
-    style = buildPresenceStyle(presence, base_style)
-    if style == widget.presence_style:
-        return
-    if widget.presence_style is not None:
-        widget.removeStyleName(widget.presence_style)
-    widget.addStyleName(style)
-    widget.presence_style = style
-
-
-class GroupLabel(base_widget.DragLabel, Label, ClickHandler):
-    def __init__(self, host, group):
-        self.group = group
-        self.host = host
-        Label.__init__(self, group)  # , Element=DOM.createElement('div')
-        self.setStyleName('group')
-        base_widget.DragLabel.__init__(self, group, "GROUP")
-        ClickHandler.__init__(self)
-        self.addClickListener(self)
-
-    def onClick(self, sender):
-        self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, {'item': self.group})
-
-
-class ContactLabel(HTML):
-    def __init__(self, jid, name=None):
-        HTML.__init__(self)
-        self.name = name or jid
-        self.waiting = False
-        self.refresh()
-        self.setStyleName('contactLabel')
-
-    def refresh(self):
-        if self.waiting:
-            wait_html = "<b>(*)</b>&nbsp;"
-        self.setHTML("%(wait)s%(name)s" % {'wait': wait_html,
-                                           'name': html_tools.html_sanitize(self.name)})
-
-    def setMessageWaiting(self, waiting):
-        """Show a visual indicator if message are waiting
-
-        @param waiting: True if message are waiting"""
-        self.waiting = waiting
-        self.refresh()
-
-
-class ContactMenuBar(base_widget.WidgetMenuBar):
-
-    def onBrowserEvent(self, event):
-        base_widget.WidgetMenuBar.onBrowserEvent(self, event)
-        event.stopPropagation()  # prevent opening the chat dialog
-
-    @classmethod
-    def getCategoryHTML(cls, menu_name_i18n, type_):
-        return '<img src="%s"/>' % C.DEFAULT_AVATAR
-
-    def setUrl(self, url):
-        """Set the URL of the contact avatar."""
-        self.items[0].setHTML('<img src="%s" />' % url)
-
-
-class ContactBox(VerticalPanel, ClickHandler, base_widget.DragLabel):
-
-    def __init__(self, host, jid, name=None, click_listener=None, handle_menu=None):
-        VerticalPanel.__init__(self, StyleName='contactBox', VerticalAlignment='middle')
-        base_widget.DragLabel.__init__(self, jid, "CONTACT")
-        self.host = host
-        self.jid = jid
-        self.label = ContactLabel(jid, name)
-        self.avatar = ContactMenuBar(self, host) if handle_menu else Image()
-        self.updateAvatar(host.getAvatar(jid))
-        self.add(self.avatar)
-        self.add(self.label)
-        if click_listener:
-            ClickHandler.__init__(self)
-            self.addClickListener(self)
-            self.click_listener = click_listener
-
-    def addMenus(self, menu_bar):
-        menu_bar.addCachedMenus(C.MENU_ROSTER_JID_CONTEXT, {'jid': self.jid})
-        menu_bar.addCachedMenus(C.MENU_JID_CONTEXT, {'jid': self.jid})
-
-    def setMessageWaiting(self, waiting):
-        """Show a visual indicator if message are waiting
-
-        @param waiting: True if message are waiting"""
-        self.label.setMessageWaiting(waiting)
-
-    def updateAvatar(self, url):
-        """Update the avatar.
-
-        @param url (str): image url
-        """
-        self.avatar.setUrl(url)
-
-    def onClick(self, sender):
-        self.click_listener(self.jid)
-
-
-class 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 [child.group for child 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)
-                return
-
-    def getGroupBox(self, group):
-        """get the widget of a group
-
-        @param group (str): the group
-        @return: GroupLabel instance if present, else None"""
-        for wid in self:
-            if isinstance(wid, GroupLabel) and wid.group == group:
-                return wid
-        return None
-
-
-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, handle_click=False, handle_menu=False):
-        VerticalPanel.__init__(self)
-        self.host = host
-        self.contacts = []
-        self.click_listener = None
-        self.handle_menu = handle_menu
-
-        if handle_click:
-            def cb(contact_jid):
-                self.host.getOrCreateLiberviaWidget(panels.ChatPanel, {'item': contact_jid})
-            self.click_listener = cb
-
-    def add(self, jid_s, name=None):
-        """Add a contact to the list.
-
-        @param jid (str): JID of the contact
-        @param name (str): optional name of the contact
-        """
-        assert(isinstance(jid_s, str))
-        if jid_s in self.contacts:
-            return
-        index = 0
-        for contact_ in self.contacts:
-            if contact_ > jid_s:
-                break
-            index += 1
-        self.contacts.insert(index, jid_s)
-        box = ContactBox(self.host, jid_s, name, self.click_listener, self.handle_menu)
-        VerticalPanel.insert(self, box, index)
-
-    def remove(self, jid_s):
-        box = self.getContactBox(jid_s)
-        if not box:
-            return
-        VerticalPanel.remove(self, box)
-        self.contacts.remove(jid_s)
-
-    def isContactPresent(self, contact_jid):
-        """Return True if a contact is present in the panel"""
-        return contact_jid in self.contacts
-
-    def getContacts(self):
-        return self.contacts
-
-    def getContactBox(self, contact_jid_s):
-        """get the widget of a contact
-
-        @param contact_jid_s (str): the contact
-        @return: ContactBox instance if present, else None"""
-        for wid in self:
-            if isinstance(wid, ContactBox) and wid.jid == contact_jid_s:
-                return wid
-        return None
-
-    def updateAvatar(self, jid_s, url):
-        """Update the avatar of the given contact
-
-        @param jid_s (str): contact jid
-        @param url (str): image url
-        """
-        try:
-            self.getContactBox(jid_s).updateAvatar(url)
-        except TypeError:
-            pass
-
-
-class ContactList(GenericContactList):
-    """The contact list that is displayed on the left side."""
-
-    def __init__(self, host):
-        GenericContactList.__init__(self, host, handle_click=True, handle_menu=True)
-
-    def setState(self, jid, type_, state):
-        """Change the appearance of the contact, according to the state
-        @param jid: jid 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"""
-        contact_box = self.getContactBox(jid)
-        if contact_box:
-            if type_ == 'availability':
-                setPresenceStyle(contact_box.label, state)
-            elif type_ == 'messageWaiting':
-                contact_box.setMessageWaiting(state)
-
-
-class ContactTitleLabel(base_widget.DragLabel, Label, ClickHandler):
-    def __init__(self, host, text):
-        Label.__init__(self, text)  # , Element=DOM.createElement('div')
-        self.host = host
-        self.setStyleName('contactTitle')
-        base_widget.DragLabel.__init__(self, text, "CONTACT_TITLE")
-        ClickHandler.__init__(self)
-        self.addClickListener(self)
-
-    def onClick(self, sender):
-        self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, {'item': None})
-
-
-class 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('contactPanel')
-        Window.addWindowResizeListener(self)
-
-    def onWindowResized(self, width, height):
-        contact_panel_elt = self.getElement()
-        classname = 'widgetsPanel' if isinstance(self.getParent().getParent(), panels.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_s, attributes, groups):
-        """Add a contact to the panel if it doesn't exist, update it else
-        @param jid_s: jid userhost as unicode
-        @param attributes: cf SàT Bridge API's newContact
-        @param groups: list of groups"""
-        _current_groups = self.getContactGroups(jid_s)
-        _new_groups = set(groups)
-        _key = "@%s: "
-
-        for group in _current_groups.difference(_new_groups):
-            # We remove the contact from the groups where he isn't anymore
-            self.groups[group].remove(jid_s)
-            if not self.groups[group]:
-                # The group is now empty, we must remove it
-                del self.groups[group]
-                self._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 group not 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_s)
-
-        # We add the contact to contact list, it will check if contact already exists
-        self._contact_list.add(jid_s)
-        self.updateVisibility([jid_s], self.getContactGroups(jid_s))
-
-    def removeContact(self, jid):
-        """Remove contacts from groups where he is and contact list"""
-        self.updateContact(jid, {}, [])  # we remove contact from every group
-        self._contact_list.remove(jid)
-
-    def setConnected(self, jid_s, resource, availability, priority, statuses):
-        """Set connection status
-        @param jid_s (str): JID userhost as unicode
-        """
-        if availability == 'unavailable':
-            if jid_s in self.connected:
-                if resource in self.connected[jid_s]:
-                    del self.connected[jid_s][resource]
-                if not self.connected[jid_s]:
-                    del self.connected[jid_s]
-        else:
-            if jid_s not in self.connected:
-                self.connected[jid_s] = {}
-            self.connected[jid_s][resource] = (availability, priority, statuses)
-
-        # check if the contact is connected with another resource, use the one with highest priority
-        if jid_s in self.connected:
-            max_resource = max_priority = None
-            for tmp_resource in self.connected[jid_s]:
-                if max_priority is None or self.connected[jid_s][tmp_resource][1] >= max_priority:
-                    max_resource = tmp_resource
-                    max_priority = self.connected[jid_s][tmp_resource][1]
-            if availability == "unavailable":  # do not check the priority here, because 'unavailable' has a dummy one
-                priority = max_priority
-                availability = self.connected[jid_s][max_resource][0]
-        if jid_s not in self.connected or priority >= max_priority:
-            # case 1: jid not in self.connected means all resources are disconnected, update with 'unavailable'
-            # case 2: update (or confirm) with the values of the resource which takes precedence
-            self._contact_list.setState(jid_s, "availability", availability)
-
-        # update the connected contacts chooser live
-        if hasattr(self.host, "room_contacts_chooser") and self.host.room_contacts_chooser is not None:
-            self.host.room_contacts_chooser.resetContacts()
-
-        self.updateVisibility([jid_s], self.getContactGroups(jid_s))
-
-    def setContactMessageWaiting(self, jid, waiting):
-        """Show an visual indicator that contact has send a message
-        @param jid: jid of the contact
-        @param waiting: True if message are waiting"""
-        self._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_s):
-        """Get groups where contact is
-       @param group: string of single group, or list of string
-       @param contact_jid_s: jid to test, as unicode
-        """
-        result = set()
-        for group in self.groups:
-            if self.isContactInGroup(group, contact_jid_s):
-                result.add(group)
-        return result
-
-    def isContactInGroup(self, group, contact_jid):
-        """Test if the contact_jid is in the group
-        @param group: string of single group, or list of string
-        @param contact_jid: jid to test
-        @return: True if contact_jid is in on of the groups"""
-        if group in self.groups and contact_jid in self.groups[group]:
-            return True
-        return False
-
-    def isContactInRoster(self, contact_jid):
-        """Test if the contact is in our roster list"""
-        for contact_box in self._contact_list:
-            if contact_jid == contact_box.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.label.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.label.removeStyleName("selected")
-
-    def updateAvatar(self, jid_s, url):
-        """Update the avatar of the given contact
-
-        @param jid_s (str): contact jid
-        @param url (str): image url
-        """
-        self._contact_list.updateAvatar(jid_s, url)
-
-    def hasVisibleMembers(self, group):
-        """Tell if the given group actually has visible members
-
-        @param group (str): the group to check
-        @return: boolean
-        """
-        for jid in self.groups[group]:
-            if self._contact_list.getContactBox(jid).isVisible():
-                return True
-        return False
-
-    def offlineContactsToShow(self):
-        """Tell if offline contacts should be visible according to the user settings
-
-        @return: boolean
-        """
-        return self.host.getCachedParam('General', C.SHOW_OFFLINE_CONTACTS) == 'true'
-
-    def emtyGroupsToShow(self):
-        """Tell if empty groups should be visible according to the user settings
-
-        @return: boolean
-        """
-        return self.host.getCachedParam('General', C.SHOW_EMPTY_GROUPS) == 'true'
-
-    def updateVisibility(self, jids, groups):
-        """Set the widgets visibility for the given contacts and groups
-
-        @param jids (list[str]): list of JID
-        @param groups (list[str]): list of groups
-        """
-        for jid_s in jids:
-            try:
-                self._contact_list.getContactBox(jid_s).setVisible(jid_s in self.connected or self.offlineContactsToShow())
-            except TypeError:
-                log.warning('No box for contact %s: this code line should not be reached' % jid_s)
-        for group in groups:
-            try:
-                self._groupList.getGroupBox(group).setVisible(self.hasVisibleMembers(group) or self.emtyGroupsToShow())
-            except TypeError:
-                log.warning('No box for group %s: this code line should not be reached' % group)
-
-    def refresh(self):
-        """Show or hide disconnected contacts and empty groups"""
-        self.updateVisibility(self._contact_list.contacts, self.groups.keys())
--- a/src/browser/sat_browser/contact_group.py	Thu Oct 23 16:56:36 2014 +0200
+++ b/src/browser/sat_browser/contact_group.py	Sat Jan 24 01:45:39 2015 +0100
@@ -28,14 +28,14 @@
 
 import dialog
 import list_manager
-import contact
+import contact_list
 
 
 class ContactGroupManager(list_manager.ListManager):
     """A manager for sub-panels to assign contacts to each group."""
 
-    def __init__(self, parent, keys_dict, contact_list, offsets, style):
-        list_manager.ListManager.__init__(self, parent, keys_dict, contact_list, offsets, style)
+    def __init__(self, parent, keys_dict, contacts, offsets, style):
+        list_manager.ListManager.__init__(self, parent, keys_dict, contacts, offsets, style)
         self.registerPopupMenuPanel(entries={"Remove group": {}},
                                     callback=lambda sender, key: Timer(5, lambda timer: self.removeContactKey(sender, key)))
 
@@ -153,7 +153,7 @@
         """Add the contact list to the DockPanel"""
         self.toggle = Button("", self.toggleContacts)
         self.toggle.addStyleName("toggleAssignedContacts")
-        self.contacts = contact.GenericContactList(self.host)
+        self.contacts = contact_list.BaseContactPanel(self.host)
         for contact_ in self.all_contacts:
             self.contacts.add(contact_)
         contact_panel = VerticalPanel()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/contact_list.py	Sat Jan 24 01:45:39 2015 +0100
@@ -0,0 +1,590 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import pyjd  # this is dummy in pyjs
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat_frontends.quick_frontend.quick_contact_list import QuickContactList
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.ScrollPanel import ScrollPanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.Image import Image
+from pyjamas import Window
+from pyjamas import DOM
+from __pyjamas__ import doc
+
+from sat_frontends.tools import jid
+from constants import Const as C
+import base_widget
+import panels
+import html_tools
+import chat
+
+
+def buildPresenceStyle(presence, base_style=None):
+    """Return the CSS classname to be used for displaying the given presence information.
+    @param presence (str): presence is a value in ('', 'chat', 'away', 'dnd', 'xa')
+    @param base_style (str): base classname
+    @return: str
+    """
+    if not base_style:
+        base_style = "contactLabel"
+    return '%s-%s' % (base_style, presence or 'connected')
+
+
+def setPresenceStyle(widget, presence, base_style=None):
+    """
+    Set the CSS style of a contact's element according to its presence.
+
+    @param widget (Widget): the UI element of the contact
+    @param presence (str): a value in ("", "chat", "away", "dnd", "xa").
+    @param base_style (str): the base name of the style to apply
+    """
+    if not hasattr(widget, 'presence_style'):
+        widget.presence_style = None
+    style = buildPresenceStyle(presence, base_style)
+    if style == widget.presence_style:
+        return
+    if widget.presence_style is not None:
+        widget.removeStyleName(widget.presence_style)
+    widget.addStyleName(style)
+    widget.presence_style = style
+
+
+class GroupLabel(base_widget.DragLabel, Label, ClickHandler):
+    def __init__(self, host, group):
+        self.group = group
+        self.host = host
+        Label.__init__(self, group)  # , Element=DOM.createElement('div')
+        self.setStyleName('group')
+        base_widget.DragLabel.__init__(self, group, "GROUP")
+        ClickHandler.__init__(self)
+        self.addClickListener(self)
+
+    def onClick(self, sender):
+        self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, {'item': self.group})
+
+
+class ContactLabel(HTML):
+    def __init__(self, jid, name=None):
+        HTML.__init__(self)
+        self.name = name or str(jid)
+        self.waiting = False
+        self.refresh()
+        self.setStyleName('contactLabel')
+
+    def refresh(self):
+        if self.waiting:
+            wait_html = "<b>(*)</b>&nbsp;"
+        self.setHTML("%(wait)s%(name)s" % {'wait': wait_html,
+                                           'name': html_tools.html_sanitize(self.name)})
+
+    def setMessageWaiting(self, waiting):
+        """Show a visual indicator if message are waiting
+
+        @param waiting: True if message are waiting"""
+        self.waiting = waiting
+        self.refresh()
+
+
+class ContactMenuBar(base_widget.WidgetMenuBar):
+
+    def onBrowserEvent(self, event):
+        base_widget.WidgetMenuBar.onBrowserEvent(self, event)
+        event.stopPropagation()  # prevent opening the chat dialog
+
+    @classmethod
+    def getCategoryHTML(cls, menu_name_i18n, type_):
+        return '<img src="%s"/>' % C.DEFAULT_AVATAR
+
+    def setUrl(self, url):
+        """Set the URL of the contact avatar."""
+        self.items[0].setHTML('<img src="%s" />' % url)
+
+
+class ContactBox(VerticalPanel, ClickHandler, base_widget.DragLabel):
+
+    def __init__(self, host, jid_, name=None, click_listener=None, handle_menu=None):
+        VerticalPanel.__init__(self, StyleName='contactBox', VerticalAlignment='middle')
+        base_widget.DragLabel.__init__(self, jid_, "CONTACT")
+        self.host = host
+        self.jid = jid_
+        self.label = ContactLabel(jid_, name)
+        self.avatar = ContactMenuBar(self, host) if handle_menu else Image()
+        # self.updateAvatar(host.getAvatar(jid_)) # FIXME
+        self.add(self.avatar)
+        self.add(self.label)
+        if click_listener:
+            ClickHandler.__init__(self)
+            self.addClickListener(self)
+            self.click_listener = click_listener
+
+    def addMenus(self, menu_bar):
+        menu_bar.addCachedMenus(C.MENU_ROSTER_JID_CONTEXT, {'jid': self.jid})
+        menu_bar.addCachedMenus(C.MENU_JID_CONTEXT, {'jid': self.jid})
+
+    def setMessageWaiting(self, waiting):
+        """Show a visual indicator if message are waiting
+
+        @param waiting: True if message are waiting"""
+        self.label.setMessageWaiting(waiting)
+
+    def updateAvatar(self, url):
+        """Update the avatar.
+
+        @param url (str): image url
+        """
+        self.avatar.setUrl(url)
+
+    def onClick(self, sender):
+        self.click_listener(self.jid)
+
+
+class GroupPanel(VerticalPanel):
+
+    def __init__(self, parent):
+        VerticalPanel.__init__(self)
+        self.setStyleName('groupPanel')
+        self._parent = parent
+        self._groups = set()
+
+    def add(self, group):
+        if group in self._groups:
+            log.warning("trying to add an already existing group")
+            return
+        _item = GroupLabel(self._parent.host, group)
+        _item.addMouseListener(self._parent)
+        DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer")
+        index = 0
+        for group_ in [child.group for child in self.getChildren()]:
+            if group_ > group:
+                break
+            index += 1
+        VerticalPanel.insert(self, _item, index)
+        self._groups.add(group)
+
+    def remove(self, group):
+        for wid in self:
+            if isinstance(wid, GroupLabel) and wid.group == group:
+                VerticalPanel.remove(self, wid)
+                self._groups.remove(group)
+                return
+        log.warning("Trying to remove a non existent group")
+
+    def getGroupBox(self, group):
+        """get the widget of a group
+
+        @param group (str): the group
+        @return: GroupLabel instance if present, else None"""
+        for wid in self:
+            if isinstance(wid, GroupLabel) and wid.group == group:
+                return wid
+        return None
+
+    def getGroups(self):
+        return self._groups
+
+
+class BaseContactsPanel(VerticalPanel):
+    """Class that can be used to represent a contact list, but not necessarily
+    the one that is displayed on the left side. Special features like popup menu
+    panel or changing the contact states must be done in a sub-class."""
+
+    def __init__(self, host, handle_click=False, handle_menu=False):
+        VerticalPanel.__init__(self)
+        self.host = host
+        self.contacts = []
+        self.click_listener = None
+        self.handle_menu = handle_menu
+
+        if handle_click:
+            def cb(contact_jid):
+                host.widgets.getOrCreateWidget(chat.Chat, contact_jid, type_=C.CHAT_ONE2ONE, profile=C.PROF_KEY_NONE)
+            self.click_listener = cb
+
+    def add(self, jid_, name=None):
+        """Add a contact to the list.
+
+        @param jid_ (jid.JID): jid_ of the contact
+        @param name (str): optional name of the contact
+        """
+        assert isinstance(jid_, jid.JID)
+        if jid_ in self.contacts:
+            return
+        index = 0
+        for contact_ in self.contacts:
+            if contact_ > jid_:
+                break
+            index += 1
+        self.contacts.insert(index, jid_)
+        box = ContactBox(self.host, jid_, name, self.click_listener, self.handle_menu)
+        VerticalPanel.insert(self, box, index)
+
+    def remove(self, jid_):
+        box = self.getContactBox(jid_)
+        if not box:
+            return
+        VerticalPanel.remove(self, box)
+        self.contacts.remove(jid_)
+
+    def isContactPresent(self, contact_jid):
+        """Return True if a contact is present in the panel"""
+        return contact_jid in self.contacts
+
+    def getContacts(self):
+        return self.contacts
+
+    def getContactBox(self, contact_jid):
+        """get the widget of a contact
+
+        @param contact_jid (jid.JID): the contact
+        @return: ContactBox instance if present, else None"""
+        for wid in self:
+            if isinstance(wid, ContactBox) and wid.jid == contact_jid:
+                return wid
+        return None
+
+    def updateAvatar(self, jid_, url):
+        """Update the avatar of the given contact
+
+        @param jid_ (jid.JID): contact jid
+        @param url (str): image url
+        """
+        try:
+            self.getContactBox(jid_).updateAvatar(url)
+        except TypeError:
+            pass
+
+
+class ContactsPanel(BaseContactsPanel):
+    """The contact list that is displayed on the left side."""
+
+    def __init__(self, host):
+        BaseContactsPanel.__init__(self, host, handle_click=True, handle_menu=True)
+
+    def setState(self, jid_, type_, state):
+        """Change the appearance of the contact, according to the state
+        @param jid_ (jid.JID): jid.JID which need to change state
+        @param type_ (str): one of "availability", "messageWaiting"
+        @param state:
+            - for messageWaiting type:
+                True if message are waiting
+            - for availability type:
+                C.PRESENCE_UNAVAILABLE or None if not connected, else presence like RFC6121 #4.7.2.1"""
+        assert type_ in ('availability', 'messageWaiting')
+        contact_box = self.getContactBox(jid_)
+        if not contact_box:
+            log.warning("No contact box found for {}".format(jid_))
+        else:
+            if type_ == 'availability':
+                if state is None:
+                    state = C.PRESENCE_UNAVAILABLE
+                setPresenceStyle(contact_box.label, state)
+            elif type_ == 'messageWaiting':
+                contact_box.setMessageWaiting(state)
+
+
+class ContactTitleLabel(base_widget.DragLabel, Label, ClickHandler):
+    def __init__(self, host, text):
+        Label.__init__(self, text)  # , Element=DOM.createElement('div')
+        self.host = host
+        self.setStyleName('contactTitle')
+        base_widget.DragLabel.__init__(self, text, "CONTACT_TITLE")
+        ClickHandler.__init__(self)
+        self.addClickListener(self)
+
+    def onClick(self, sender):
+        self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, {'item': None})
+
+
+class ContactList(SimplePanel, QuickContactList):
+    """Manage the contacts and groups"""
+
+    def __init__(self, host):
+        QuickContactList.__init__(self, host, C.PROF_KEY_NONE)
+        SimplePanel.__init__(self)
+        self.scroll_panel = ScrollPanel()
+        self.vPanel = VerticalPanel()
+        _title = ContactTitleLabel(host, 'Contacts')
+        DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer")
+        self._contacts_panel = ContactsPanel(host)
+        self._contacts_panel.setStyleName('contactPanel') # FIXME: style doesn't exists !
+        self._group_panel = GroupPanel(self)
+
+        self.vPanel.add(_title)
+        self.vPanel.add(self._group_panel)
+        self.vPanel.add(self._contacts_panel)
+        self.scroll_panel.add(self.vPanel)
+        self.add(self.scroll_panel)
+        self.setStyleName('contactList')
+        Window.addWindowResizeListener(self)
+
+    @property
+    def profile(self):
+        return C.PROF_KEY_NONE
+
+    def update(self):
+        ### GROUPS ###
+        _keys = self._groups.keys()
+        try:
+            # XXX: Pyjamas doesn't do the set casting if None is present
+            _keys.remove(None)
+        except KeyError:
+            pass
+        current_groups = set(_keys)
+        shown_groups = self._group_panel.getGroups()
+        new_groups = current_groups.difference(shown_groups)
+        removed_groups = shown_groups.difference(current_groups)
+        for group in new_groups:
+            self._group_panel.add(group)
+        for group in removed_groups:
+            self._group_panel.remove(group)
+
+        ### JIDS ###
+        current_contacts = set(self._cache.keys())
+        shown_contacts = set(self._contacts_panel.getContacts())
+        new_contacts = current_contacts.difference(shown_contacts)
+        removed_contacts = shown_contacts.difference(current_contacts)
+
+        for contact in new_contacts:
+            self._contacts_panel.add(contact)
+        for contact in removed_contacts:
+            self._contacts_panel.remove(contact)
+
+    def onWindowResized(self, width, height):
+        contact_panel_elt = self.getElement()
+        # FIXME: still needed ?
+        # classname = 'widgetsPanel' if isinstance(self.getParent().getParent(), panels.UniBoxPanel) else 'gwt-TabBar'
+        classname = 'gwt-TabBar'
+        _elts = doc().getElementsByClassName(classname)
+        if not _elts.length:
+            log.error("no element of class %s found, it should exist !" % classname)
+            tab_bar_h = height
+        else:
+            tab_bar_h = DOM.getAbsoluteTop(_elts.item(0)) or height  # getAbsoluteTop can be 0 if tabBar is hidden
+
+        ideal_height = tab_bar_h - DOM.getAbsoluteTop(contact_panel_elt) - 5
+        self.scroll_panel.setHeight("%s%s" % (ideal_height, "px"))
+
+    # def updateContact(self, jid_s, attributes, groups):
+    #     """Add a contact to the panel if it doesn't exist, update it else
+
+    #     @param jid_s: jid userhost as unicode
+    #     @param attributes: cf SàT Bridge API's newContact
+    #     @param groups: list of groups"""
+    #     _current_groups = self.getContactGroups(jid_s)
+    #     _new_groups = set(groups)
+    #     _key = "@%s: "
+
+    #     for group in _current_groups.difference(_new_groups):
+    #         # We remove the contact from the groups where he isn't anymore
+    #         self.groups[group].remove(jid_s)
+    #         if not self.groups[group]:
+    #             # The group is now empty, we must remove it
+    #             del self.groups[group]
+    #             self._group_panel.remove(group)
+    #             if self.host.uni_box:
+    #                 self.host.uni_box.removeKey(_key % group)
+
+    #     for group in _new_groups.difference(_current_groups):
+    #         # We add the contact to the groups he joined
+    #         if group not in self.groups.keys():
+    #             self.groups[group] = set()
+    #             self._group_panel.add(group)
+    #             if self.host.uni_box:
+    #                 self.host.uni_box.addKey(_key % group)
+    #         self.groups[group].add(jid_s)
+
+    #     # We add the contact to contact list, it will check if contact already exists
+    #     self._contacts_panel.add(jid_s)
+    #     self.updateVisibility([jid_s], self.getContactGroups(jid_s))
+
+    # def removeContact(self, jid):
+    #     """Remove contacts from groups where he is and contact list"""
+    #     self.updateContact(jid, {}, [])  # we remove contact from every group
+    #     self._contacts_panel.remove(jid)
+
+    # def setConnected(self, jid_s, resource, availability, priority, statuses):
+    #     """Set connection status
+    #     @param jid_s (str): JID userhost as unicode
+    #     """
+    #     if availability == 'unavailable':
+    #         if jid_s in self.connected:
+    #             if resource in self.connected[jid_s]:
+    #                 del self.connected[jid_s][resource]
+    #             if not self.connected[jid_s]:
+    #                 del self.connected[jid_s]
+    #     else:
+    #         if jid_s not in self.connected:
+    #             self.connected[jid_s] = {}
+    #         self.connected[jid_s][resource] = (availability, priority, statuses)
+
+    #     # check if the contact is connected with another resource, use the one with highest priority
+    #     if jid_s in self.connected:
+    #         max_resource = max_priority = None
+    #         for tmp_resource in self.connected[jid_s]:
+    #             if max_priority is None or self.connected[jid_s][tmp_resource][1] >= max_priority:
+    #                 max_resource = tmp_resource
+    #                 max_priority = self.connected[jid_s][tmp_resource][1]
+    #         if availability == "unavailable":  # do not check the priority here, because 'unavailable' has a dummy one
+    #             priority = max_priority
+    #             availability = self.connected[jid_s][max_resource][0]
+    #     if jid_s not in self.connected or priority >= max_priority:
+    #         # case 1: jid not in self.connected means all resources are disconnected, update with 'unavailable'
+    #         # case 2: update (or confirm) with the values of the resource which takes precedence
+    #         self._contacts_panel.setState(jid_s, "availability", availability)
+
+    #     # update the connected contacts chooser live
+    #     if hasattr(self.host, "room_contacts_chooser") and self.host.room_contacts_chooser is not None:
+    #         self.host.room_contacts_chooser.resetContacts()
+
+    #     self.updateVisibility([jid_s], self.getContactGroups(jid_s))
+
+    def setContactMessageWaiting(self, jid, waiting):
+        """Show an visual indicator that contact has send a message
+        @param jid: jid of the contact
+        @param waiting: True if message are waiting"""
+        self._contacts_panel.setState(jid, "messageWaiting", waiting)
+
+    # def getConnected(self, filter_muc=False):
+    #     """return a list of all jid (bare jid) connected
+    #     @param filter_muc: if True, remove the groups from the list
+    #     """
+    #     contacts = self.connected.keys()
+    #     contacts.sort()
+    #     return contacts if not filter_muc else list(set(contacts).intersection(set(self.getContacts())))
+
+    # def getContactGroups(self, contact_jid_s):
+    #     """Get groups where contact is
+    #    @param group: string of single group, or list of string
+    #    @param contact_jid_s: jid to test, as unicode
+    #     """
+    #     result = set()
+    #     for group in self.groups:
+    #         if self.isContactInGroup(group, contact_jid_s):
+    #             result.add(group)
+    #     return result
+
+    # def isContactInGroup(self, group, contact_jid):
+    #     """Test if the contact_jid is in the group
+    #     @param group: string of single group, or list of string
+    #     @param contact_jid: jid to test
+    #     @return: True if contact_jid is in on of the groups"""
+    #     if group in self.groups and contact_jid in self.groups[group]:
+    #         return True
+    #     return False
+
+    def isContactInRoster(self, contact_jid):
+        """Test if the contact is in our roster list"""
+        for contact_box in self._contacts_panel:
+            if contact_jid == contact_box.jid:
+                return True
+        return False
+
+    # def getContacts(self):
+    #     return self._contacts_panel.getContacts()
+
+    def getGroups(self):
+        return self.groups.keys()
+
+    def onMouseMove(self, sender, x, y):
+        pass
+
+    def onMouseDown(self, sender, x, y):
+        pass
+
+    def onMouseUp(self, sender, x, y):
+        pass
+
+    def onMouseEnter(self, sender):
+        if isinstance(sender, GroupLabel):
+            jids = self.getGroupData(sender.group, "jids")
+            for contact in self._contacts_panel:
+                if contact.jid in jids:
+                    contact.label.addStyleName("selected")
+
+    def onMouseLeave(self, sender):
+        if isinstance(sender, GroupLabel):
+            jids = self.getGroupData(sender.group, "jids")
+            for contact in self._contacts_panel:
+                if contact.jid in jids:
+                    contact.label.removeStyleName("selected")
+
+    def updateAvatar(self, jid_s, url):
+        """Update the avatar of the given contact
+
+        @param jid_s (str): contact jid
+        @param url (str): image url
+        """
+        self._contacts_panel.updateAvatar(jid_s, url)
+
+    def hasVisibleMembers(self, group):
+        """Tell if the given group actually has visible members
+
+        @param group (str): the group to check
+        @return: boolean
+        """
+        for jid_ in self.groups[group]:
+            if self._contacts_panel.getContactBox(jid_).isVisible():
+                return True
+        return False
+
+    def offlineContactsToShow(self):
+        """Tell if offline contacts should be visible according to the user settings
+
+        @return: boolean
+        """
+        return self.host.getCachedParam('General', C.SHOW_OFFLINE_CONTACTS) == 'true'
+
+    def emtyGroupsToShow(self):
+        """Tell if empty groups should be visible according to the user settings
+
+        @return: boolean
+        """
+        return self.host.getCachedParam('General', C.SHOW_EMPTY_GROUPS) == 'true'
+
+    def updatePresence(self, entity, show, priority, statuses):
+        QuickContactList.updatePresence(self, entity, show, priority, statuses)
+        entity_bare = entity.bare
+        show = self.getCache(entity_bare, C.PRESENCE_SHOW) # we use cache to have the show nformation of main resource only
+        self._contacts_panel.setState(entity_bare, "availability", show)
+
+    # def updateVisibility(self, jids, groups):
+    #     """Set the widgets visibility for the given contacts and groups
+
+    #     @param jids (list[str]): list of JID
+    #     @param groups (list[str]): list of groups
+    #     """
+    #     for jid_s in jids:
+    #         try:
+    #             self._contacts_panel.getContactBox(jid_s).setVisible(jid_s in self.connected or self.offlineContactsToShow())
+    #         except TypeError:
+    #             log.warning('No box for contact %s: this code line should not be reached' % jid_s)
+    #     for group in groups:
+    #         try:
+    #             self._group_panel.getGroupBox(group).setVisible(self.hasVisibleMembers(group) or self.emtyGroupsToShow())
+    #         except TypeError:
+    #             log.warning('No box for group %s: this code line should not be reached' % group)
+
+    # def refresh(self):
+    #     """Show or hide disconnected contacts and empty groups"""
+    #     self.updateVisibility(self._contacts_panel.contacts, self.groups.keys())
--- a/src/browser/sat_browser/jid.py	Thu Oct 23 16:56:36 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,118 +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/>.
-
-# FIXME: Libervia should use sat_frontends.tools.jid but pyjamas doesn't
-# know the unicode type and also experiences issues with __new__.
-
-
-class JID(object):
-    """This class help manage JID (Node@Domaine/Resource)"""
-
-    def __init__(self, jid):
-        assert(isinstance(jid, str))
-        self.__raw = str(JID.__normalize(jid))
-        self.__parse()
-
-    @classmethod
-    def __normalize(cls, jid):
-        """Naive normalization before instantiating and parsing the JID"""
-        if not jid:
-            return jid
-        tokens = jid.split('/')
-        tokens[0] = tokens[0].lower()  # force node and domain to lower-case
-        return '/'.join(tokens)
-
-    @property
-    def node(self):
-        return self.__node
-
-    @property
-    def domain(self):
-        return self.__domain
-
-    @property
-    def resource(self):
-        return self.__resource
-
-    @property
-    def bare(self):
-        return self.domain if not self.node else (self.node + "@" + self.domain)
-
-    # XXX: Avoid property setters, Pyjamas doesn't seem to handle them well. It seems
-    # that it will just naively assign the value without actually calling the method.
-    # FIXME: find a way to raise an error if the (undefined!) setter is called.
-    def setNode(self, node):
-        self.__node = node
-        self.__build()
-
-    def setDomain(self, domain):
-        self.__domain = domain
-        self.__build()
-
-    def setResource(self, resource):
-        self.__resource = resource
-        self.__build()
-
-    def setBare(self, bare):
-        self.__parseBare(bare)
-        self.__build()
-
-    def __build(self):
-        """Build the JID string from the node, domain and resource"""
-        self.__raw = self.bare if not self.resource else (self.bare + '/' + self.resource)
-
-    def __parse(self):
-        """Parse the JID string to extract the node, domain and resource"""
-        tokens = self.__raw.split('/')
-        bare, self.__resource = (tokens[0], tokens[1]) if len(tokens) > 1 else (self.__raw, '')
-        self.__parseBare(bare)
-
-    def __parseBare(self, bare):
-        """Parse the given JID bare to extract the node and domain
-
-        @param bare (str): JID bare to parse
-        """
-        tokens = bare.split('@')
-        self.__node, self.__domain = (tokens[0], tokens[1]) if len(tokens) > 1 else ('', bare)
-
-    def __str__(self):
-        try:
-            return self.__raw
-        except AttributeError:
-            raise AttributeError("Trying to output a JID which has not been parsed yet")
-
-    def is_valid(self):
-        """
-        @return: True if the JID is XMPP compliant
-        """
-        return self.domain != ''
-
-    def __eq__(self, other):
-        """Redefine equality operator to implement the naturally expected behavior"""
-        return self.__raw == other.__raw
-
-    def __hash__(self):
-        """Redefine hash to implement the naturally expected behavior"""
-        return hash(self.__raw)
-
-    def full(self):
-        return str(self)
-
-    def bareJID(self):
-        return JID(self.bare)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/json.py	Sat Jan 24 01:45:39 2015 +0100
@@ -0,0 +1,268 @@
+#!/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/>.
+
+
+### logging configuration ###
+from sat.core.log import getLogger
+log = getLogger(__name__)
+###
+
+from pyjamas.Timer import Timer
+from pyjamas import Window
+from pyjamas import JSONService
+
+from sat_browser.constants import Const as C
+
+
+class LiberviaMethodProxy(object):
+    """This class manage calling for one method"""
+
+    def __init__(self, parent, method):
+        self._parent = parent
+        self._method = method
+
+    def call(self, *args, **kwargs):
+        """Method called when self._method attribue is used in JSON_PROXY_PARENT
+
+        This method manage callback/errback in kwargs, and profile(_key) removing
+        @param *args: positional arguments of self._method
+        @param **kwargs: keyword arguments of self._method
+        """
+        callback=kwargs.pop('callback', None)
+        errback=kwargs.pop('errback', None)
+
+        # as profile is linked to browser session and managed server side, we remove them
+        profile_removed = False
+        try:
+            kwargs['profile'] # FIXME: workaround for pyjamas bug: KeyError is not raised iwith del
+            del kwargs['profile']
+            profile_removed = True
+        except KeyError:
+            pass
+
+        try:
+            kwargs['profile_key'] # FIXME: workaround for pyjamas bug: KeyError is not raised iwith del
+            del kwargs['profile_key']
+            profile_removed = True
+        except KeyError:
+            pass
+
+        if not profile_removed and args:
+            # if profile was not in kwargs, there is most probably one in args
+            args = list(args)
+            assert isinstance(args[-1], basestring) # Detect when we want to remove a callback (or something else) instead of the profile
+            del args[-1]
+
+        if kwargs:
+            # kwargs should be empty here, we don't manage keyword arguments on bridge calls
+            log.error("kwargs is not empty after treatment on method call: kwargs={}".format(kwargs))
+
+        id_ = self._parent.callMethod(self._method, args)
+
+        # callback or errback are managed in parent LiberviaJsonProxy with call id
+        if callback is not None:
+            self._parent.cb[id_] = callback
+        if errback is not None:
+            self._parent.eb[id_] = errback
+
+
+class LiberviaJsonProxy(JSONService.JSONService):
+
+    def __init__(self, url, methods):
+        self._serviceURL = url
+        self.methods = methods
+        JSONService.JSONService.__init__(self, url, self)
+        self.cb = {}
+        self.eb = {}
+        self._registerMethods(methods)
+
+    def _registerMethods(self, methods):
+        if methods:
+            for method in methods:
+                log.debug("Registering JSON method call [{}]".format(method))
+                setattr(self,
+                        method,
+                        getattr(LiberviaMethodProxy(self, method), 'call')
+                       )
+
+    def callMethod(self, method, params, handler = None):
+        ret = super(LiberviaJsonProxy, self).callMethod(method, params, handler)
+        return ret
+
+    def call(self, method, cb, *args):
+        # FIXME: deprecated call method, must be removed once it's not used anymore
+        id_ = self.callMethod(method, args)
+        log.debug("call: method={} [id={}], args={}".format(method, id_, 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):
+        try:
+            _cb = self.cb[request_info.id]
+        except KeyError:
+            pass
+        else:
+            # 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]
+
+        try:
+            del self.eb[request_info.id]
+        except KeyError:
+            pass
+
+    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))"""
+        try:
+            _eb = self.eb[request_info.id]
+        except KeyError:
+            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'])
+        else:
+            _eb((code, errobj))
+            del self.eb[request_info.id]
+
+        try:
+            del self.cb[request_info.id]
+        except KeyError:
+            pass
+
+
+class RegisterCall(LiberviaJsonProxy):
+    def __init__(self):
+        LiberviaJsonProxy.__init__(self, "/register_api",
+                        ["isRegistered", "isConnected", "asyncConnect", "registerParams", "getMenus"])
+
+
+class BridgeCall(LiberviaJsonProxy):
+    def __init__(self):
+        LiberviaJsonProxy.__init__(self, "/json_api",
+                        ["getContacts", "addContact", "sendMessage", "sendMblog", "sendMblogComment",
+                         "getLastMblogs", "getMassiveLastMblogs", "getMblogComments",
+                         "getHistory", "getPresenceStatuses", "joinMUC", "mucLeave", "getRoomsJoined",
+                         "getRoomsSubjects", "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady",
+                         "tarotGamePlayCards", "launchRadioCollective", "getMblogs", "getMblogsWithComments",
+                         "getWaitingSub", "subscription", "delContact", "updateContact", "getCard",
+                         "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction",
+                         "disconnect", "chatStateComposing", "getNewAccountDomain", "confirmationAnswer",
+                         "syntaxConvert", "getAccountDialogUI", "getLastResource", "getWaitingConf",
+                        ])
+    def __call__(self, *args, **kwargs):
+        return LiberviaJsonProxy.__call__(self, *args, **kwargs)
+
+    def getConfig(self, dummy1, dummy2): # FIXME
+        log.warning("getConfig is not implemeted in Libervia yet")
+        return ''
+
+    def isConnected(self, dummy): # FIXME
+        log.warning("isConnected is not implemeted in Libervia as for now profile is connected if session is opened")
+        return True
+
+    def getAvatarFile(self, hash_, callback=None):
+        log.warning("getAvatarFile only return hash in Libervia")
+        if callback is not None:
+            callback(hash_)
+        return hash_
+
+
+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"])
+        self._signals = {} # key: signal name, value: callback
+
+    def onRemoteResponse(self, response, request_info):
+        if self.retry_delay != self.RETRY_BASE_DELAY:
+            log.info("Connection with server restablished")
+        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.alert (u"Empty reponse bridgeSignal\ncode={}\nrequest_info: id={} method={} handler={}".format(code, request_info.id, request_info.method, request_info.handler))
+            # FIXME: to check/replace by a proper session end on disconnected signal
+            # 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):
+                log.info("Trying to reconnect to server...")
+                self.getSignals(callback=self.signalHandler)
+            log.warning("Lost connection, trying to reconnect in {} s".format(self.retry_delay/1000))
+            Timer(notify=_timerCb).schedule(self.retry_delay)
+            self.retry_delay *= 2
+
+    def register(self, name, callback, with_profile=True):
+        """Register a signal
+
+        @param: name of the signal to register
+        @param callback: method to call
+        @param with_profile: True if the original bridge method need a profile
+        """
+        log.debug("Registering signal {}".format(name))
+        if name in self._signals:
+            log.error("Trying to register and already registered signal ({})".format(name))
+        else:
+            self._signals[name] = (callback, with_profile)
+
+    def signalHandler(self,  signal_data):
+        self.getSignals(callback=self.signalHandler)
+        if len(signal_data) == 1:
+            signal_data.append([])
+        log.debug("Got signal ==> name: %s, params: %s" % (signal_data[0], signal_data[1]))
+        name, args = signal_data
+        try:
+            callback, with_profile = self._signals[name]
+        except KeyError:
+            log.warning("Ignoring {} signal: no handler registered !".format(name))
+            return
+        if with_profile:
+            args.append(C.PROF_KEY_NONE)
+        callback(*args)
--- a/src/browser/sat_browser/menu.py	Thu Oct 23 16:56:36 2014 +0200
+++ b/src/browser/sat_browser/menu.py	Sat Jan 24 01:45:39 2015 +0100
@@ -23,13 +23,14 @@
 
 from sat.core.i18n import _
 
+from sat_frontends.tools import jid
+
 from pyjamas.ui.SimplePanel import SimplePanel
 from pyjamas.ui.HTML import HTML
 from pyjamas.ui.Frame import Frame
 from pyjamas import Window
 
 from constants import Const as C
-import jid
 import file_tools
 import xmlui
 import panels
@@ -97,9 +98,8 @@
 
     # 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)
+        web_widget = self.host.widgets.getOrCreateWidget(panels.WebPanel, C.WEB_PANEL_DEFAULT_URL, profile=C.PROF_KEY_NONE)
+        self.host.setSelected(web_widget)
 
     def onDisconnect(self):
         def confirm_cb(answer):
@@ -124,9 +124,9 @@
     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 />
+You can contact the authors at <a href="mailto:contact@salut-a-toi.org">contact@salut-a-toi.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 />
+Project page: <a href="http://salut-a-toi.org"target="_blank">http://salut-a-toi.org</a><br />
 <br />
 Any help welcome :)
 <p style='font-size:small;text-align:center'>This project is dedicated to Roger Poisson</p>
--- a/src/browser/sat_browser/nativedom.py	Thu Oct 23 16:56:36 2014 +0200
+++ b/src/browser/sat_browser/nativedom.py	Sat Jan 24 01:45:39 2015 +0100
@@ -34,7 +34,7 @@
         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
+            ret.append(self.__class__(js_nodes_list.item(i)))  # XXX: Ugly, but used to work around a Pyjamas's bug
         return ret
 
     def __getattr__(self, name):
@@ -42,7 +42,7 @@
             return getattr(self._node, name)
         return object.__getattribute__(self, name)
 
-    @property
+    @property # XXX: doesn't work in --strict mode in pyjs
     def childNodes(self):
         return self._jsNodesList2List(self._node.childNodes)
 
--- 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):
--- a/src/browser/sat_browser/plugin_sec_otr.py	Thu Oct 23 16:56:36 2014 +0200
+++ b/src/browser/sat_browser/plugin_sec_otr.py	Sat Jan 24 01:45:39 2015 +0100
@@ -29,7 +29,7 @@
 log = getLogger(__name__)
 
 from constants import Const as C
-import jid
+from sat_frontends.tools import jid
 import otrjs_wrapper as otr
 import dialog
 import panels
@@ -127,7 +127,7 @@
 
         @param host (satWebFrontend)
         @param account (Account)
-        @param other_jid (JID): JID of the person your chat correspondent
+        @param other_jid (jid.JID): JID of the person your chat correspondent
         """
         super(Context, self).__init__(account, other_jid)
         self.host = host
@@ -157,14 +157,14 @@
         log.debug("message received (was %s): %s" % ('encrypted' if encrypted else 'plain', msg))
         if not encrypted:
             if self.state == otr.context.STATE_ENCRYPTED:
-                log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': self.peer.full()})
+                log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': self.peer})
                 self.host.newMessageCb(self.peer, RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT, C.MESS_TYPE_INFO, self.host.whoami, {})
         self.host.newMessageCb(self.peer, msg, "chat", self.host.whoami, {})
 
     def sendMessageCb(self, msg, meta=None):
         assert isinstance(self.peer, jid.JID)
         log.debug("message to send%s: %s" % ((' (attached meta data: %s)' % meta) if meta else '', msg))
-        self.host.bridge.call('sendMessage', (None, self.host.sendError), self.peer.full(), msg, '', 'chat', {'send_only': 'true'})
+        self.host.bridge.call('sendMessage', (None, self.host.sendError), self.peer, msg, '', 'chat', {'send_only': 'true'})
 
     def messageErrorCb(self, error):
         log.error('error occured: %s' % error)
@@ -173,7 +173,7 @@
         if status == otr.context.STATUS_AKE_INIT:
             return
 
-        other_jid_s = self.peer.full()
+        other_jid_s = self.peer
         feedback = _(u"Error: the state of the conversation with %s is unknown!")
         trust = self.getCurrentTrust()
 
@@ -196,7 +196,7 @@
 
     def setCurrentTrust(self, new_trust='', act='asked', type_='trust'):
         log.debug("setCurrentTrust: trust={trust}, act={act}, type={type}".format(type=type_, trust=new_trust, act=act))
-        title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer.full())
+        title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer)
         old_trust = self.getCurrentTrust()
         if type_ == 'abort':
             msg = AUTH_ABORTED_TXT
@@ -236,7 +236,7 @@
             self.setCurrentTrust('fingerprint' if confirm else '')
 
         text = (AUTH_INFO_TXT + "<i>" + AUTH_FINGERPRINT_TXT + "</i>" + AUTH_FINGERPRINT_VERIFY).format(you=self.host.whoami, your_fp=priv_key.fingerprint(), other=self.peer, other_fp=other_key.fingerprint(), eol=DIALOG_EOL)
-        title = AUTH_OTHER_TITLE.format(jid=self.peer.full())
+        title = AUTH_OTHER_TITLE.format(jid=self.peer)
         dialog.ConfirmDialog(setTrust, text, title, AddStyleName="maxWidthLimit").show()
 
     def smpAuthCb(self, type_, data, act=None):
@@ -261,7 +261,7 @@
                 # make us need the dirty self.smpAuthAbort.
             else:
                 log.error("FIXME: unmanaged ambiguous 'act' value in Context.smpAuthCb!")
-        title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer.full())
+        title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer)
         if type_ == 'question':
             if act == 'asked':
                 def cb(question, answer=None):
@@ -297,7 +297,7 @@
 class Account(otr.context.Account):
 
     def __init__(self, host):
-        log.debug(u"new account: %s" % host.whoami.full())
+        log.debug(u"new account: %s" % host.whoami)
         if not host.whoami.resource:
             log.warning("Account created without resource")
         super(Account, self).__init__(host.whoami)
@@ -337,17 +337,17 @@
     def getContextForUser(self, other_jid, start=True):
         """Get the context for the given JID
 
-        @param other_jid (JID): your correspondent
+        @param other_jid (jid.JID): your correspondent
         @param start (bool): start non-existing context if True
         @return: Context
         """
         log.debug(u"getContextForUser [%s]" % other_jid)
         if not other_jid.resource:
             log.error("getContextForUser called with a bare jid")
-            running_sessions = [jid.bareJID() for jid in self.contexts.keys() if self.contexts[jid].state == otr.context.STATE_ENCRYPTED]
+            running_sessions = [jid_.bareJID() for jid_ in self.contexts.keys() if self.contexts[jid_].state == otr.context.STATE_ENCRYPTED]
             if start or (other_jid in running_sessions):
                 users_ml = DIALOG_USERS_ML.format(subject=D_("OTR issue in Libervia: getContextForUser called with a bare jid in an encrypted context"))
-                text = RESOURCE_ISSUE.format(eol=DIALOG_EOL, jid=other_jid.full(), users_ml=users_ml)
+                text = RESOURCE_ISSUE.format(eol=DIALOG_EOL, jid=other_jid, users_ml=users_ml)
                 dialog.InfoDialog(RESOURCE_ISSUE_TITLE, text, AddStyleName="maxWidthLimit").show()
             return None  # never start an OTR session with a bare JID
         if start:
@@ -380,7 +380,7 @@
     def infoTextCallback(self, other_jid, cb):
         """Get the current info text for a conversation and run a callback.
 
-        @param other_jid (JID): JID of the correspondant
+        @param other_jid (jid.JID): JID of the correspondant
         @paam cb (callable): method to be called with the computed info text
         """
         def gotResource(other_jid):
@@ -414,21 +414,24 @@
         for context in self.context_manager.contexts.values():
             context.disconnect()
 
-    def fixResource(self, jid, cb):
+    def fixResource(self, jid_, cb):
         # FIXME: it's dirty, but libervia doesn't manage resources correctly now, refactoring is planed
-        if jid.resource:
-            self.last_resources[jid.bare] = jid.resource
-            cb(jid)
-        elif jid.bare in self.last_resources:
-            jid.setResource(self.last_resources[jid.bare])
-            cb(jid)
+        if jid_.resource:
+            self.last_resources[jid_.bare] = jid_.resource
+            cb(jid_)
+        elif jid_.bare in self.last_resources:
+            # FIXME: to be removed: must use new resource system
+            # jid_.setResource(self.last_resources[jid_.bare])
+            cb(jid_)
         else:
-            def gotResource(resource):
-                if resource:
-                    jid.setResource(resource)
-                    self.last_resources[jid.bare] = jid.resource
-                cb(jid)
-            self.host.bridge.call('getLastResource', gotResource, jid.full())
+            pass # FIXME: to be removed: must use new resource system
+            # def gotResource(resource):
+            #     if resource:
+            #         jid_.setResource(resource)
+            #         self.last_resources[jid_.bare] = jid_.resource
+            #     cb(jid_)
+            #
+            # self.host.bridge.call('getLastResource', gotResource, jid_)
 
     def messageReceivedTrigger(self, from_jid, msg, msg_type, to_jid, extra):
         if msg_type == C.MESS_TYPE_INFO:
@@ -441,19 +444,19 @@
         def decrypt(context):
             context.receiveMessage(msg)
 
-        def cb(jid):
-            otrctx = self.context_manager.getContextForUser(jid, start=False)
+        def cb(jid_):
+            otrctx = self.context_manager.getContextForUser(jid_, start=False)
             if otrctx is None:
                 def confirm(confirm):
                     if confirm:
-                        self.host.getOrCreateLiberviaWidget(panels.ChatPanel, {'item': jid})
-                        decrypt(self.context_manager.startContext(jid))
+                        self.host.getOrCreateLiberviaWidget(panels.ChatPanel, {'item': jid_})
+                        decrypt(self.context_manager.startContext(jid_))
                     else:
                         # FIXME: plain text messages with whitespaces would be lost here when WHITESPACE_START_AKE is True
                         pass
                 key = self.context_manager.account.privkey
                 msg = QUERY_RECEIVED + QUERY_SLOWDOWN + (QUERY_KEY if key else QUERY_NO_KEY) + QUERY_CONFIRM
-                dialog.ConfirmDialog(confirm, msg.format(jid=jid.full(), eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show()
+                dialog.ConfirmDialog(confirm, msg.format(jid=jid_, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show()
             else:  # do not ask if the context exist
                 decrypt(otrctx)
 
@@ -462,19 +465,19 @@
         return False  # interrupt the main process
 
     def sendMessageTrigger(self, to_jid, msg, msg_type, extra):
-        def cb(jid):
-            otrctx = self.context_manager.getContextForUser(jid, start=False)
+        def cb(jid_):
+            otrctx = self.context_manager.getContextForUser(jid_, start=False)
             if otrctx is not None and otrctx.state != otr.context.STATE_PLAINTEXT:
                 if otrctx.state == otr.context.STATE_ENCRYPTED:
                     log.debug(u"encrypting message")
                     otrctx.sendMessage(msg)
-                    self.host.newMessageCb(self.host.whoami, msg, msg_type, jid, extra)
+                    self.host.newMessageCb(self.host.whoami, msg, msg_type, jid_, extra)
                 else:
                     feedback = SEND_PLAIN_IN_FINISHED_CONTEXT
-                    dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid.full()), feedback, AddStyleName="maxWidthLimit").show()
+                    dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid), feedback, AddStyleName="maxWidthLimit").show()
             else:
                 log.debug(u"sending message unencrypted")
-                self.host.bridge.call('sendMessage', (None, self.host.sendError), to_jid.full(), msg, '', msg_type, extra)
+                self.host.bridge.call('sendMessage', (None, self.host.sendError), to_jid, msg, '', msg_type, extra)
 
         if msg_type == 'groupchat':
             return True
@@ -489,14 +492,14 @@
     def endSession(self, other_jid, profile, finish=False):
         """Finish or disconnect an OTR session
 
-        @param other_jid (JID): str
+        @param other_jid (jid.JID): str
         @param finish: if True, finish the session but do not disconnect it
         @return: True if the session has been finished or disconnected, False if there was nothing to do
         """
         def cb(other_jid):
             def not_available():
                 if not finish:
-                    self.host.newMessageCb(other_jid, END_PLAIN.format(jid=other_jid.full()), C.MESS_TYPE_INFO, self.host.whoami, {})
+                    self.host.newMessageCb(other_jid, END_PLAIN.format(jid=other_jid), C.MESS_TYPE_INFO, self.host.whoami, {})
 
             priv_key = self.context_manager.account.privkey
             if priv_key is None:
@@ -526,16 +529,16 @@
             if otrctx:
                 otrctx.sendQueryMessage()
 
-        def cb(jid):
+        def cb(jid_):
             key = self.context_manager.account.privkey
             if key is None:
                 def confirm(confirm):
                     if confirm:
-                        query(jid)
+                        query(jid_)
                 msg = QUERY_SEND + QUERY_SLOWDOWN + QUERY_NO_KEY + QUERY_CONFIRM
-                dialog.ConfirmDialog(confirm, msg.format(jid=jid.full(), eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show()
+                dialog.ConfirmDialog(confirm, msg.format(jid=jid_, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show()
             else:  # on query reception we ask always, if we initiate we just ask the first time
-                query(jid)
+                query(jid_)
 
         try:
             other_jid = menu_data['jid']
--- a/src/browser/sat_browser/register.py	Thu Oct 23 16:56:36 2014 +0200
+++ b/src/browser/sat_browser/register.py	Sat Jan 24 01:45:39 2015 +0100
@@ -186,7 +186,7 @@
             self.login_warning_msg.setVisible(True)
         else:
             self.submit_type.setValue('login')
-            self.submit()
+            self.submit(None)
 
     def onRegister(self, button):
         # XXX: for now libervia forces the creation to lower case
@@ -203,7 +203,7 @@
         else:
             self.register_warning_msg.setVisible(False)
             self.submit_type.setValue('register')
-            self.submit()
+            self.submit(None)
 
     def onSubmit(self, event):
         pass
--- a/src/browser/sat_browser/xmlui.py	Thu Oct 23 16:56:36 2014 +0200
+++ b/src/browser/sat_browser/xmlui.py	Sat Jan 24 01:45:39 2015 +0100
@@ -407,11 +407,11 @@
 class XMLUIPanel(LiberviaXMLUIBase, xmlui.XMLUIPanel, VerticalPanel):
     widget_factory = WidgetFactory()
 
-    def __init__(self, host, parsed_xml, title=None, flags=None):
+    def __init__(self, host, parsed_xml, title=None, flags=None, callback=None, profile=C.PROF_KEY_NONE):
         self.widget_factory._xmlui_main = self
         VerticalPanel.__init__(self)
         self.setSize('100%', '100%')
-        xmlui.XMLUIPanel.__init__(self, host, parsed_xml, title, flags)
+        xmlui.XMLUIPanel.__init__(self, host, parsed_xml, title, flags, callback, profile)
 
     def setCloseCb(self, close_cb):
         self.close_cb = close_cb
@@ -445,7 +445,7 @@
 
     def show(self):
         options = ['NO_CLOSE'] if self.type == C.XMLUI_FORM else []
-        _dialog = dialog.GenericDialog(self.title, self, options=options)
+        _dialog = dialog.GenericDialog(self.xmlui_title, self, options=options)
         self.setCloseCb(_dialog.close)
         _dialog.show()
 
@@ -453,8 +453,8 @@
 class XMLUIDialog(LiberviaXMLUIBase, xmlui.XMLUIDialog):
     dialog_factory = GenericFactory()
 
-    def __init__(self, host, parsed_dom, title = None, flags = None):
-        xmlui.XMLUIDialog.__init__(self, host, parsed_dom, title, flags)
+    def __init__(self, host, parsed_dom, title = None, flags = None, callback=None, profile=C.PROF_KEY_NONE):
+        xmlui.XMLUIDialog.__init__(self, host, parsed_dom, title, flags, callback, profile)
 
 xmlui.registerClass(xmlui.CLASS_PANEL, XMLUIPanel)
 xmlui.registerClass(xmlui.CLASS_DIALOG, XMLUIDialog)
--- a/src/server/server.py	Thu Oct 23 16:56:36 2014 +0200
+++ b/src/server/server.py	Sat Jan 24 01:45:39 2015 +0100
@@ -184,13 +184,6 @@
             return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))  # pylint: disable=E1103
         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)
@@ -231,6 +224,11 @@
         profile = ISATSession(self.session).profile
         return self.sat_host.bridge.getWaitingSub(profile)
 
+    def jsonrpc_getWaitingConf(self):
+        """Return list of waiting confirmations"""
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.getWaitingConf(profile)
+
     def jsonrpc_setStatus(self, presence, status):
         """Change the presence and/or status
         @param presence: value from ("", "chat", "away", "dnd", "xa")
@@ -359,10 +357,10 @@
         profile = sat_session.profile
         sat_jid = sat_session.jid
         if not sat_jid:
-            log.error("No jid saved for this profile")
-            return {}
+            # we keep a session cache for jid to avoir jid spoofing
+            sat_jid = sat_session.jid = JID(self.sat_host.bridge.getParamA("JabberID", "Connection", profile_key=profile))
         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 ?")
+            log.error("Trying to get history from a different jid (given (browser): {}, real (backend): {}), maybe a hack attempt ?".format(from_jid, sat_jid))
             return {}
         d = self.asyncBridgeCall("getHistory", from_jid, to_jid, size, between, search, profile)
 
@@ -418,6 +416,11 @@
         profile = ISATSession(self.session).profile
         return self.sat_host.bridge.getRoomsJoined(profile)
 
+    def jsonrpc_getRoomsSubjects(self):
+        """Return list of room subjects"""
+        profile = ISATSession(self.session).profile
+        return self.sat_host.bridge.getRoomsSubjects(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
@@ -466,7 +469,10 @@
         @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)
+        try:
+            return self.sat_host.bridge.getEntityData(jid, keys, profile)
+        except Exception as e:
+            raise Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(e)))
 
     def jsonrpc_getCard(self, jid):
         """Get VCard for entiry
@@ -761,6 +767,7 @@
             request.write(C.SESSION_ACTIVE)
             request.finish()
             return
+        # we manage profile server side to avoid profile spoofing
         sat_session.profile = profile
         self.sat_host.prof_connected.add(profile)