changeset 1124:28e3eb3bb217

files reorganisation and installation rework: - files have been reorganised to follow other SàT projects and usual Python organisation (no more "/src" directory) - VERSION file is now used, as for other SàT projects - replace the overcomplicated setup.py be a more sane one. Pyjamas part is not compiled anymore by setup.py, it must be done separatly - removed check for data_dir if it's empty - installation tested working in virtual env - libervia launching script is now in bin/libervia
author Goffi <goffi@goffi.org>
date Sat, 25 Aug 2018 17:59:48 +0200
parents 63a4b8fe9782
children 56ace2d45783
files bin/libervia browser/collections.py browser/libervia_main.py browser/libervia_test.py browser/otr.min.js browser/otr.min.js_README browser/public/contrat_social.html browser/public/favico.min.js browser/public/libervia.css browser/public/libervia.html browser/public/robots.txt browser/public/sat_logo_16.png browser/sat_browser/__init__.py browser/sat_browser/base_menu.py browser/sat_browser/base_panel.py browser/sat_browser/base_widget.py browser/sat_browser/blog.py browser/sat_browser/chat.py browser/sat_browser/constants.py browser/sat_browser/contact_group.py browser/sat_browser/contact_list.py browser/sat_browser/contact_panel.py browser/sat_browser/contact_widget.py browser/sat_browser/dialog.py browser/sat_browser/editor_widget.py browser/sat_browser/file_tools.py browser/sat_browser/game_radiocol.py browser/sat_browser/game_tarot.py browser/sat_browser/html_tools.py browser/sat_browser/json.py browser/sat_browser/libervia_widget.py browser/sat_browser/list_manager.py browser/sat_browser/logging.py browser/sat_browser/main_panel.py browser/sat_browser/menu.py browser/sat_browser/nativedom.py browser/sat_browser/notification.py browser/sat_browser/otrjs_wrapper.py browser/sat_browser/plugin_sec_otr.py browser/sat_browser/plugin_xep_0085.py browser/sat_browser/register.py browser/sat_browser/richtext.py browser/sat_browser/strings.py browser/sat_browser/web_widget.py browser/sat_browser/xmlui.py ez_setup.py libervia/VERSION libervia/__init__.py libervia/common/__init__.py libervia/common/constants.py libervia/pages/app/page_meta.py libervia/pages/blog/page_meta.py libervia/pages/blog/view/atom.xml/page_meta.py libervia/pages/blog/view/page_meta.py libervia/pages/chat/page_meta.py libervia/pages/chat/select/page_meta.py libervia/pages/events/admin/page_meta.py libervia/pages/events/new/page_meta.py libervia/pages/events/page_meta.py libervia/pages/events/rsvp/page_meta.py libervia/pages/events/view/page_meta.py libervia/pages/files/list/page_meta.py libervia/pages/files/page_meta.py libervia/pages/files/view/page_meta.py libervia/pages/forums/list/page_meta.py libervia/pages/forums/page_meta.py libervia/pages/forums/topics/page_meta.py libervia/pages/forums/view/page_meta.py libervia/pages/g/e/page_meta.py libervia/pages/g/page_meta.py libervia/pages/login/logged/page_meta.py libervia/pages/login/page_meta.py libervia/pages/merge-requests/disco/page_meta.py libervia/pages/merge-requests/edit/page_meta.py libervia/pages/merge-requests/new/page_meta.py libervia/pages/merge-requests/page_meta.py libervia/pages/merge-requests/view/page_meta.py libervia/pages/photos/album/page_meta.py libervia/pages/photos/page_meta.py libervia/pages/register/page_meta.py libervia/pages/tickets/disco/page_meta.py libervia/pages/tickets/edit/page_meta.py libervia/pages/tickets/new/page_meta.py libervia/pages/tickets/page_meta.py libervia/pages/tickets/view/page_meta.py libervia/pages/u/atom.xml/page_meta.py libervia/pages/u/blog/page_meta.py libervia/pages/u/page_meta.py libervia/server/__init__.py libervia/server/blog.py libervia/server/constants.py libervia/server/html_tools.py libervia/server/pages.py libervia/server/pages_tools.py libervia/server/server.py libervia/server/session_iface.py libervia/server/utils.py libervia/server/websockets.py setup.py src/__init__.py src/browser/collections.py src/browser/libervia_main.py src/browser/libervia_test.py src/browser/otr.min.js src/browser/otr.min.js_README src/browser/public/contrat_social.html src/browser/public/favico.min.js src/browser/public/libervia.css src/browser/public/libervia.html src/browser/public/robots.txt src/browser/public/sat_logo_16.png src/browser/sat_browser/__init__.py src/browser/sat_browser/base_menu.py src/browser/sat_browser/base_panel.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_group.py src/browser/sat_browser/contact_list.py src/browser/sat_browser/contact_panel.py src/browser/sat_browser/contact_widget.py src/browser/sat_browser/dialog.py src/browser/sat_browser/editor_widget.py src/browser/sat_browser/file_tools.py src/browser/sat_browser/game_radiocol.py src/browser/sat_browser/game_tarot.py src/browser/sat_browser/html_tools.py src/browser/sat_browser/json.py src/browser/sat_browser/libervia_widget.py src/browser/sat_browser/list_manager.py src/browser/sat_browser/logging.py src/browser/sat_browser/main_panel.py src/browser/sat_browser/menu.py src/browser/sat_browser/nativedom.py src/browser/sat_browser/notification.py src/browser/sat_browser/otrjs_wrapper.py src/browser/sat_browser/plugin_sec_otr.py src/browser/sat_browser/plugin_xep_0085.py src/browser/sat_browser/register.py src/browser/sat_browser/richtext.py src/browser/sat_browser/strings.py src/browser/sat_browser/web_widget.py src/browser/sat_browser/xmlui.py src/common/__init__.py src/common/constants.py src/libervia.sh src/pages/app/page_meta.py src/pages/blog/page_meta.py src/pages/blog/view/atom.xml/page_meta.py src/pages/blog/view/page_meta.py src/pages/chat/page_meta.py src/pages/chat/select/page_meta.py src/pages/events/admin/page_meta.py src/pages/events/new/page_meta.py src/pages/events/page_meta.py src/pages/events/rsvp/page_meta.py src/pages/events/view/page_meta.py src/pages/files/list/page_meta.py src/pages/files/page_meta.py src/pages/files/view/page_meta.py src/pages/forums/list/page_meta.py src/pages/forums/page_meta.py src/pages/forums/topics/page_meta.py src/pages/forums/view/page_meta.py src/pages/g/e/page_meta.py src/pages/g/page_meta.py src/pages/login/logged/page_meta.py src/pages/login/page_meta.py src/pages/merge-requests/disco/page_meta.py src/pages/merge-requests/edit/page_meta.py src/pages/merge-requests/new/page_meta.py src/pages/merge-requests/page_meta.py src/pages/merge-requests/view/page_meta.py src/pages/photos/album/page_meta.py src/pages/photos/page_meta.py src/pages/register/page_meta.py src/pages/tickets/disco/page_meta.py src/pages/tickets/edit/page_meta.py src/pages/tickets/new/page_meta.py src/pages/tickets/page_meta.py src/pages/tickets/view/page_meta.py src/pages/u/atom.xml/page_meta.py src/pages/u/blog/page_meta.py src/pages/u/page_meta.py src/server/__init__.py src/server/blog.py src/server/constants.py src/server/html_tools.py src/server/pages.py src/server/pages_tools.py src/server/server.py src/server/session_iface.py src/server/utils.py src/server/websockets.py src/twisted/plugins/libervia_server.py twisted/plugins/libervia_server.py
diffstat 189 files changed, 20449 insertions(+), 20993 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/libervia	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,107 @@
+#!/bin/sh
+
+DEBUG=""
+DAEMON=""
+PYTHON="python2"
+TWISTD="$(which twistd)"
+
+kill_process() {
+    # $1 is the file containing the PID to kill, $2 is the process name
+    if [ -f $1 ]; then
+        PID=`cat $1`
+        if ps -p $PID > /dev/null; then
+            echo "Terminating $2... "
+            kill -INT $PID
+        else
+            echo "No running process of ID $PID... removing PID file"
+            rm -f $1
+        fi
+    else
+        echo "$2 is probably not running (PID file doesn't exist)"
+    fi
+}
+
+#We use python to parse config files
+eval `"$PYTHON" << PYTHONEND
+from libervia.server.constants import Const as C
+from sat.memory.memory import fixLocalDir
+from ConfigParser import SafeConfigParser
+from os.path import expanduser, join
+import sys
+import codecs
+import locale
+
+sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout)
+
+fixLocalDir()  # XXX: tmp update code, will be removed in the future
+
+config = SafeConfigParser(defaults=C.DEFAULT_CONFIG)
+try:
+    config.read(C.CONFIG_FILES)
+except:
+    print ("echo \"/!\\ Can't read main config ! Please check the syntax\";")
+    print ("exit 1")
+    sys.exit()
+
+env=[]
+env.append("PID_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'pid_dir')),''))
+env.append("LOG_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'log_dir')),''))
+env.append("APP_NAME='%s'" % C.APP_NAME)
+env.append("APP_NAME_FILE='%s'" % C.APP_NAME_FILE)
+print ";".join(env)
+PYTHONEND
+`
+APP_NAME="$APP_NAME"
+PID_FILE="$PID_DIR$APP_NAME_FILE.pid"
+LOG_FILE="$LOG_DIR$APP_NAME_FILE.log"
+RUNNING_MSG="$APP_NAME is running"
+NOT_RUNNING_MSG="$APP_NAME is *NOT* running"
+
+# if there is one argument which is "stop", then we kill Libervia
+if [ $# -ge 1 ];then
+    if [ $1 = "stop" ];then
+        kill_process $PID_FILE "$APP_NAME"
+        exit 0
+    elif [ $1 = "debug" ];then
+        echo "Launching $APP_NAME in debug mode"
+        DEBUG="--debug"
+    elif [ $1 = "fg" ];then
+        echo "Launching $APP_NAME in foreground mode"
+        DAEMON="n"
+    elif [ $1 = "status" ];then
+		if [ -f $PID_FILE ]; then
+			PID=`cat $PID_FILE`
+			ps -p$PID 2>&1 > /dev/null
+			if [ $? = 0  ];then
+				echo "$RUNNING_MSG (pid: $PID)"
+				exit 0
+			else
+				echo "$NOT_RUNNING_MSG, but a pid file is present (bad exit ?): $PID_FILE"
+				exit 2
+			fi
+		else
+			echo "$NOT_RUNNING_MSG"
+			exit 1
+		fi
+	else
+		echo "bad argument, please use one of (stop, debug, fg, status) or no argument"
+		exit 1
+    fi
+    shift
+fi
+
+
+#Don't change the next lines
+PLUGIN_OPTIONS=""
+AUTO_OPTIONS=""
+ADDITIONAL_OPTIONS="--pidfile $PID_FILE --logfile $LOG_FILE $AUTO_OPTIONS $DEBUG"
+
+
+MAIN_OPTIONS="-${DAEMON}o"
+
+log_dir=`dirname "$LOG_FILE"`
+if [ ! -d $log_dir ] ; then
+    mkdir $log_dir
+fi
+
+exec $PYTHON $TWISTD $MAIN_OPTIONS $ADDITIONAL_OPTIONS $APP_NAME_FILE $PLUGIN_OPTIONS $@
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/collections.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,149 @@
+#!/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 __len__(self):
+        return len(self.__keys)
+
+    def __setitem__(self, key, value):
+        if key not in self.__keys:
+            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 __contains__(self, key):
+        return key in self.__keys
+
+    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[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(self):
+        raise NotImplementedError
+
+    def viewkeys(self):
+        raise NotImplementedError
+
+    def viewvalues(self):
+        raise NotImplementedError
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/libervia_main.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,709 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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_browser import logging
+logging.configure()
+from sat.core.log import getLogger
+log = getLogger(__name__)
+###
+
+from sat.core.i18n import D_
+
+from sat_frontends.quick_frontend.quick_app import QuickApp
+from sat_frontends.quick_frontend import quick_widgets
+from sat_frontends.quick_frontend import quick_menus
+
+from sat_frontends.tools.misc import InputHistory
+from sat_browser import strings
+from sat_frontends.tools import jid
+from sat_frontends.tools import host_listener
+from sat.core.i18n import _
+
+from pyjamas.ui.RootPanel import RootPanel
+# from pyjamas.ui.HTML import HTML
+from pyjamas.ui.KeyboardListener import KEY_ESCAPE
+from pyjamas.Timer import Timer
+from pyjamas import Window, DOM
+
+from sat_browser import json
+from sat_browser import register
+from sat_browser.contact_list import ContactList
+from sat_browser import main_panel
+# from sat_browser import chat
+from sat_browser import blog
+from sat_browser import xmlui
+from sat_browser import dialog
+from sat_browser import html_tools
+from sat_browser import notification
+from sat_browser import libervia_widget
+from sat_browser import web_widget
+assert web_widget # XXX: just here to avoid pyflakes warning
+
+from sat_browser.constants import Const as C
+
+
+try:
+    # FIXME: import plugin dynamically
+    from sat_browser import plugin_sec_otr
+except ImportError:
+    pass
+
+
+unicode = str  # FIXME: pyjamas workaround
+
+
+# MAX_MBLOG_CACHE = 500  # Max microblog entries kept in memories # FIXME
+
+
+class SatWebFrontend(InputHistory, QuickApp):
+    ENCRYPTION_HANDLERS = False  # e2e encryption is handled directly by Libervia,
+                                 # not backend
+
+    def onModuleLoad(self):
+        log.info("============ onModuleLoad ==============")
+        self.bridge_signals = json.BridgeSignals(self)
+        QuickApp.__init__(self, json.BridgeCall, xmlui=xmlui, connect_bridge=False)
+        self.connectBridge()
+        self._profile_plugged = False
+        self.signals_cache[C.PROF_KEY_NONE] = []
+        self.panel = main_panel.MainPanel(self)
+        self.tab_panel = self.panel.tab_panel
+        self.tab_panel.addTabListener(self)
+        self._register_box = None
+        RootPanel().add(self.panel)
+
+        self.alerts_counter = notification.FaviconCounter()
+        self.notification = notification.Notification(self.alerts_counter)
+        DOM.addEventPreview(self)
+        self.importPlugins()
+        self._register = json.RegisterCall()
+        self._register.call('menusGet', self.gotMenus)
+        self._register.call('registerParams', None)
+        self._register.call('getSessionMetadata', self._getSessionMetadataCB)
+        self.initialised = False
+        self.init_cache = []  # used to cache events until initialisation is done
+        self.cached_params = {}
+        self.next_rsm_index = 0
+
+        #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
+
+        self._versions={} # SàT and Libervia versions cache
+
+    @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]
+
+    @property
+    def visible_widgets(self):
+        widgets_panel = self.tab_panel.getCurrentPanel()
+        return [wid for wid in widgets_panel.widgets if isinstance(wid, quick_widgets.QuickWidget)]
+
+    @property
+    def base_location(self):
+        """Return absolute base url of this Libervia instance"""
+        url = Window.getLocation().getHref()
+        if url.endswith(C.LIBERVIA_MAIN_PAGE):
+            url = url[:-len(C.LIBERVIA_MAIN_PAGE)]
+        if url.endswith("/"):
+            url = url[:-1]
+        return url
+
+    @property
+    def sat_version(self):
+        return self._versions["sat"]
+
+    @property
+    def libervia_version(self):
+        return self._versions["libervia"]
+
+    def getVersions(self, callback=None):
+        """Ask libervia server for SàT and Libervia version and fill local cache
+
+        @param callback: method to call when both versions have been received
+        """
+        def gotVersion():
+            if len(self._versions) == 2 and callback is not None:
+                callback()
+
+        if len(self._versions) == 2:
+            # we already have versions in cache
+            gotVersion()
+            return
+
+        def gotSat(version):
+            self._versions["sat"] = version
+            gotVersion()
+
+        def gotLibervia(version):
+            self._versions["libervia"] = version
+            gotVersion()
+
+        self.bridge.getVersion(callback=gotSat, profile=None)
+        self.bridge.getLiberviaVersion(callback=gotLibervia, profile=None) # XXX: bridge direct call expect a profile, even for method with no profile needed
+
+    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_signal(functionName, callback, with_profile=with_profile)
+
+    def importPlugins(self):
+        self.plugins = {}
+        try:
+            self.plugins['otr'] = plugin_sec_otr.OTR(self)
+        except TypeError:  # plugin_sec_otr has not been imported
+            pass
+
+    def getSelected(self):
+        wid = self.tab_panel.getCurrentPanel()
+        if not isinstance(wid, libervia_widget.WidgetsPanel):
+            log.error("Tab widget is not a WidgetsPanel, can't get selected widget")
+            return None
+        return wid.selected
+
+    def setSelected(self, widget):
+        """Define the selected widget"""
+        widgets_panel = self.tab_panel.getCurrentPanel()
+        if not isinstance(widgets_panel, libervia_widget.WidgetsPanel):
+            return
+
+        selected = widgets_panel.selected
+
+        if selected == widget:
+            return
+
+        if selected:
+            selected.removeStyleName('selected_widget')
+
+        # FIXME: check that widget is in the current WidgetsPanel
+        widgets_panel.selected = widget
+        self.selected_widget = widget
+
+        if widget:
+            widgets_panel.selected.addStyleName('selected_widget')
+
+    def resize(self):
+        """Resize elements"""
+        Window.onResize()
+
+    def onBeforeTabSelected(self, sender, tab_index):
+        return True
+
+    def onTabSelected(self, sender, tab_index):
+        pass
+    # def onTabSelected(self, sender, tab_index):
+    #     for widget in self.tab_panel.getCurrentPanel().widgets:
+    #         if isinstance(widget, chat.Chat):
+    #             clist = self.contact_list
+    #             clist.removeAlerts(widget.current_target, True)
+
+    def onEventPreview(self, event):
+        if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE:
+            #needed to prevent request cancellation in Firefox
+            event.preventDefault()
+        return True
+
+    def getAvatarURL(self, jid_):
+        """Return avatar of a jid if in cache, else ask for it.
+
+        @param jid_ (jid.JID): JID of the contact
+        @return: the URL to the avatar (unicode)
+        """
+        return self.getAvatar(jid_) or self.getDefaultAvatar()
+
+    def getDefaultAvatar(self):
+        return C.DEFAULT_AVATAR_URL
+
+    def registerWidget(self, wid):
+        log.debug(u"Registering %s" % wid.getDebugName())
+        self.libervia_widgets.add(wid)
+
+    def unregisterWidget(self, wid):
+        try:
+            self.libervia_widgets.remove(wid)
+        except KeyError:
+            log.warning(u'trying to remove a non registered Widget: %s' % wid.getDebugName())
+
+    def refresh(self):
+        """Refresh the general display."""
+        self.contact_list.refresh()
+        for lib_wid in self.libervia_widgets:
+            lib_wid.refresh()
+        self.resize()
+
+    def addWidget(self, wid, tab_index=None):
+        """ Add a widget at the bottom of the current or specified tab
+
+        @param wid: LiberviaWidget to add
+        @param tab_index: index of the tab to add the widget to
+        """
+        if tab_index is None or tab_index < 0 or tab_index >= self.tab_panel.getWidgetCount():
+            panel = self.tab_panel.getCurrentPanel()
+        else:
+            panel = self.tab_panel.deck.getWidget(tab_index)
+        panel.addWidget(wid)
+
+    def gotMenus(self, backend_menus):
+        """Put the menus data in cache and build the main menu bar
+
+        @param backend_menus (list[tuple]): menu data from backend
+        """
+        main_menu = self.panel.menu # most of global menu callbacks are in main_menu
+
+        # Categories (with icons)
+        self.menus.addCategory(C.MENU_GLOBAL, [D_(u"General")], extra={'icon': 'home'})
+        self.menus.addCategory(C.MENU_GLOBAL, [D_(u"Contacts")], extra={'icon': 'social'})
+        self.menus.addCategory(C.MENU_GLOBAL, [D_(u"Groups")], extra={'icon': 'social'})
+        #self.menus.addCategory(C.MENU_GLOBAL, [D_(u"Games")], extra={'icon': 'games'})
+
+        # menus to have before backend menus
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Groups"), D_(u"Discussion")), callback=main_menu.onJoinRoom)
+
+        # menus added by the backend/plugins (include other types than C.MENU_GLOBAL)
+        self.menus.addMenus(backend_menus, top_extra={'icon': 'plugins'})
+
+        # menus to have under backend menus
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Contacts"), D_(u"Manage contact groups")), callback=main_menu.onManageContactGroups)
+
+        # separator and right hand menus
+        self.menus.addMenuItem(C.MENU_GLOBAL, [], quick_menus.MenuSeparator())
+
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Help"), D_("Official chat room")), top_extra={'icon': 'help'}, callback=main_menu.onOfficialChatRoom)
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Help"), D_("Social contract")), top_extra={'icon': 'help'}, callback=main_menu.onSocialContract)
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Help"), D_("About")), callback=main_menu.onAbout)
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Settings"), D_("Account")), top_extra={'icon': 'settings'}, callback=main_menu.onAccount)
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Settings"), D_("Parameters")), callback=main_menu.onParameters)
+        # XXX: temporary, will change when a full profile will be managed in SàT
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Settings"), D_("Upload avatar")), callback=main_menu.onAvatarUpload)
+
+        # we call listener to have menu added by local classes/plugins
+        self.callListeners('gotMenus')  # FIXME: to be done another way or moved to quick_app
+
+        # and finally the menus which must appear at the bottom
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"General"), D_(u"Disconnect")), callback=main_menu.onDisconnect)
+
+        # we can now display all the menus
+        main_menu.update(C.MENU_GLOBAL)
+
+        # XXX: temp, will be reworked in the backed static blog plugin
+        self.menus.addMenu(C.MENU_JID_CONTEXT, (D_(u"User"), D_("Public blog")), callback=main_menu.onPublicBlog)
+
+    def removeListener(self, type_, callback):
+        """Remove a callback from listeners
+
+        @param type_: same as for [addListener]
+        @param callback: callback to remove
+        """
+        # FIXME: workaround for pyjamas
+        #        check KeyError issue
+        assert type_ in C.LISTENERS
+        try:
+            self._listeners[type_].pop(callback)
+        except KeyError:
+            pass
+
+    def _getSessionMetadataCB(self, metadata):
+        if not metadata['plugged']:
+            warning = metadata.get("warning")
+            self.panel.setStyleAttribute("opacity", "0.25")  # set background transparency
+            self._register_box = register.RegisterBox(self.logged, metadata)
+            self._register_box.centerBox()
+            self._register_box.show()
+            if warning:
+                dialog.InfoDialog(_('Security warning'), warning).show()
+            self._tryAutoConnect(skip_validation=not not warning)
+        else:
+            self._register.call('isConnected', self._isConnectedCB)
+
+    def _isConnectedCB(self, connected):
+        if not connected:
+            self._register.call('connect', lambda x: self.logged())
+        else:
+            self.logged()
+
+    def logged(self):
+        self.panel.setStyleAttribute("opacity", "1")  # background becomes foreground
+        if self._register_box:
+            self._register_box.hide()
+            del self._register_box  # don't work if self._register_box is None
+
+        # display the presence status panel and tab bar
+        self.presence_status_panel = main_panel.PresenceStatusPanel(self)
+        self.panel.addPresenceStatusPanel(self.presence_status_panel)
+        self.panel.tab_panel.getTabBar().setVisible(True)
+
+        self.bridge_signals.getSignals(callback=self.bridge_signals.signalHandler, profile=None)
+
+        def domain_cb(value):
+            self._defaultDomain = value
+            log.info(u"new account domain: %s" % value)
+
+        def domain_eb(value):
+            self._defaultDomain = "libervia.org"
+
+        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.
+
+    def profilePlugged(self, dummy):
+        self._profile_plugged = True
+        QuickApp.profilePlugged(self, C.PROF_KEY_NONE)
+        contact_list = self.widgets.getOrCreateWidget(ContactList, None, on_new_widget=None, profile=C.PROF_KEY_NONE)
+        self.contact_list_widget = contact_list
+        self.panel.addContactList(contact_list)
+
+        # FIXME: the contact list height has to be set manually the first time
+        self.resize()
+
+        # XXX: as contact_list.update() is slow and it's called a lot of time
+        #      during profile plugging, we prevent it before it's plugged
+        #      and do all at once now
+        contact_list.update()
+
+        try:
+            self.mblog_available = C.bool(self.features['XEP-0277']['available'])
+        except KeyError:
+            self.mblog_available = False
+
+        try:
+            self.groupblog_available = C.bool(self.features['GROUPBLOG']['available'])
+        except KeyError:
+            self.groupblog_available = False
+
+        blog_widget = self.displayWidget(blog.Blog, ())
+        self.setSelected(blog_widget)
+
+        if self.mblog_available:
+            if not self.groupblog_available:
+                dialog.InfoDialog(_(u"Group blogging not available"), _(u"Your server can manage (micro)blogging, but not fine permissions.<br />You'll only be able to blog publicly.")).show()
+
+        else:
+            dialog.InfoDialog(_(u"Blogging not available"), _(u"Your server can't handle (micro)blogging.<br />You'll be able to see your contacts (micro)blogs, but not to post yourself.")).show()
+
+        # we fill the panels already here
+        # for wid in self.widgets.getWidgets(blog.MicroblogPanel):
+        #     if wid.accept_all():
+        #         self.bridge.getMassiveMblogs('ALL', (), None, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert)
+        #     else:
+        #         self.bridge.getMassiveMblogs('GROUP', list(wid.accepted_groups), None, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert)
+
+        #we ask for our own microblogs:
+        # self.loadOurMainEntries()
+
+        def gotDefaultMUC(default_muc):
+            self.default_muc = default_muc
+        self.bridge.mucGetDefaultService(profile=None, callback=gotDefaultMUC)
+
+    def newWidget(self, wid):
+        log.debug(u"newWidget: {}".format(wid))
+        self.addWidget(wid)
+
+    def newMessageHandler(self, from_jid_s, msg, type_, to_jid_s, extra, profile=C.PROF_KEY_NONE):
+        if type_ == C.MESS_TYPE_HEADLINE:
+            from_jid = jid.JID(from_jid_s)
+            if from_jid.domain == self._defaultDomain:
+                # we display announcement from the server in a dialog for better visibility
+                try:
+                    title = extra['subject']
+                except KeyError:
+                    title = _('Announcement from %s') % from_jid
+                msg = strings.addURLToText(html_tools.XHTML2Text(msg))
+                dialog.InfoDialog(title, msg).show()
+                return
+        QuickApp.newMessageHandler(self, from_jid_s, msg, type_, to_jid_s, extra, profile)
+
+    def disconnectedHandler(self, profile):
+        QuickApp.disconnectedHandler(self, profile)
+        Window.getLocation().reload()
+
+    def setPresenceStatus(self, show='', status=None, profile=C.PROF_KEY_NONE):
+        self.presence_status_panel.setPresence(show)
+        if status is not None:
+            self.presence_status_panel.setStatus(status)
+
+    def _tryAutoConnect(self, skip_validation=False):
+        """This method retrieve the eventual URL parameters to auto-connect the user.
+        @param skip_validation: if True, set the form values but do not validate it
+        """
+        params = strings.getURLParams(Window.getLocation().getSearch())
+        if "login" in params:
+            self._register_box._form.right_side.showStack(0)
+            self._register_box._form.login_box.setText(params["login"])
+            self._register_box._form.login_pass_box.setFocus(True)
+            if "passwd" in params:
+                # try to connect
+                self._register_box._form.login_pass_box.setText(params["passwd"])
+                if not skip_validation:
+                    self._register_box._form.onLogin(None)
+                return True
+            else:
+                # this would eventually set the browser saved password
+                Timer(5, lambda: self._register_box._form.login_pass_box.setFocus(True))
+
+    def _actionManagerUnknownError(self):
+        dialog.InfoDialog("Error",
+                          "Unmanaged action result", Width="400px").center()
+
+    # def _ownBlogsFills(self, mblogs, mblog_panel=None):
+    #     """Put our own microblogs in cache, then fill the panels with them.
+
+    #     @param mblogs (dict): dictionary mapping a publisher JID to blogs data.
+    #     @param mblog_panel (MicroblogPanel): the panel to fill, or all if None.
+    #     """
+    #     cache = []
+    #     for publisher in mblogs:
+    #         for mblog in mblogs[publisher][0]:
+    #             if 'content' not in mblog:
+    #                 log.warning(u"No content found in microblog [%s]" % mblog)
+    #                 continue
+    #             if 'groups' in mblog:
+    #                 _groups = set(mblog['groups'].split() if mblog['groups'] else [])
+    #             else:
+    #                 _groups = None
+    #             mblog_entry = blog.MicroblogItem(mblog)
+    #             cache.append((_groups, mblog_entry))
+
+    #     self.mblog_cache.extend(cache)
+    #     if len(self.mblog_cache) > MAX_MBLOG_CACHE:
+    #         del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)]
+
+    #     widget_list = [mblog_panel] if mblog_panel else self.widgets.getWidgets(blog.MicroblogPanel)
+
+    #     for wid in widget_list:
+    #         self.fillMicroblogPanel(wid, cache)
+
+    #     # FIXME
+
+    #     if self.initialised:
+    #         return
+    #     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.personalEventHandler(*event_data)
+    #     del self.init_cache
+
+    # def loadOurMainEntries(self, index=0, mblog_panel=None):
+    #     """Load a page of our own blogs from the cache or ask them to the
+    #     backend. Then fill the panels with them.
+
+    #     @param index (int): starting index of the blog page to retrieve.
+    #     @param mblog_panel (MicroblogPanel): the panel to fill, or all if None.
+    #     """
+    #     delta = index - self.next_rsm_index
+    #     if delta < 0:
+    #         assert mblog_panel is not None
+    #         self.fillMicroblogPanel(mblog_panel, self.mblog_cache[index:index + C.RSM_MAX_ITEMS])
+    #         return
+
+    #     def cb(result):
+    #         self._ownBlogsFills(result, mblog_panel)
+
+    #     rsm = {'max_': str(delta + C.RSM_MAX_ITEMS), 'index': str(self.next_rsm_index)}
+    #     self.bridge.getMassiveMblogs('JID', [unicode(self.whoami.bare)], rsm, callback=cb, profile=C.PROF_KEY_NONE)
+    #     self.next_rsm_index = index + C.RSM_MAX_ITEMS
+
+    ## Signals callbacks ##
+
+    # def personalEventHandler(self, sender, event_type, data):
+        # elif event_type == 'MICROBLOG_DELETE':
+        #     for wid in self.widgets.getWidgets(blog.MicroblogPanel):
+        #         wid.removeEntry(data['type'], data['id'])
+
+        #     if sender == self.whoami.bare and data['type'] == 'main_item':
+        #         for index in xrange(0, len(self.mblog_cache)):
+        #             entry = self.mblog_cache[index]
+        #             if entry[1].id == data['id']:
+        #                 self.mblog_cache.remove(entry)
+        #                 break
+
+    # def fillMicroblogPanel(self, mblog_panel, mblogs):
+    #     """Fill a microblog panel with entries in cache
+
+    #     @param mblog_panel: MicroblogPanel instance
+    #     """
+    #     #XXX: only our own entries are cached
+    #     for cache_entry in mblogs:
+    #         _groups, mblog_entry = cache_entry
+    #         mblog_panel.addEntryIfAccepted(self.whoami.bare, *cache_entry)
+
+    # def getEntityMBlog(self, entity):
+    #     # FIXME: call this after a contact has been added to roster
+    #     log.info(u"geting mblog for entity [%s]" % (entity,))
+    #     for lib_wid in self.libervia_widgets:
+    #         if isinstance(lib_wid, blog.MicroblogPanel):
+    #             if lib_wid.isJidAccepted(entity):
+    #                 self.bridge.call('getMassiveMblogs', lib_wid.massiveInsert, 'JID', [unicode(entity)])
+
+    def displayWidget(self, class_, target, dropped=False, new_tab=None, *args, **kwargs):
+        """Get or create a LiberviaWidget and select it. When the user dropped
+        something, a new widget is always created, otherwise we look for an
+        existing widget and re-use it if it's in the current tab.
+
+        @arg class_(class): see quick_widgets.getOrCreateWidget
+        @arg target: see quick_widgets.getOrCreateWidget
+        @arg dropped(bool): if True, assume the widget has been dropped
+        @arg new_tab(unicode): if not None, it holds the name of a new tab to
+            open for the widget. If None, use the default behavior.
+        @param args(list): optional args to create a new instance of class_
+        @param kwargs(list): optional kwargs to create a new instance of class_
+        @return: the widget
+        """
+        kwargs['profile'] = C.PROF_KEY_NONE
+
+        if dropped:
+            kwargs['on_new_widget'] = None
+            kwargs['on_existing_widget'] = C.WIDGET_RECREATE
+            wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs)
+            self.setSelected(wid)
+            return wid
+
+        if new_tab:
+            kwargs['on_new_widget'] = None
+            kwargs['on_existing_widget'] = C.WIDGET_RECREATE
+            wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs)
+            self.tab_panel.addWidgetsTab(new_tab)
+            self.addWidget(wid, tab_index=self.tab_panel.getWidgetCount() - 1)
+            return wid
+
+        kwargs['on_existing_widget'] = C.WIDGET_RAISE
+        try:
+            wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs)
+        except quick_widgets.WidgetAlreadyExistsError:
+            kwargs['on_existing_widget'] = C.WIDGET_KEEP
+            wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs)
+            widgets_panel = wid.getParent(libervia_widget.WidgetsPanel, expect=False)
+            if widgets_panel is None:
+                # The widget exists but is hidden
+                self.addWidget(wid)
+            elif widgets_panel != self.tab_panel.getCurrentPanel():
+                # the widget is on an other tab, so we add a new one here
+                kwargs['on_existing_widget'] = C.WIDGET_RECREATE
+                wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs)
+                self.addWidget(wid)
+        self.setSelected(wid)
+        return wid
+
+    def isHidden(self):
+        """Tells if the frontend window is hidden.
+
+        @return bool
+        """
+        return self.notification.isHidden()
+
+    def updateAlertsCounter(self, extra_inc=0):
+        """Update the over whole alerts counter
+
+        @param extra_inc (int): extra counter
+        """
+        extra = self.alerts_counter.extra + extra_inc
+        self.alerts_counter.update(self.alerts_count, extra=extra)
+
+    def _paramUpdate(self, name, value, category, refresh=True):
+        """This is called when the paramUpdate signal is received, but also
+        during initialization when the UI parameters values are retrieved.
+        @param refresh: set to True to refresh the general UI
+        """
+        for param_cat, param_name in C.CACHED_PARAMS:
+            if name == param_name and category == param_cat:
+                self.cached_params[(category, name)] = value
+                if refresh:
+                    self.refresh()
+                break
+
+    def getCachedParam(self, category, name):
+        """Return a parameter cached value (e.g for refreshing the UI)
+
+        @param category (unicode): the parameter category
+        @pram name (unicode): the parameter name
+        """
+        return self.cached_params[(category, name)] if (category, name) in self.cached_params else None
+
+    def sendError(self, errorData):
+        dialog.InfoDialog("Error while sending message",
+                          "Your message can't be sent", Width="400px").center()
+        log.error("sendError: %s" % unicode(errorData))
+
+    def showWarning(self, type_=None, msg=None):
+        """Display a popup information message, e.g. to notify the recipient of a message being composed.
+        If type_ is None, a popup being currently displayed will be hidden.
+        @type_: a type determining the CSS style to be applied (see WarningPopup.showWarning)
+        @msg: message to be displayed
+        """
+        if not hasattr(self, "warning_popup"):
+            self.warning_popup = main_panel.WarningPopup()
+        self.warning_popup.showWarning(type_, msg)
+
+    def showDialog(self, message, title="", type_="info", answer_cb=None, answer_data=None):
+        if type_ == 'info':
+            popup = dialog.InfoDialog(unicode(title), unicode(message), callback=answer_cb)
+        elif type_ == 'error':
+            popup = dialog.InfoDialog(unicode(title), unicode(message), callback=answer_cb)
+        elif type_ == 'yes/no':
+            popup = dialog.ConfirmDialog(lambda answer: answer_cb(answer, answer_data),
+                                         text=unicode(message), title=unicode(title))
+            popup.cancel_button.setText(_("No"))
+        else:
+            popup = dialog.InfoDialog(unicode(title), unicode(message), callback=answer_cb)
+            log.error(_('unmanaged dialog type: %s'), type_)
+        popup.show()
+
+    def dialogFailure(self, failure):
+        dialog.InfoDialog("Error",
+                          unicode(failure), Width="400px").center()
+
+    def showFailure(self, err_data, msg=''):
+        """Show a failure that has been returned by an asynchronous bridge method.
+
+        @param failure (defer.Failure): Failure instance
+        @param msg (unicode): message to display
+        """
+        # FIXME: message is lost by JSON, we hardcode it for now... remove msg argument when possible
+        err_code, err_obj = err_data
+        title = err_obj['message']['faultString'] if isinstance(err_obj['message'], dict) else err_obj['message']
+        self.showDialog(msg, title, 'error')
+
+    def onJoinMUCFailure(self, err_data):
+        """Show a failure that has been returned when trying to join a room.
+
+        @param failure (defer.Failure): Failure instance
+        """
+        # FIXME: remove asap, see self.showFailure
+        err_code, err_obj = err_data
+        if err_obj["data"] == "AlreadyJoinedRoom":
+            msg = _(u"The room has already been joined.")
+            err_obj["message"] = _(u"Information")
+        else:
+            msg = _(u"Invalid room identifier. Please give a room short or full identifier like 'room' or '%s'.") % self.default_muc
+            err_obj["message"] = _(u"Error")
+        self.showFailure(err_data, msg)
+
+
+if __name__ == '__main__':
+    app = SatWebFrontend()
+    app.onModuleLoad()
+    host_listener.callListeners(app)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/libervia_test.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,78 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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/>.
+
+
+# Just visit <root_url>/test. If you don't get any AssertError pop-up,
+# everything is fine. #TODO: nicely display the results in HTML output.
+
+
+### logging configuration ###
+from sat_browser import logging
+logging.configure()
+from sat.core.log import getLogger
+log = getLogger(__name__)
+###
+
+from sat_frontends.tools import jid
+from sat_browser import contact_list
+
+
+def test_JID():
+    """Check that the JID class reproduces the Twisted behavior"""
+    j1 = jid.JID("t1@test.org")
+    j1b = jid.JID("t1@test.org")
+    t1 = "t1@test.org"
+
+    assert j1 == j1b
+    assert j1 != t1
+    assert t1 != j1
+    assert hash(j1) == hash(j1b)
+    assert hash(j1) != hash(t1)
+
+
+def test_JIDIterable():
+    """Check that our iterables reproduce the Twisted behavior"""
+
+    j1 = jid.JID("t1@test.org")
+    j1b = jid.JID("t1@test.org")
+    j2 = jid.JID("t2@test.org")
+    t1 = "t1@test.org"
+    t2 = "t2@test.org"
+    jid_set = set([j1, t2])
+    jid_list = contact_list.JIDList([j1, t2])
+    jid_dict = {j1: "dummy 1", t2: "dummy 2"}
+    for iterable in (jid_set, jid_list, jid_dict):
+        log.info("Testing %s" % type(iterable))
+        assert j1 in iterable
+        assert j1b in iterable
+        assert j2 not in iterable
+        assert t1 not in iterable
+        assert t2 in iterable
+
+    # Check that the extra JIDList class is still needed
+    log.info("Testing Pyjamas native list")
+    jid_native_list = ([j1, t2])
+    assert j1 in jid_native_list
+    assert j1b not in jid_native_list  # this is NOT Twisted's behavior
+    assert j2 in jid_native_list  # this is NOT Twisted's behavior
+    assert t1 in jid_native_list  # this is NOT Twisted's behavior
+    assert t2 in jid_native_list
+
+test_JID()
+test_JIDIterable()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/otr.min.js	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,42 @@
+/*
+
+  This file contains minified versions of otr.js, Big Integer Library, CryptoJS, EventEmitter.
+  It is sublicensed under AGPL v3 (or any later version) as allowed by the original licenses.
+
+  Below some information about the authors and the licences of the included libraries:
+
+  Big Integer Library v. 5.5
+  Created 2000, last modified 2013
+  Leemon Baird
+  www.leemon.com
+  Original licence information:
+  // This file is public domain.   You can use it for any purpose without restriction.
+  // I do not guarantee that it is correct, so use it at your own risk.  If you use 
+  // it for something interesting, I'd appreciate hearing about it.  If you find 
+  // any bugs or make any improvements, I'd appreciate hearing about those too.
+  // It would also be nice if my name and URL were left in the comments.  But none 
+  // of that is required.
+
+  CryptoJS v3.1.2
+  code.google.com/p/crypto-js
+  (c) 2009-2013 by Jeff Mott. All rights reserved.
+  code.google.com/p/crypto-js/wiki/License (MIT licence)
+
+  EventEmitter v4.2.3 - git.io/ee
+  Oliver Caldwell
+  MIT license
+
+  otr.js v0.2.12 - 2014-04-15
+  (c) 2014 - Arlo Breault <arlolra@gmail.com>
+  Freely distributed under the MPL v2.0 license.
+
+*/
+
+if (typeof crypto !== 'undefined' && (typeof crypto.randomBytes === 'function' || typeof crypto.getRandomValues === 'function')) {
+
+(function(a,b){if(typeof define==="function"&&define.amd){define(b.bind(a,a.crypto||a.msCrypto))}else{if(typeof module!=="undefined"&&module.exports){module.exports=b(require("crypto"))}else{a.BigInt=b(a.crypto||a.msCrypto)}}}(this,function(v){var x=26;var E=1<<x;var aw=E-1;var bd="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_=!@#$%^&*()[]{}|;:,.<>/?`~ \\'\"+-";var o=a0(1,1,1);var W=new Array(0);var aT=W;var n=W;var k=W;var j=W;var i=W;var h=W,g=W;var f=W;var e=W;var ar=W;var a2=W;var aR=W,F=W,R=W;var S=W,V=W,aG=W,aE=W,aC=W,aA=W;var ah=W,ag=W,af=W,aL=W,P=W,O=W,aH=W;var N=W,al=W,aa=W,aQ=W,ao=W,bf=W,U=W,a8=W;var ae=W,H=W,X=W,ad=W,ab=W,M=W,L=W,aW=W;var a7=W;function ba(bl){var T,bj,bk,t;bj=new Array(bl);for(T=0;T<bl;T++){bj[T]=0}bj[0]=2;bk=0;for(;bj[bk]<bl;){for(T=bj[bk]*bj[bk];T<bl;T+=bj[bk]){bj[T]=1}bk++;bj[bk]=bj[bk-1]+1;for(;bj[bk]<bl&&bj[bj[bk]];bj[bk]++){}}t=new Array(bk);for(T=0;T<bk;T++){t[T]=bj[T]}return t}function l(T,t){if(aR.length!=T.length){aR=K(T);F=K(T);R=K(T)}y(R,t);return aN(T,R)}function aN(T,t){var bl,bk,bj,bm;if(aR.length!=T.length){aR=K(T);F=K(T);R=K(T)}ax(R,t);ax(F,T);ax(aR,T);at(F,-1);at(aR,-1);if(aZ(F)){return 0}for(bj=0;F[bj]==0;bj++){}for(bl=1,bk=2;F[bj]%bk==0;bk*=2,bl++){}bm=bj*x+bl-1;if(bm){G(F,bm)}aS(R,F,T);if(!z(R,1)&&!Q(R,aR)){bk=1;while(bk<=bm-1&&!Q(R,aR)){aJ(R,T);if(z(R,1)){return 0}bk++}if(!Q(R,aR)){return 0}}return 1}function ay(t){var bj,bk,T;for(bj=t.length-1;(t[bj]==0)&&(bj>0);bj--){}for(bk=0,T=t[bj];T;(T>>=1),bk++){}bk+=x*bj;return bk}function aD(t,bj){var T=a0(0,(t.length>bj?t.length:bj)*x,0);ax(T,t);return T}function ak(T){var t=a0(0,T,0);aq(t,T);return a4(t,1)}function D(t){if(t>=600){return q(t,2)}if(t>=550){return q(t,4)}if(t>=500){return q(t,5)}if(t>=400){return q(t,6)}if(t>=350){return q(t,7)}if(t>=300){return q(t,9)}if(t>=250){return q(t,12)}if(t>=200){return q(t,15)}if(t>=150){return q(t,18)}if(t>=100){return q(t,27)}return q(t,40)}function q(bj,bm){var T,bk,t,bl;bl=30000;T=a0(0,bj,0);if(N.length==0){N=ba(30000)}if(a7.length!=T.length){a7=K(T)}for(;;){a1(T,bj,0);T[0]|=1;t=0;for(bk=0;(bk<N.length)&&(N[bk]<=bl);bk++){if(be(T,N[bk])==0&&!z(T,N[bk])){t=1;break}}for(bk=0;bk<bm&&!t;bk++){a1(a7,bj,0);while(!B(T,a7)){a1(a7,bj,0)}if(!aN(T,a7)){t=1}}if(!t){return T}}}function b(t,bj){var T=K(t);aB(T,bj);return a4(T,1)}function p(t,bj){var T=aD(t,t.length+1);at(T,bj);return a4(T,1)}function d(t,bj){var T=aD(t,t.length+bj.length);aY(T,bj);return a4(T,1)}function aP(t,bk,bj){var T=aD(t,bj.length);aS(T,a4(bk,2),a4(bj,2),0);return a4(T,1)}function a5(t,bj){var T=aD(t,(t.length>bj.length?t.length+1:bj.length+1));bc(T,bj);return a4(T,1)}function w(t,bj){var T=aD(t,(t.length>bj.length?t.length+1:bj.length+1));aI(T,bj);return a4(T,1)}function bg(t,bk){var T=aD(t,bk.length);var bj;bj=Z(T,bk);return bj?a4(T,1):null}function an(t,bk,bj){var T=aD(t,bj.length);aU(T,bk,bj);return a4(T,1)}function aq(bt,bm){var br,bu,bk,bl,bv,bn,t,T,bp,bq,bj,bo,bs;if(N.length==0){N=ba(30000)}if(al.length==0){al=new Array(512);for(bn=0;bn<512;bn++){al[bn]=Math.pow(2,bn/511-1)}}br=0.1;bk=20;bs=20;if(aQ.length!=bt.length){aQ=K(bt);ao=K(bt);a8=K(bt);H=K(bt);ab=K(bt);M=K(bt);L=K(bt);ad=K(bt);X=K(bt);aa=K(bt);bf=K(bt);U=K(bt);ae=K(bt);aW=K(bt)}if(bm<=bs){bl=(1<<((bm+2)>>1))-1;y(bt,0);for(bv=1;bv;){bv=0;bt[0]=1|(1<<(bm-1))|aV(bm);for(bn=1;(bn<N.length)&&((N[bn]&bl)==N[bn]);bn++){if(0==(bt[0]%N[bn])){bv=1;break}}}Y(bt);return}T=br*bm*bm;if(bm>2*bk){for(t=1;bm-bm*t<=bk;){t=al[aV(9)]}}else{t=0.5}bo=Math.floor(t*bm)+1;aq(U,bo);y(aQ,0);aQ[Math.floor((bm-2)/x)]|=(1<<((bm-2)%x));c(aQ,U,aa,bf);bq=ay(aa);for(;;){for(;;){a1(ao,bq,0);if(B(aa,ao)){break}}at(ao,1);aI(ao,aa);ax(X,U);aY(X,ao);m(X,2);at(X,1);ax(H,ao);m(H,2);for(bp=0,bn=0;(bn<N.length)&&(N[bn]<T);bn++){if(be(X,N[bn])==0&&!z(X,N[bn])){bp=1;break}}if(!bp){if(!l(X,2)){bp=1}}if(!bp){at(X,-3);for(bn=X.length-1;(X[bn]==0)&&(bn>0);bn--){}for(bj=0,bu=X[bn];bu;(bu>>=1),bj++){}bj+=x*bn;for(;;){a1(ae,bj,0);if(B(X,ae)){break}}at(X,3);at(ae,2);ax(ad,ae);ax(a8,X);at(a8,-1);aS(ad,a8,X);at(ad,-1);if(aZ(ad)){ax(ad,ae);aS(ad,H,X);at(ad,-1);ax(aW,X);ax(ab,ad);A(ab,X);if(z(ab,1)){ax(bt,aW);return}}}}}function aO(bk,bj){var T,t;T=Math.floor((bk-1)/x)+2;t=a0(0,0,T);a1(t,bk,bj);return t}function a1(t,bl,bk){var bj,T;for(bj=0;bj<t.length;bj++){t[bj]=0}T=Math.floor((bl-1)/x)+1;for(bj=0;bj<T;bj++){t[bj]=aV(x)}t[T-1]&=(2<<((bl-1)%x))-1;if(bk==1){t[T-1]|=(1<<((bl-1)%x))}}function ap(t,bk){var T,bj;T=K(t);bj=K(bk);A(T,bj);return T}function A(br,bq){var bp,bo,T,bn,bm,bl,t,bk,bj,bs;if(ar.length!=br.length){ar=K(br)}bj=1;while(bj){bj=0;for(bp=1;bp<bq.length;bp++){if(bq[bp]){bj=1;break}}if(!bj){break}for(bp=br.length;!br[bp]&&bp>=0;bp--){}bo=br[bp];T=bq[bp];bn=1;bm=0;bl=0;t=1;while((T+bl)&&(T+t)){bk=Math.floor((bo+bn)/(T+bl));bs=Math.floor((bo+bm)/(T+t));if(bk!=bs){break}W=bn-bk*bl;bn=bl;bl=W;W=bm-bk*t;bm=t;t=W;W=bo-bk*T;bo=T;T=W}if(bm){ax(ar,br);J(br,bq,bn,bm);J(bq,ar,t,bl)}else{aB(br,bq);ax(ar,br);ax(br,bq);ax(bq,ar)}}if(bq[0]==0){return}W=be(br,bq[0]);y(br,bq[0]);bq[0]=W;while(bq[0]){br[0]%=bq[0];W=br[0];br[0]=bq[0];bq[0]=W}}function Z(t,bj){var T=1+2*Math.max(t.length,bj.length);if(!(t[0]&1)&&!(bj[0]&1)){y(t,0);return 0}if(V.length!=T){V=new Array(T);S=new Array(T);aG=new Array(T);aE=new Array(T);aC=new Array(T);aA=new Array(T)}ax(V,t);ax(S,bj);y(aG,1);y(aE,0);y(aC,0);y(aA,1);for(;;){while(!(V[0]&1)){s(V);if(!(aG[0]&1)&&!(aE[0]&1)){s(aG);s(aE)}else{aI(aG,bj);s(aG);bc(aE,t);s(aE)}}while(!(S[0]&1)){s(S);if(!(aC[0]&1)&&!(aA[0]&1)){s(aC);s(aA)}else{aI(aC,bj);s(aC);bc(aA,t);s(aA)}}if(!B(S,V)){bc(V,S);bc(aG,aC);bc(aE,aA)}else{bc(S,V);bc(aC,aG);bc(aA,aE)}if(z(V,0)){while(r(aC)){aI(aC,bj)}ax(t,aC);if(!z(S,1)){y(t,0);return 0}return 1}}}function aX(bj,bm){var bk=1,T=0,bl;for(;;){if(bj==1){return bk}if(bj==0){return 0}T-=bk*Math.floor(bm/bj);bm%=bj;if(bm==1){return T}if(bm==0){return 0}bk-=T*Math.floor(bj/bm);bj%=bm}}function C(t,T){return aX(t,T)}function u(T,bn,bl,bj,t){var bm=0;var bk=Math.max(T.length,bn.length);if(V.length!=bk){V=new Array(bk);aG=new Array(bk);aE=new Array(bk);aC=new Array(bk);aA=new Array(bk)}while(!(T[0]&1)&&!(bn[0]&1)){s(T);s(bn);bm++}ax(V,T);ax(bl,bn);y(aG,1);y(aE,0);y(aC,0);y(aA,1);for(;;){while(!(V[0]&1)){s(V);if(!(aG[0]&1)&&!(aE[0]&1)){s(aG);s(aE)}else{aI(aG,bn);s(aG);bc(aE,T);s(aE)}}while(!(bl[0]&1)){s(bl);if(!(aC[0]&1)&&!(aA[0]&1)){s(aC);s(aA)}else{aI(aC,bn);s(aC);bc(aA,T);s(aA)}}if(!B(bl,V)){bc(V,bl);bc(aG,aC);bc(aE,aA)}else{bc(bl,V);bc(aC,aG);bc(aA,aE)}if(z(V,0)){while(r(aC)){aI(aC,bn);bc(aA,T)}m(aA,-1);ax(bj,aC);ax(t,aA);bi(bl,bm);return}}}function r(t){return((t[t.length-1]>>(x-1))&1)}function a(t,bn,T){var bl,bm=t.length,bk=bn.length;var bj=((bm+T)<bk)?(bm+T):bk;for(bl=bk-1-T;bl<bm&&bl>=0;bl++){if(t[bl]>0){return 1}}for(bl=bm-1+T;bl<bk;bl++){if(bn[bl]>0){return 0}}for(bl=bj-1;bl>=T;bl--){if(t[bl-T]>bn[bl]){return 1}else{if(t[bl-T]<bn[bl]){return 0}}}return 0}function B(t,bk){var bj;var T=(t.length<bk.length)?t.length:bk.length;for(bj=t.length;bj<bk.length;bj++){if(bk[bj]){return 0}}for(bj=bk.length;bj<t.length;bj++){if(t[bj]){return 1}}for(bj=T-1;bj>=0;bj--){if(t[bj]>bk[bj]){return 1}else{if(t[bj]<bk[bj]){return 0}}}return 0}function c(bt,bq,T,t){var bm,bl;var bk,bj,bs,bp,bn,br,bo;ax(t,bt);for(bl=bq.length;bq[bl-1]==0;bl--){}bo=bq[bl-1];for(br=0;bo;br++){bo>>=1}br=x-br;bi(bq,br);bi(t,br);for(bm=t.length;t[bm-1]==0&&bm>bl;bm--){}y(T,0);while(!a(bq,t,bm-bl)){az(t,bq,bm-bl);T[bm-bl]++}for(bk=bm-1;bk>=bl;bk--){if(t[bk]==bq[bl-1]){T[bk-bl]=aw}else{T[bk-bl]=Math.floor((t[bk]*E+t[bk-1])/bq[bl-1])}for(;;){bp=(bl>1?bq[bl-2]:0)*T[bk-bl];bn=bp;bp=bp&aw;bn=(bn-bp)/E;bs=bn+T[bk-bl]*bq[bl-1];bn=bs;bs=bs&aw;bn=(bn-bs)/E;if(bn==t[bk]?bs==t[bk-1]?bp>(bk>1?t[bk-2]:0):bs>t[bk-1]:bn>t[bk]){T[bk-bl]--}else{break}}av(t,bq,-T[bk-bl],bk-bl);if(r(t)){a9(t,bq,bk-bl);T[bk-bl]--}}G(bq,br);G(t,br)}function Y(T){var bk,bj,bl,t;bj=T.length;bl=0;for(bk=0;bk<bj;bk++){bl+=T[bk];t=0;if(bl<0){t=bl&aw;t=-((bl-t)/E);bl+=t*E}T[bk]=bl&aw;bl=((bl-T[bk])/E)-t}}function be(t,bk){var T,bj=0;for(T=t.length-1;T>=0;T--){bj=(bj*E+t[T])%bk}return bj}function a0(bk,bl,bm){var bj,T,bn;T=Math.ceil(bl/x)+1;T=bm>T?bm:T;bn=new Array(T);y(bn,bk);return bn}function a6(bt,bk,bj){var bp,bn,bm,bs,br,T;var bl=bt.length;if(bk==-1){bs=new Array(0);for(;;){br=new Array(bs.length+1);for(bn=0;bn<bs.length;bn++){br[bn+1]=bs[bn]}br[0]=parseInt(bt,10);bs=br;bp=bt.indexOf(",",0);if(bp<1){break}bt=bt.substring(bp+1);if(bt.length==0){break}}if(bs.length<bj){br=new Array(bj);ax(br,bs);return br}return bs}var bo=bk,t=0;var bq=bk==1?bl:0;while(bo>1){if(bo&1){t=1}bq+=bl;bo>>=1}bq+=t*bl;bs=a0(0,bq,0);for(bn=0;bn<bl;bn++){bp=bd.indexOf(bt.substring(bn,bn+1),0);if(bk<=36&&bp>=36){bp-=26}if(bp>=bk||bp<0){break}m(bs,bk);at(bs,bp)}for(bl=bs.length;bl>0&&!bs[bl-1];bl--){}bl=bj>bl+1?bj:bl+1;br=new Array(bl);T=bl<bs.length?bl:bs.length;for(bn=0;bn<T;bn++){br[bn]=bs[bn]}for(;bn<bl;bn++){br[bn]=0}return br}function z(t,bj){var T;if(t[0]!=bj){return 0}for(T=1;T<t.length;T++){if(t[T]){return 0}}return 1}function Q(t,bk){var bj;var T=t.length<bk.length?t.length:bk.length;for(bj=0;bj<T;bj++){if(t[bj]!=bk[bj]){return 0}}if(t.length>bk.length){for(;bj<t.length;bj++){if(t[bj]){return 0}}}else{for(;bj<bk.length;bj++){if(bk[bj]){return 0}}}return 1}function aZ(t){var T;for(T=0;T<t.length;T++){if(t[T]){return 0}}return 1}function aM(T,bm){var bk,bj,bl="";if(f.length!=T.length){f=K(T)}else{ax(f,T)}if(bm==-1){for(bk=T.length-1;bk>0;bk--){bl+=T[bk]+","}bl+=T[0]}else{while(!aZ(f)){bj=ac(f,bm);bl=bd.substring(bj,bj+1)+bl}}if(bl.length==0){bl="0"}return bl}function K(t){var T,bj;bj=new Array(t.length);ax(bj,t);return bj}function ax(t,bk){var bj;var T=t.length<bk.length?t.length:bk.length;for(bj=0;bj<T;bj++){t[bj]=bk[bj]}for(bj=T;bj<t.length;bj++){t[bj]=0}}function y(t,bk){var T,bj;for(bj=bk,T=0;T<t.length;T++){t[T]=bj&aw;bj>>=x}}function at(T,bm){var bk,bj,bl,t;T[0]+=bm;bj=T.length;bl=0;for(bk=0;bk<bj;bk++){bl+=T[bk];t=0;if(bl<0){t=bl&aw;t=-((bl-t)/E);bl+=t*E}T[bk]=bl&aw;bl=((bl-T[bk])/E)-t;if(!bl){return}}}function G(t,bk){var bj;var T=Math.floor(bk/x);if(T){for(bj=0;bj<t.length-T;bj++){t[bj]=t[bj+T]}for(;bj<t.length;bj++){t[bj]=0}bk%=x}for(bj=0;bj<t.length-1;bj++){t[bj]=aw&((t[bj+1]<<(x-bk))|(t[bj]>>bk))}t[bj]>>=bk}function s(t){var T;for(T=0;T<t.length-1;T++){t[T]=aw&((t[T+1]<<(x-1))|(t[T]>>1))}t[T]=(t[T]>>1)|(t[T]&(E>>1))}function bi(t,bk){var bj;var T=Math.floor(bk/x);if(T){for(bj=t.length;bj>=T;bj--){t[bj]=t[bj-T]}for(;bj>=0;bj--){t[bj]=0}bk%=x}if(!bk){return}for(bj=t.length-1;bj>0;bj--){t[bj]=aw&((t[bj]<<bk)|(t[bj-1]>>(x-bk)))}t[bj]=aw&(t[bj]<<bk)}function m(T,bm){var bk,bj,bl,t;if(!bm){return}bj=T.length;bl=0;for(bk=0;bk<bj;bk++){bl+=T[bk]*bm;t=0;if(bl<0){t=bl&aw;t=-((bl-t)/E);bl+=t*E}T[bk]=bl&aw;bl=((bl-T[bk])/E)-t}}function ac(t,bl){var T,bk=0,bj;for(T=t.length-1;T>=0;T--){bj=bk*E+t[T];t[T]=Math.floor(bj/bl);bk=bj%bl}return bk}function J(T,bo,bj,t){var bl,bn,bk,bm;bk=T.length<bo.length?T.length:bo.length;bm=T.length;for(bn=0,bl=0;bl<bk;bl++){bn+=bj*T[bl]+t*bo[bl];T[bl]=bn&aw;bn=(bn-T[bl])/E}for(bl=bk;bl<bm;bl++){bn+=bj*T[bl];T[bl]=bn&aw;bn=(bn-T[bl])/E}}function av(T,bo,t,bl){var bk,bn,bj,bm;bj=T.length<bl+bo.length?T.length:bl+bo.length;bm=T.length;for(bn=0,bk=bl;bk<bj;bk++){bn+=T[bk]+t*bo[bk-bl];T[bk]=bn&aw;bn=(bn-T[bk])/E}for(bk=bj;bn&&bk<bm;bk++){bn+=T[bk];T[bk]=bn&aw;bn=(bn-T[bk])/E}}function a9(t,bn,bk){var bj,bm,T,bl;T=t.length<bk+bn.length?t.length:bk+bn.length;bl=t.length;for(bm=0,bj=bk;bj<T;bj++){bm+=t[bj]+bn[bj-bk];t[bj]=bm&aw;bm=(bm-t[bj])/E}for(bj=T;bm&&bj<bl;bj++){bm+=t[bj];t[bj]=bm&aw;bm=(bm-t[bj])/E}}function az(t,bn,bk){var bj,bm,T,bl;T=t.length<bk+bn.length?t.length:bk+bn.length;bl=t.length;for(bm=0,bj=bk;bj<T;bj++){bm+=t[bj]-bn[bj-bk];t[bj]=bm&aw;bm=(bm-t[bj])/E}for(bj=T;bm&&bj<bl;bj++){bm+=t[bj];t[bj]=bm&aw;bm=(bm-t[bj])/E}}function bc(t,bm){var bj,bl,T,bk;T=t.length<bm.length?t.length:bm.length;for(bl=0,bj=0;bj<T;bj++){bl+=t[bj]-bm[bj];t[bj]=bl&aw;bl=(bl-t[bj])/E}for(bj=T;bl&&bj<t.length;bj++){bl+=t[bj];t[bj]=bl&aw;bl=(bl-t[bj])/E}}function aI(t,bm){var bj,bl,T,bk;T=t.length<bm.length?t.length:bm.length;for(bl=0,bj=0;bj<T;bj++){bl+=t[bj]+bm[bj];t[bj]=bl&aw;bl=(bl-t[bj])/E}for(bj=T;bl&&bj<t.length;bj++){bl+=t[bj];t[bj]=bl&aw;bl=(bl-t[bj])/E}}function aY(t,bj){var T;if(aT.length!=2*t.length){aT=new Array(2*t.length)}y(aT,0);for(T=0;T<bj.length;T++){if(bj[T]){av(aT,t,bj[T],T)}}ax(t,aT)}function aB(t,T){if(h.length!=t.length){h=K(t)}else{ax(h,t)}if(g.length!=t.length){g=K(t)}c(h,T,g,t)}function aU(t,bk,bj){var T;if(n.length!=2*t.length){n=new Array(2*t.length)}y(n,0);for(T=0;T<bk.length;T++){if(bk[T]){av(n,t,bk[T],T)}}aB(n,bj);ax(t,n)}function aJ(bo,t){var bk,bj,bm,bn,bl,bp,T;for(bl=bo.length;bl>0&&!bo[bl-1];bl--){}T=bl>t.length?2*bl:2*t.length;if(n.length!=T){n=new Array(T)}y(n,0);for(bk=0;bk<bl;bk++){bn=n[2*bk]+bo[bk]*bo[bk];n[2*bk]=bn&aw;bn=(bn-n[2*bk])/E;for(bj=bk+1;bj<bl;bj++){bn=n[bk+bj]+2*bo[bk]*bo[bj]+bn;n[bk+bj]=(bn&aw);bn=(bn-n[bk+bj])/E}n[bk+bl]=bn}aB(n,t);ax(bo,n)}function a4(t,T){var bj,bk;for(bj=t.length;bj>0&&!t[bj-1];bj--){}bk=new Array(bj+T);ax(bk,t);return bk}function aS(t,bn,bm){var bl,bk,T,bj;if(e.length!=bm.length){e=K(bm)}if((bm[0]&1)==0){ax(e,t);y(t,1);while(!z(bn,0)){if(bn[0]&1){aU(t,e,bm)}ac(bn,2);aJ(e,bm)}return}y(e,0);for(T=bm.length;T>0&&!bm[T-1];T--){}bj=E-aX(be(bm,E),E);e[T]=1;aU(t,e,bm);if(i.length!=t.length){i=K(t)}else{ax(i,t)}for(bl=bn.length-1;bl>0&!bn[bl];bl--){}if(bn[bl]==0){y(t,1);return}for(bk=1<<(x-1);bk&&!(bn[bl]&bk);bk>>=1){}for(;;){if(!(bk>>=1)){bl--;if(bl<0){au(t,o,bm,bj);return}bk=1<<(x-1)}au(t,t,bm,bj);if(bk&bn[bl]){au(t,i,bm,bj)}}}function au(br,bo,T,bs){var bk,bj,bn,bp,bt,bl,bq;var bu=T.length;var bm=bo.length;if(a2.length!=bu){a2=new Array(bu)}y(a2,0);for(;bu>0&&T[bu-1]==0;bu--){}for(;bm>0&&bo[bm-1]==0;bm--){}bq=a2.length-1;for(bk=0;bk<bu;bk++){bt=a2[0]+br[bk]*bo[0];bp=((bt&aw)*bs)&aw;bn=(bt+bp*T[0]);bn=(bn-(bn&aw))/E;bt=br[bk];bj=1;for(;bj<bm-4;){bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++}for(;bj<bm;){bn+=a2[bj]+bp*T[bj]+bt*bo[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++}for(;bj<bu-4;){bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++;bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++}for(;bj<bu;){bn+=a2[bj]+bp*T[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++}for(;bj<bq;){bn+=a2[bj];bl=a2[bj-1]=bn&aw;bn=(bn-bl)/E;bj++}a2[bj-1]=bn&aw}if(!B(T,a2)){bc(a2,T)}ax(br,a2)}function bb(t,bj,T){return an(t,bg(bj,T),T)}function aj(T,t,bj){T=b(T,bj);t=b(t,bj);if(B(t,T)){T=w(T,bj)}return a5(T,t)}function I(bj){var T=Math.floor(bj/x)+2;var bl=new Array(T);for(var bk=0;bk<T;bk++){bl[bk]=0}bl[T-2]=1<<(bj%x);return bl}var bh=(function(){var t=0,T={};for(;t<256;++t){T[t]=String.fromCharCode(t)}return T}());function ai(t,T){T||(T=0);t=K(t);var bj="";while(!aZ(t)){bj=bh[t[0]&255]+bj;G(t,8)}while(bj.length<T){bj="\x00"+bj}return bj}function aK(T){var t=a6("0",10,T.length);T.forEach(function(bk,bj){if(bj){bi(t,8)}t[0]|=bk});return t}var a3=(function(){if(typeof v!=="undefined"&&typeof v.randomBytes==="function"){return function(bj){try{var t=v.randomBytes(bj)}catch(T){throw T}return Array.prototype.slice.call(t,0)}}else{if(typeof v!=="undefined"&&typeof v.getRandomValues==="function"){return function(T){var t=new Uint8Array(T);v.getRandomValues(t);return Array.prototype.slice.call(t,0)}}else{throw new Error("Keys should not be generated without CSPRNG.")}}}());function aF(){return a3(40)}function am(){return a3(1)[0]}function aV(bj){if(bj>31){throw new Error("Too many bits.")}var bk=0,bl=0;var t=Math.floor(bj/8);var T=(1<<(bj%8))-1;if(T){bl=am()&T}for(;bk<t;bk++){bl=(256*bl)+am()}return bl}return{str2bigInt:a6,bigInt2str:aM,int2bigInt:a0,multMod:an,powMod:aP,inverseMod:bg,randBigInt:aO,randBigInt_:a1,equals:Q,equalsInt:z,sub:a5,mod:b,modInt:be,mult:d,divInt_:ac,rightShift_:G,dup:K,greater:B,add:w,isZero:aZ,bitSize:ay,millerRabin:aN,divide_:c,trim:a4,primes:N,findPrimes:ba,getSeed:aF,divMod:bb,subMod:aj,twoToThe:I,bigInt2bits:ai,ba2bigInt:aK}}));(function(a,b){if(typeof define==="function"&&define.amd){define(b)}else{if(typeof module!=="undefined"&&module.exports){module.exports=b()}else{a.CryptoJS=b()}}}(this,function(){var a=a||(function(f,h){var b={};var c=b.lib={};var k=c.Base=(function(){function o(){}return{extend:function(q){o.prototype=this;var p=new o();if(q){p.mixIn(q)}if(!p.hasOwnProperty("init")){p.init=function(){p.$super.init.apply(this,arguments)}}p.init.prototype=p;p.$super=this;return p},create:function(){var p=this.extend();p.init.apply(p,arguments);return p},init:function(){},mixIn:function(q){for(var p in q){if(q.hasOwnProperty(p)){this[p]=q[p]}}if(q.hasOwnProperty("toString")){this.toString=q.toString}},clone:function(){return this.init.prototype.extend(this)}}}());var m=c.WordArray=k.extend({init:function(p,o){p=this.words=p||[];if(o!=h){this.sigBytes=o}else{this.sigBytes=p.length*4}},toString:function(o){return(o||i).stringify(this)},concat:function(u){var r=this.words;var q=u.words;var o=this.sigBytes;var t=u.sigBytes;this.clamp();if(o%4){for(var s=0;s<t;s++){var p=(q[s>>>2]>>>(24-(s%4)*8))&255;r[(o+s)>>>2]|=p<<(24-((o+s)%4)*8)}}else{if(q.length>65535){for(var s=0;s<t;s+=4){r[(o+s)>>>2]=q[s>>>2]}}else{r.push.apply(r,q)}}this.sigBytes+=t;return this},clamp:function(){var p=this.words;var o=this.sigBytes;p[o>>>2]&=4294967295<<(32-(o%4)*8);p.length=f.ceil(o/4)},clone:function(){var o=k.clone.call(this);o.words=this.words.slice(0);return o},random:function(q){var p=[];for(var o=0;o<q;o+=4){p.push((f.random()*4294967296)|0)}return new m.init(p,q)}});var n=b.enc={};var i=n.Hex={stringify:function(q){var s=q.words;var p=q.sigBytes;var r=[];for(var o=0;o<p;o++){var t=(s[o>>>2]>>>(24-(o%4)*8))&255;r.push((t>>>4).toString(16));r.push((t&15).toString(16))}return r.join("")},parse:function(q){var o=q.length;var r=[];for(var p=0;p<o;p+=2){r[p>>>3]|=parseInt(q.substr(p,2),16)<<(24-(p%8)*4)}return new m.init(r,o/2)}};var e=n.Latin1={stringify:function(r){var s=r.words;var q=r.sigBytes;var o=[];for(var p=0;p<q;p++){var t=(s[p>>>2]>>>(24-(p%4)*8))&255;o.push(String.fromCharCode(t))}return o.join("")},parse:function(q){var o=q.length;var r=[];for(var p=0;p<o;p++){r[p>>>2]|=(q.charCodeAt(p)&255)<<(24-(p%4)*8)}return new m.init(r,o)}};var d=n.Utf8={stringify:function(o){try{return decodeURIComponent(escape(e.stringify(o)))}catch(p){throw new Error("Malformed UTF-8 data")}},parse:function(o){return e.parse(unescape(encodeURIComponent(o)))}};var j=c.BufferedBlockAlgorithm=k.extend({reset:function(){this._data=new m.init();this._nDataBytes=0},_append:function(o){if(typeof o=="string"){o=d.parse(o)}this._data.concat(o);this._nDataBytes+=o.sigBytes},_process:function(x){var r=this._data;var y=r.words;var o=r.sigBytes;var u=this.blockSize;var w=u*4;var v=o/w;if(x){v=f.ceil(v)}else{v=f.max((v|0)-this._minBufferSize,0)}var t=v*u;var s=f.min(t*4,o);if(t){for(var q=0;q<t;q+=u){this._doProcessBlock(y,q)}var p=y.splice(0,t);r.sigBytes-=s}return new m.init(p,s)},clone:function(){var o=k.clone.call(this);o._data=this._data.clone();return o},_minBufferSize:0});var g=c.Hasher=j.extend({cfg:k.extend(),init:function(o){this.cfg=this.cfg.extend(o);this.reset()},reset:function(){j.reset.call(this);this._doReset()},update:function(o){this._append(o);this._process();return this},finalize:function(o){if(o){this._append(o)}var p=this._doFinalize();return p},blockSize:512/32,_createHelper:function(o){return function(q,p){return new o.init(p).finalize(q)}},_createHmacHelper:function(o){return function(q,p){return new l.HMAC.init(o,p).finalize(q)}}});var l=b.algo={};return b}(Math));(function(){var f=a;var b=f.lib;var c=b.WordArray;var e=f.enc;var d=e.Base64={stringify:function(m){var o=m.words;var q=m.sigBytes;var h=this._map;m.clamp();var n=[];for(var l=0;l<q;l+=3){var t=(o[l>>>2]>>>(24-(l%4)*8))&255;var r=(o[(l+1)>>>2]>>>(24-((l+1)%4)*8))&255;var p=(o[(l+2)>>>2]>>>(24-((l+2)%4)*8))&255;var s=(t<<16)|(r<<8)|p;for(var k=0;(k<4)&&(l+k*0.75<q);k++){n.push(h.charAt((s>>>(6*(3-k)))&63))}}var g=h.charAt(64);if(g){while(n.length%4){n.push(g)}}return n.join("")},parse:function(p){var n=p.length;var h=this._map;var g=h.charAt(64);if(g){var q=p.indexOf(g);if(q!=-1){n=q}}var o=[];var m=0;for(var l=0;l<n;l++){if(l%4){var k=h.indexOf(p.charAt(l-1))<<((l%4)*2);var j=h.indexOf(p.charAt(l))>>>(6-(l%4)*2);o[m>>>2]|=(k|j)<<(24-(m%4)*8);m++}}return c.create(o,m)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}());a.lib.Cipher||(function(e){var n=a;var x=n.lib;var j=x.Base;var u=x.WordArray;var w=x.BufferedBlockAlgorithm;var s=n.enc;var g=s.Utf8;var m=s.Base64;var c=n.algo;var i=c.EvpKDF;var k=x.Cipher=w.extend({cfg:j.extend(),createEncryptor:function(C,B){return this.create(this._ENC_XFORM_MODE,C,B)},createDecryptor:function(C,B){return this.create(this._DEC_XFORM_MODE,C,B)},init:function(D,C,B){this.cfg=this.cfg.extend(B);this._xformMode=D;this._key=C;this.reset()},reset:function(){w.reset.call(this);this._doReset()},process:function(B){this._append(B);return this._process()},finalize:function(C){if(C){this._append(C)}var B=this._doFinalize();return B},keySize:128/32,ivSize:128/32,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:(function(){function B(C){if(typeof C=="string"){return h}else{return A}}return function(C){return{encrypt:function(F,E,D){return B(E).encrypt(C,F,E,D)},decrypt:function(F,E,D){return B(E).decrypt(C,F,E,D)}}}}())});var q=x.StreamCipher=k.extend({_doFinalize:function(){var B=this._process(!!"flush");return B},blockSize:1});var t=n.mode={};var z=x.BlockCipherMode=j.extend({createEncryptor:function(B,C){return this.Encryptor.create(B,C)},createDecryptor:function(B,C){return this.Decryptor.create(B,C)},init:function(B,C){this._cipher=B;this._iv=C}});var d=t.CBC=(function(){var B=z.extend();B.Encryptor=B.extend({processBlock:function(G,F){var D=this._cipher;var E=D.blockSize;C.call(this,G,F,E);D.encryptBlock(G,F);this._prevBlock=G.slice(F,F+E)}});B.Decryptor=B.extend({processBlock:function(H,G){var D=this._cipher;var F=D.blockSize;var E=H.slice(G,G+F);D.decryptBlock(H,G);C.call(this,H,G,F);this._prevBlock=E}});function C(I,H,F){var D=this._iv;if(D){var G=D;this._iv=e}else{var G=this._prevBlock}for(var E=0;E<F;E++){I[H+E]^=G[E]}}return B}());var f=n.pad={};var b=f.Pkcs7={pad:function(G,E){var F=E*4;var I=F-G.sigBytes%F;var B=(I<<24)|(I<<16)|(I<<8)|I;var D=[];for(var C=0;C<I;C+=4){D.push(B)}var H=u.create(D,I);G.concat(H)},unpad:function(B){var C=B.words[(B.sigBytes-1)>>>2]&255;B.sigBytes-=C}};var r=x.BlockCipher=k.extend({cfg:k.cfg.extend({mode:d,padding:b}),reset:function(){k.reset.call(this);var B=this.cfg;var C=B.iv;var E=B.mode;if(this._xformMode==this._ENC_XFORM_MODE){var D=E.createEncryptor}else{var D=E.createDecryptor;this._minBufferSize=1}this._mode=D.call(E,this,C&&C.words)},_doProcessBlock:function(C,B){this._mode.processBlock(C,B)},_doFinalize:function(){var C=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){C.pad(this._data,this.blockSize);var B=this._process(!!"flush")}else{var B=this._process(!!"flush");C.unpad(B)}return B},blockSize:128/32});var p=x.CipherParams=j.extend({init:function(B){this.mixIn(B)},toString:function(B){return(B||this.formatter).stringify(this)}});var o=n.format={};var v=o.OpenSSL={stringify:function(B){var E=B.ciphertext;var C=B.salt;if(C){var D=u.create([1398893684,1701076831]).concat(C).concat(E)}else{var D=E}return D.toString(m)},parse:function(D){var C=m.parse(D);var E=C.words;if(E[0]==1398893684&&E[1]==1701076831){var B=u.create(E.slice(2,4));E.splice(0,4);C.sigBytes-=16}return p.create({ciphertext:C,salt:B})}};var A=x.SerializableCipher=j.extend({cfg:j.extend({format:v}),encrypt:function(B,G,E,C){C=this.cfg.extend(C);var D=B.createEncryptor(E,C);var H=D.finalize(G);var F=D.cfg;return p.create({ciphertext:H,key:E,iv:F.iv,algorithm:B,mode:F.mode,padding:F.padding,blockSize:B.blockSize,formatter:C.format})},decrypt:function(B,F,D,C){C=this.cfg.extend(C);F=this._parse(F,C.format);var E=B.createDecryptor(D,C).finalize(F.ciphertext);return E},_parse:function(B,C){if(typeof B=="string"){return C.parse(B,this)}else{return B}}});var l=n.kdf={};var y=l.OpenSSL={execute:function(D,G,B,F){if(!F){F=u.random(64/8)}var E=i.create({keySize:G+B}).compute(D,F);var C=u.create(E.words.slice(G),B*4);E.sigBytes=G*4;return p.create({key:E,iv:C,salt:F})}};var h=x.PasswordBasedCipher=A.extend({cfg:A.cfg.extend({kdf:y}),encrypt:function(B,E,D,C){C=this.cfg.extend(C);var G=C.kdf.execute(D,B.keySize,B.ivSize);C.iv=G.iv;var F=A.encrypt.call(this,B,E,G.key,C);F.mixIn(G);return F},decrypt:function(B,F,D,C){C=this.cfg.extend(C);F=this._parse(F,C.format);var G=C.kdf.execute(D,B.keySize,B.ivSize,F.salt);C.iv=G.iv;var E=A.decrypt.call(this,B,F,G.key,C);return E}})}());(function(){var b=a;var c=b.lib;var q=c.BlockCipher;var l=b.algo;var e=[];var m=[];var p=[];var o=[];var n=[];var k=[];var j=[];var i=[];var h=[];var g=[];(function(){var u=[];for(var s=0;s<256;s++){if(s<128){u[s]=s<<1}else{u[s]=(s<<1)^283}}var y=0;var v=0;for(var s=0;s<256;s++){var w=v^(v<<1)^(v<<2)^(v<<3)^(v<<4);w=(w>>>8)^(w&255)^99;e[y]=w;m[w]=y;var r=u[y];var B=u[r];var z=u[B];var A=(u[w]*257)^(w*16843008);p[y]=(A<<24)|(A>>>8);o[y]=(A<<16)|(A>>>16);n[y]=(A<<8)|(A>>>24);k[y]=A;var A=(z*16843009)^(B*65537)^(r*257)^(y*16843008);j[w]=(A<<24)|(A>>>8);i[w]=(A<<16)|(A>>>16);h[w]=(A<<8)|(A>>>24);g[w]=A;if(!y){y=v=1}else{y=r^u[u[u[z^r]]];v^=u[u[v]]}}}());var d=[0,1,2,4,8,16,32,64,128,27,54];var f=l.AES=q.extend({_doReset:function(){var A=this._key;var s=A.words;var z=A.sigBytes/4;var y=this._nRounds=z+6;var r=(y+1)*4;var u=this._keySchedule=[];for(var x=0;x<r;x++){if(x<z){u[x]=s[x]}else{var B=u[x-1];if(!(x%z)){B=(B<<8)|(B>>>24);B=(e[B>>>24]<<24)|(e[(B>>>16)&255]<<16)|(e[(B>>>8)&255]<<8)|e[B&255];B^=d[(x/z)|0]<<24}else{if(z>6&&x%z==4){B=(e[B>>>24]<<24)|(e[(B>>>16)&255]<<16)|(e[(B>>>8)&255]<<8)|e[B&255]}}u[x]=u[x-z]^B}}var v=this._invKeySchedule=[];for(var w=0;w<r;w++){var x=r-w;if(w%4){var B=u[x]}else{var B=u[x-4]}if(w<4||x<=4){v[w]=B}else{v[w]=j[e[B>>>24]]^i[e[(B>>>16)&255]]^h[e[(B>>>8)&255]]^g[e[B&255]]}}},encryptBlock:function(s,r){this._doCryptBlock(s,r,this._keySchedule,p,o,n,k,e)},decryptBlock:function(u,s){var r=u[s+1];u[s+1]=u[s+3];u[s+3]=r;this._doCryptBlock(u,s,this._invKeySchedule,j,i,h,g,m);var r=u[s+1];u[s+1]=u[s+3];u[s+3]=r},_doCryptBlock:function(A,z,I,w,u,s,r,H){var F=this._nRounds;var y=A[z]^I[0];var x=A[z+1]^I[1];var v=A[z+2]^I[2];var t=A[z+3]^I[3];var G=4;for(var J=1;J<F;J++){var E=w[y>>>24]^u[(x>>>16)&255]^s[(v>>>8)&255]^r[t&255]^I[G++];var D=w[x>>>24]^u[(v>>>16)&255]^s[(t>>>8)&255]^r[y&255]^I[G++];var C=w[v>>>24]^u[(t>>>16)&255]^s[(y>>>8)&255]^r[x&255]^I[G++];var B=w[t>>>24]^u[(y>>>16)&255]^s[(x>>>8)&255]^r[v&255]^I[G++];y=E;x=D;v=C;t=B}var E=((H[y>>>24]<<24)|(H[(x>>>16)&255]<<16)|(H[(v>>>8)&255]<<8)|H[t&255])^I[G++];var D=((H[x>>>24]<<24)|(H[(v>>>16)&255]<<16)|(H[(t>>>8)&255]<<8)|H[y&255])^I[G++];var C=((H[v>>>24]<<24)|(H[(t>>>16)&255]<<16)|(H[(y>>>8)&255]<<8)|H[x&255])^I[G++];var B=((H[t>>>24]<<24)|(H[(y>>>16)&255]<<16)|(H[(x>>>8)&255]<<8)|H[v&255])^I[G++];A[z]=E;A[z+1]=D;A[z+2]=C;A[z+3]=B},keySize:256/32});b.AES=q._createHelper(f)}());(function(){var h=a;var e=h.lib;var g=e.WordArray;var c=e.Hasher;var f=h.algo;var b=[];var d=f.SHA1=c.extend({_doReset:function(){this._hash=new g.init([1732584193,4023233417,2562383102,271733878,3285377520])},_doProcessBlock:function(o,k){var u=this._hash.words;var s=u[0];var r=u[1];var q=u[2];var p=u[3];var m=u[4];for(var l=0;l<80;l++){if(l<16){b[l]=o[k+l]|0}else{var j=b[l-3]^b[l-8]^b[l-14]^b[l-16];b[l]=(j<<1)|(j>>>31)}var v=((s<<5)|(s>>>27))+m+b[l];if(l<20){v+=((r&q)|(~r&p))+1518500249}else{if(l<40){v+=(r^q^p)+1859775393}else{if(l<60){v+=((r&q)|(r&p)|(q&p))-1894007588}else{v+=(r^q^p)-899497514}}}m=p;p=q;q=(r<<30)|(r>>>2);r=s;s=v}u[0]=(u[0]+s)|0;u[1]=(u[1]+r)|0;u[2]=(u[2]+q)|0;u[3]=(u[3]+p)|0;u[4]=(u[4]+m)|0},_doFinalize:function(){var k=this._data;var l=k.words;var i=this._nDataBytes*8;var j=k.sigBytes*8;l[j>>>5]|=128<<(24-j%32);l[(((j+64)>>>9)<<4)+14]=Math.floor(i/4294967296);l[(((j+64)>>>9)<<4)+15]=i;k.sigBytes=l.length*4;this._process();return this._hash},clone:function(){var i=c.clone.call(this);i._hash=this._hash.clone();return i}});h.SHA1=c._createHelper(d);h.HmacSHA1=c._createHmacHelper(d)}());(function(d){var b=a;var c=b.lib;var h=c.WordArray;var f=c.Hasher;var i=b.algo;var k=[];var j=[];(function(){function o(s){var r=d.sqrt(s);for(var q=2;q<=r;q++){if(!(s%q)){return false}}return true}function m(q){return((q-(q|0))*4294967296)|0}var p=2;var l=0;while(l<64){if(o(p)){if(l<8){k[l]=m(d.pow(p,1/2))}j[l]=m(d.pow(p,1/3));l++}p++}}());var e=[];var g=i.SHA256=f.extend({_doReset:function(){this._hash=new h.init(k.slice(0))},_doProcessBlock:function(o,n){var r=this._hash.words;var E=r[0];var D=r[1];var C=r[2];var B=r[3];var A=r[4];var z=r[5];var y=r[6];var x=r[7];for(var w=0;w<64;w++){if(w<16){e[w]=o[n+w]|0}else{var m=e[w-15];var G=((m<<25)|(m>>>7))^((m<<14)|(m>>>18))^(m>>>3);var s=e[w-2];var F=((s<<15)|(s>>>17))^((s<<13)|(s>>>19))^(s>>>10);e[w]=G+e[w-7]+F+e[w-16]}var t=(A&z)^(~A&y);var l=(E&D)^(E&C)^(D&C);var v=((E<<30)|(E>>>2))^((E<<19)|(E>>>13))^((E<<10)|(E>>>22));var u=((A<<26)|(A>>>6))^((A<<21)|(A>>>11))^((A<<7)|(A>>>25));var q=x+u+t+j[w]+e[w];var p=v+l;x=y;y=z;z=A;A=(B+q)|0;B=C;C=D;D=E;E=(q+p)|0}r[0]=(r[0]+E)|0;r[1]=(r[1]+D)|0;r[2]=(r[2]+C)|0;r[3]=(r[3]+B)|0;r[4]=(r[4]+A)|0;r[5]=(r[5]+z)|0;r[6]=(r[6]+y)|0;r[7]=(r[7]+x)|0},_doFinalize:function(){var n=this._data;var o=n.words;var l=this._nDataBytes*8;var m=n.sigBytes*8;o[m>>>5]|=128<<(24-m%32);o[(((m+64)>>>9)<<4)+14]=d.floor(l/4294967296);o[(((m+64)>>>9)<<4)+15]=l;n.sigBytes=o.length*4;this._process();return this._hash},clone:function(){var l=f.clone.call(this);l._hash=this._hash.clone();return l}});b.SHA256=f._createHelper(g);b.HmacSHA256=f._createHmacHelper(g)}(Math));(function(){var h=a;var e=h.lib;var d=e.Base;var g=h.enc;var c=g.Utf8;var f=h.algo;var b=f.HMAC=d.extend({init:function(r,o){r=this._hasher=new r.init();if(typeof o=="string"){o=c.parse(o)}var l=r.blockSize;var j=l*4;if(o.sigBytes>j){o=r.finalize(o)}o.clamp();var q=this._oKey=o.clone();var n=this._iKey=o.clone();var p=q.words;var k=n.words;for(var m=0;m<l;m++){p[m]^=1549556828;k[m]^=909522486}q.sigBytes=n.sigBytes=j;this.reset()},reset:function(){var i=this._hasher;i.reset();i.update(this._iKey)},update:function(i){this._hasher.update(i);return this},finalize:function(i){var j=this._hasher;var l=j.finalize(i);j.reset();var k=j.finalize(this._oKey.clone().concat(l));return k}})}());a.pad.NoPadding={pad:function(){},unpad:function(){}};a.mode.CTR=(function(){var c=a.lib.BlockCipherMode.extend();var b=c.Encryptor=c.extend({processBlock:function(l,k){var d=this._cipher;var h=d.blockSize;var f=this._iv;var e=this._counter;if(f){e=this._counter=f.slice(0);this._iv=undefined}var j=e.slice(0);d.encryptBlock(j,0);e[h-1]=(e[h-1]+1)|0;for(var g=0;g<h;g++){l[k+g]^=j[g]}}});c.Decryptor=b;return c}());return a}));
+(function(){function c(){}var l=c.prototype;function h(w,x){var v=w.length;while(v--){if(w[v].listener===x){return v}}return -1}function j(v){return function w(){return this[v].apply(this,arguments)}}l.getListeners=function t(v){var y=this._getEvents();var w;var x;if(typeof v==="object"){w={};for(x in y){if(y.hasOwnProperty(x)&&v.test(x)){w[x]=y[x]}}}else{w=y[v]||(y[v]=[])}return w};l.flattenListeners=function r(x){var v=[];var w;for(w=0;w<x.length;w+=1){v.push(x[w].listener)}return v};l.getListenersAsObject=function e(v){var x=this.getListeners(v);var w;if(x instanceof Array){w={};w[v]=x}return w||x};l.addListener=function f(v,y){var x=this.getListenersAsObject(v);var z=typeof y==="object";var w;for(w in x){if(x.hasOwnProperty(w)&&h(x[w],y)===-1){x[w].push(z?y:{listener:y,once:false})}}return this};l.on=j("addListener");l.addOnceListener=function a(v,w){return this.addListener(v,{listener:w,once:true})};l.once=j("addOnceListener");l.defineEvent=function p(v){this.getListeners(v);return this};l.defineEvents=function q(v){for(var w=0;w<v.length;w+=1){this.defineEvent(v[w])}return this};l.removeListener=function b(v,z){var y=this.getListenersAsObject(v);var w;var x;for(x in y){if(y.hasOwnProperty(x)){w=h(y[x],z);if(w!==-1){y[x].splice(w,1)}}}return this};l.off=j("removeListener");l.addListeners=function m(v,w){return this.manipulateListeners(false,v,w)};l.removeListeners=function s(v,w){return this.manipulateListeners(true,v,w)};l.manipulateListeners=function g(w,x,z){var y;var A;var B=w?this.removeListener:this.addListener;var v=w?this.removeListeners:this.addListeners;if(typeof x==="object"&&!(x instanceof RegExp)){for(y in x){if(x.hasOwnProperty(y)&&(A=x[y])){if(typeof A==="function"){B.call(this,y,A)}else{v.call(this,y,A)}}}}else{y=z.length;while(y--){B.call(this,x,z[y])}}return this};l.removeEvent=function o(v){var y=typeof v;var x=this._getEvents();var w;if(y==="string"){delete x[v]}else{if(y==="object"){for(w in x){if(x.hasOwnProperty(w)&&v.test(w)){delete x[w]}}}else{delete this._events}}return this};l.emitEvent=function u(v,x){var A=this.getListenersAsObject(v);var B;var z;var y;var w;for(y in A){if(A.hasOwnProperty(y)){z=A[y].length;while(z--){B=A[y][z];if(B.once===true){this.removeListener(v,B.listener)}w=B.listener.apply(this,x||[]);if(w===this._getOnceReturnValue()){this.removeListener(v,B.listener)}}}}return this};l.trigger=j("emitEvent");l.emit=function k(v){var w=Array.prototype.slice.call(arguments,1);return this.emitEvent(v,w)};l.setOnceReturnValue=function i(v){this._onceReturnValue=v;return this};l._getOnceReturnValue=function n(){if(this.hasOwnProperty("_onceReturnValue")){return this._onceReturnValue}else{return true}};l._getEvents=function d(){return this._events||(this._events={})};if(typeof define==="function"&&define.amd){define(function(){return c})}else{if(typeof module==="object"&&module.exports){module.exports=c}else{this.EventEmitter=c}}}.call(this));
+!function(root,factory){"function"==typeof define&&define.amd?define(["bigint","crypto","eventemitter"],function(BigInt,CryptoJS,EventEmitter){var root={BigInt:BigInt,CryptoJS:CryptoJS,EventEmitter:EventEmitter,OTR:{},DSA:{}};return factory.call(root)}):(root.OTR={},root.DSA={},factory.call(root))}(this,function(){return function(){"use strict";var root=this,CONST={N:"FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF",G:"2",MSGSTATE_PLAINTEXT:0,MSGSTATE_ENCRYPTED:1,MSGSTATE_FINISHED:2,AUTHSTATE_NONE:0,AUTHSTATE_AWAITING_DHKEY:1,AUTHSTATE_AWAITING_REVEALSIG:2,AUTHSTATE_AWAITING_SIG:3,WHITESPACE_TAG:" 	  				 	 	 	  ",WHITESPACE_TAG_V2:"  		  	 ",WHITESPACE_TAG_V3:"  		  		",OTR_TAG:"?OTR",OTR_VERSION_1:"\x00",OTR_VERSION_2:"\x00",OTR_VERSION_3:"\x00",SMPSTATE_EXPECT0:0,SMPSTATE_EXPECT1:1,SMPSTATE_EXPECT2:2,SMPSTATE_EXPECT3:3,SMPSTATE_EXPECT4:4,STATUS_SEND_QUERY:0,STATUS_AKE_INIT:1,STATUS_AKE_SUCCESS:2,STATUS_END_OTR:3};"undefined"!=typeof module&&module.exports?module.exports=CONST:root.OTR.CONST=CONST}.call(this),function(){"use strict";var CryptoJS,BigInt,root=this,HLP={};"undefined"!=typeof module&&module.exports?(module.exports=HLP={},CryptoJS=require("../vendor/crypto.js"),BigInt=require("../vendor/bigint.js")):(root.OTR&&(root.OTR.HLP=HLP),root.DSA&&(root.DSA.HLP=HLP),CryptoJS=root.CryptoJS,BigInt=root.BigInt);var DTS={BYTE:1,SHORT:2,INT:4,CTR:8,MAC:20,SIG:40},WRAPPER_BEGIN="?OTR",WRAPPER_END=".",TWO=BigInt.str2bigInt("2",10);HLP.debug=function(msg){this.debug&&"function"!=typeof this.debug&&"undefined"!=typeof console&&console.log(msg)},HLP.extend=function(child,parent){function Ctor(){this.constructor=child}for(var key in parent)Object.hasOwnProperty.call(parent,key)&&(child[key]=parent[key]);Ctor.prototype=parent.prototype,child.prototype=new Ctor,child.__super__=parent.prototype},HLP.compare=function(str1,str2){if(str1.length!==str2.length)return!1;for(var i=0,result=0;i<str1.length;i++)result|=str1[i].charCodeAt(0)^str2[i].charCodeAt(0);return 0===result},HLP.randomExponent=function(){return BigInt.randBigInt(1536)},HLP.smpHash=function(version,fmpi,smpi){var sha256=CryptoJS.algo.SHA256.create();sha256.update(CryptoJS.enc.Latin1.parse(HLP.packBytes(version,DTS.BYTE))),sha256.update(CryptoJS.enc.Latin1.parse(HLP.packMPI(fmpi))),smpi&&sha256.update(CryptoJS.enc.Latin1.parse(HLP.packMPI(smpi)));var hash=sha256.finalize();return HLP.bits2bigInt(hash.toString(CryptoJS.enc.Latin1))},HLP.makeMac=function(aesctr,m){var pass=CryptoJS.enc.Latin1.parse(m),mac=CryptoJS.HmacSHA256(CryptoJS.enc.Latin1.parse(aesctr),pass);return HLP.mask(mac.toString(CryptoJS.enc.Latin1),0,160)},HLP.make1Mac=function(aesctr,m){var pass=CryptoJS.enc.Latin1.parse(m),mac=CryptoJS.HmacSHA1(CryptoJS.enc.Latin1.parse(aesctr),pass);return mac.toString(CryptoJS.enc.Latin1)},HLP.encryptAes=function(msg,c,iv){var opts={mode:CryptoJS.mode.CTR,iv:CryptoJS.enc.Latin1.parse(iv),padding:CryptoJS.pad.NoPadding},aesctr=CryptoJS.AES.encrypt(msg,CryptoJS.enc.Latin1.parse(c),opts),aesctr_decoded=CryptoJS.enc.Base64.parse(aesctr.toString());return CryptoJS.enc.Latin1.stringify(aesctr_decoded)},HLP.decryptAes=function(msg,c,iv){msg=CryptoJS.enc.Latin1.parse(msg);var opts={mode:CryptoJS.mode.CTR,iv:CryptoJS.enc.Latin1.parse(iv),padding:CryptoJS.pad.NoPadding};return CryptoJS.AES.decrypt(CryptoJS.enc.Base64.stringify(msg),CryptoJS.enc.Latin1.parse(c),opts)},HLP.multPowMod=function(a,b,c,d,e){return BigInt.multMod(BigInt.powMod(a,b,e),BigInt.powMod(c,d,e),e)},HLP.ZKP=function(v,c,d,e){return BigInt.equals(c,HLP.smpHash(v,d,e))},HLP.GTOE=function(a,b){return BigInt.equals(a,b)||BigInt.greater(a,b)},HLP.between=function(x,a,b){return BigInt.greater(x,a)&&BigInt.greater(b,x)},HLP.checkGroup=function(g,N_MINUS_2){return HLP.GTOE(g,TWO)&&HLP.GTOE(N_MINUS_2,g)},HLP.h1=function(b,secbytes){var sha1=CryptoJS.algo.SHA1.create();return sha1.update(CryptoJS.enc.Latin1.parse(b)),sha1.update(CryptoJS.enc.Latin1.parse(secbytes)),sha1.finalize().toString(CryptoJS.enc.Latin1)},HLP.h2=function(b,secbytes){var sha256=CryptoJS.algo.SHA256.create();return sha256.update(CryptoJS.enc.Latin1.parse(b)),sha256.update(CryptoJS.enc.Latin1.parse(secbytes)),sha256.finalize().toString(CryptoJS.enc.Latin1)},HLP.mask=function(bytes,start,n){return bytes.substr(start/8,n/8)};var _toString=String.fromCharCode;HLP.packBytes=function(val,bytes){val=val.toString(16);for(var nex,res="";bytes>0;bytes--)nex=val.length?val.substr(-2,2):"0",val=val.substr(0,val.length-2),res=_toString(parseInt(nex,16))+res;return res},HLP.packINT=function(d){return HLP.packBytes(d,DTS.INT)},HLP.packCtr=function(d){return HLP.padCtr(HLP.packBytes(d,DTS.CTR))},HLP.padCtr=function(ctr){return ctr+"\x00\x00\x00\x00\x00\x00\x00\x00"},HLP.unpackCtr=function(d){return d=HLP.toByteArray(d.substring(0,8)),HLP.unpack(d)},HLP.unpack=function(arr){for(var val=0,i=0,len=arr.length;len>i;i++)val=256*val+arr[i];return val},HLP.packData=function(d){return HLP.packINT(d.length)+d},HLP.bits2bigInt=function(bits){return bits=HLP.toByteArray(bits),BigInt.ba2bigInt(bits)},HLP.packMPI=function(mpi){return HLP.packData(BigInt.bigInt2bits(BigInt.trim(mpi,0)))},HLP.packSHORT=function(short){return HLP.packBytes(short,DTS.SHORT)},HLP.unpackSHORT=function(short){return short=HLP.toByteArray(short),HLP.unpack(short)},HLP.packTLV=function(type,value){return HLP.packSHORT(type)+HLP.packSHORT(value.length)+value},HLP.readLen=function(msg){return msg=HLP.toByteArray(msg.substring(0,4)),HLP.unpack(msg)},HLP.readData=function(data){var n=HLP.unpack(data.splice(0,4));return[n,data]},HLP.readMPI=function(data){return data=HLP.toByteArray(data),data=HLP.readData(data),BigInt.ba2bigInt(data[1])},HLP.packMPIs=function(arr){return arr.reduce(function(prv,cur){return prv+HLP.packMPI(cur)},"")},HLP.unpackMPIs=function(num,mpis){for(var i=0,arr=[];num>i;i++)arr.push("MPI");return HLP.splitype(arr,mpis).map(function(m){return HLP.readMPI(m)})},HLP.wrapMsg=function(msg,fs,v3,our_it,their_it){msg=CryptoJS.enc.Base64.stringify(CryptoJS.enc.Latin1.parse(msg)),msg=WRAPPER_BEGIN+":"+msg+WRAPPER_END;var its;if(v3&&(its="|",its+=HLP.readLen(our_it).toString(16),its+="|",its+=HLP.readLen(their_it).toString(16)),!fs)return[null,msg];var n=Math.ceil(msg.length/fs);if(n>65535)return["Too many fragments"];if(1==n)return[null,msg];var k,bi,ei,frag,mf,mfs=[];for(k=1;n>=k;k++)bi=(k-1)*fs,ei=k*fs,frag=msg.slice(bi,ei),mf=WRAPPER_BEGIN,v3&&(mf+=its),mf+=","+k+",",mf+=n+",",mf+=frag+",",mfs.push(mf);return[null,mfs]},HLP.splitype=function splitype(arr,msg){var data=[];return arr.forEach(function(a){var str;switch(a){case"PUBKEY":str=splitype(["SHORT","MPI","MPI","MPI","MPI"],msg).join("");break;case"DATA":case"MPI":str=msg.substring(0,HLP.readLen(msg)+4);break;default:str=msg.substring(0,DTS[a])}data.push(str),msg=msg.substring(str.length)}),data};var _bin2num=function(){for(var i=0,_bin2num={};256>i;++i)_bin2num[String.fromCharCode(i)]=i;for(i=128;256>i;++i)_bin2num[String.fromCharCode(63232+i)]=i;return _bin2num}();HLP.toByteArray=function(data){for(var rv=[],ary=data.split(""),i=-1,iz=ary.length,remain=iz%8;remain--;)++i,rv[i]=_bin2num[ary[i]];for(remain=iz>>3;remain--;)rv.push(_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]],_bin2num[ary[++i]]);return rv}}.call(this),function(){"use strict";function timer(){var start=(new Date).getTime();return function(s){if(DEBUG&&"undefined"!=typeof console){var t=(new Date).getTime();console.log(s+": "+(t-start)),start=t}}}function makeRandom(min,max){var c=BigInt.randBigInt(BigInt.bitSize(max));return HLP.between(c,min,max)?c:makeRandom(min,max)}function isProbPrime(k,n){var i,B=3e4,l=BigInt.bitSize(k),primes=BigInt.primes;for(0===primes.length&&(primes=BigInt.findPrimes(B)),rpprb.length!=k.length&&(rpprb=BigInt.dup(k)),i=0;i<primes.length&&primes[i]<=B;i++)if(0===BigInt.modInt(k,primes[i])&&!BigInt.equalsInt(k,primes[i]))return 0;for(i=0;n>i;i++){for(BigInt.randBigInt_(rpprb,l,0);!BigInt.greater(k,rpprb);)BigInt.randBigInt_(rpprb,l,0);if(!BigInt.millerRabin(k,rpprb))return 0}return 1}function generatePrimes(bit_length){for(var q,p,rem,counter,t=timer(),repeat=bit_lengths[bit_length].repeat,N=bit_lengths[bit_length].N,LM1=BigInt.twoToThe(bit_length-1),bl4=4*bit_length,brk=!1;;)if(q=BigInt.randBigInt(N,1),q[0]|=1,isProbPrime(q,repeat)){for(t("q"),counter=0;bl4>counter;counter++)if(p=BigInt.randBigInt(bit_length,1),p[0]|=1,rem=BigInt.mod(p,q),rem=BigInt.sub(rem,ONE),p=BigInt.sub(p,rem),!BigInt.greater(LM1,p)&&isProbPrime(p,repeat)){t("p"),primes[bit_length]={p:p,q:q},brk=!0;break}if(brk)break}for(var g,h=BigInt.dup(TWO),pm1=BigInt.sub(p,ONE),e=BigInt.multMod(pm1,BigInt.inverseMod(q,p),p);;){g=BigInt.powMod(h,e,p);{if(!BigInt.equals(g,ONE))return primes[bit_length].g=g,void t("g");h=BigInt.add(h,ONE)}}throw new Error("Unreachable!")}function DSA(obj,opts){if(!(this instanceof DSA))return new DSA(obj,opts);if(opts=opts||{},obj){var self=this;return["p","q","g","y","x"].forEach(function(prop){self[prop]=obj[prop]}),void(this.type=obj.type||KEY_TYPE)}var bit_length=parseInt(opts.bit_length?opts.bit_length:1024,10);if(!bit_lengths[bit_length])throw new Error("Unsupported bit length.");primes[bit_length]||generatePrimes(bit_length),this.p=primes[bit_length].p,this.q=primes[bit_length].q,this.g=primes[bit_length].g,this.type=KEY_TYPE,this.x=makeRandom(ZERO,this.q),this.y=BigInt.powMod(this.g,this.x,this.p),opts.nocache&&(primes[bit_length]=null)}function tokenizeStr(str){var start,end;if(start=str.indexOf("("),end=str.lastIndexOf(")"),0>start||0>end)throw new Error("Malformed S-Expression");str=str.substring(start+1,end);var splt=str.search(/\s/),obj={type:str.substring(0,splt),val:[]};if(str=str.substring(splt+1,end),start=str.indexOf("("),0>start)obj.val.push(str);else for(var i,len,ss,es;start>-1;){for(i=start+1,len=str.length,ss=1,es=0;len>i&&ss>es;i++)"("===str[i]&&ss++,")"===str[i]&&es++;obj.val.push(tokenizeStr(str.substring(start,++i))),str=str.substring(++i),start=str.indexOf("(")}return obj}function parseLibotr(obj){if(!obj.type)throw new Error("Parse error.");var o,val;return"privkeys"===obj.type?(o=[],obj.val.forEach(function(i){o.push(parseLibotr(i))}),o):(o={},obj.val.forEach(function(i){val=i.val[0],"string"==typeof val?0===val.indexOf("#")&&(val=val.substring(1,val.lastIndexOf("#")),val=BigInt.str2bigInt(val,16)):val=parseLibotr(i),o[i.type]=val}),o)}var CryptoJS,BigInt,Worker,WWPath,HLP,root=this;"undefined"!=typeof module&&module.exports?(module.exports=DSA,CryptoJS=require("../vendor/crypto.js"),BigInt=require("../vendor/bigint.js"),WWPath=require("path").join(__dirname,"/dsa-webworker.js"),HLP=require("./helpers.js")):(Object.keys(root.DSA).forEach(function(k){DSA[k]=root.DSA[k]}),root.DSA=DSA,CryptoJS=root.CryptoJS,BigInt=root.BigInt,Worker=root.Worker,WWPath="dsa-webworker.js",HLP=DSA.HLP);var ZERO=BigInt.str2bigInt("0",10),ONE=BigInt.str2bigInt("1",10),TWO=BigInt.str2bigInt("2",10),KEY_TYPE="\x00\x00",DEBUG=!1,rpprb=[],bit_lengths={1024:{N:160,repeat:40},2048:{N:224,repeat:56}},primes={};DSA.prototype={constructor:DSA,packPublic:function(){var str=this.type;return str+=HLP.packMPI(this.p),str+=HLP.packMPI(this.q),str+=HLP.packMPI(this.g),str+=HLP.packMPI(this.y)},packPrivate:function(){var str=this.packPublic()+HLP.packMPI(this.x);return str=CryptoJS.enc.Latin1.parse(str),str.toString(CryptoJS.enc.Base64)},generateNonce:function(m){var priv=BigInt.bigInt2bits(BigInt.trim(this.x,0)),rand=BigInt.bigInt2bits(BigInt.randBigInt(256)),sha256=CryptoJS.algo.SHA256.create();sha256.update(CryptoJS.enc.Latin1.parse(priv)),sha256.update(m),sha256.update(CryptoJS.enc.Latin1.parse(rand));var hash=sha256.finalize();return hash=HLP.bits2bigInt(hash.toString(CryptoJS.enc.Latin1)),BigInt.rightShift_(hash,256-BigInt.bitSize(this.q)),HLP.between(hash,ZERO,this.q)?hash:this.generateNonce(m)},sign:function(m){m=CryptoJS.enc.Latin1.parse(m);for(var k,b=BigInt.str2bigInt(m.toString(CryptoJS.enc.Hex),16),r=ZERO,s=ZERO;BigInt.isZero(s)||BigInt.isZero(r);)k=this.generateNonce(m),r=BigInt.mod(BigInt.powMod(this.g,k,this.p),this.q),BigInt.isZero(r)||(s=BigInt.inverseMod(k,this.q),s=BigInt.mult(s,BigInt.add(b,BigInt.mult(this.x,r))),s=BigInt.mod(s,this.q));return[r,s]},fingerprint:function(){var pk=this.packPublic();return this.type===KEY_TYPE&&(pk=pk.substring(2)),pk=CryptoJS.enc.Latin1.parse(pk),CryptoJS.SHA1(pk).toString(CryptoJS.enc.Hex)}},DSA.parsePublic=function(str,priv){var fields=["SHORT","MPI","MPI","MPI","MPI"];priv&&fields.push("MPI"),str=HLP.splitype(fields,str);var obj={type:str[0],p:HLP.readMPI(str[1]),q:HLP.readMPI(str[2]),g:HLP.readMPI(str[3]),y:HLP.readMPI(str[4])};return priv&&(obj.x=HLP.readMPI(str[5])),new DSA(obj)},DSA.parsePrivate=function(str,libotr){return libotr?parseLibotr(tokenizeStr(str))[0]["private-key"].dsa:(str=CryptoJS.enc.Base64.parse(str),str=str.toString(CryptoJS.enc.Latin1),DSA.parsePublic(str,!0))},DSA.verify=function(key,m,r,s){if(!HLP.between(r,ZERO,key.q)||!HLP.between(s,ZERO,key.q))return!1;var hm=CryptoJS.enc.Latin1.parse(m);hm=BigInt.str2bigInt(hm.toString(CryptoJS.enc.Hex),16);var w=BigInt.inverseMod(s,key.q),u1=BigInt.multMod(hm,w,key.q),u2=BigInt.multMod(r,w,key.q);u1=BigInt.powMod(key.g,u1,key.p),u2=BigInt.powMod(key.y,u2,key.p);var v=BigInt.mod(BigInt.multMod(u1,u2,key.p),key.q);return BigInt.equals(v,r)},DSA.createInWebWorker=function(options,cb){var opts={path:WWPath,seed:BigInt.getSeed};options&&"object"==typeof options&&Object.keys(options).forEach(function(k){opts[k]=options[k]}),"undefined"!=typeof module&&module.exports&&(Worker=require("webworker-threads").Worker);var worker=new Worker(opts.path);worker.onmessage=function(e){var data=e.data;switch(data.type){case"debug":if(!DEBUG||"undefined"==typeof console)return;console.log(data.val);break;case"data":worker.terminate(),cb(DSA.parsePrivate(data.val));break;default:throw new Error("Unrecognized type.")}},worker.postMessage({seed:opts.seed(),imports:opts.imports,debug:DEBUG})}}.call(this),function(){"use strict";var CryptoJS,CONST,HLP,root=this,Parse={};"undefined"!=typeof module&&module.exports?(module.exports=Parse,CryptoJS=require("../vendor/crypto.js"),CONST=require("./const.js"),HLP=require("./helpers.js")):(root.OTR.Parse=Parse,CryptoJS=root.CryptoJS,CONST=root.OTR.CONST,HLP=root.OTR.HLP);var tags={};tags[CONST.WHITESPACE_TAG_V2]=CONST.OTR_VERSION_2,tags[CONST.WHITESPACE_TAG_V3]=CONST.OTR_VERSION_3,Parse.parseMsg=function(otr,msg){var ver=[],start=msg.indexOf(CONST.OTR_TAG);if(!~start){if(this.initFragment(otr),ind=msg.indexOf(CONST.WHITESPACE_TAG),~ind){msg=msg.split(""),msg.splice(ind,16);for(var tag,len=msg.length;len>ind;)tag=msg.slice(ind,ind+8).join(""),Object.hasOwnProperty.call(tags,tag)?(msg.splice(ind,8),ver.push(tags[tag])):ind+=8;msg=msg.join("")}return{msg:msg,ver:ver}}var ind=start+CONST.OTR_TAG.length,com=msg[ind];if(","===com||"|"===com)return this.msgFragment(otr,msg.substring(ind+1),"|"===com);if(this.initFragment(otr),~["?","v"].indexOf(com)){"?"===msg[ind]&&(ver.push(CONST.OTR_VERSION_1),ind+=1);var vers={2:CONST.OTR_VERSION_2,3:CONST.OTR_VERSION_3},qs=msg.substring(ind+1),qi=qs.indexOf("?");return qi>=1&&(qs=qs.substring(0,qi).split(""),"v"===msg[ind]&&qs.forEach(function(q){Object.hasOwnProperty.call(vers,q)&&ver.push(vers[q])})),{cls:"query",ver:ver}}if(":"===com){ind+=1;var info=msg.substring(ind,ind+4);if(info.length<4)return{msg:msg};info=CryptoJS.enc.Base64.parse(info).toString(CryptoJS.enc.Latin1);var version=info.substring(0,2),type=info.substring(2);if(!otr["ALLOW_V"+HLP.unpackSHORT(version)])return{msg:msg};ind+=4;var end=msg.substring(ind).indexOf(".");if(!~end)return{msg:msg};msg=CryptoJS.enc.Base64.parse(msg.substring(ind,ind+end)),msg=CryptoJS.enc.Latin1.stringify(msg);var instance_tags;version===CONST.OTR_VERSION_3&&(instance_tags=msg.substring(0,8),msg=msg.substring(8));var cls;return~["","\n","",""].indexOf(type)?cls="ake":""===type&&(cls="data"),{version:version,type:type,msg:msg,cls:cls,instance_tags:instance_tags}}return" Error:"===msg.substring(ind,ind+7)?(otr.ERROR_START_AKE&&otr.sendQueryMsg(),{msg:msg.substring(ind+7),cls:"error"}):{msg:msg}},Parse.initFragment=function(otr){otr.fragment={s:"",j:0,k:0}},Parse.msgFragment=function(otr,msg,v3){if(msg=msg.split(","),v3){var its=msg.shift().split("|"),their_it=HLP.packINT(parseInt(its[0],16)),our_it=HLP.packINT(parseInt(its[1],16));if(otr.checkInstanceTags(their_it+our_it))return}if(!(msg.length<4||isNaN(parseInt(msg[0],10))||isNaN(parseInt(msg[1],10)))){var k=parseInt(msg[0],10),n=parseInt(msg[1],10);return msg=msg[2],k>n||0===n||0===k?void this.initFragment(otr):(1===k?(this.initFragment(otr),otr.fragment={k:1,n:n,s:msg}):n===otr.fragment.n&&k===otr.fragment.k+1?(otr.fragment.s+=msg,otr.fragment.k+=1):this.initFragment(otr),n===k?(msg=otr.fragment.s,this.initFragment(otr),this.parseMsg(otr,msg)):void 0)}}}.call(this),function(){"use strict";function hMac(gx,gy,pk,kid,m){var pass=CryptoJS.enc.Latin1.parse(m),hmac=CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256,pass);return hmac.update(CryptoJS.enc.Latin1.parse(HLP.packMPI(gx))),hmac.update(CryptoJS.enc.Latin1.parse(HLP.packMPI(gy))),hmac.update(CryptoJS.enc.Latin1.parse(pk)),hmac.update(CryptoJS.enc.Latin1.parse(kid)),hmac.finalize().toString(CryptoJS.enc.Latin1)}function AKE(otr){if(!(this instanceof AKE))return new AKE(otr);this.otr=otr,this.our_dh=otr.our_old_dh,this.our_keyid=otr.our_keyid-1,this.their_y=null,this.their_keyid=null,this.their_priv_pk=null,this.ssid=null,this.transmittedRS=!1,this.r=null;var self=this;["sendMsg"].forEach(function(meth){self[meth]=self[meth].bind(self)})}var CryptoJS,BigInt,CONST,HLP,DSA,root=this;"undefined"!=typeof module&&module.exports?(module.exports=AKE,CryptoJS=require("../vendor/crypto.js"),BigInt=require("../vendor/bigint.js"),CONST=require("./const.js"),HLP=require("./helpers.js"),DSA=require("./dsa.js")):(root.OTR.AKE=AKE,CryptoJS=root.CryptoJS,BigInt=root.BigInt,CONST=root.OTR.CONST,HLP=root.OTR.HLP,DSA=root.DSA);var N=BigInt.str2bigInt(CONST.N,16),N_MINUS_2=BigInt.sub(N,BigInt.str2bigInt("2",10));AKE.prototype={constructor:AKE,createKeys:function(g){var s=BigInt.powMod(g,this.our_dh.privateKey,N),secbytes=HLP.packMPI(s);this.ssid=HLP.mask(HLP.h2("\x00",secbytes),0,64);var tmp=HLP.h2("",secbytes);this.c=HLP.mask(tmp,0,128),this.c_prime=HLP.mask(tmp,128,128),this.m1=HLP.h2("",secbytes),this.m2=HLP.h2("",secbytes),this.m1_prime=HLP.h2("",secbytes),this.m2_prime=HLP.h2("",secbytes)},verifySignMac:function(mac,aesctr,m2,c,their_y,our_dh_pk,m1,ctr){var vmac=HLP.makeMac(aesctr,m2);if(!HLP.compare(mac,vmac))return["MACs do not match."];var x=HLP.decryptAes(aesctr.substring(4),c,ctr);x=HLP.splitype(["PUBKEY","INT","SIG"],x.toString(CryptoJS.enc.Latin1));var m=hMac(their_y,our_dh_pk,x[0],x[1],m1),pub=DSA.parsePublic(x[0]),r=HLP.bits2bigInt(x[2].substring(0,20)),s=HLP.bits2bigInt(x[2].substring(20));return DSA.verify(pub,m,r,s)?[null,HLP.readLen(x[1]),pub]:["Cannot verify signature of m."]},makeM:function(their_y,m1,c,m2){var pk=this.otr.priv.packPublic(),kid=HLP.packINT(this.our_keyid),m=hMac(this.our_dh.publicKey,their_y,pk,kid,m1);m=this.otr.priv.sign(m);var msg=pk+kid;msg+=BigInt.bigInt2bits(m[0],20),msg+=BigInt.bigInt2bits(m[1],20),msg=CryptoJS.enc.Latin1.parse(msg);var aesctr=HLP.packData(HLP.encryptAes(msg,c,HLP.packCtr(0))),mac=HLP.makeMac(aesctr,m2);return aesctr+mac},akeSuccess:function(version){return HLP.debug.call(this.otr,"success"),BigInt.equals(this.their_y,this.our_dh.publicKey)?this.otr.error("equal keys - we have a problem.",!0):(this.otr.our_old_dh=this.our_dh,this.otr.their_priv_pk=this.their_priv_pk,this.their_keyid===this.otr.their_keyid&&BigInt.equals(this.their_y,this.otr.their_y)||this.their_keyid===this.otr.their_keyid-1&&BigInt.equals(this.their_y,this.otr.their_old_y)||(this.otr.their_y=this.their_y,this.otr.their_old_y=null,this.otr.their_keyid=this.their_keyid,this.otr.sessKeys[0]=[new this.otr.DHSession(this.otr.our_dh,this.otr.their_y),null],this.otr.sessKeys[1]=[new this.otr.DHSession(this.otr.our_old_dh,this.otr.their_y),null]),this.otr.ssid=this.ssid,this.otr.transmittedRS=this.transmittedRS,this.otr_version=version,this.otr.authstate=CONST.AUTHSTATE_NONE,this.otr.msgstate=CONST.MSGSTATE_ENCRYPTED,this.r=null,this.myhashed=null,this.dhcommit=null,this.encrypted=null,this.hashed=null,this.otr.trigger("status",[CONST.STATUS_AKE_SUCCESS]),void this.otr.sendStored())},handleAKE:function(msg){var send,vsm,type,version=msg.version;switch(msg.type){case"":if(HLP.debug.call(this.otr,"d-h key message"),msg=HLP.splitype(["DATA","DATA"],msg.msg),this.otr.authstate===CONST.AUTHSTATE_AWAITING_DHKEY){var ourHash=HLP.readMPI(this.myhashed),theirHash=HLP.readMPI(msg[1]);if(BigInt.greater(ourHash,theirHash)){type="",send=this.dhcommit;break}this.our_dh=this.otr.dh(),this.otr.authstate=CONST.AUTHSTATE_NONE,this.r=null,this.myhashed=null}else this.otr.authstate===CONST.AUTHSTATE_AWAITING_SIG&&(this.our_dh=this.otr.dh());this.otr.authstate=CONST.AUTHSTATE_AWAITING_REVEALSIG,this.encrypted=msg[0].substring(4),this.hashed=msg[1].substring(4),type="\n",send=HLP.packMPI(this.our_dh.publicKey);break;case"\n":if(HLP.debug.call(this.otr,"reveal signature message"),msg=HLP.splitype(["MPI"],msg.msg),this.otr.authstate!==CONST.AUTHSTATE_AWAITING_DHKEY){if(this.otr.authstate!==CONST.AUTHSTATE_AWAITING_SIG)return;if(!BigInt.equals(this.their_y,HLP.readMPI(msg[0])))return}if(this.otr.authstate=CONST.AUTHSTATE_AWAITING_SIG,this.their_y=HLP.readMPI(msg[0]),!HLP.checkGroup(this.their_y,N_MINUS_2))return this.otr.error("Illegal g^y.",!0);this.createKeys(this.their_y),type="",send=HLP.packMPI(this.r),send+=this.makeM(this.their_y,this.m1,this.c,this.m2),this.m1=null,this.m2=null,this.c=null;break;case"":if(HLP.debug.call(this.otr,"signature message"),this.otr.authstate!==CONST.AUTHSTATE_AWAITING_REVEALSIG)return;msg=HLP.splitype(["DATA","DATA","MAC"],msg.msg),this.r=HLP.readMPI(msg[0]);var key=CryptoJS.enc.Hex.parse(BigInt.bigInt2str(this.r,16));key=CryptoJS.enc.Latin1.stringify(key);var gxmpi=HLP.decryptAes(this.encrypted,key,HLP.packCtr(0));gxmpi=gxmpi.toString(CryptoJS.enc.Latin1),this.their_y=HLP.readMPI(gxmpi);var hash=CryptoJS.SHA256(CryptoJS.enc.Latin1.parse(gxmpi));return HLP.compare(this.hashed,hash.toString(CryptoJS.enc.Latin1))?HLP.checkGroup(this.their_y,N_MINUS_2)?(this.createKeys(this.their_y),vsm=this.verifySignMac(msg[2],msg[1],this.m2,this.c,this.their_y,this.our_dh.publicKey,this.m1,HLP.packCtr(0)),vsm[0]?this.otr.error(vsm[0],!0):(this.their_keyid=vsm[1],this.their_priv_pk=vsm[2],send=this.makeM(this.their_y,this.m1_prime,this.c_prime,this.m2_prime),this.m1=null,this.m2=null,this.m1_prime=null,this.m2_prime=null,this.c=null,this.c_prime=null,this.sendMsg(version,"",send),void this.akeSuccess(version))):this.otr.error("Illegal g^x.",!0):this.otr.error("Hashed g^x does not match.",!0);case"":if(HLP.debug.call(this.otr,"data message"),this.otr.authstate!==CONST.AUTHSTATE_AWAITING_SIG)return;return msg=HLP.splitype(["DATA","MAC"],msg.msg),vsm=this.verifySignMac(msg[1],msg[0],this.m2_prime,this.c_prime,this.their_y,this.our_dh.publicKey,this.m1_prime,HLP.packCtr(0)),vsm[0]?this.otr.error(vsm[0],!0):(this.their_keyid=vsm[1],this.their_priv_pk=vsm[2],this.m1_prime=null,this.m2_prime=null,this.c_prime=null,this.transmittedRS=!0,void this.akeSuccess(version));default:return}this.sendMsg(version,type,send)},sendMsg:function(version,type,msg){var send=version+type,v3=version===CONST.OTR_VERSION_3;return v3&&(HLP.debug.call(this.otr,"instance tags"),send+=this.otr.our_instance_tag,send+=this.otr.their_instance_tag),send+=msg,send=HLP.wrapMsg(send,this.otr.fragment_size,v3,this.otr.our_instance_tag,this.otr.their_instance_tag),send[0]?this.otr.error(send[0]):void this.otr.io(send[1])},initiateAKE:function(version){HLP.debug.call(this.otr,"d-h commit message"),this.otr.trigger("status",[CONST.STATUS_AKE_INIT]),this.otr.authstate=CONST.AUTHSTATE_AWAITING_DHKEY;var gxmpi=HLP.packMPI(this.our_dh.publicKey);gxmpi=CryptoJS.enc.Latin1.parse(gxmpi),this.r=BigInt.randBigInt(128);var key=CryptoJS.enc.Hex.parse(BigInt.bigInt2str(this.r,16));key=CryptoJS.enc.Latin1.stringify(key),this.myhashed=CryptoJS.SHA256(gxmpi),this.myhashed=HLP.packData(this.myhashed.toString(CryptoJS.enc.Latin1)),this.dhcommit=HLP.packData(HLP.encryptAes(gxmpi,key,HLP.packCtr(0))),this.dhcommit+=this.myhashed,this.sendMsg(version,"",this.dhcommit)}}}.call(this),function(){"use strict";function SM(reqs){return this instanceof SM?(this.version=1,this.our_fp=reqs.our_fp,this.their_fp=reqs.their_fp,this.ssid=reqs.ssid,this.debug=!!reqs.debug,void this.init()):new SM(reqs)}var CryptoJS,BigInt,EventEmitter,CONST,HLP,root=this;"undefined"!=typeof module&&module.exports?(module.exports=SM,CryptoJS=require("../vendor/crypto.js"),BigInt=require("../vendor/bigint.js"),EventEmitter=require("../vendor/eventemitter.js"),CONST=require("./const.js"),HLP=require("./helpers.js")):(root.OTR.SM=SM,CryptoJS=root.CryptoJS,BigInt=root.BigInt,EventEmitter=root.EventEmitter,CONST=root.OTR.CONST,HLP=root.OTR.HLP);var G=BigInt.str2bigInt(CONST.G,10),N=BigInt.str2bigInt(CONST.N,16),N_MINUS_2=BigInt.sub(N,BigInt.str2bigInt("2",10)),Q=BigInt.sub(N,BigInt.str2bigInt("1",10));BigInt.divInt_(Q,2),HLP.extend(SM,EventEmitter),SM.prototype.init=function(){this.smpstate=CONST.SMPSTATE_EXPECT1,this.secret=null},SM.prototype.makeSecret=function(our,secret){var sha256=CryptoJS.algo.SHA256.create();sha256.update(CryptoJS.enc.Latin1.parse(HLP.packBytes(this.version,1))),sha256.update(CryptoJS.enc.Hex.parse(our?this.our_fp:this.their_fp)),sha256.update(CryptoJS.enc.Hex.parse(our?this.their_fp:this.our_fp)),sha256.update(CryptoJS.enc.Latin1.parse(this.ssid)),sha256.update(CryptoJS.enc.Latin1.parse(secret));var hash=sha256.finalize();this.secret=HLP.bits2bigInt(hash.toString(CryptoJS.enc.Latin1))},SM.prototype.makeG2s=function(){this.a2=HLP.randomExponent(),this.a3=HLP.randomExponent(),this.g2a=BigInt.powMod(G,this.a2,N),this.g3a=BigInt.powMod(G,this.a3,N),HLP.checkGroup(this.g2a,N_MINUS_2)&&HLP.checkGroup(this.g3a,N_MINUS_2)||this.makeG2s()},SM.prototype.computeGs=function(g2a,g3a){this.g2=BigInt.powMod(g2a,this.a2,N),this.g3=BigInt.powMod(g3a,this.a3,N)},SM.prototype.computePQ=function(r){this.p=BigInt.powMod(this.g3,r,N),this.q=HLP.multPowMod(G,r,this.g2,this.secret,N)},SM.prototype.computeR=function(){this.r=BigInt.powMod(this.QoQ,this.a3,N)},SM.prototype.computeRab=function(r){return BigInt.powMod(r,this.a3,N)},SM.prototype.computeC=function(v,r){return HLP.smpHash(v,BigInt.powMod(G,r,N))},SM.prototype.computeD=function(r,a,c){return BigInt.subMod(r,BigInt.multMod(a,c,Q),Q)},SM.prototype.handleSM=function(msg){var send,r2,r3,r7,t1,t2,t3,t4,rab,tmp2,cR,d7,ms,trust,expectStates={2:CONST.SMPSTATE_EXPECT1,3:CONST.SMPSTATE_EXPECT2,4:CONST.SMPSTATE_EXPECT3,5:CONST.SMPSTATE_EXPECT4,7:CONST.SMPSTATE_EXPECT1};if(6===msg.type)return this.init(),void this.trigger("abort");if(this.smpstate!==expectStates[msg.type])return this.abort();switch(this.smpstate){case CONST.SMPSTATE_EXPECT1:HLP.debug.call(this,"smp tlv 2");var ind,question;return 7===msg.type&&(ind=msg.msg.indexOf("\x00"),question=msg.msg.substring(0,ind),msg.msg=msg.msg.substring(ind+1)),ms=HLP.readLen(msg.msg.substr(0,4)),6!==ms?this.abort():(msg=HLP.unpackMPIs(6,msg.msg.substring(4)),HLP.checkGroup(msg[0],N_MINUS_2)&&HLP.checkGroup(msg[3],N_MINUS_2)&&HLP.ZKP(1,msg[1],HLP.multPowMod(G,msg[2],msg[0],msg[1],N))&&HLP.ZKP(2,msg[4],HLP.multPowMod(G,msg[5],msg[3],msg[4],N))?(this.g3ao=msg[3],this.makeG2s(),r2=HLP.randomExponent(),r3=HLP.randomExponent(),this.c2=this.computeC(3,r2),this.c3=this.computeC(4,r3),this.d2=this.computeD(r2,this.a2,this.c2),this.d3=this.computeD(r3,this.a3,this.c3),this.computeGs(msg[0],msg[3]),this.smpstate=CONST.SMPSTATE_EXPECT0,question=CryptoJS.enc.Latin1.parse(question).toString(CryptoJS.enc.Utf8),void this.trigger("question",[question])):this.abort());case CONST.SMPSTATE_EXPECT2:if(HLP.debug.call(this,"smp tlv 3"),ms=HLP.readLen(msg.msg.substr(0,4)),11!==ms)return this.abort();if(msg=HLP.unpackMPIs(11,msg.msg.substring(4)),!(HLP.checkGroup(msg[0],N_MINUS_2)&&HLP.checkGroup(msg[3],N_MINUS_2)&&HLP.checkGroup(msg[6],N_MINUS_2)&&HLP.checkGroup(msg[7],N_MINUS_2)))return this.abort();if(!HLP.ZKP(3,msg[1],HLP.multPowMod(G,msg[2],msg[0],msg[1],N)))return this.abort();if(!HLP.ZKP(4,msg[4],HLP.multPowMod(G,msg[5],msg[3],msg[4],N)))return this.abort();if(this.g3ao=msg[3],this.computeGs(msg[0],msg[3]),t1=HLP.multPowMod(this.g3,msg[9],msg[6],msg[8],N),t2=HLP.multPowMod(G,msg[9],this.g2,msg[10],N),t2=BigInt.multMod(t2,BigInt.powMod(msg[7],msg[8],N),N),!HLP.ZKP(5,msg[8],t1,t2))return this.abort();var r4=HLP.randomExponent();this.computePQ(r4);var r5=HLP.randomExponent(),r6=HLP.randomExponent(),tmp=HLP.multPowMod(G,r5,this.g2,r6,N),cP=HLP.smpHash(6,BigInt.powMod(this.g3,r5,N),tmp),d5=this.computeD(r5,r4,cP),d6=this.computeD(r6,this.secret,cP);this.QoQ=BigInt.divMod(this.q,msg[7],N),this.PoP=BigInt.divMod(this.p,msg[6],N),this.computeR(),r7=HLP.randomExponent(),tmp2=BigInt.powMod(this.QoQ,r7,N),cR=HLP.smpHash(7,BigInt.powMod(G,r7,N),tmp2),d7=this.computeD(r7,this.a3,cR),this.smpstate=CONST.SMPSTATE_EXPECT4,send=HLP.packINT(8)+HLP.packMPIs([this.p,this.q,cP,d5,d6,this.r,cR,d7]),send=HLP.packTLV(4,send);break;case CONST.SMPSTATE_EXPECT3:if(HLP.debug.call(this,"smp tlv 4"),ms=HLP.readLen(msg.msg.substr(0,4)),8!==ms)return this.abort();if(msg=HLP.unpackMPIs(8,msg.msg.substring(4)),!HLP.checkGroup(msg[0],N_MINUS_2)||!HLP.checkGroup(msg[1],N_MINUS_2)||!HLP.checkGroup(msg[5],N_MINUS_2))return this.abort();if(t1=HLP.multPowMod(this.g3,msg[3],msg[0],msg[2],N),t2=HLP.multPowMod(G,msg[3],this.g2,msg[4],N),t2=BigInt.multMod(t2,BigInt.powMod(msg[1],msg[2],N),N),!HLP.ZKP(6,msg[2],t1,t2))return this.abort();if(t3=HLP.multPowMod(G,msg[7],this.g3ao,msg[6],N),this.QoQ=BigInt.divMod(msg[1],this.q,N),t4=HLP.multPowMod(this.QoQ,msg[7],msg[5],msg[6],N),!HLP.ZKP(7,msg[6],t3,t4))return this.abort();this.computeR(),r7=HLP.randomExponent(),tmp2=BigInt.powMod(this.QoQ,r7,N),cR=HLP.smpHash(8,BigInt.powMod(G,r7,N),tmp2),d7=this.computeD(r7,this.a3,cR),send=HLP.packINT(3)+HLP.packMPIs([this.r,cR,d7]),send=HLP.packTLV(5,send),rab=this.computeRab(msg[5]),trust=!!BigInt.equals(rab,BigInt.divMod(msg[0],this.p,N)),this.trigger("trust",[trust,"answered"]),this.init();break;case CONST.SMPSTATE_EXPECT4:return HLP.debug.call(this,"smp tlv 5"),ms=HLP.readLen(msg.msg.substr(0,4)),3!==ms?this.abort():(msg=HLP.unpackMPIs(3,msg.msg.substring(4)),HLP.checkGroup(msg[0],N_MINUS_2)?(t3=HLP.multPowMod(G,msg[2],this.g3ao,msg[1],N),t4=HLP.multPowMod(this.QoQ,msg[2],msg[0],msg[1],N),HLP.ZKP(8,msg[1],t3,t4)?(rab=this.computeRab(msg[0]),trust=!!BigInt.equals(rab,this.PoP),this.trigger("trust",[trust,"asked"]),void this.init()):this.abort()):this.abort())}this.sendMsg(send)},SM.prototype.sendMsg=function(send){this.trigger("send",[this.ssid,"\x00"+send])},SM.prototype.rcvSecret=function(secret,question){HLP.debug.call(this,"receive secret");var fn,our=!1;this.smpstate===CONST.SMPSTATE_EXPECT0?fn=this.answer:(fn=this.initiate,our=!0),this.makeSecret(our,secret),fn.call(this,question)},SM.prototype.answer=function(){HLP.debug.call(this,"smp answer");var r4=HLP.randomExponent();this.computePQ(r4);var r5=HLP.randomExponent(),r6=HLP.randomExponent(),tmp=HLP.multPowMod(G,r5,this.g2,r6,N),cP=HLP.smpHash(5,BigInt.powMod(this.g3,r5,N),tmp),d5=this.computeD(r5,r4,cP),d6=this.computeD(r6,this.secret,cP);this.smpstate=CONST.SMPSTATE_EXPECT3;var send=HLP.packINT(11)+HLP.packMPIs([this.g2a,this.c2,this.d2,this.g3a,this.c3,this.d3,this.p,this.q,cP,d5,d6]);this.sendMsg(HLP.packTLV(3,send))
+},SM.prototype.initiate=function(question){HLP.debug.call(this,"smp initiate"),this.smpstate!==CONST.SMPSTATE_EXPECT1&&this.abort(),this.makeG2s();var r2=HLP.randomExponent(),r3=HLP.randomExponent();this.c2=this.computeC(1,r2),this.c3=this.computeC(2,r3),this.d2=this.computeD(r2,this.a2,this.c2),this.d3=this.computeD(r3,this.a3,this.c3),this.smpstate=CONST.SMPSTATE_EXPECT2;var send="",type=2;question&&(send+=question,send+="\x00",type=7),send+=HLP.packINT(6)+HLP.packMPIs([this.g2a,this.c2,this.d2,this.g3a,this.c3,this.d3]),this.sendMsg(HLP.packTLV(type,send))},SM.prototype.abort=function(){this.init(),this.sendMsg(HLP.packTLV(6,"")),this.trigger("abort")}}.call(this),function(){"use strict";function OTR(options){if(!(this instanceof OTR))return new OTR(options);if(options=options||{},options.priv&&!(options.priv instanceof DSA))throw new Error("Requires long-lived DSA key.");if(this.priv=options.priv?options.priv:new DSA,this.fragment_size=options.fragment_size||0,this.fragment_size<0)throw new Error("Fragment size must be a positive integer.");if(this.send_interval=options.send_interval||0,this.send_interval<0)throw new Error("Send interval must be a positive integer.");this.outgoing=[],this.our_instance_tag=options.instance_tag||OTR.makeInstanceTag(),this.debug=!!options.debug,this.smw=options.smw,this.init();var self=this;["sendMsg","receiveMsg"].forEach(function(meth){self[meth]=self[meth].bind(self)}),EventEmitter.call(this)}var CryptoJS,BigInt,EventEmitter,Worker,SMWPath,CONST,HLP,Parse,AKE,SM,DSA,root=this;"undefined"!=typeof module&&module.exports?(module.exports=OTR,CryptoJS=require("../vendor/crypto.js"),BigInt=require("../vendor/bigint.js"),EventEmitter=require("../vendor/eventemitter.js"),SMWPath=require("path").join(__dirname,"/sm-webworker.js"),CONST=require("./const.js"),HLP=require("./helpers.js"),Parse=require("./parse.js"),AKE=require("./ake.js"),SM=require("./sm.js"),DSA=require("./dsa.js"),OTR.CONST=CONST):(Object.keys(root.OTR).forEach(function(k){OTR[k]=root.OTR[k]}),root.OTR=OTR,CryptoJS=root.CryptoJS,BigInt=root.BigInt,EventEmitter=root.EventEmitter,Worker=root.Worker,SMWPath="sm-webworker.js",CONST=OTR.CONST,HLP=OTR.HLP,Parse=OTR.Parse,AKE=OTR.AKE,SM=OTR.SM,DSA=root.DSA);var G=BigInt.str2bigInt(CONST.G,10),N=BigInt.str2bigInt(CONST.N,16),MAX_INT=Math.pow(2,53)-1,MAX_UINT=Math.pow(2,31)-1;HLP.extend(OTR,EventEmitter),OTR.prototype.init=function(){this.msgstate=CONST.MSGSTATE_PLAINTEXT,this.authstate=CONST.AUTHSTATE_NONE,this.ALLOW_V2=!0,this.ALLOW_V3=!0,this.REQUIRE_ENCRYPTION=!1,this.SEND_WHITESPACE_TAG=!1,this.WHITESPACE_START_AKE=!1,this.ERROR_START_AKE=!1,Parse.initFragment(this),this.their_y=null,this.their_old_y=null,this.their_keyid=0,this.their_priv_pk=null,this.their_instance_tag="\x00\x00\x00\x00",this.our_dh=this.dh(),this.our_old_dh=this.dh(),this.our_keyid=2,this.sessKeys=[new Array(2),new Array(2)],this.storedMgs=[],this.oldMacKeys=[],this.sm=null,this._akeInit(),this.receivedPlaintext=!1},OTR.prototype._akeInit=function(){this.ake=new AKE(this),this.transmittedRS=!1,this.ssid=null},OTR.prototype._SMW=function(otr,reqs){this.otr=otr;var opts={path:SMWPath,seed:BigInt.getSeed};"object"==typeof otr.smw&&Object.keys(otr.smw).forEach(function(k){opts[k]=otr.smw[k]}),"undefined"!=typeof module&&module.exports&&(Worker=require("webworker-threads").Worker),this.worker=new Worker(opts.path);var self=this;this.worker.onmessage=function(e){var d=e.data;d&&self.trigger(d.method,d.args)},this.worker.postMessage({type:"seed",seed:opts.seed(),imports:opts.imports}),this.worker.postMessage({type:"init",reqs:reqs})},HLP.extend(OTR.prototype._SMW,EventEmitter),["handleSM","rcvSecret","abort"].forEach(function(m){OTR.prototype._SMW.prototype[m]=function(){this.worker.postMessage({type:"method",method:m,args:Array.prototype.slice.call(arguments,0)})}}),OTR.prototype._smInit=function(){var reqs={ssid:this.ssid,our_fp:this.priv.fingerprint(),their_fp:this.their_priv_pk.fingerprint(),debug:this.debug};this.smw?(this.sm&&this.sm.worker.terminate(),this.sm=new this._SMW(this,reqs)):this.sm=new SM(reqs);var self=this;["trust","abort","question"].forEach(function(e){self.sm.on(e,function(){self.trigger("smp",[e].concat(Array.prototype.slice.call(arguments)))})}),this.sm.on("send",function(ssid,send){self.ssid===ssid&&(send=self.prepareMsg(send),self.io(send))})},OTR.prototype.io=function(msg,meta){msg=[].concat(msg).map(function(m){return{msg:m,meta:meta}}),this.outgoing=this.outgoing.concat(msg);var self=this;!function send(first){if(!first){if(!self.outgoing.length)return;var elem=self.outgoing.shift();self.trigger("io",[elem.msg,elem.meta])}setTimeout(send,first?0:self.send_interval)}(!0)},OTR.prototype.dh=function(){var keys={privateKey:BigInt.randBigInt(320)};return keys.publicKey=BigInt.powMod(G,keys.privateKey,N),keys},OTR.prototype.DHSession=function DHSession(our_dh,their_y){if(!(this instanceof DHSession))return new DHSession(our_dh,their_y);var s=BigInt.powMod(their_y,our_dh.privateKey,N),secbytes=HLP.packMPI(s);this.id=HLP.mask(HLP.h2("\x00",secbytes),0,64);var sq=BigInt.greater(our_dh.publicKey,their_y),sendbyte=sq?"":"",rcvbyte=sq?"":"";this.sendenc=HLP.mask(HLP.h1(sendbyte,secbytes),0,128),this.sendmac=CryptoJS.SHA1(CryptoJS.enc.Latin1.parse(this.sendenc)),this.sendmac=this.sendmac.toString(CryptoJS.enc.Latin1),this.rcvenc=HLP.mask(HLP.h1(rcvbyte,secbytes),0,128),this.rcvmac=CryptoJS.SHA1(CryptoJS.enc.Latin1.parse(this.rcvenc)),this.rcvmac=this.rcvmac.toString(CryptoJS.enc.Latin1),this.rcvmacused=!1,this.extra_symkey=HLP.h2("ÿ",secbytes),this.send_counter=0,this.rcv_counter=0},OTR.prototype.rotateOurKeys=function(){var self=this;this.sessKeys[1].forEach(function(sk){sk&&sk.rcvmacused&&self.oldMacKeys.push(sk.rcvmac)}),this.our_old_dh=this.our_dh,this.our_dh=this.dh(),this.our_keyid+=1,this.sessKeys[1][0]=this.sessKeys[0][0],this.sessKeys[1][1]=this.sessKeys[0][1],this.sessKeys[0]=[this.their_y?new this.DHSession(this.our_dh,this.their_y):null,this.their_old_y?new this.DHSession(this.our_dh,this.their_old_y):null]},OTR.prototype.rotateTheirKeys=function(their_y){this.their_keyid+=1;var self=this;this.sessKeys.forEach(function(sk){sk[1]&&sk[1].rcvmacused&&self.oldMacKeys.push(sk[1].rcvmac)}),this.their_old_y=this.their_y,this.sessKeys[0][1]=this.sessKeys[0][0],this.sessKeys[1][1]=this.sessKeys[1][0],this.their_y=their_y,this.sessKeys[0][0]=new this.DHSession(this.our_dh,this.their_y),this.sessKeys[1][0]=new this.DHSession(this.our_old_dh,this.their_y)},OTR.prototype.prepareMsg=function(msg,esk){if(this.msgstate!==CONST.MSGSTATE_ENCRYPTED||0===this.their_keyid)return this.error("Not ready to encrypt.");var sessKeys=this.sessKeys[1][0];if(sessKeys.send_counter>=MAX_INT)return this.error("Should have rekeyed by now.");sessKeys.send_counter+=1;var ctr=HLP.packCtr(sessKeys.send_counter),send=this.ake.otr_version+"",v3=this.ake.otr_version===CONST.OTR_VERSION_3;if(v3&&(send+=this.our_instance_tag,send+=this.their_instance_tag),send+="\x00",send+=HLP.packINT(this.our_keyid-1),send+=HLP.packINT(this.their_keyid),send+=HLP.packMPI(this.our_dh.publicKey),send+=ctr.substring(0,8),Math.ceil(msg.length/8)>=MAX_UINT)return this.error("Message is too long.");var aes=HLP.encryptAes(CryptoJS.enc.Latin1.parse(msg),sessKeys.sendenc,ctr);return send+=HLP.packData(aes),send+=HLP.make1Mac(send,sessKeys.sendmac),send+=HLP.packData(this.oldMacKeys.splice(0).join("")),send=HLP.wrapMsg(send,this.fragment_size,v3,this.our_instance_tag,this.their_instance_tag),send[0]?this.error(send[0]):(esk&&this.trigger("file",["send",sessKeys.extra_symkey,esk]),send[1])},OTR.prototype.handleDataMsg=function(msg){var vt=msg.version+msg.type;this.ake.otr_version===CONST.OTR_VERSION_3&&(vt+=msg.instance_tags);var types=["BYTE","INT","INT","MPI","CTR","DATA","MAC","DATA"];msg=HLP.splitype(types,msg.msg);var ign=""===msg[0];if(this.msgstate!==CONST.MSGSTATE_ENCRYPTED||8!==msg.length)return void(ign||this.error("Received an unreadable encrypted message.",!0));var our_keyid=this.our_keyid-HLP.readLen(msg[2]),their_keyid=this.their_keyid-HLP.readLen(msg[1]);if(0>our_keyid||our_keyid>1)return void(ign||this.error("Not of our latest keys.",!0));if(0>their_keyid||their_keyid>1)return void(ign||this.error("Not of your latest keys.",!0));var their_y=their_keyid?this.their_old_y:this.their_y;if(1===their_keyid&&!their_y)return void(ign||this.error("Do not have that key."));var sessKeys=this.sessKeys[our_keyid][their_keyid],ctr=HLP.unpackCtr(msg[4]);if(ctr<=sessKeys.rcv_counter)return void(ign||this.error("Counter in message is not larger."));sessKeys.rcv_counter=ctr,vt+=msg.slice(0,6).join("");var vmac=HLP.make1Mac(vt,sessKeys.rcvmac);if(!HLP.compare(msg[6],vmac))return void(ign||this.error("MACs do not match."));sessKeys.rcvmacused=!0;var out=HLP.decryptAes(msg[5].substring(4),sessKeys.rcvenc,HLP.padCtr(msg[4]));out=out.toString(CryptoJS.enc.Latin1),our_keyid||this.rotateOurKeys(),their_keyid||this.rotateTheirKeys(HLP.readMPI(msg[3]));var ind=out.indexOf("\x00");return~ind&&(this.handleTLVs(out.substring(ind+1),sessKeys),out=out.substring(0,ind)),out=CryptoJS.enc.Latin1.parse(out),out.toString(CryptoJS.enc.Utf8)},OTR.prototype.handleTLVs=function(tlvs,sessKeys){for(var type,len,msg;tlvs.length&&(type=HLP.unpackSHORT(tlvs.substr(0,2)),len=HLP.unpackSHORT(tlvs.substr(2,2)),msg=tlvs.substr(4,len),!(msg.length<len));){switch(type){case 1:this.msgstate=CONST.MSGSTATE_FINISHED,this.trigger("status",[CONST.STATUS_END_OTR]);break;case 2:case 3:case 4:case 5:case 6:case 7:if(this.msgstate!==CONST.MSGSTATE_ENCRYPTED)return void(this.sm&&this.sm.abort());this.sm||this._smInit(),this.sm.handleSM({msg:msg,type:type});break;case 8:msg=msg.substring(4),msg=CryptoJS.enc.Latin1.parse(msg),msg=msg.toString(CryptoJS.enc.Utf8),this.trigger("file",["receive",sessKeys.extra_symkey,msg])}tlvs=tlvs.substring(4+len)}},OTR.prototype.smpSecret=function(secret,question){return this.msgstate!==CONST.MSGSTATE_ENCRYPTED?this.error("Must be encrypted for SMP."):"string"!=typeof secret||secret.length<1?this.error("Secret is required."):(this.sm||this._smInit(),secret=CryptoJS.enc.Utf8.parse(secret).toString(CryptoJS.enc.Latin1),question=CryptoJS.enc.Utf8.parse(question).toString(CryptoJS.enc.Latin1),void this.sm.rcvSecret(secret,question))},OTR.prototype.sendQueryMsg=function(){var versions={},msg=CONST.OTR_TAG;this.ALLOW_V2&&(versions[2]=!0),this.ALLOW_V3&&(versions[3]=!0);var vs=Object.keys(versions);vs.length&&(msg+="v",vs.forEach(function(v){"1"!==v&&(msg+=v)}),msg+="?"),this.io(msg),this.trigger("status",[CONST.STATUS_SEND_QUERY])},OTR.prototype.sendMsg=function(msg,meta){switch((this.REQUIRE_ENCRYPTION||this.msgstate!==CONST.MSGSTATE_PLAINTEXT)&&(msg=CryptoJS.enc.Utf8.parse(msg),msg=msg.toString(CryptoJS.enc.Latin1)),this.msgstate){case CONST.MSGSTATE_PLAINTEXT:if(this.REQUIRE_ENCRYPTION)return this.storedMgs.push({msg:msg,meta:meta}),void this.sendQueryMsg();this.SEND_WHITESPACE_TAG&&!this.receivedPlaintext&&(msg+=CONST.WHITESPACE_TAG,this.ALLOW_V3&&(msg+=CONST.WHITESPACE_TAG_V3),this.ALLOW_V2&&(msg+=CONST.WHITESPACE_TAG_V2));break;case CONST.MSGSTATE_FINISHED:return this.storedMgs.push({msg:msg,meta:meta}),void this.error("Message cannot be sent at this time.");case CONST.MSGSTATE_ENCRYPTED:msg=this.prepareMsg(msg);break;default:throw new Error("Unknown message state.")}msg&&this.io(msg,meta)},OTR.prototype.receiveMsg=function(msg){if(msg=Parse.parseMsg(this,msg)){switch(msg.cls){case"error":return void this.error(msg.msg);case"ake":if(msg.version===CONST.OTR_VERSION_3&&this.checkInstanceTags(msg.instance_tags))return;return void this.ake.handleAKE(msg);case"data":if(msg.version===CONST.OTR_VERSION_3&&this.checkInstanceTags(msg.instance_tags))return;msg.msg=this.handleDataMsg(msg),msg.encrypted=!0;break;case"query":this.msgstate===CONST.MSGSTATE_ENCRYPTED&&this._akeInit(),this.doAKE(msg);break;default:(this.REQUIRE_ENCRYPTION||this.msgstate!==CONST.MSGSTATE_PLAINTEXT)&&this.error("Received an unencrypted message."),this.receivedPlaintext=!0,this.WHITESPACE_START_AKE&&msg.ver.length>0&&this.doAKE(msg)}msg.msg&&this.trigger("ui",[msg.msg,!!msg.encrypted])}},OTR.prototype.checkInstanceTags=function(it){var their_it=HLP.readLen(it.substr(0,4)),our_it=HLP.readLen(it.substr(4,4));if(our_it&&our_it!==HLP.readLen(this.our_instance_tag))return!0;if(HLP.readLen(this.their_instance_tag)){if(HLP.readLen(this.their_instance_tag)!==their_it)return!0}else{if(100>their_it)return!0;this.their_instance_tag=HLP.packINT(their_it)}},OTR.prototype.doAKE=function(msg){this.ALLOW_V3&&~msg.ver.indexOf(CONST.OTR_VERSION_3)?this.ake.initiateAKE(CONST.OTR_VERSION_3):this.ALLOW_V2&&~msg.ver.indexOf(CONST.OTR_VERSION_2)?this.ake.initiateAKE(CONST.OTR_VERSION_2):this.error("OTR conversation requested, but no compatible protocol version found.")},OTR.prototype.error=function(err,send){return send?(this.debug||(err="An OTR error has occurred."),err="?OTR Error:"+err,void this.io(err)):void this.trigger("error",[err])},OTR.prototype.sendStored=function(){var self=this;this.storedMgs.splice(0).forEach(function(elem){var msg=self.prepareMsg(elem.msg);self.io(msg,elem.meta)})},OTR.prototype.sendFile=function(filename){if(this.msgstate!==CONST.MSGSTATE_ENCRYPTED)return this.error("Not ready to encrypt.");if(this.ake.otr_version!==CONST.OTR_VERSION_3)return this.error("Protocol v3 required.");if(!filename)return this.error("Please specify a filename.");var l1name=CryptoJS.enc.Utf8.parse(filename);if(l1name=l1name.toString(CryptoJS.enc.Latin1),l1name.length>=65532)return this.error("filename is too long.");var msg="\x00";msg+="\x00\b",msg+=HLP.packSHORT(4+l1name.length),msg+="\x00\x00\x00",msg+=l1name,msg=this.prepareMsg(msg,filename),this.io(msg)},OTR.prototype.endOtr=function(){this.msgstate===CONST.MSGSTATE_ENCRYPTED&&(this.sendMsg("\x00\x00\x00\x00"),this.sm&&(this.smw&&this.sm.worker.terminate(),this.sm=null)),this.msgstate=CONST.MSGSTATE_PLAINTEXT,this.receivedPlaintext=!1,this.trigger("status",[CONST.STATUS_END_OTR])},OTR.makeInstanceTag=function(){var num=BigInt.randBigInt(32);return BigInt.greater(BigInt.str2bigInt("100",16),num)?OTR.makeInstanceTag():HLP.packINT(parseInt(BigInt.bigInt2str(num,10),10))}}.call(this),{OTR:this.OTR,DSA:this.DSA}});
+
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/otr.min.js_README	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,10 @@
+The file otr.min.js includes:
+    - minified versions of the following otr.js dependencies:
+      otr/dep/bigint.js otr/dep/crypto.js otr/dep/eventemitter.js
+      The following minification tool has been used: yui compressor
+    - minified version of otr.js taken from the project homepage
+
+All original files can be retrieved from otr.js repository:
+    https://github.com/arlolra/otr/tree/master/build
+
+See the README file of Libervia, or inside otr.min.js itself for licence information.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/public/contrat_social.html	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,110 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html><head>
+  
+  <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type">
+  <title>Salut  Toi: Contrat Social</title>
+
+  
+</head><body>
+Le projet  Salut  Toi  est n d'un besoin de protection de nos
+liberts, de notre vie prive et de notre indpendance. Il se veut
+garant des droits et liberts qu'un utilisateur a vis  vis de ses
+propres informations, des informations numriques sur sa vie ou celles
+de ses connaissances, des donnes qu'il manipule; et se veut galement
+un point de contact humain, ne se substituant pas aux rapports rels,
+mais au contraire les facilitant.<br>
+
+Salut  Toi lutte et luttera toujours contre toute forme de main mise
+sur les technologies par des intrts privs. Le rseau global doit
+appartenir  tous, et tre un point d'expression et de libert pour
+l'Humanit.<br>
+
+<br>
+
+ ce titre,  Salut  Toi  et ceux qui y participent se basent sur un
+contrat social, un engagement vis  vis de ceux qui l'utilisent. Ce
+contrat consiste en les points suivants:<br>
+
+<ul>
+
+  <li>nous plaons la <span style="font-style: italic;">Libert</span> en tte de nos priorits: libert de
+l'utilisateur, libert vis  vis de ses donnes. Pour cela,  Salut 
+Toi  est un logiciel Libre - condition essentielle -, et son
+infrastructure se base galement sur des logiciels Libres, c'est  dire
+des logiciels qui respectent ces 4 liberts fondamentales
+    <ul>
+
+    <li>la libert d'excuter le programme, pour tous les usages,</li>
+  
+    </ul>
+    <ul>
+
+    <li>la libert d'tudier le fonctionnement du programme et de
+l'adapter  ses besoins,</li>
+  
+    </ul>
+    <ul>
+
+    <li>la libert de redistribuer des copies du programme,</li>
+  
+    </ul>
+    <ul>
+
+    <li>la libert d'amliorer le programme et de distribuer ces
+amliorations au public.<br>
+</li>
+  
+    </ul>
+</li>
+  
+  
+  
+  
+
+Vous avez ainsi la possibilit d'installer votre propre version de 
+Salut  Toi  sur votre propre machine, d'en vrifier - et de
+comprendre - ainsi son fonctionnement, de l'adapter  vos besoins, d'en
+faire profiter vos amis.
+
+  <li>Les informations vous concernant vous appartiennent, et nous
+n'aurons pas la prtention - et l'indcence ! - de considrer le
+contenu que vous produisez ou faites circuler via  Salut  Toi  comme
+nous appartenant. De mme, nous nous engageons  ne jamais faire de
+profit en revendant vos informations personnelles.</li>
+  <li>Nous incitons fortement  la <span style="text-decoration: underline;">dcentralisation gnralise</span>. 
+Salut  Toi  tant bas sur un protocole dcentralis (XMPP), il l'est
+lui-mme par nature. La dcentralisation est essentielle pour une
+meilleure protection de vos informations, une meilleure rsistance  la
+censure ou aux pannes, et pour viter les drives autoritaires.</li>
+  <li>Luttant contre les tentatives de contrle priv et les abus
+commerciaux du rseau global, et afin de garder notre indpendance,
+nous nous refusons  toute forme de publicit: vous ne verrez <span style="font-weight: bold;">jamais</span>
+de forme de rclame commerciale de notre fait.</li>
+  <li>L'<span style="font-style: italic;">galit</span> des utilisateurs est essentielle pour nous, nous
+refusons toute forme de discrimination, que ce soit pour une zone
+gographique, une catgorie de la population, ou tout autre raison.</li>
+  <li>Nous ferons tout notre possible pour lutter contre toute
+tentative de censure. Le rseau global doit tre un moyen d'expression
+pour tous.</li>
+  <li>Nous refusons toute ide d'autorit absolue en ce qui concerne
+les dcisions prises pour  Salut  Toi  et son fonctionnement, et le
+choix de la dcentralisation et l'utilisation de logiciel Libre permet
+de lutter contre toute forme de hirarchie.</li>
+  
+  <li>L'ide de <span style="font-style: italic;">Fraternit</span> est essentielle, aussi:
+    <ul>
+      <li>nous ferons notre
+possible pour aider les utilisateurs, quel que soit leur niveau</li>
+      <li>de mme, des efforts seront fait quant 
+l'accessibilit pour tous</li>
+      <li> Salut  Toi ,
+XMPP, et les technologies utilises facilitent les changes
+lectroniques, mais nous dsirons mettre l'accent sur les rencontres
+relles et humaines: nous favoriserons toujours le rel sur le virtuel.</li>
+    </ul>
+</li>
+  
+  
+</ul>
+
+</body></html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/public/favico.min.js	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,9 @@
+/**
+ * This file is distributed with Libervia and is sublicensed under AGPL v3 (or any later version) as allowed by its original license.
+ *
+ * @license MIT
+ * @fileOverview Favico animations
+ * @author Miroslav Magda, http://blog.ejci.net
+ * @version 0.3.9
+ */
+!function(){var e=function(e){"use strict";function t(e){if(e.paused||e.ended||g)return!1;try{f.clearRect(0,0,s,l),f.drawImage(e,0,0,s,l)}catch(o){}p=setTimeout(t,S.duration,e),O.setIcon(h)}function o(e){var t=/^#?([a-f\d])([a-f\d])([a-f\d])$/i;e=e.replace(t,function(e,t,o,n){return t+t+o+o+n+n});var o=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return o?{r:parseInt(o[1],16),g:parseInt(o[2],16),b:parseInt(o[3],16)}:!1}function n(e,t){var o,n={};for(o in e)n[o]=e[o];for(o in t)n[o]=t[o];return n}function r(){return b.hidden||b.msHidden||b.webkitHidden||b.mozHidden}e=e?e:{};var i,a,l,s,h,f,c,d,u,y,w,g,x,m,p,b,v={bgColor:"#d00",textColor:"#fff",fontFamily:"sans-serif",fontStyle:"bold",type:"circle",position:"down",animation:"slide",elementId:!1,dataUrl:!1,win:window};x={},x.ff="undefined"!=typeof InstallTrigger,x.chrome=!!window.chrome,x.opera=!!window.opera||navigator.userAgent.indexOf("Opera")>=0,x.ie=/*@cc_on!@*/!1,x.safari=Object.prototype.toString.call(window.HTMLElement).indexOf("Constructor")>0,x.supported=x.chrome||x.ff||x.opera;var C=[];w=function(){},d=g=!1;var E=function(){i=n(v,e),i.bgColor=o(i.bgColor),i.textColor=o(i.textColor),i.position=i.position.toLowerCase(),i.animation=S.types[""+i.animation]?i.animation:v.animation,b=i.win.document;var t=i.position.indexOf("up")>-1,r=i.position.indexOf("left")>-1;if(t||r)for(var d=0;d<S.types[""+i.animation].length;d++){var u=S.types[""+i.animation][d];t&&(u.y=u.y<.6?u.y-.4:u.y-2*u.y+(1-u.w)),r&&(u.x=u.x<.6?u.x-.4:u.x-2*u.x+(1-u.h)),S.types[""+i.animation][d]=u}i.type=A[""+i.type]?i.type:v.type,a=O.getIcon(),h=document.createElement("canvas"),c=document.createElement("img"),a.hasAttribute("href")?(c.setAttribute("crossOrigin","anonymous"),c.setAttribute("src",a.getAttribute("href")),c.onload=function(){l=c.height>0?c.height:32,s=c.width>0?c.width:32,h.height=l,h.width=s,f=h.getContext("2d"),M.ready()}):(c.setAttribute("src",""),l=32,s=32,c.height=l,c.width=s,h.height=l,h.width=s,f=h.getContext("2d"),M.ready())},M={};M.ready=function(){d=!0,M.reset(),w()},M.reset=function(){d&&(C=[],u=!1,y=!1,f.clearRect(0,0,s,l),f.drawImage(c,0,0,s,l),O.setIcon(h),window.clearTimeout(m),window.clearTimeout(p))},M.start=function(){if(d&&!y){var e=function(){u=C[0],y=!1,C.length>0&&(C.shift(),M.start())};if(C.length>0){y=!0;var t=function(){["type","animation","bgColor","textColor","fontFamily","fontStyle"].forEach(function(e){e in C[0].options&&(i[e]=C[0].options[e])}),S.run(C[0].options,function(){e()},!1)};u?S.run(u.options,function(){t()},!0):t()}}};var A={},I=function(e){return e.n="number"==typeof e.n?Math.abs(0|e.n):e.n,e.x=s*e.x,e.y=l*e.y,e.w=s*e.w,e.h=l*e.h,e.len=(""+e.n).length,e};A.circle=function(e){e=I(e);var t=!1;2===e.len?(e.x=e.x-.4*e.w,e.w=1.4*e.w,t=!0):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w,t=!0),f.clearRect(0,0,s,l),f.drawImage(c,0,0,s,l),f.beginPath(),f.font=i.fontStyle+" "+Math.floor(e.h*(e.n>99?.85:1))+"px "+i.fontFamily,f.textAlign="center",t?(f.moveTo(e.x+e.w/2,e.y),f.lineTo(e.x+e.w-e.h/2,e.y),f.quadraticCurveTo(e.x+e.w,e.y,e.x+e.w,e.y+e.h/2),f.lineTo(e.x+e.w,e.y+e.h-e.h/2),f.quadraticCurveTo(e.x+e.w,e.y+e.h,e.x+e.w-e.h/2,e.y+e.h),f.lineTo(e.x+e.h/2,e.y+e.h),f.quadraticCurveTo(e.x,e.y+e.h,e.x,e.y+e.h-e.h/2),f.lineTo(e.x,e.y+e.h/2),f.quadraticCurveTo(e.x,e.y,e.x+e.h/2,e.y)):f.arc(e.x+e.w/2,e.y+e.h/2,e.h/2,0,2*Math.PI),f.fillStyle="rgba("+i.bgColor.r+","+i.bgColor.g+","+i.bgColor.b+","+e.o+")",f.fill(),f.closePath(),f.beginPath(),f.stroke(),f.fillStyle="rgba("+i.textColor.r+","+i.textColor.g+","+i.textColor.b+","+e.o+")","number"==typeof e.n&&e.n>999?f.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):f.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),f.closePath()},A.rectangle=function(e){e=I(e);var t=!1;2===e.len?(e.x=e.x-.4*e.w,e.w=1.4*e.w,t=!0):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w,t=!0),f.clearRect(0,0,s,l),f.drawImage(c,0,0,s,l),f.beginPath(),f.font=i.fontStyle+" "+Math.floor(e.h*(e.n>99?.9:1))+"px "+i.fontFamily,f.textAlign="center",f.fillStyle="rgba("+i.bgColor.r+","+i.bgColor.g+","+i.bgColor.b+","+e.o+")",f.fillRect(e.x,e.y,e.w,e.h),f.fillStyle="rgba("+i.textColor.r+","+i.textColor.g+","+i.textColor.b+","+e.o+")","number"==typeof e.n&&e.n>999?f.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):f.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),f.closePath()};var T=function(e,t){t=("string"==typeof t?{animation:t}:t)||{},w=function(){try{if("number"==typeof e?e>0:""!==e){var n={type:"badge",options:{n:e}};if("animation"in t&&S.types[""+t.animation]&&(n.options.animation=""+t.animation),"type"in t&&A[""+t.type]&&(n.options.type=""+t.type),["bgColor","textColor"].forEach(function(e){e in t&&(n.options[e]=o(t[e]))}),["fontStyle","fontFamily"].forEach(function(e){e in t&&(n.options[e]=t[e])}),C.push(n),C.length>100)throw new Error("Too many badges requests in queue.");M.start()}else M.reset()}catch(r){throw new Error("Error setting badge. Message: "+r.message)}},d&&w()},U=function(e){w=function(){try{var t=e.width,o=e.height,n=document.createElement("img"),r=o/l>t/s?t/s:o/l;n.setAttribute("crossOrigin","anonymous"),n.setAttribute("src",e.getAttribute("src")),n.height=o/r,n.width=t/r,f.clearRect(0,0,s,l),f.drawImage(n,0,0,s,l),O.setIcon(h)}catch(i){throw new Error("Error setting image. Message: "+i.message)}},d&&w()},R=function(e){w=function(){try{if("stop"===e)return g=!0,M.reset(),void(g=!1);e.addEventListener("play",function(){t(this)},!1)}catch(o){throw new Error("Error setting video. Message: "+o.message)}},d&&w()},L=function(e){if(window.URL&&window.URL.createObjectURL||(window.URL=window.URL||{},window.URL.createObjectURL=function(e){return e}),x.supported){var o=!1;navigator.getUserMedia=navigator.getUserMedia||navigator.oGetUserMedia||navigator.msGetUserMedia||navigator.mozGetUserMedia||navigator.webkitGetUserMedia,w=function(){try{if("stop"===e)return g=!0,M.reset(),void(g=!1);o=document.createElement("video"),o.width=s,o.height=l,navigator.getUserMedia({video:!0,audio:!1},function(e){o.src=URL.createObjectURL(e),o.play(),t(o)},function(){})}catch(n){throw new Error("Error setting webcam. Message: "+n.message)}},d&&w()}},O={};O.getIcon=function(){var e=!1,t=function(){for(var e=b.getElementsByTagName("head")[0].getElementsByTagName("link"),t=e.length,o=t-1;o>=0;o--)if(/(^|\s)icon(\s|$)/i.test(e[o].getAttribute("rel")))return e[o];return!1};return i.element?e=i.element:i.elementId?(e=b.getElementById(i.elementId),e.setAttribute("href",e.getAttribute("src"))):(e=t(),e===!1&&(e=b.createElement("link"),e.setAttribute("rel","icon"),b.getElementsByTagName("head")[0].appendChild(e))),e.setAttribute("type","image/png"),e},O.setIcon=function(e){var t=e.toDataURL("image/png");if(i.dataUrl&&i.dataUrl(t),i.element)i.element.setAttribute("href",t),i.element.setAttribute("src",t);else if(i.elementId){var o=b.getElementById(i.elementId);o.setAttribute("href",t),o.setAttribute("src",t)}else if(x.ff||x.opera){var n=a;a=b.createElement("link"),x.opera&&a.setAttribute("rel","icon"),a.setAttribute("rel","icon"),a.setAttribute("type","image/png"),b.getElementsByTagName("head")[0].appendChild(a),a.setAttribute("href",t),n.parentNode&&n.parentNode.removeChild(n)}else a.setAttribute("href",t)};var S={};return S.duration=40,S.types={},S.types.fade=[{x:.4,y:.4,w:.6,h:.6,o:0},{x:.4,y:.4,w:.6,h:.6,o:.1},{x:.4,y:.4,w:.6,h:.6,o:.2},{x:.4,y:.4,w:.6,h:.6,o:.3},{x:.4,y:.4,w:.6,h:.6,o:.4},{x:.4,y:.4,w:.6,h:.6,o:.5},{x:.4,y:.4,w:.6,h:.6,o:.6},{x:.4,y:.4,w:.6,h:.6,o:.7},{x:.4,y:.4,w:.6,h:.6,o:.8},{x:.4,y:.4,w:.6,h:.6,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],S.types.none=[{x:.4,y:.4,w:.6,h:.6,o:1}],S.types.pop=[{x:1,y:1,w:0,h:0,o:1},{x:.9,y:.9,w:.1,h:.1,o:1},{x:.8,y:.8,w:.2,h:.2,o:1},{x:.7,y:.7,w:.3,h:.3,o:1},{x:.6,y:.6,w:.4,h:.4,o:1},{x:.5,y:.5,w:.5,h:.5,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],S.types.popFade=[{x:.75,y:.75,w:0,h:0,o:0},{x:.65,y:.65,w:.1,h:.1,o:.2},{x:.6,y:.6,w:.2,h:.2,o:.4},{x:.55,y:.55,w:.3,h:.3,o:.6},{x:.5,y:.5,w:.4,h:.4,o:.8},{x:.45,y:.45,w:.5,h:.5,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],S.types.slide=[{x:.4,y:1,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.8,w:.6,h:.6,o:1},{x:.4,y:.7,w:.6,h:.6,o:1},{x:.4,y:.6,w:.6,h:.6,o:1},{x:.4,y:.5,w:.6,h:.6,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],S.run=function(e,t,o,a){var l=S.types[r()?"none":i.animation];return a=o===!0?"undefined"!=typeof a?a:l.length-1:"undefined"!=typeof a?a:0,t=t?t:function(){},a<l.length&&a>=0?(A[i.type](n(e,l[a])),m=setTimeout(function(){o?a-=1:a+=1,S.run(e,t,o,a)},S.duration),O.setIcon(h),void 0):void t()},E(),{badge:T,video:R,image:U,webcam:L,reset:M.reset,browser:{supported:x.supported}}};"undefined"!=typeof define&&define.amd?define([],function(){return e}):"undefined"!=typeof module&&module.exports?module.exports=e:this.Favico=e}();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/public/libervia.css	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,1687 @@
+/*
+Libervia: a Salut à Toi frontend
+Copyright (C) 2011-2016  Jérôme Poisson <goffi@goffi.org>
+Copyright (C) 2011  Adrien Vigneron <adrienvigneron@mailoo.org>
+Copyright (C) 2013-2016  Adrien Cossa <souliane@mailoo.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+
+/*
+ * CSS Reset: see http://pyjs.org/wiki/csshellandhowtodealwithit/
+ */
+
+/* reset/default styles */
+
+html, body, div, span, applet, object, iframe,
+p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, font, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center, dl, dt, dd, li,
+fieldset, form, label, legend, table, caption,
+tbody, tfoot, thead, tr, th, td {
+    margin: 0;
+    padding: 0;
+    border: 0;
+    outline: 0;
+    font-size: 100%;
+    vertical-align: baseline;
+    background: transparent;
+    color: #444;
+}
+
+/* styles for displaying rich text - START */
+h1, h2, h3, h4, h5, h6 {
+    margin: 0;
+    padding: 0;
+    border: 0;
+    outline: 0;
+    vertical-align: baseline;
+    background: transparent;
+    color: #444;
+    border-bottom: 1px solid rgb(170, 170, 170);
+    margin-bottom: 0.6em;
+}
+ol, ul {
+    margin: 0;
+    border: 0;
+    outline: 0;
+    font-size: 100%;
+    vertical-align: baseline;
+    background: transparent;
+    color: #444;
+}
+a:link {
+    color: blue;
+}
+.bubble p {
+    margin: 0.4em 0em;
+}
+.bubble img {
+    /* /!\ setting a max-width percentage value affects the toolbar icons */
+    max-width: 600px;
+}
+
+/* styles for displaying rich text - END */
+
+blockquote, q { quotes: none; }
+
+blockquote:before, blockquote:after,
+q:before, q:after {
+    content: '';
+    content: none;
+}
+
+:focus { outline: 0; }
+ins { text-decoration: none; }
+del { text-decoration: line-through; }
+
+table {
+    border-collapse: collapse;
+    border-spacing: 0;
+}
+
+/* pyjamas iframe hide */
+iframe { position: absolute; }
+
+
+html, body {
+    width: 100%;
+    height: 100%;
+    min-height: 100%;
+
+}
+
+body {
+    line-height: 1em;
+    font-size: 1em;
+    overflow: auto;
+
+}
+
+.scrollpanel {
+    margin-bottom: -10000px;
+
+}
+
+.iescrollpanelfix {
+    position: relative;
+    top: 100%;
+    margin-bottom: -10000px;
+
+}
+
+/* undo part of the above (non-IE) */
+html>body .iescrollpanelfix { position: static; }
+
+/* CSS Reset END */
+
+body {
+    background-color: #fff;
+    font: normal 0.8em/1.5em Arial, Helvetica, sans-serif;
+}
+
+.header {
+    background-color: #eee;
+    border-bottom: 1px solid #ddd;
+    width: 100%;
+    height: 64px;
+}
+
+.mainPanel {
+    width: 100%;
+    height: 100%;
+}
+
+.mainMenuBar {
+    background-color: #222;
+    background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222));
+    background: -webkit-linear-gradient(top, #444444, #222222);
+    background: linear-gradient(to bottom, #444444, #222222);
+    height: 28px;
+    padding: 5px 5px 0 5px;    
+    border: 1px solid #ddd;
+    border-radius: 0 0 1em 1em;
+    line-height: 100%;
+    -webkit-box-shadow: 0px 1px 4px #000;
+    box-shadow: 0px 1px 4px #000;
+    display: inline-block;
+    position: absolute;
+    left: 20px;
+    right: 20px;
+    width: auto;
+}
+
+.mainMenuBar .gwt-MenuItem {
+    padding: 3px 15px;
+    text-decoration: none;    
+    font-weight: bold;
+    height: 100%;
+    color: #e7e5e5;
+    border-radius: 1em 1em 1em 1em;
+    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); 
+    -webkit-transition: color 0.2s linear; 
+    transition: color 0.2s linear;
+}
+
+.mainMenuBar .gwt-MenuItem-selected {
+    background-color: #eee;
+    background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa));
+    background: -webkit-linear-gradient(top, #eee, #aaa);
+    background: linear-gradient(to bottom, #eee, #aaa);
+    color: #444;
+    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
+}
+
+/* Menu bars and items */
+
+.gwt-MenuBar {
+    /* Common to all menu bars */
+    margin: 0;
+}
+
+.gwt-MenuBar table {
+    /* Common to all tables within a menu bar */
+    width: 100%;
+    display: inline-table;
+}
+
+.gwt-MenuBar-horizontal {
+    /* Specific to horizontal menu bars*/
+}
+
+.gwt-MenuBar-vertical {
+    /* Specific to vertical menu bars*/
+    background-color: #fff;
+    background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc));
+    background: -webkit-linear-gradient(top, #fff, #ccc);
+    background: linear-gradient(to bottom, #fff, #ccc);
+    height: 100%;
+    min-width: 148px;
+    padding: 0;
+    border: solid 1px #aaa;
+    border-radius: 0 0 10px 10px;
+    -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
+    box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
+}
+
+.gwt-MenuItem img {
+    /* Common to all images within a menu item */
+    padding-right: 2px;
+}
+
+.gwt-MenuBar .gwt-MenuItem {
+    /* Common to items of all menu bars */
+}
+
+.gwt-MenuBar-horizontal .gwt-MenuItem {
+    /* Specific to items of horizontal menu bars*/
+}
+
+.gwt-MenuBar-vertical .gwt-MenuItem {
+    /* Specific to items of vertical menu bars*/
+    padding: 8px 15px;
+}
+
+.gwt-MenuBar .gwt-MenuItem-selected {
+    /* Common to all selected items */
+    cursor: pointer;
+}
+
+.gwt-MenuBar-horizontal .gwt-MenuItem-selected {
+    /* Specific to selected items of horizontal menu bars */
+}
+
+.gwt-MenuBar-vertical .gwt-MenuItem-selected {
+    /* Specific to selected items of vertical menu bars */
+    background: #cf2828 !important;
+    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)) !important;
+    background: -webkit-linear-gradient(top, #cf2828, #981a1a) !important;
+    background: linear-gradient(to bottom, #cf2828, #981a1a) !important;
+    color: #fff !important;
+    border-radius: 0 0 0 0;
+    text-shadow: 0 1px 1px rgba(0, 0, 0, .1);
+    -webkit-transition: color 0.2s linear;
+    transition: color 0.2s linear; 
+}
+
+.gwt-MenuBar-vertical tr:last-child td {
+    /* Specific to last items of vertical menus */
+    border-radius: 0 0 9px 9px !important;
+}
+
+.menuLastPopup .gwt-MenuBar-vertical {
+    /* Specific to the last popup menu of the main menu bar */
+    border-top-right-radius: 9px 9px;
+}
+
+.menuLastPopup .gwt-MenuBar-vertical tr:first-child td {
+    /* Specific to the first item of the last popup menu of the main menu bar */
+    border-radius: 0px 9px 0px 0px !important;
+}
+
+.menuSeparator {
+    width: 100%;
+}
+
+.menuSeparator.gwt-MenuItem-selected {
+    border: 0;
+    background: inherit;
+    cursor: default;
+}
+
+.menuFlattenedCategory {
+    font-weight: bold;
+    font-style: italic;
+    padding: 8px 5px;
+    cursor: default;
+}
+
+.menuFlattenedCategory.gwt-MenuItem-selected {
+    /* !important are needed for the style to not be overwritten when the item is selected */
+    background-color: inherit !important;
+    background: inherit !important;
+    color: #444 !important;
+    cursor: default !important;
+}
+
+/* Misc Pyjamas stuff */
+
+.gwt-DialogBox {
+    padding: 10px;
+    border: 1px solid #aaa;
+    background-color: #fff;
+    background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc));
+    background: -webkit-linear-gradient(top, #fff, #ccc);
+    background: linear-gradient(to bottom, #fff, #ccc);
+    border-radius: 9px 9px 9px 9px; 
+    -webkit-box-shadow: 0px 1px 4px #000; 
+    box-shadow: 0px 1px 4px #000;
+}
+
+.gwt-DialogBox .Caption {
+    height: 20px;
+    font-size: 1.3em !important;
+    background-color: #cf2828;
+    background: #cf2828 !important;
+    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)) !important;
+    background: -webkit-linear-gradient(top, #cf2828, #981a1a) !important;
+    background: linear-gradient(to bottom, #cf2828, #981a1a) !important;
+    color: #fff;
+    padding: 3px 3px 4px 3px;
+    margin: -10px;
+    margin-bottom: 5px;
+    font-weight: bold;
+    cursor: default;
+    text-align: center;
+    border-radius: 7px 7px 0 0; 
+}
+
+/*DIALOG: button, listbox, textbox, label */
+
+.gwt-DialogBox .gwt-button {
+    background-color: #ccc;
+    border-radius: 5px 5px 5px 5px;
+    -webkit-box-shadow: 0px 1px 4px #000;
+    box-shadow: 0px 1px 4px #000;
+    background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222));
+    background: -webkit-linear-gradient(top, #444, #222);
+    background: linear-gradient(to bottom, #444, #222);
+    text-shadow: 1px 1px 1px rgba(0,0,0,0.2);
+    padding: 3px 5px 3px 5px;
+    margin: 10px 5px 10px 5px;
+    font-weight: bold;
+    font-size: 1em;
+    border: none; 
+    -webkit-transition: color 0.2s linear; 
+    transition: color 0.2s linear;
+}
+
+.gwt-DialogBox .gwt-button:enabled {
+    cursor: pointer;
+    color: #fff;
+}
+
+.gwt-DialogBox .gwt-button:enabled:hover {
+    background-color: #cf2828;
+    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a));
+    background: -webkit-linear-gradient(top, #cf2828, #981a1a);
+    background: linear-gradient(to bottom, #cf2828, #981a1a);
+    color: #fff;
+    text-shadow: 1px 1px 1px rgba(0,0,0,0.25);  
+}
+
+.gwt-DialogBox .gwt-TextBox, .gwt-DialogBox .gwt-PasswordTextBox {
+    background-color: #fff;
+    border-radius: 5px 5px 5px 5px;
+    -webkit-box-shadow:inset 0px 1px 4px #000;
+    box-shadow:inset 0px 1px 4px #000;
+    padding: 3px 5px 3px 5px;
+    margin: 10px 5px 10px 5px;
+    color: #444;
+    font-size: 1em;
+    border: none;
+}
+
+.gwt-DialogBox .gwt-TextArea {
+    background-color: #fff;
+    border-radius: 5px 5px 5px 5px;
+    -webkit-box-shadow:inset 0px 1px 4px #000;
+    box-shadow:inset 0px 1px 4px #000;
+    padding: 3px 5px 3px 5px;
+    margin: 0px 5px 10px 5px;
+    color: #444;
+    border: none;
+    vertical-align: text-top;
+}
+
+.gwt-DialogBox .gwt-ListBox {
+    overflow: auto;
+    width: 100%;
+    background-color: #fff;
+    border-radius: 5px 5px 5px 5px;
+    -webkit-box-shadow:inset 0px 1px 4px #000;
+    box-shadow:inset 0px 1px 4px #000;
+    padding: 3px 5px 3px 5px;
+    margin: 9px 5px 9px 5px;
+    color: #444;
+    font-size: 1em;
+    border: none;
+}
+
+.gwt-DialogBox .gwt-Label {
+    margin-top: 13px;
+}
+
+.gwt-DialogBox .gwt-CheckBox {
+    margin-top: 12px;
+    display: block;
+}
+
+.gwt-DialogBox .gwt-RadioButton {
+    margin-top: 13px;
+    display: block;
+}
+
+.gwt-DialogBox .gwt-RadioButton label {
+    vertical-align: bottom;
+}
+
+.gwt-DialogBox tr td:first-child {
+    vertical-align: top !important;
+}
+
+/* Custom Dialogs */
+
+.formWarning { /* used when a form is not valid and must be corrected before submission */
+    font-weight: bold;
+    color: lightcoral !important;
+    height: 34px;  /* a higher value will screw up the display of registration tab, check before you modify */
+    text-align: center;
+}
+
+.formInfo { /* used when a form is being edited and we want to tell something to the user */
+    color: lightcyan !important;
+}
+
+.contactsChooser {
+    text-align: center;
+    margin:auto;
+    cursor: pointer;
+}
+
+.infoDialogBody {
+    width: 100%;
+    height: 100%
+}
+/* Contact List */
+
+div.contactList {
+    width: 100%;
+    margin-top: 9px;
+}
+
+.contactTitle {
+    color: #cf2828;
+    font-size: 1.7em;
+    text-indent: 5px;
+    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+    width: 200px;
+    height: 30px; 
+}
+
+.contactsSwitch {
+    /* Button used to switch contacts panel */
+    background: none;
+    border: 0;
+    padding: 0;
+    font-size: large;
+    margin-top: 9px;
+}
+
+.groupPanel {
+    width: 100%;    
+}
+
+.groupPanel tr:first-child td {
+    padding-top: 10px;
+}
+
+.group {
+    curser: pointer;
+    padding: 2px 15px;
+    margin: 5px;
+    display: inline-block;
+    text-decoration: none;
+    font-weight: bold; 
+    color: #e7e5e5;
+    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); 
+    border-radius: 1em 1em 1em 1em; 
+    background-color: #eee;
+    background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa));
+    background: -webkit-linear-gradient(top, #eee, #aaa);
+    background: linear-gradient(to bottom, #eee, #aaa);
+    color: #444;
+    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
+    -webkit-box-shadow: 0px 1px 1px #000;
+    box-shadow: 0px 1px 1px #000;
+}
+
+div.group:hover {
+    color: #fff;
+    text-shadow: 0 1px 0 rgba(0, 0, 0, 0.6);
+    background-color: #cf2828;
+    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a));
+    background: -webkit-linear-gradient(top, #cf2828, #981a1a);
+    background: linear-gradient(to bottom, #cf2828, #981a1a); 
+    -webkit-transition: color 0.1s linear; 
+    transition: color 0.1s linear;  
+}
+
+.contactBox {
+    cursor: pointer;
+    width: 100%;
+    margin: 5px;
+    border-radius: 5px;
+    background: #EDEDED;
+}
+
+.contactBox img, .muc_contact img {
+    width: 32px;
+    height: 32px;
+    border-radius: 5px;
+    margin: 5px 5px 0px 10px;
+}
+
+.contactBox .widgetHeader_buttonGroup {
+    float: left;
+}
+
+.contactBox .widgetHeader_buttonGroup img {
+    width: 32px;
+    height: 32px;
+    border-radius: 5px;
+    border: 1px solid #ededed;
+    padding: 0px 0px 0px 0px;
+    background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa));
+    background: -webkit-linear-gradient(top, #eee, #aaa);
+    background: linear-gradient(to bottom, #eee, #aaa);
+}
+
+.contactBox .widgetHeader_buttonGroup img:hover {
+    border: 1px solid #cf2828;
+}
+
+.contactBox table {
+    width: 100%;
+}
+
+.contactLabel {
+    font-size: 1em;
+    margin-top: 3px;
+    padding: 3px 10px 3px 10px;
+}
+
+.contact-menu-selected {
+    font-size: 1em;
+    margin-top: 3px;
+    padding: 3px 10px 3px 10px;
+    border-radius: 5px;
+    background-color: rgb(175, 175, 175);
+}
+
+.gwt-ScrollPanel {
+    padding-right: 15px; /* avoid systematic horizontal scroll when only the vertical one is needed */
+}
+
+.xmlui-JidsListWidget {
+    padding-right: 20px; /* avoid systematic horizontal scroll when only the vertical one is needed */
+    height: 300px;
+}
+
+/* Contacts in MUC */
+
+.muc_contact {
+    border-radius: 5px;
+    background: #EDEDED;
+    margin: 2px;
+    width: 100%;
+}
+
+/* START - contact presence status */
+.contactLabel-connected {
+    color: #3c7e0c;
+    font-weight: bold;
+}
+.contactLabel-unavailable {
+}
+.contactLabel-chat {
+    color: #3c7e0c;
+    font-weight: bold;
+}
+.contactLabel-away {
+    color: brown;
+    font-weight: bold;
+}
+.contactLabel-dnd {
+    color: red;
+    font-weight: bold;
+}
+.contactLabel-xa {
+    color: red;
+    font-weight: bold;
+}
+/* END - contact presence status */
+
+.selected {
+    color: #fff;
+    background-color: #cf2828;
+    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a));
+    background: -webkit-linear-gradient(top, #cf2828, #981a1a);
+    background: linear-gradient(to bottom, #cf2828, #981a1a); 
+    border-radius: 1em 1em 1em 1em; 
+    -webkit-transition: color 0.2s linear; 
+    transition: color 0.2s linear;
+}
+
+.messageBox {
+    width: 100%;
+    padding: 5px;
+    border: 1px solid #bbb;
+    color: #444;
+    background: #fff url('media/libervia/unibox_2.png') right bottom no-repeat;
+    -webkit-box-shadow:inset 0 0 10px #ddd;
+    box-shadow:inset 0 0 10px #ddd;
+    border-radius: 0px 0px 10px 10px;
+    height: 28px;
+    margin: 0px;
+}
+
+.presenceStatusPanel {
+    margin: auto;
+    text-align: center;
+    padding: 5px 0px;
+    text-shadow: 0 -1px 1px rgba(255,255,255,0.25);
+    font-size: 1.2em;
+    background-color: #eee;
+    font-style: italic;
+    font-weight: bold;
+    color: #666;
+    cursor: pointer;
+}
+
+.presence-button {
+    font-size: x-large;
+    padding-right: 5px;
+    cursor: pointer;
+}
+
+/* RegisterBox */
+
+.registerPanel_main button {
+    margin: 0;
+    padding: 0;
+    border: 0;
+}
+
+.registerPanel_main div, .registerPanel_main button {
+    color: #fff;
+    text-decoration: none;
+}
+
+.registerPanel_main{
+    height: 100%;
+    border: 5px solid #222;
+    -webkit-box-shadow: 0px 1px 4px #000;
+    box-shadow: 0px 1px 4px #000;
+}
+
+.registerPanel_right_side {
+    background: #111 url('media/libervia/register_right.png');
+    height: 100%;
+    width: 100%;
+}
+
+.registerPanel_right_side .gwt-StackPanelItem {
+    margin: 15px;
+    height: auto;
+    text-align: center;
+    cursor: pointer;
+    color: #fff;
+	display: block;
+	text-shadow: 1px 1px 0px rgba(255, 255, 255, 0.2);
+}
+
+.registerPanel_right_side .gwt-StackPanelItem-selected {
+    display: none;
+}
+
+.registerPanel_content {
+    margin: auto 50px;
+}
+
+.registerPanel_content div {
+    font-size: 1em;
+    margin-left: 10px;
+    margin-top: 15px;
+    font-weight: bold;
+    color: #aaa;
+}
+
+.registerPanel_content input {
+    height: 25px;
+    line-height: 25px;
+    width: 200px;
+    text-indent: 11px;
+    background: #000;
+    color: #aaa;
+    border: 1px solid #222;
+    border-radius: 15px 15px 15px 15px;
+}
+
+.registerPanel_content input:focus {
+    border: 1px solid #444;
+}
+
+
+.registerPanel_content .button, .registerPanel_content .button:visited {
+    background: #222 url('media/libervia/gradient.png') repeat-x;
+    display: block;
+    text-decoration: none;
+    border-radius: 6px 6px 6px 6px;
+    border-bottom: 1px solid rgba(0,0,0,0.25);
+    cursor: pointer;
+    margin: 30px auto;
+}
+
+/* Fix for Opera */
+.button, .button:visited {
+    border-radius: 6px 6px 6px 6px !important;
+}
+
+.registerPanel_content .button:hover { background-color: #111; color: #fff; }
+.registerPanel_content .button:active    { top: 1px; }
+.registerPanel_content .button, .registerPanel_content .button:visited { font-size: 1em; font-weight: bold; line-height: 1; text-shadow: 0 -1px 1px rgba(0,0,0,0.25); padding: 7px 10px 8px; }
+.registerPanel_content .red.button, .registerPanel_content .red.button:visited { background-color: #000; }
+.registerPanel_content .red.button:hover { background-color: #bc0000; }
+
+/* Widgets */
+
+.widgetsPanel td {
+    vertical-align: top;
+}
+
+.widgetsPanel > div > table {
+    border-collapse: separate !important;
+    border-spacing: 7px;
+}
+
+.widgetHeader {
+    margin: auto;
+    height: 25px;
+    border-radius: 10px 10px 0 0; 
+    background-color: #222;
+    background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222));
+    background: -webkit-linear-gradient(top, #444, #222);
+    background: linear-gradient(to bottom, #444, #222); 
+}
+
+.widgetHeader_title {
+    color: #fff;
+    font-weight: bold;
+    text-align: left;
+    text-indent: 15px;
+    margin-top: 4px;
+}
+
+.widgetHeader_info {
+    position: absolute;
+    right: 90px;  # FIXME: temporary dirty setting to fit a header menu with 3 icon buttons
+    color: white;
+    background-color: white;
+    border-radius: 5px;
+    padding: 0px 4px;
+    top: 2px !important;
+}
+
+.widgetHeader_info img {
+    padding: 2px;
+    height: 16px;
+}
+
+.widgetHeader_buttonsWrapper {
+    position: absolute;
+    top: 0;
+    height: 100%;
+    width: 100%;
+}
+
+.widgetHeader_buttonGroup {
+    float: right;
+}
+
+.widgetHeader_buttonGroup img {
+    background-color: transparent;
+    width: 25px;
+    height: 20px;
+    padding: 2px 0px 3px 0px;
+    border-left: 1px solid #666;
+    border-top: 0;
+    border-radius: 0 0 0 0;
+    background: -webkit-gradient(linear, left top, left bottom, from(#555), to(#333));
+    background: -webkit-linear-gradient(top, #555, #333);
+    background: linear-gradient(to bottom, #555, #333); 
+    cursor: pointer;
+}
+
+.widgetHeader_buttonGroup img:hover {
+    background-color: #cf2828;
+    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a));
+    background: -webkit-linear-gradient(top, #cf2828, #981a1a);
+    background: linear-gradient(to bottom, #cf2828, #981a1a); 
+}
+
+.widgetBody {
+    border-radius: 0 0 10px 10px; 
+    background-color: #fff;  
+    min-width: 200px;
+    min-height: 150px;
+    -webkit-box-shadow:inset 0px 0 1px #444;
+    box-shadow:inset 0px 0 1px #444;
+}
+
+/* BorderWidgets */
+
+.borderWidgetOnDrag {
+    background-color: lightgray;
+    border: 1px dashed #000;
+    border-radius: 1em;
+}
+
+.bottomBorderWidget {
+    height: 10px !important;
+}
+
+.leftBorderWidget, .rightBorderWidget {
+    width: 10px !important;
+}
+
+.leftBorderWidget {
+    float: right;
+}
+
+.rightBorderWidget {
+    float: left;
+}
+
+/* Microblog */
+
+.microblogPanel {
+    width: 100%;
+}
+
+.microblogPanel_footer {
+    cursor: pointer;
+    text-align: center;
+    background-color: #ededed;
+    border-radius: 5px;
+    width: 85%;
+    margin: auto;
+    margin-top: 5px;
+    margin-bottom: 5px;
+}
+
+.microblogPanel_footer a {
+    color: blue;
+}
+
+.microblogNewButton {
+    width: 100%;
+    height: 35px;
+}
+
+.subPanel {
+}
+
+.subpanel .mb_entry {
+    padding-left: 65px;
+}
+
+.mb_entry {
+    min-height: 64px;
+}
+
+.mb_entry_header
+{
+    width: 100%;
+}
+
+.mb_entry_header_info {
+    cursor: pointer;
+    padding: 0px 5px 0px 5px;
+}
+
+.selected_widget .selected_entry .mb_entry_header_info
+{
+    background: #cf2828;
+    border-radius: 5px 5px 0px 0px;
+}
+
+.mb_entry_comments {
+    float: right;
+    padding-right: 5px;
+}
+
+.mb_entry_comments a {
+    color: blue;
+    cursor: pointer;
+}
+
+.mb_entry_author {
+    font-weight: bold;
+}
+
+.mb_entry_avatar {
+    float: left;
+}
+
+.mb_entry_avatar img {
+    width: 48px;
+    height: 48px;
+    padding: 8px;
+    border-radius: 13px;  /* padding value + 5px */
+}
+
+.mb_entry_dialog {
+    float: left;
+    min-height: 54px;
+    padding: 5px 20px 5px 20px;
+    border-collapse: separate;  /* for the bubble queue since the entry dialog is now a HorizontalPanel */
+}
+
+.bubble {
+    position: relative;
+    padding: 15px;
+    margin: 2px;
+    border-radius:10px;
+    background: #EDEDED;
+    border-color: #C1C1C1;
+    border-width: 1px;
+    border-style: solid;
+    display: block;
+    border-collapse: separate;
+    min-height: 15px;  /* for the bubble queue to be aligned when the bubble is empty */
+}
+
+.bubble:after {
+    background: transparent url('media/libervia/bubble_after.png') top right no-repeat;
+    border: none;   
+    content: "";
+    position: absolute;
+    bottom: auto;
+    left: -20px;
+    top: 16px;
+    display: block;
+    height: 20px;
+    width: 20px;
+}
+
+.bubble textarea{
+    width: 100%;
+    min-width: 350px;
+}
+
+.mb_entry_timestamp {
+    font-style: italic;
+}
+
+.mb_entry_actions {
+    float: right;
+    margin: 5px;
+    cursor: pointer;
+    font-size: large;
+}
+
+.mb_entry_action_larger {
+    font-size: x-large;
+}
+
+.mb_entry_toggle_syntax {
+    cursor: pointer; 
+    float: right;
+    display: block;
+    position: relative;
+    top: -20px;
+    left: -20px;
+}
+
+.mb_entry_publish_button {
+    cursor: pointer; 
+    float: left;
+    display: block;
+    position: relative;
+    top: -20px;
+    left: 20px;
+}
+
+/* START TAGS: styles are adapted from Dotclear */
+.mblog_tags {
+    background: #fbfbfb none repeat scroll 0% 0%;
+    padding: 5px;
+    margin: 8px 0px 5px 0px;
+    overflow: hidden;
+    border-radius: 5px;
+}
+
+.mblog_tags li {
+    display: inline;
+    font-size: small;
+}
+
+.mblog_tags li a {
+    float: left;
+    padding: 2px 8px 2px 18px;
+    white-space: nowrap;
+    color: #005D99;
+    text-decoration: none;
+    background: transparent url("../themes/default/images/flaticon/tag67.png") no-repeat scroll 0px 0px;
+}
+/* END TAGS */
+
+
+/* Chat & MUC Room */
+
+.chatPanel {
+    height: 100%;
+    width: 100%;
+}
+
+.chatPanel_body {
+    height: 100%;
+    width: 100%;
+}
+
+.chatContent {
+    overflow: auto;
+    padding: 5px 15px 5px 15px;
+}
+
+.chatText {
+    margin-top: 7px;
+}
+
+.chatTextMe {
+    margin-top: 7px;
+    font-style: italic;
+}
+
+.chatTextInfo {
+    margin-top: 7px;
+    font-weight: bold;
+    font-style: italic;
+}
+
+.chatTextInfo-link {
+    font-weight: bold;
+    font-style: italic;
+    cursor: pointer;
+    display: inline;
+}
+
+.chatArea {
+    height:100%;
+    width:100%;
+}
+
+.chat_text_timestamp {
+    font-style: italic;
+    margin-right: -4px;
+    padding: 1px 3px 1px 3px;
+    border-radius: 15px 0 0 15px;
+    background-color: #eee;
+    color: #888;
+    border: 1px solid #ddd;
+    border-right: none;
+}
+
+.chat_text_nick {
+    font-weight: bold;
+    padding: 1px 3px 1px 3px;
+    border-radius: 0 15px 15px 0;
+    background-color: #eee;
+    color: #b01e1e;
+    border: 1px solid #ddd;
+    border-left: none;
+}
+
+.chat_text_msg {
+    white-space: pre-wrap;
+}
+
+.chat_text_mymess {
+    color: #006600;
+}
+
+.occupantsPanelCell {
+    border-right: 2px dotted #ddd;
+    padding-left: 5px;
+    height: 100%;
+}
+
+/* Games */
+
+.cardPanel {
+    background: #02FE03;
+    margin: 0 auto;
+}
+
+.cardGamePlayerNick {
+    font-weight: bold;
+}
+
+/* Radiocol */
+
+.radiocolPanel {
+
+}
+
+.radiocol_metadata_lbl {
+    font-weight: bold;
+    padding-right: 5px;
+}
+
+.radiocol_next_song {
+    margin-right: 5px;
+    font-style:italic;
+}
+
+.radiocol_status {
+    margin-left: 10px;
+    margin-right: 10px;
+    font-weight: bold;
+    color: black;
+}
+
+.radiocol_upload_status_ok {
+    margin-left: 10px;
+    margin-right: 10px;
+    font-weight: bold;
+    color: #28F215;
+}
+
+.radiocol_upload_status_ko {
+    margin-left: 10px;
+    margin-right: 10px;
+    font-weight: bold;
+    color: #B80000;
+}
+
+/* Drag and drop */
+
+.dragover {
+    background: #cf2828 !important;
+    border-radius: 1em 1em 1em 1em !important;
+}
+
+.dragover .widgetHeader, .dragover .widgetBody, .dragover .widgetBody span, .dragover .widgetHeader img {
+    background: #cf2828 !important;
+}
+
+.dragover.widgetHeader {
+    border-radius: 1em 1em 0 0 !important;
+}
+
+.dragover.widgetBody {
+    border-radius: 0 0 1em 1em !important;
+}
+
+/* Warning message */
+
+.warningPopup {
+    font-size: 1em;
+    width: 100%;
+    height: 26px;
+    text-align: center;
+    padding: 5px 0;
+    border-bottom: 1px solid #444;
+}
+
+.warningTarget {
+    font-weight: bold;
+   
+}
+
+.targetPublic {
+    background-color: red;
+}
+
+.targetGroup {
+    background-color: #00FFFB;
+}
+
+.targetOne2One {
+    background-color: #66FF00;
+}
+
+.targetStatus {
+    background-color: #fff;
+}
+
+.notifInfo {
+    background-color: #66FF00;
+}
+
+.notifWarning {
+    background-color: #DB1616;
+}
+
+/* Tab panel */
+
+.gwt-TabPanel {
+}
+
+.gwt-TabPanelBottom {
+    height: 100%;
+}
+
+.gwt-TabBar {
+    font-weight: bold;
+    text-decoration: none;
+    border-bottom: 3px solid #a01c1c;  
+}
+
+.gwt-TabBar .gwt-TabBarFirst {
+    height: 100%;
+}
+
+.gwt-TabBar .gwt-TabBarRest {
+}
+
+.mainPanel .gwt-TabBar {
+    z-index: 10;
+}
+
+.mainPanel .gwt-TabBar-oneTab {
+    position: fixed;
+    left: 0px;
+    bottom: 0px;
+    border: none;
+}
+
+.mainPanel .gwt-TabBar-oneTab .gwt-TabBarItem-wrapper {
+    display: none;
+}
+
+.mainPanel .gwt-TabBar-oneTab .gwt-TabBarItem-wrapper:nth-child(3) {
+    display: block;
+}
+
+.liberviaTabPanel {
+    width: 100%;
+    height: 100%;
+}
+
+.liberviaTabPanel .gwt-TabBarItem div {
+    color: #fff;
+}
+
+.liberviaTabPanel .gwt-TabBarItem {
+    color: #444 !important;
+    background-color: #222;
+    background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222));
+    background: -webkit-linear-gradient(top, #444, #222);
+    background: linear-gradient(to bottom, #444, #222);
+    -webkit-box-shadow: 0px 1px 4px #000;
+    box-shadow: 0px 1px 4px #000;
+    padding: 4px 15px 4px 15px;
+    border-radius: 1em 1em 0 0;
+    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
+    cursor: pointer;
+    margin-right: 5px;
+}
+
+.liberviaTabPanel .gwt-TabBarItem-selected {
+    color: #fff;
+    background-color: #cf2828;
+    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a));
+    background: -webkit-linear-gradient(top, #cf2828, #981a1a);
+    background: linear-gradient(to bottom, #cf2828, #981a1a);
+    -webkit-box-shadow: 0px 1px 4px #000;
+    box-shadow: 0px 1px 4px #000;
+    padding: 4px 15px 4px 15px;
+    border-radius: 1em 1em 0 0;
+    text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
+    cursor: default;
+}
+
+.liberviaTabPanel div.gwt-TabBarItem:hover {
+    color: #fff;
+    background-color: #cf2828;
+    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a));
+    background: -webkit-linear-gradient(top, #cf2828, #981a1a);
+    background: linear-gradient(to bottom, #cf2828, #981a1a);
+    -webkit-box-shadow: 0px 1px 4px #000;
+    box-shadow: 0px 1px 4px #000;
+    padding: 4px 15px 4px 15px;
+    border-radius: 1em 1em 0 0;
+    text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); 
+}
+
+.globalLeftArea {
+    margin-top: 9px;
+}
+
+
+/* Misc */
+
+.selected_widget .widgetHeader  {
+    background-color: #cf2828;
+    background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a));
+    background: -webkit-linear-gradient(top, #cf2828, #981a1a);
+    background: linear-gradient(to bottom, #cf2828, #981a1a);
+}
+
+.infoFrame {
+    position: relative;
+    width: 100%;
+    height: 100%;
+}
+
+.marginAuto {
+    margin: auto;
+}
+
+.maxWidthLimit {
+    max-width: 500px;
+}
+
+.transparent {
+    opacity: 0;
+}
+
+/* URLs */
+
+a.url {
+    color: blue;
+    text-decoration: none
+}
+
+a:hover.url {
+    text-decoration: underline
+}
+
+/* Rich Text/Message Editor */
+
+.richTextEditor {
+}
+
+.richTextEditor tbody {
+    width: 100%;
+    display: table;
+}
+
+.richTextTitle {
+    margin-bottom: 5px;
+}
+
+.richTextTitle textarea {
+    height: 22px;
+    width: 99%;
+    margin: auto;
+    padding: 4px;
+    display: block;
+    border: 0px;
+    border-radius: 5px;
+}
+
+.richTextToolbar {
+    white-space: nowrap;
+    width: 100%;
+}
+
+.richTextArea {
+    width: 100%;
+    height: 250px;
+}
+
+.richTextWysiwyg {
+    min-height: 50px;
+    background-color: white;
+    border: 1px solid #a0a0a0;
+    border-radius: 5px;
+    display: block;
+    font-size: larger;
+    white-space: pre;
+}
+
+.richTextSyntaxLabel {
+    text-align: right;
+    margin: 14px 0px 0px 14px;
+    font-size: 12px;
+}
+
+.richTextToolButton {
+    cursor: pointer;
+    width:26px;
+    height:26px;
+    vertical-align: middle;
+    margin: 2px 1px;
+    border-radius: 5px 5px 5px 5px;
+    -webkit-box-shadow: 0px 1px 4px #000;
+    box-shadow: 0px 1px 4px #000;
+    border: none; 
+    -webkit-transition: color 0.2s linear; 
+    transition: color 0.2s linear;
+}
+
+.richTextIcon {
+    width:16px;
+    height:16px;
+    vertical-align: middle;
+}
+
+/* List panel */
+
+.itemButtonCell {
+    width:55px;
+}
+
+.itemKeyMenu {
+}
+
+.itemKey {
+    cursor: pointer;
+    border-radius: 5px;
+    width: 50px;
+}
+
+.listItem {
+}
+
+.listItem-box {
+    cursor: pointer;
+    width: auto;
+    border: 1px solid #87B3FF;
+    border-radius: 5px 5px 5px 5px;
+    -webkit-box-shadow: inset 0px 1px 0px rgba(135, 179, 255, 0.6);
+    box-shadow: inset 0px 1px 2px rgba(135, 179, 255, 0.6);
+    padding: 2px 1px;
+}
+
+.listItem-box-invalid {
+    border: 1px solid rgb(255, 0, 0);
+    -webkit-box-shadow: inset 0px 1px 0px rgba(255, 0, 0, 0.6);
+    box-shadow: inset 0px 1px 0px rgba(255, 0, 0, 0.6);
+}
+
+.listItem-button {
+    cursor: pointer;
+    margin: 0px;
+    padding: 0px;
+    border: none;
+    background: transparent;
+}
+
+.listItem-button span {
+    color: red;
+}
+
+/* Popup (context) menu */
+
+.popupMenuItem {
+    cursor: pointer;
+    border-radius: 5px;
+    width: 100%;
+}
+
+/* Contact group manager */
+
+.contactGroupEditor {
+    width: 680px !important;
+}
+
+.contactGroupManager {
+    width: 400px !important;
+    height: 300px !important;
+    margin: 20px 0px;
+}
+
+.contactGroupRoster {
+    width: 280px !important;
+    height: 300px !important;
+    margin: 20px 0px;
+}
+
+.addContactGroupPanel {
+   
+}
+
+.listPanel {
+    vertical-align:top;
+    padding: 10px 0px;
+}
+
+.listPanel.dragover {
+    border-radius: 5px !important;
+    background: none repeat scroll 0% 0% rgb(135, 179, 255) !important;
+    border: 1px dashed rgb(35,79,255) !important;
+}
+
+.toggleAssignedContacts {
+    white-space: nowrap;
+}
+
+.listManager-button-cell {
+    vertical-align: top;
+    padding: 10px 0px;
+    width: 55px;
+    white-space: top;
+}
+
+.listManager-button-cell .group {
+    border: 0px;
+	margin: 0px 5px;
+}
+
+.tagsPanel-main {
+    margin-bottom: 10px;
+}
+
+.tagsPanel-tags {
+    padding: 0px;
+    display: flex;
+    flex-wrap: wrap;
+}
+
+/* Room and contacts chooser */
+
+.room-contact-chooser {
+    width:380px;
+}
+
+/* StackPanel */
+
+.gwt-StackPanel {
+}
+
+.gwt-StackPanel .gwt-StackPanelItem {
+    background-color: #222;
+    background: -webkit-gradient(linear, left top, left bottom, from(#888888), to(#666666));
+    background: -webkit-linear-gradient(top, #888888, #666666);
+    background: linear-gradient(to bottom, #888888, #666666);
+    text-decoration: none;    
+    font-weight: bold;
+    height: 100%;
+    color: #e7e5e5;
+    padding: 3px 15px;
+    border-radius: 1em 1em 1em 1em;
+    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); 
+    -webkit-transition: color 0.2s linear; 
+    transition: color 0.2s linear;
+}
+
+.gwt-StackPanel .gwt-StackPanelItem:hover {
+    background-color: #eee;
+    background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa));
+    background: -webkit-linear-gradient(top, #eee, #aaa);
+    background: linear-gradient(to bottom, #eee, #aaa);
+    color: #444;
+    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);    
+    cursor: pointer;
+}
+
+.gwt-StackPanel .gwt-StackPanelItem-selected {
+    background-color: #eee;
+    background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa));
+    background: -webkit-linear-gradient(top, #eee, #aaa);
+    background: linear-gradient(to bottom, #eee,#aaa);
+    color: #444;
+    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);    
+    cursor: pointer;
+}
+
+/* Caption Panel */
+
+.gwt-CaptionPanel {
+    overflow: auto;
+    background-color: #fff;
+    border-radius: 5px 5px 5px 5px;
+    padding: 3px 5px 3px 5px;
+    margin: 10px 5px 10px 5px;
+    color: #444;
+    font-size: 1em;
+    border: solid 1px gray;
+}
+
+/* Radio buttons */
+
+.gwt-RadioButton {
+    white-space: nowrap;
+}
+
+[contenteditable="true"] {
+}
+
+/* XMLUI styles */
+
+.AdvancedListSelectable tr{
+    cursor: pointer;
+}
+
+.AdvancedListSelectable tr:hover{
+    background: none repeat scroll 0 0 #EE0000;
+}
+
+.line hr {
+
+}
+
+.dot hr {
+    height: 0px;
+    border-top: 1px dotted;
+    border-bottom: 0px;
+}
+
+.dash hr {
+    height: 0px;
+    border-top: 1px dashed;
+    border-bottom: 0px;
+}
+
+.plain hr {
+    height: 10px;
+    color: black;
+    background-color: black;
+}
+
+.blank hr {
+    border: 0px;
+}
+
+
+/* Some CSS to style the quote XHTML generated by Movim */
+
+.mb_entry_dialog .bubble div.quote {
+    display: block;
+    border-radius: 2px;
+    border: 1px solid rgba(0, 0, 0, 0.12);
+    padding: 2rem;
+    box-sizing: border-box;
+}
+
+.mb_entry_dialog .bubble div.quote:before,
+.mb_entry_dialog .bubble div.quote:after {
+    content: '';
+    display: none;
+}
+
+.mb_entry_dialog .bubble div.quote ul {
+    display: flex;
+    flex-flow: row wrap;
+}
+
+.mb_entry_dialog .bubble div.quote li {
+    flex: 1 25%;
+    list-style-type: none;
+    padding-left: 0;
+}
+
+.mb_entry_dialog .bubble div.quote ul li > * {
+    margin-right: 1rem;
+}
+
+.mb_entry_dialog .bubble div.quote li:first-child {
+    flex: 1 75%;
+}
+
+@media screen and (max-width: 1024px) {
+    .mb_entry_dialog .bubble div.quote li {
+        flex: 1 100%;
+    }
+}
+
+.mb_entry_dialog .bubble div.quote li img {
+    max-height: 10rem;
+    max-width: 100%;
+    float: right;
+}
+
+.parameters {
+}
+
+.parameters .xmlui-JidsListWidget {
+    height: auto;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/public/libervia.html	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,33 @@
+<!--
+Libervia: a Salut à Toi frontend
+Copyright (C) 2011  Jérôme Poisson (goffi@goffi.org)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<html>
+<head profile="http://www.w3.org/2005/10/profile">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<meta name="pygwt:module" content="libervia_main">
+<link rel='stylesheet' href='libervia.css'>
+<link rel="icon" type="image/png" href="sat_logo_16.png">
+
+<title>Libervia</title>
+</head>
+<body bgcolor="white">
+<script language="javascript" src="bootstrap.js"></script>
+<script language="javascript" src="favico.min.js"></script>
+<iframe id='__pygwt_historyFrame' style='display:none;width:0;height:0;border:0'></iframe>
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/public/robots.txt	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,10 @@
+User-agent: *
+Allow: /blog/
+Allow: /u/
+Allow: /b/
+Disallow: /libervia.html
+Disallow: /mr
+Disallow: /login
+
+User-agent: Mediapartners-*
+Disallow: /
Binary file browser/public/sat_logo_16.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/base_menu.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,183 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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/>.
+
+
+"""Base classes for building a menu.
+
+These classes have been moved here from menu.py because they are also used
+by base_widget.py, and the import sequence caused a JS runtime error."""
+
+
+from sat.core.log import getLogger
+log = getLogger(__name__)
+
+from pyjamas.ui.MenuBar import MenuBar
+from pyjamas.ui.MenuItem import MenuItem
+from pyjamas import Window
+from sat_frontends.quick_frontend import quick_menus
+from sat_browser import html_tools
+
+
+unicode = str  # FIXME: pyjamas workaround
+
+
+class MenuCmd(object):
+    """Return an object with an "execute" method that can be set to a menu item callback"""
+
+    def __init__(self, menu_item, caller=None):
+        """
+        @param menu_item(quick_menu.MenuItem): instance of a callbable MenuItem
+        @param caller: menu caller
+        """
+        self.item = menu_item
+        self._caller = caller
+
+    def execute(self):
+        self.item.call(self._caller)
+
+
+class SimpleCmd(object):
+    """Return an object with an "executre" method that launch a callback"""
+
+    def __init__(self, callback):
+        """
+        @param callback: method to call when menu is selected
+        """
+        self.callback = callback
+
+    def execute(self):
+        self.callback()
+
+
+class GenericMenuBar(MenuBar):
+    """A menu bar with sub-categories and items"""
+
+    def __init__(self, host, vertical=False, styles=None, flat_level=0, **kwargs):
+        """
+        @param host (SatWebFrontend): host instance
+        @param vertical (bool): True to display the popup menu vertically
+        @param styles (dict): specific styles to be applied:
+            - key: a value in ('moved_popup', 'menu_bar')
+            - value: a CSS class name
+        @param flat_level (int): sub-menus until that level see their items
+        displayed in the parent menu bar instead of in a callback popup.
+        """
+        MenuBar.__init__(self, vertical, **kwargs)
+        self.host = host
+        self.styles = {}
+        if styles:
+            self.styles.update(styles)
+        try:
+            self.setStyleName(self.styles['menu_bar'])
+        except KeyError:
+            pass
+        self.menus_container = None
+        self.flat_level = flat_level
+
+    def update(self, type_, caller=None):
+        """Method to call when menus have changed
+
+        @param type_: menu type like in sat.core.sat_main.importMenu
+        @param caller: instance linked to the menus
+        """
+        self.menus_container = self.host.menus.getMainContainer(type_)
+        self._caller=caller
+        self.createMenus()
+
+    @classmethod
+    def getCategoryHTML(cls, category):
+        """Build the html to be used for displaying a category item.
+
+        Inheriting classes may overwrite this method.
+        @param category (quick_menus.MenuCategory): category to add
+        @return unicode: HTML to display
+        """
+        return html_tools.html_sanitize(category.name)
+
+    def _buildMenus(self, container, flat_level, caller=None):
+        """Recursively build menus of the container
+
+        @param container: a quick_menus.MenuContainer instance
+        @param caller: instance linked to the menus
+        """
+        for child in container.getActiveMenus():
+            if isinstance(child, quick_menus.MenuContainer):
+                item = self.addCategory(child, flat=bool(flat_level))
+                submenu = item.getSubMenu()
+                if submenu is None:
+                    submenu = self
+                submenu._buildMenus(child, flat_level-1 if flat_level else 0, caller)
+            elif isinstance(child, quick_menus.MenuSeparator):
+                item = MenuItem(text='', asHTML=None, StyleName="menuSeparator")
+                self.addItem(item)
+            elif isinstance(child, quick_menus.MenuItem):
+                self.addItem(child.name, False, MenuCmd(child, caller) if child.CALLABLE else None)
+            else:
+                log.error(u"Unknown child type: {}".format(child))
+
+    def createMenus(self):
+        self.clearItems()
+        if self.menus_container is None:
+            log.debug("Menu is empty")
+            return
+        self._buildMenus(self.menus_container, self.flat_level, self._caller)
+
+    def doItemAction(self, item, fireCommand):
+        """Overwrites the default behavior for the popup menu to fit in the screen"""
+        MenuBar.doItemAction(self, item, fireCommand)
+        if not self.popup:
+            return
+        if self.vertical:
+            # move the popup if it would go over the screen's viewport
+            max_left = Window.getClientWidth() - self.getOffsetWidth() + 1 - self.popup.getOffsetWidth()
+            new_left = self.getAbsoluteLeft() - self.popup.getOffsetWidth() + 1
+            top = item.getAbsoluteTop()
+        else:
+            # move the popup if it would go over the menu bar right extremity
+            max_left = self.getAbsoluteLeft() + self.getOffsetWidth() - self.popup.getOffsetWidth()
+            new_left = max_left
+            top = self.getAbsoluteTop() + self.getOffsetHeight() - 1
+        if item.getAbsoluteLeft() > max_left:
+            self.popup.setPopupPosition(new_left, top)
+            # eventually smooth the popup edges to fit the menu own style
+            try:
+                self.popup.addStyleName(self.styles['moved_popup'])
+            except KeyError:
+                pass
+
+    def addCategory(self, category, menu_bar=None, flat=False):
+        """Add a new category.
+
+        @param category (quick_menus.MenuCategory): category to add
+        @param menu_bar (GenericMenuBar): instance to popup as the category sub-menu.
+        """
+        html = self.getCategoryHTML(category)
+
+        if menu_bar is not None:
+            assert not flat # can't have a menu_bar and be flat at the same time
+            sub_menu = menu_bar
+        elif not flat:
+            sub_menu = GenericMenuBar(self.host, vertical=True)
+        else:
+            sub_menu = None
+
+        item = self.addItem(html, True, sub_menu)
+        if flat:
+            item.setStyleName("menuFlattenedCategory")
+        return item
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/base_panel.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,236 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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.core.i18n import _
+
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.ScrollPanel import ScrollPanel
+from pyjamas.ui.Button import Button
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.PopupPanel import PopupPanel
+from pyjamas.ui.StackPanel import StackPanel
+from pyjamas.ui.TextArea import TextArea
+from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT
+from pyjamas import DOM
+
+
+### Menus ###
+
+
+class PopupMenuPanel(PopupPanel):
+    """Popup menu (contextual menu) with common callbacks for all the items.
+    
+    This implementation of a popup menu allow you to assign two special methods which
+    are common to all the items, in order to hide certain items and define their callbacks.
+    callbacks. The menu can be bound to any button of the mouse (left, middle, right).
+    """
+    
+    def __init__(self, entries, hide=None, callback=None, vertical=True, style=None, **kwargs):
+        """
+        @param entries (dict{unicode: dict{unicode: unicode}:
+            - menu item keys 
+            - values: dict{unicode: unicode}:
+                - item data lile "title", "desc"...
+                - value
+        @param hide (callable): function of signature Widget, unicode: bool
+            which takes the sender and the item key, and returns True if that
+            item has to be hidden from the context menu.
+        @param callback (callbable): function of signature Widget, unicode: None
+            which takes the sender and the item key.
+        @param vertical (bool): set the direction vertical or horizontal
+        @param item_style (unicode): alternative CSS class for the menu items
+        @param menu_style (unicode): supplementary CSS class for the sender widget
+        """
+        PopupPanel.__init__(self, autoHide=True, **kwargs)
+        self.entries = entries
+        self.hideMenu = hide
+        self.callback = callback
+        self.vertical = vertical
+        self.style = {"selected": None, "menu": "itemKeyMenu", "item": "popupMenuItem"}
+        if isinstance(style, dict):
+            self.style.update(style)
+        self.senders = {}
+
+    def showMenu(self, sender):
+        """Popup the menu on the screen, where it fits to the sender's position.
+
+        @param sender (Widget): the widget that has been clicked
+        """
+        menu = VerticalPanel() if self.vertical is True else HorizontalPanel()
+        menu.setStyleName(self.style["menu"])
+
+        def button_cb(item):
+            # XXX: you can not put that method in the loop and rely on key
+            if self.callback is not None:
+                self.callback(sender=sender, key=item.key)
+            self.hide(autoClosed=True)
+
+        for key, entry in self.entries.iteritems():
+            if self.hideMenu is not None and self.hideMenu(sender=sender, key=key) is True:
+                continue
+            title = entry.get("title", key)
+            item = Button(title, button_cb, StyleName=self.style["item"])
+            item.key = key  # XXX: copy the key because we loop on it and it will change
+            item.setTitle(entry.get("desc", title))
+            menu.add(item)
+
+        if menu.getWidgetCount() == 0:
+            return  # no item to display means no menu at all
+        
+        self.add(menu)
+        
+        if self.vertical is True:
+            x = sender.getAbsoluteLeft() + sender.getOffsetWidth()
+            y = sender.getAbsoluteTop()
+        else:
+            x = sender.getAbsoluteLeft()
+            y = sender.getAbsoluteTop() + sender.getOffsetHeight()
+        
+        self.setPopupPosition(x, y)
+        self.show()
+        
+        if self.style["selected"]:
+            sender.addStyleDependentName(self.style["selected"])
+
+        def onHide(popup):
+            if self.style["selected"]:
+                sender.removeStyleDependentName(self.style["selected"])
+            return PopupPanel.onHideImpl(self, popup)
+
+        self.onHideImpl = onHide
+
+    def registerClickSender(self, sender, button=BUTTON_LEFT):
+        """Bind the menu to the specified sender.
+
+        @param sender (Widget): bind the menu to this widget
+        @param (int): BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT
+        """
+        self.senders.setdefault(sender, [])
+        self.senders[sender].append(button)
+
+        if button == BUTTON_RIGHT:
+            # WARNING: to disable the context menu is a bit tricky...
+            # The following seems to work on Firefox 24.0, but:
+            # TODO: find a cleaner way to disable the context menu
+            sender.getElement().setAttribute("oncontextmenu", "return false")
+
+        def onBrowserEvent(event):
+            button = DOM.eventGetButton(event)
+            if DOM.eventGetType(event) == "mousedown" and button in self.senders[sender]:
+                self.showMenu(sender)
+            return sender.__class__.onBrowserEvent(sender, event)
+
+        sender.onBrowserEvent = onBrowserEvent
+
+    def registerMiddleClickSender(self, sender):
+        self.registerClickSender(sender, BUTTON_MIDDLE)
+
+    def registerRightClickSender(self, sender):
+        self.registerClickSender(sender, BUTTON_RIGHT)
+
+
+### Generic panels ###
+
+
+class ToggleStackPanel(StackPanel):
+    """This is a pyjamas.ui.StackPanel with modified behavior. All sub-panels ca be
+    visible at the same time, clicking a sub-panel header will not display it and hide
+    the others but only toggle its own visibility. The argument 'visibleStack' is ignored.
+    Note that the argument 'visible' has been added to listener's 'onStackChanged' method.
+    """
+
+    def __init__(self, **kwargs):
+        StackPanel.__init__(self, **kwargs)
+
+    def onBrowserEvent(self, event):
+        if DOM.eventGetType(event) == "click":
+            index = self.getDividerIndex(DOM.eventGetTarget(event))
+            if index != -1:
+                self.toggleStack(index)
+
+    def add(self, widget, stackText="", asHTML=False, visible=False):
+        StackPanel.add(self, widget, stackText, asHTML)
+        self.setStackVisible(self.getWidgetCount() - 1, visible)
+
+    def toggleStack(self, index):
+        if index >= self.getWidgetCount():
+            return
+        visible = not self.getWidget(index).getVisible()
+        self.setStackVisible(index, visible)
+        for listener in self.stackListeners:
+            listener.onStackChanged(self, index, visible)
+
+
+class TitlePanel(ToggleStackPanel):
+    """A toggle panel to set the message title"""
+    
+    TITLE = _("Title")
+
+    def __init__(self, text=None):
+        ToggleStackPanel.__init__(self, Width="100%")
+        self.text_area = TextArea()
+        self.add(self.text_area, self.TITLE)
+        self.addStackChangeListener(self)
+        if text:
+            self.setText(text)
+
+    def onStackChanged(self, sender, index, visible=None):
+        if visible is None:
+            visible = sender.getWidget(index).getVisible()
+        text = self.getText()
+        suffix = "" if (visible or not text) else (": %s" % text)
+        sender.setStackText(index, self.TITLE + suffix)
+
+    def getText(self):
+        return self.text_area.getText()
+
+    def setText(self, text):
+        self.text_area.setText(text)
+
+
+class ScrollPanelWrapper(SimplePanel):
+    """Scroll Panel like component, wich use the full available space
+    to work around percent size issue, it use some of the ideas found
+    here: http://code.google.com/p/google-web-toolkit/issues/detail?id=316
+    specially in code given at comment #46, thanks to Stefan Bachert"""
+
+    def __init__(self, *args, **kwargs):
+        SimplePanel.__init__(self)
+        self.spanel = ScrollPanel(*args, **kwargs)
+        SimplePanel.setWidget(self, self.spanel)
+        DOM.setStyleAttribute(self.getElement(), "position", "relative")
+        DOM.setStyleAttribute(self.getElement(), "top", "0px")
+        DOM.setStyleAttribute(self.getElement(), "left", "0px")
+        DOM.setStyleAttribute(self.getElement(), "width", "100%")
+        DOM.setStyleAttribute(self.getElement(), "height", "100%")
+        DOM.setStyleAttribute(self.spanel.getElement(), "position", "absolute")
+        DOM.setStyleAttribute(self.spanel.getElement(), "width", "100%")
+        DOM.setStyleAttribute(self.spanel.getElement(), "height", "100%")
+
+    def setWidget(self, widget):
+        self.spanel.setWidget(widget)
+
+    def setScrollPosition(self, position):
+        self.spanel.setScrollPosition(position)
+
+    def scrollToBottom(self):
+        self.setScrollPosition(self.spanel.getElement().scrollHeight)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/base_widget.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,71 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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__)
+import base_menu
+from sat_frontends.quick_frontend import quick_menus
+
+
+### Exceptions ###
+
+
+class NoLiberviaWidgetException(Exception):
+    """A Libervia widget was expected"""
+    pass
+
+
+### Menus ###
+
+
+class WidgetMenuBar(base_menu.GenericMenuBar):
+
+    ITEM_TPL = "<img src='media/icons/misc/%s.png' />"
+
+    def __init__(self, parent, host, vertical=False, styles=None):
+        """
+
+        @param parent (Widget): LiberviaWidget, or instance of another class
+            implementing the method addMenus
+        @param host (SatWebFrontend)
+        @param vertical (bool): if True, set the menu vertically
+        @param styles (dict): optional styles dict
+        """
+        menu_styles = {'menu_bar': 'widgetHeader_buttonGroup'}
+        if styles:
+            menu_styles.update(styles)
+        base_menu.GenericMenuBar.__init__(self, host, vertical=vertical, styles=menu_styles)
+
+        # regroup all the dynamic menu categories in a sub-menu
+        for menu_context in parent.plugin_menu_context:
+            main_cont = host.menus.getMainContainer(menu_context)
+            if len(main_cont)>0: # we don't add the icon if the menu is empty
+                sub_menu = base_menu.GenericMenuBar(host, vertical=True, flat_level=1)
+                sub_menu.update(menu_context, parent)
+                menu_category = quick_menus.MenuCategory("plugins", extra={'icon':'plugins'})
+                self.addCategory(menu_category, sub_menu)
+
+    @classmethod
+    def getCategoryHTML(cls, category):
+        """Build the html to be used for displaying a category item.
+
+        @param category (quick_menus.MenuCategory): category to add
+        @return unicode: HTML to display
+        """
+        return cls.ITEM_TPL % category.icon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/blog.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,560 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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.tools.common import data_format
+from sat.core.i18n import _ #, D_
+
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.ScrollPanel import ScrollPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.Label import Label
+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 import KeyboardListener as keyb
+from pyjamas.ui.KeyboardListener import KeyboardHandler
+from pyjamas.ui.FocusListener import FocusHandler
+from pyjamas.ui.MouseListener import MouseHandler
+from pyjamas.Timer import Timer
+
+from datetime import datetime
+
+import html_tools
+import dialog
+import richtext
+import editor_widget
+import libervia_widget
+from constants import Const as C
+from sat_frontends.quick_frontend import quick_widgets
+from sat_frontends.quick_frontend import quick_blog
+
+unicode = str # XXX: pyjamas doesn't manage unicode
+ENTRY_RICH = (C.ENTRY_MODE_RICH, C.ENTRY_MODE_XHTML)
+
+
+class Entry(quick_blog.Entry, VerticalPanel, ClickHandler, FocusHandler, KeyboardHandler):
+    """Graphical representation of a quick_blog.Item"""
+
+    def __init__(self, manager, item_data=None, comments_data=None, service=None, node=None):
+        quick_blog.Entry.__init__(self, manager, item_data, comments_data, service, node)
+
+        VerticalPanel.__init__(self)
+
+        self.panel = FlowPanel()
+        self.panel.setStyleName('mb_entry')
+
+        self.header = HorizontalPanel(StyleName='mb_entry_header')
+        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')
+        author_jid = self.author_jid
+        self.avatar = Image(self.blog.host.getAvatarURL(author_jid) if author_jid is not None else C.DEFAULT_AVATAR_URL)
+        # TODO: show a warning icon if author is not validated
+        entry_avatar.add(self.avatar)
+        self.panel.add(entry_avatar)
+
+        self.entry_dialog = VerticalPanel()
+        self.entry_dialog.setStyleName('mb_entry_dialog')
+        self.panel.add(self.entry_dialog)
+
+        self.comments_panel = None
+        self._current_comment = None
+
+        self.add(self.panel)
+        ClickHandler.__init__(self)
+        self.addClickListener(self)
+
+        self.refresh()
+        self.displayed = False # True when entry is added to parent
+        if comments_data:
+            self.addComments(comments_data)
+
+    def refresh(self):
+        self.comment_label = None
+        self.update_label = None
+        self.delete_label = None
+        self.header.clear()
+        self.entry_dialog.clear()
+        self.entry_actions.clear()
+        self._setHeader()
+        self._setBubble()
+        self._setIcons()
+
+    def _setHeader(self):
+        """Set the entry header."""
+        if not self.new:
+            author = html_tools.html_sanitize(unicode(self.item.author))
+            author_jid = html_tools.html_sanitize(unicode(self.item.author_jid))
+            if author_jid and not self.item.author_verified:
+                author_jid += u' <span style="color:red; font-weight: bold;">⚠</span>'
+            if author:
+                author += " &lt;%s&gt;" % author_jid
+            elif author_jid:
+                author = author_jid
+            else:
+                author = _("<unknown author>")
+
+            update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.item.updated)
+            self.header.add(HTML("""<span class='mb_entry_header_info'>
+                                      <span class='mb_entry_author'>%(author)s</span> on
+                                      <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s
+                                    </span>""" % {'author': author,
+                                                  'published': datetime.fromtimestamp(self.item.published) if self.item.published is not None else '',
+                                                  'updated': update_text if self.item.published != self.item.updated else ''
+                                                  }))
+            if self.item.comments:
+                self.show_comments_link = HTML('')
+                self.header.add(self.show_comments_link)
+
+    def _setBubble(self):
+        """Set the bubble displaying the initial content."""
+        content = {'text': self.item.content_xhtml if self.item.content_xhtml else self.item.content or '',
+                   'title': self.item.title_xhtml if self.item.title_xhtml else self.item.title or ''}
+        data_format.iter2dict('tag', self.item.tags, content)
+
+        if self.mode == C.ENTRY_MODE_TEXT:
+            # assume raw text message have no title
+            self.bubble = editor_widget.LightTextEditor(content, modifiedCb=self._modifiedCb, afterEditCb=self._afterEditCb, options={'no_xhtml': True})
+        elif self.mode in ENTRY_RICH:
+            content['syntax'] = C.SYNTAX_XHTML
+            if self.new:
+                options = []
+            elif self.item.author_jid == self.blog.host.whoami.bare:
+                options = ['update_msg']
+            else:
+                options = ['read_only']
+            self.bubble = richtext.RichTextEditor(self.blog.host, content, modifiedCb=self._modifiedCb, afterEditCb=self._afterEditCb, options=options)
+        else:
+            log.error("Bad entry mode: %s" % self.mode)
+        self.bubble.addStyleName("bubble")
+        self.entry_dialog.add(self.bubble)
+        self.bubble.addEditListener(self._showWarning) # FIXME: remove edit listeners
+        self.setEditable(self.editable)
+
+    def _setIcons(self):
+        """Set the entry icons (delete, update, comment)"""
+        if self.new:
+            return
+
+        def addIcon(label, title):
+            label = Label(label)
+            label.setTitle(title)
+            label.addClickListener(self)
+            self.entry_actions.add(label)
+            return label
+
+        if self.item.comments:
+            self.comment_label = addIcon(u"↶", "Comment this message")
+            self.comment_label.setStyleName('mb_entry_action_larger')
+        else:
+            self.comment_label = None
+        is_publisher = self.item.author_jid == self.blog.host.whoami.bare
+        if is_publisher:
+            self.update_label = addIcon(u"✍", "Edit this message")
+            # TODO: add delete button if we are the owner of the node
+            self.delete_label = addIcon(u"✗", "Delete this message")
+        else:
+            self.update_label = self.delete_label = None
+
+    def _createCommentsPanel(self):
+        """Create the panel if it doesn't exists"""
+        if self.comments_panel is None:
+            self.comments_panel = VerticalPanel()
+            self.comments_panel.setStyleName('microblogPanel')
+            self.comments_panel.addStyleName('subPanel')
+            self.add(self.comments_panel)
+
+    def setEditable(self, editable=True):
+        """Toggle the bubble between display and edit mode.
+
+        @param editable (bool)
+        """
+        self.editable = editable
+        self.bubble.edit(self.editable)
+        self.updateIconsAndButtons()
+
+    def updateIconsAndButtons(self):
+        """Set the visibility of the icons and the button to switch between blog and microblog."""
+        try:
+            self.bubble_commands.removeFromParent()
+        except (AttributeError, TypeError):
+            pass
+        if self.editable:
+            if self.mode == C.ENTRY_MODE_TEXT:
+                html = _(u'<a style="color: blue;">switch to blog</a>')
+                title = _(u'compose a rich text message with a title - suitable for writing articles')
+            else:
+                html = _(u'<a style="color: blue;">switch to microblog</a>')
+                title = _(u'compose a short message without title - suitable for sharing news')
+            toggle_syntax_button = HTML(html, Title=title)
+            toggle_syntax_button.addClickListener(self.toggleContentSyntax)
+            toggle_syntax_button.addStyleName('mb_entry_toggle_syntax')
+            toggle_syntax_button.setStyleAttribute('top', '-20px')  # XXX: need to force CSS
+            toggle_syntax_button.setStyleAttribute('left', '-20px')
+
+            self.bubble_commands = HorizontalPanel(Width="100%")
+
+            if self.mode == C.ENTRY_MODE_TEXT:
+                publish_button = HTML(_(u'<a style="color: blue;">shift + enter to publish</a>'), Title=_(u"... or click here"))
+                publish_button.addStyleName('mb_entry_publish_button')
+                publish_button.addClickListener(lambda dummy: self.bubble.edit(False))
+                publish_button.setStyleAttribute('top', '-20px')  # XXX: need to force CSS
+                publish_button.setStyleAttribute('left', '20px')
+                self.bubble_commands.add(publish_button)
+
+            self.bubble_commands.add(toggle_syntax_button)
+            self.entry_dialog.add(self.bubble_commands)
+
+        # hide these icons while editing
+        try:
+            self.delete_label.setVisible(not self.editable)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.update_label.setVisible(not self.editable)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.comment_label.setVisible(not self.editable)
+        except (TypeError, AttributeError):
+            pass
+
+    def onClick(self, sender):
+
+        if sender == self:
+            self.blog.setSelectedEntry(self)
+        elif sender == self.delete_label:
+            self._onRetractClick()
+        elif sender == self.update_label:
+            self.setEditable(True)
+        elif sender == self.comment_label:
+            self._onCommentClick()
+        # elif sender == self.show_comments_link:
+        #     self._blog_panel.loadAllCommentsForEntry(self)
+
+    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
+            if not self.new:
+                self._onRetractClick()
+            return False
+
+        self.item.content = self.item.content_rich = self.item.content_xhtml = None
+        self.item.title = self.item.title_rich = self.item.title_xhtml = None
+
+        if self.mode in ENTRY_RICH:
+            # 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 mb_data for the frontend to use it instead of current syntax.
+            self.item.content_rich = content['text']  # XXX: this also works if the syntax is XHTML
+            self.item.title = content['title']
+            self.item.tags = list(data_format.dict2iter('tag', content))
+        else:
+            self.item.content = content['text']
+
+        self.send()
+
+        return True
+
+    def _afterEditCb(self, content):
+        """Post edition treatments
+
+        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
+        @param content(dict): edited content
+        """
+        if self.new:
+            if self.level == 0:
+                # we have a main item, we keep the edit entry
+                self.reset(None)
+                # FIXME: would be better to reset bubble
+                # but bubble.setContent() doesn't seem to work
+                self.bubble.removeFromParent()
+                self._setBubble()
+            else:
+                # we don't keep edit entries for comments
+                self.delete()
+        else:
+            self.editable = False
+            self.updateIconsAndButtons()
+
+    def _showWarning(self, sender, keycode, modifiers):
+        if keycode == keyb.KEY_ENTER & keyb.MODIFIER_SHIFT: # FIXME: fix edit_listeners, it's dirty (we have to check keycode/modifiers twice !)
+            self.blog.host.showWarning(None, None)
+        else:
+            # self.blog.host.showWarning(*self.blog.getWarningData(self.type == 'comment'))
+            self.blog.host.showWarning(*self.blog.getWarningData(False)) # FIXME: comments are not yet reimplemented
+
+    def _onRetractClick(self):
+        """Ask confirmation then retract current entry."""
+        assert not self.new
+
+        def confirm_cb(answer):
+            if answer:
+                self.retract()
+
+        entry_type = _("message") if self.level == 0 else _("comment")
+        and_comments = _(" All comments will be also deleted!") if self.item.comments else ""
+        text = _("Do you really want to delete this {entry_type}?{and_comments}").format(
+                entry_type=entry_type, and_comments=and_comments)
+        dialog.ConfirmDialog(confirm_cb, text=text).show()
+
+    def _onCommentClick(self):
+        """Add an empty entry for a new comment"""
+        if self._current_comment is None:
+            if not self.item.comments_service or not self.item.comments_node:
+                log.warning("Invalid service and node for comments, can't create a comment")
+            self._current_comment = self.addEntry(editable=True, service=self.item.comments_service, node=self.item.comments_node, edit_entry=True)
+        self.blog.setSelectedEntry(self._current_comment, True)
+        self._current_comment.bubble.setFocus(True)  # FIXME: should be done elsewhere (automatically)?
+
+    def _changeMode(self, original_content, text):
+        self.mode = C.ENTRY_MODE_RICH if self.mode == C.ENTRY_MODE_TEXT else C.ENTRY_MODE_TEXT
+        if self.mode in ENTRY_RICH and not text:
+            text = ' ' # something different than empty string is needed to initialize the rich text editor
+        self.item.content = text
+        if self.mode in ENTRY_RICH:
+            self.item.content_rich = text  # XXX: this also works if the syntax is XHTML
+            self.bubble.setDisplayContent()  # needed in case the edition is aborted, to not end with an empty bubble
+        else:
+            self.item.content_xhtml = ''
+        self.bubble.removeFromParent()
+        self._setBubble()
+        self.bubble.setOriginalContent(original_content)
+
+    def toggleContentSyntax(self):
+        """Toggle the editor between raw and rich text"""
+        original_content = self.bubble.getOriginalContent()
+        rich = self.mode in ENTRY_RICH
+        if rich:
+            original_content['syntax'] = C.SYNTAX_XHTML
+
+        text = self.bubble.getContent()['text']
+
+        if not text.strip():
+            self._changeMode(original_content,'')
+        else:
+            if rich:
+                def confirm_cb(answer):
+                    if answer:
+                        self.blog.host.bridge.syntaxConvert(text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT, profile=None,
+                                                            callback=lambda converted: self._changeMode(original_content, converted))
+                dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show()
+            else:
+                self.blog.host.bridge.syntaxConvert(text, C.SYNTAX_TEXT, C.SYNTAX_XHTML, profile=None,
+                                                    callback=lambda converted: self._changeMode(original_content, converted))
+
+    def update(self, entry=None):
+        """Update comments"""
+        self._createCommentsPanel()
+        self.entries.sort(key=lambda entry: entry.item.published)
+        # we put edit_entry at the end
+        edit_entry = [] if self.edit_entry is None else [self.edit_entry]
+        for idx, entry in enumerate(self.entries + edit_entry):
+            if not entry.displayed:
+                self.comments_panel.insert(entry, idx)
+                entry.displayed = True
+
+    def delete(self):
+        quick_blog.Entry.delete(self)
+
+        # _current comment is specific to libervia, we remove it
+        if isinstance(self.manager, Entry):
+            self.manager._current_comment = None
+
+        # now we remove the pyjamas widgets
+        parent = self.parent
+        assert isinstance(parent, VerticalPanel)
+        self.removeFromParent()
+        if not parent.children:
+            # the vpanel is empty, we remove it
+            parent.removeFromParent()
+            try:
+                if self.manager.comments_panel == parent:
+                    self.manager.comments_panel = None
+            except AttributeError:
+                assert isinstance(self.manager, quick_blog.QuickBlog)
+
+
+class Blog(quick_blog.QuickBlog, libervia_widget.LiberviaWidget, MouseHandler):
+    """Panel used to show microblog"""
+    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 following groups: <span class='warningTarget'>%s</span>"
+
+    def __init__(self, host, targets, profiles=None):
+        quick_blog.QuickBlog.__init__(self, host, targets, C.PROF_KEY_NONE)
+        title = ", ".join(targets) if targets else "Blog"
+        libervia_widget.LiberviaWidget.__init__(self, host, title, selectable=True)
+        MouseHandler.__init__(self)
+        self.vpanel = VerticalPanel()
+        self.vpanel.setStyleName('microblogPanel')
+        self.setWidget(self.vpanel)
+        if ((self._targets_type == C.ALL and self.host.mblog_available) or
+            (self._targets_type == C.GROUP and self.host.groupblog_available)):
+            self.addEntry(editable=True, edit_entry=True)
+
+        self.getAll()
+
+        # self.footer = HTML('', StyleName='microblogPanel_footer')
+        # self.footer.waiting = False
+        # self.footer.addClickListener(self)
+        # self.footer.addMouseListener(self)
+        # self.vpanel.add(self.footer)
+        # self.next_rsm_index = 0
+
+    def __str__(self):
+        return u"Blog Widget [targets: {}, profile: {}]".format(", ".join(self.targets) if self.targets else "meta blog", self.profile)
+
+    def update(self):
+        self.entries.sort(key=lambda entry: entry.item.published, reverse=True)
+
+        start_idx = 0
+        if self.edit_entry is not None:
+            start_idx = 1
+            if not self.edit_entry.displayed:
+                self.vpanel.insert(self.edit_entry, 0)
+                self.edit_entry.displayed = True
+
+        # XXX: enumerate is buggued in pyjamas (start is not used)
+        #       we have to use idx
+        idx = start_idx
+        for entry in self.entries:
+            if not entry.displayed:
+                self.vpanel.insert(entry, idx)
+                entry.displayed = True
+            idx += 1
+
+    # def onDelete(self):
+    #     quick_widgets.QuickWidget.onDelete(self)
+    #     self.host.removeListener('avatar', self.avatarListener)
+
+    # def onAvatarUpdate(self, jid_, hash_, profile):
+    #     """Called on avatar update events
+
+    #     @param jid_: jid of the entity with updated avatar
+    #     @param hash_: hash of the avatar
+    #     @param profile: %(doc_profile)s
+    #     """
+    #     whoami = self.host.profiles[self.profile].whoami
+    #     if self.isJidAccepted(jid_) or jid_.bare == whoami.bare:
+    #         self.updateValue('avatar', jid_, hash_)
+
+    @staticmethod
+    def onGroupDrop(host, targets):
+        """Create a microblog panel for one, several or all contact groups.
+
+        @param host (SatWebFrontend): the SatWebFrontend instance
+        @param targets (tuple(unicode)): tuple of groups (empty for "all groups")
+        @return: the created MicroblogPanel
+        """
+        # XXX: pyjamas doesn't support use of cls directly
+        widget = host.displayWidget(Blog, targets, dropped=True)
+        return widget
+
+    # @property
+    # def accepted_groups(self):
+    #     """Return a set of the accepted groups"""
+    #     return set().union(*self.targets)
+
+    def getWarningData(self, comment):
+        """
+        @param comment: set to True if the composed message is a comment
+        @return: a couple (type, msg) for calling self.host.showWarning"""
+        if comment:
+            return ("PUBLIC", "This is a <span class='warningTarget'>comment</span> and keep the initial post visibility, so it is potentialy public")
+        elif self._targets_type == C.ALL:
+            # we have a meta MicroblogPanel, we publish publicly
+            return ("PUBLIC", self.warning_msg_public)
+        else:
+            # FIXME: manage several groups
+            return (self._targets_type, self.warning_msg_group % ' '.join(self.targets))
+
+    def ensureVisible(self, entry):
+        """Scroll to an entry to ensure its visibility
+
+        @param entry (MicroblogEntry): the entry
+        """
+        current = entry
+        while True:
+            parent = current.getParent()
+            if parent is None:
+                log.warning("Can't find any parent ScrollPanel")
+                return
+            elif isinstance(parent, ScrollPanel):
+                parent.ensureVisible(entry)
+                return
+            else:
+                current = parent
+
+    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)
+
+        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'))
+
+    # def updateValue(self, type_, jid_, value):
+    #     """Update a jid value in entries
+
+    #     @param type_: one of 'avatar', 'nick'
+    #     @param jid_(jid.JID): jid concerned
+    #     @param value: new value"""
+    #     assert isinstance(jid_, jid.JID) # FIXME: temporary
+    #     def updateVPanel(vpanel):
+    #         avatar_url = self.host.getAvatarURL(jid_)
+    #         for child in vpanel.children:
+    #             if isinstance(child, MicroblogEntry) and child.author == jid_:
+    #                 child.updateAvatar(avatar_url)
+    #             elif isinstance(child, VerticalPanel):
+    #                 updateVPanel(child)
+    #     if type_ == 'avatar':
+    #         updateVPanel(self.vpanel)
+
+    # def onClick(self, sender):
+    #     if sender == self.footer:
+    #         self.loadMoreMainEntries()
+
+    # def onMouseEnter(self, sender):
+    #     if sender == self.footer:
+    #         self.loadMoreMainEntries()
+
+
+libervia_widget.LiberviaWidget.addDropKey("GROUP", lambda host, item: Blog.onGroupDrop(host, (item,)))
+libervia_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", lambda host, item: Blog.onGroupDrop(host, ()))
+quick_blog.registerClass("ENTRY", Entry)
+quick_widgets.register(quick_blog.QuickBlog, Blog)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/chat.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,345 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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_browser import strings
+from sat_frontends.tools import jid
+from sat_frontends.quick_frontend import quick_widgets, quick_games, quick_menus
+from sat_frontends.quick_frontend.quick_chat import QuickChat
+
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler
+from pyjamas.ui.HTMLPanel import HTMLPanel
+from pyjamas import DOM
+from pyjamas import Window
+
+from datetime import datetime
+
+import html_tools
+import libervia_widget
+import base_panel
+import contact_panel
+import editor_widget
+from constants import Const as C
+import plugin_xep_0085
+import game_tarot
+import game_radiocol
+
+
+unicode = str  # FIXME: pyjamas workaround
+
+
+class MessageWidget(HTMLPanel):
+
+    def __init__(self, mess_data):
+        """
+        @param mess_data(quick_chat.Message, None): message data
+            None: used only for non text widgets (e.g.: focus separator)
+        """
+        self.mess_data = mess_data
+        mess_data.widgets.add(self)
+        _msg_class = []
+        if mess_data.type == C.MESS_TYPE_INFO:
+            markup = "<span class='{msg_class}'>{msg}</span>"
+
+            if mess_data.extra.get('info_type') == 'me':
+                _msg_class.append('chatTextMe')
+            else:
+                _msg_class.append('chatTextInfo')
+            # FIXME: following code was in printInfo before refactoring
+            #        seems to be used only in radiocol
+            # elif type_ == 'link':
+            #     _wid = HTML(msg)
+            #     _wid.setStyleName('chatTextInfo-link')
+            #     if link_cb:
+            #         _wid.addClickListener(link_cb)
+        else:
+            markup = "<span class='chat_text_timestamp'>{timestamp}</span> <span class='chat_text_nick'>{nick}</span> <span class='{msg_class}'>{msg}</span>"
+            _msg_class.append("chat_text_msg")
+            if mess_data.own_mess:
+                _msg_class.append("chat_text_mymess")
+
+        xhtml = mess_data.main_message_xhtml
+        _date = datetime.fromtimestamp(float(mess_data.timestamp))
+        HTMLPanel.__init__(self, markup.format(
+                               timestamp = _date.strftime("%H:%M"),
+                               nick =  "[{}]".format(html_tools.html_sanitize(mess_data.nick)),
+                               msg_class = ' '.join(_msg_class),
+                               msg = strings.addURLToText(html_tools.html_sanitize(mess_data.main_message)) if not xhtml else html_tools.inlineRoot(xhtml)  # FIXME: images and external links must be removed according to preferences
+                           ))
+        if mess_data.type != C.MESS_TYPE_INFO:
+            self.setStyleName('chatText')
+
+
+class Chat(QuickChat, libervia_widget.LiberviaWidget, KeyboardHandler):
+
+    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, subject=None, 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_, nick, occupants, subject, profiles=profiles)
+        self.vpanel = VerticalPanel()
+        self.vpanel.setSize('100%', '100%')
+
+        # FIXME: temporary dirty initialization to display the OTR state
+        header_info = host.plugins['otr'].getInfoTextForUser(target) if (type_ == C.CHAT_ONE2ONE and 'otr' in host.plugins) else None
+
+        libervia_widget.LiberviaWidget.__init__(self, host, title=unicode(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_panel = contact_panel.ContactsPanel(host, merge_resources=False,
+                                                               contacts_style="muc_contact",
+                                                               contacts_menus=(C.MENU_JID_CONTEXT),
+                                                               contacts_display=('resource',))
+            chat_area.add(self.occupants_panel)
+            DOM.setAttribute(chat_area.getWidgetTd(self.occupants_panel), "className", "occupantsPanelCell")
+            # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword)
+            self.presenceListener = self.onPresenceUpdate
+            self.host.addListener('presence', self.presenceListener, [C.PROF_KEY_NONE])
+            self.avatarListener = self.onAvatarUpdate
+            host.addListener('avatar', self.avatarListener, [C.PROF_KEY_NONE])
+            Window.addWindowResizeListener(self)
+
+        else:
+            self.chat_state = None
+
+        self._body.add(chat_area)
+        self.content = AbsolutePanel()
+        self.content.setStyleName('chatContent')
+        self.content_scroll = base_panel.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.chat_state_machine = plugin_xep_0085.ChatStateMachine(self.host, unicode(self.target))
+
+        self.message_box = editor_widget.MessageBox(self.host)
+        self.message_box.onSelectedChange(self)
+        self.message_box.addKeyboardListener(self)
+        self.vpanel.add(self.message_box)
+        self.postInit()
+
+    def onWindowResized(self, width=None, height=None):
+        if self.type == C.CHAT_GROUP:
+            ideal_height = self.content_scroll.getOffsetHeight()
+            self.occupants_panel.setHeight("%s%s" % (ideal_height, "px"))
+
+    @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]
+
+    @property
+    def plugin_menu_context(self):
+        return (C.MENU_ROOM,) if self.type == C.CHAT_GROUP else (C.MENU_SINGLE,)
+
+    def onKeyDown(self, sender, keycode, modifiers):
+        if keycode == KEY_ENTER:
+            self.host.showWarning(None, None)
+        else:
+            self.host.showWarning(*self.getWarningData())
+
+    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.messageSend(self.target,
+                              {'': text},
+                              {},
+                              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.chat_state_machine._onEvent("active")
+
+    def onPresenceUpdate(self, entity, show, priority, statuses, profile):
+        """Update entity's presence status
+
+        @param entity(jid.JID): entity updated
+        @param show: availability
+        @parap priority: resource's priority
+        @param statuses: dict of statuses
+        @param profile: %(doc_profile)s
+        """
+        assert self.type == C.CHAT_GROUP
+        if entity.bare != self.target:
+            return
+        self.update(entity)
+
+    def onAvatarUpdate(self, entity, hash_, profile):
+        """Called on avatar update events
+
+        @param jid_: jid of the entity with updated avatar
+        @param hash_: hash of the avatar
+        @param profile: %(doc_profile)s
+        """
+        assert self.type == C.CHAT_GROUP
+        if entity.bare != self.target:
+            return
+        self.update(entity)
+
+    def onQuit(self):
+        libervia_widget.LiberviaWidget.onQuit(self)
+        if self.type == C.CHAT_GROUP:
+            self.host.removeListener('presence', self.presenceListener)
+            self.host.bridge.mucLeave(self.target.bare, profile=C.PROF_KEY_NONE)
+
+    def newMessage(self, from_jid, target, msg, type_, extra, profile):
+        header_info = extra.pop('header_info', None)
+        if header_info:
+            self.setHeaderInfo(header_info)
+        QuickChat.newMessage(self, from_jid, target, msg, type_, extra, profile)
+
+    def _onHistoryPrinted(self):
+        """Refresh or scroll down the focus after the history is printed"""
+        self.printMessages(clear=False)
+        super(Chat, self)._onHistoryPrinted()
+
+    def printMessages(self, clear=True):
+        """generate message widgets
+
+        @param clear(bool): clear message before printing if true
+        """
+        if clear:
+            # FIXME: clear is not handler
+            pass
+        for message in self.messages.itervalues():
+            self.appendMessage(message)
+
+    def createMessage(self, message):
+        self.appendMessage(message)
+
+    def appendMessage(self, message):
+        self.content.add(MessageWidget(message))
+        self.content_scroll.scrollToBottom()
+
+    def notify(self, contact="somebody", msg=""):
+        """Notify the user of a new message if primitivus doesn't have the focus.
+
+        @param contact (unicode): contact who wrote to the users
+        @param msg (unicode): the message that has been received
+        """
+        self.host.notification.notify(contact, msg)
+
+    # def printDayChange(self, day):
+    #     """Display the day on a new line.
+
+    #     @param day(unicode): day to display (or not if this method is not overwritten)
+    #     """
+    #     self.printInfo("* " + day)
+
+    def setTitle(self, title=None, extra=None):
+        """Refresh the title of this Chat dialog
+
+        @param title (unicode): main title or None to use default
+        @param suffix (unicode): extra title (e.g. for chat states) or None
+        """
+        if title is None:
+            title = unicode(self.target.bare)
+        if extra:
+            title += ' %s' % extra
+        libervia_widget.LiberviaWidget.setTitle(self, title)
+
+    def onChatState(self, from_jid, state, profile):
+        super(Chat, self).onChatState(from_jid, state, profile)
+        if self.type == C.CHAT_ONE2ONE:
+            self.title_dynamic = C.CHAT_STATE_ICON[state]
+
+    def update(self, entity=None):
+        """Update one or all entities.
+
+        @param entity (jid.JID): entity to update
+        """
+        if self.type == C.CHAT_ONE2ONE:  # only update the chat title
+            if self.chat_state:
+                self.setTitle(extra='({})'.format(self.chat_state))
+        else:
+            if entity is None:  # rebuild all the occupants list
+                nicks = list(self.occupants)
+                nicks.sort()
+                self.occupants_panel.setList([jid.newResource(self.target, nick) for nick in nicks])
+            else:  # add, remove or update only one occupant
+                contact_list = self.host.contact_lists[self.profile]
+                show = contact_list.getCache(entity, C.PRESENCE_SHOW)
+                if show == C.PRESENCE_UNAVAILABLE or show is None:
+                    self.occupants_panel.removeContactBox(entity)
+                else:
+                    pass
+                    # FIXME: legacy code, chat state must be checked
+                    # box = self.occupants_panel.updateContactBox(entity)
+                    # box.states.setHTML(u''.join(states.values()))
+
+        # FIXME: legacy code, chat state must be checked
+        # if 'chat_state' in states.keys():  # start/stop sending "composing" state from now
+        #     self.chat_state_machine.started = not not states['chat_state']
+
+        self.onWindowResized()  # be sure to set the good height
+
+    def addGamePanel(self, widget):
+        """Insert a game panel to this Chat dialog.
+
+        @param widget (Widget): the game panel
+        """
+        self.vpanel.insert(widget, 0)
+        self.vpanel.setCellHeight(widget, widget.getHeight())
+
+    def removeGamePanel(self, widget):
+        """Remove the game panel from this Chat dialog.
+
+        @param widget (Widget): the game panel
+        """
+        self.vpanel.remove(widget)
+
+
+quick_widgets.register(QuickChat, Chat)
+quick_widgets.register(quick_games.Tarot, game_tarot.TarotPanel)
+quick_widgets.register(quick_games.Radiocol, game_radiocol.RadioColPanel)
+libervia_widget.LiberviaWidget.addDropKey("CONTACT", lambda host, item: host.displayWidget(Chat, jid.JID(item), dropped=True))
+quick_menus.QuickMenusManager.addDataCollector(C.MENU_ROOM, {'room_jid': 'target'})
+quick_menus.QuickMenusManager.addDataCollector(C.MENU_SINGLE, {'jid': 'target'})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/constants.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,40 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a SAT frontend
+# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from libervia.common.constants import Const as C
+
+
+# Auxiliary functions
+param_to_bool = lambda value: value == 'true'
+
+
+class Const(C):
+    """Add here the constants that are only used by the browser side."""
+
+    # Cached parameters, e.g those that have an incidence on UI display/refresh:
+    #     - they can be any parameter (not necessarily specific to Libervia)
+    #     - list them as a couple (category, name)
+    CACHED_PARAMS = [('General', C.SHOW_OFFLINE_CONTACTS),
+                     ('General', C.SHOW_EMPTY_GROUPS),
+                     ]
+
+    WEB_PANEL_DEFAULT_URL = "http://salut-a-toi.org"
+    WEB_PANEL_SCHEMES = {'http', 'https', 'ftp', 'file'}
+
+    CONTACT_DEFAULT_DISPLAY=('bare', 'nick')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/contact_group.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,245 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2013-2016 Adrien Cossa <souliane@mailoo.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from pyjamas.ui.Button import Button
+from pyjamas.ui.CheckBox import CheckBox
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.DialogBox import DialogBox
+from pyjamas.ui.ScrollPanel import ScrollPanel
+from pyjamas.ui import HasAlignment
+
+import dialog
+import list_manager
+import contact_panel
+import contact_list
+from sat_frontends.tools import jid
+
+
+unicode = str  # FIXME: pyjamas workaround
+
+
+class ContactGroupManager(list_manager.ListManager):
+
+    def __init__(self, editor, data, contacts, offsets):
+        """
+        @param container (FlexTable): FlexTable parent widget
+        @param keys (dict{unicode: dict{unicode: unicode}}): dict binding items
+            keys to their display config data.
+        @param contacts (list): list of contacts
+        """
+        self.editor = editor
+        list_manager.ListManager.__init__(self, data, contacts)
+        self.registerPopupMenuPanel(entries={"Remove group": {}},
+                                    callback=lambda sender, key: self.removeGroup(sender))
+
+    def removeGroup(self, sender):
+        group = sender.getHTML()
+
+        def confirm_cb(answer):
+            if answer:
+                list_manager.ListManager.removeList(self, group)
+                self.editor.add_group_panel.groups.remove(group)
+
+        _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to delete the group '%s'?" % group)
+        _dialog.show()
+
+    def tag(self, contacts):
+        list_manager.ListManager.tag(self, contacts)
+        self.editor.updateContactList(contacts)
+
+    def untag(self, contacts, ignore_key=None):
+        list_manager.ListManager.untag(self, contacts, ignore_key)
+        self.editor.updateContactList(contacts)
+
+
+class ContactGroupEditor(VerticalPanel):
+    """A big panel including a ContactGroupManager and other UI stuff."""
+
+    def __init__(self, host, container=None, onCloseCallback=None):
+        """
+
+        @param host (SatWebFrontend)
+        @param container (PanelBase): parent panel or None to display in a popup
+        @param onCloseCallback (callable)
+        """
+        VerticalPanel.__init__(self, StyleName="contactGroupEditor")
+        self.host = host
+
+        # eventually display in a popup
+        if container is None:
+            container = DialogBox(autoHide=False, centered=True)
+            container.setHTML("Manage contact groups")
+        self.container = container
+        self._on_close_callback = onCloseCallback
+
+        self.all_contacts = contact_list.JIDList(self.host.contact_list.roster)
+        roster_entities_by_group = self.host.contact_list.roster_entities_by_group
+        del roster_entities_by_group[None]  # remove the empty group
+        roster_groups = roster_entities_by_group.keys()
+        roster_groups.sort()
+
+        # groups on the left
+        manager = self.initContactGroupManager(roster_entities_by_group)
+        self.add_group_panel = self.initAddGroupPanel(roster_groups)
+        left_container = VerticalPanel(Width="100%")
+        left_container.add(manager)
+        left_container.add(self.add_group_panel)
+        left_container.setCellHorizontalAlignment(self.add_group_panel, HasAlignment.ALIGN_CENTER)
+        left_panel = ScrollPanel(left_container, StyleName="contactGroupManager")
+        left_panel.setAlwaysShowScrollBars(True)
+
+        # contact list on the right
+        east_panel = ScrollPanel(self.initContactList(), StyleName="contactGroupRoster")
+        east_panel.setAlwaysShowScrollBars(True)
+
+        south_panel = self.initCloseSaveButtons()
+
+        main_panel = HorizontalPanel()
+        main_panel.add(left_panel)
+        main_panel.add(east_panel)
+        self.add(Label("You get here an over whole view of your contact groups. There are two ways to assign your contacts to an existing group: write them into auto-completed textboxes or use the right panel to drag and drop them into the group."))
+        self.add(main_panel)
+        self.add(south_panel)
+
+        self.setCellHorizontalAlignment(south_panel, HasAlignment.ALIGN_CENTER)
+
+        # need to be done after the contact list has been initialized
+        self.updateContactList()
+
+        # Hide the contacts list from the main panel to not confuse the user
+        self.restore_contact_panel = False
+        clist = self.host.contact_list_widget
+        if clist.getVisible():
+            self.restore_contact_panel = True
+            self.host.panel._contactsSwitch()
+
+        container.add(self)
+        container.setVisible(True)
+        if isinstance(container, DialogBox):
+            container.center()
+
+    def initContactGroupManager(self, data):
+        """Initialise the contact group manager.
+
+        @param groups (list[unicode]): contact groups
+        """
+        self.groups = ContactGroupManager(self, data, self.all_contacts)
+        return self.groups
+
+    def initAddGroupPanel(self, groups):
+        """Initialise the 'Add group' panel.
+
+        @param groups (list[unicode]): contact groups
+        """
+
+        def add_group_cb(key):
+            self.groups.addList(key)
+            self.add_group_panel.textbox.setFocus(True)
+
+        add_group_panel = dialog.AddGroupPanel(groups, add_group_cb)
+        add_group_panel.addStyleName("addContactGroupPanel")
+        return add_group_panel
+
+    def initCloseSaveButtons(self):
+        """Add the buttons to close the dialog and save the groups."""
+        buttons = HorizontalPanel()
+        buttons.addStyleName("marginAuto")
+        buttons.add(Button("Cancel", listener=self.cancelWithoutSaving))
+        buttons.add(Button("Save", listener=self.closeAndSave))
+        return buttons
+
+    def initContactList(self):
+        """Add the contact list to the DockPanel."""
+
+        self.toggle = CheckBox("Hide assigned contacts")
+        self.toggle.addClickListener(lambda dummy: self.updateContactList())
+        self.toggle.addStyleName("toggleAssignedContacts")
+        self.contacts = contact_panel.ContactsPanel(self.host)
+        for contact in self.all_contacts:
+            self.contacts.updateContactBox(contact)
+        panel = VerticalPanel()
+        panel.add(self.toggle)
+        panel.add(self.contacts)
+        return panel
+
+    def updateContactList(self, contacts=None):
+        """Update the contact list's items visibility, depending of the toggle
+        checkbox and the "contacts" attribute.
+
+        @param contacts (list): contacts to be updated, or None to update all.
+        """
+        if not hasattr(self, "toggle"):
+            return
+        if contacts is not None:
+            contacts = [jid.JID(contact) for contact in contacts]
+            contacts = set(contacts).intersection(self.all_contacts)
+        else:
+            contacts = self.all_contacts
+
+        for contact in contacts:
+            if not self.toggle.getChecked():  # show all contacts
+                self.contacts.updateContactBox(contact).setVisible(True)
+            else:  # show only non-assigned contacts
+                if contact in self.groups.untagged:
+                    self.contacts.updateContactBox(contact).setVisible(True)
+                else:
+                    self.contacts.updateContactBox(contact).setVisible(False)
+
+    def __close(self):
+        """Remove the widget from parent or close the popup."""
+        if isinstance(self.container, DialogBox):
+            self.container.hide()
+        self.container.remove(self)
+        if self._on_close_callback is not None:
+            self._on_close_callback()
+        if self.restore_contact_panel:
+            self.host.panel._contactsSwitch()
+
+    def cancelWithoutSaving(self):
+        """Ask for confirmation before closing the dialog."""
+        def confirm_cb(answer):
+            if answer:
+                self.__close()
+
+        _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to cancel without saving?")
+        _dialog.show()
+
+    def closeAndSave(self):
+        """Call bridge methods to save the changes and close the dialog"""
+        old_groups_by_entity = contact_list.JIDDict(self.host.contact_list.roster_groups_by_entity)
+        old_entities = old_groups_by_entity.keys()
+        result = {jid.JID(item): keys for item, keys in self.groups.getKeysByItem().iteritems()}
+        groups_by_entity = contact_list.JIDDict(result)
+        entities = groups_by_entity.keys()
+
+        for invalid in entities.difference(self.all_contacts):
+            dialog.InfoDialog("Invalid contact(s)",
+                              "The contact '%s' is not in your contact list but has been assigned to: '%s'." % (invalid, "', '".join(groups_by_entity[invalid])) +
+                              "Your changes could not be saved: please check your assignments and save again.", Width="400px").center()
+            return
+
+        for entity in old_entities.difference(entities):
+            self.host.bridge.call('updateContact', None, unicode(entity), '', [])
+
+        for entity, groups in groups_by_entity.iteritems():
+            if entity not in old_groups_by_entity or groups != old_groups_by_entity[entity]:
+                self.host.bridge.call('updateContact', None, unicode(entity), '', list(groups))
+        self.__close()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/contact_list.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,319 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import pyjd  # this is dummy in pyjs
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat_frontends.quick_frontend.quick_contact_list import QuickContactList
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.ScrollPanel import ScrollPanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui.Label import Label
+from pyjamas import Window
+from pyjamas import DOM
+
+from constants import Const as C
+from sat_frontends.tools import jid
+import libervia_widget
+import contact_panel
+import blog
+import chat
+
+unicode = str # XXX: pyjama doesn't manage unicode
+
+
+class GroupLabel(libervia_widget.DragLabel, Label, ClickHandler):
+    def __init__(self, host, group):
+        """
+
+        @param host (SatWebFrontend)
+        @param group (unicode): group name
+        """
+        self.group = group
+        Label.__init__(self, group)  # , Element=DOM.createElement('div')
+        self.setStyleName('group')
+        libervia_widget.DragLabel.__init__(self, group, "GROUP", host)
+        ClickHandler.__init__(self)
+        self.addClickListener(self)
+
+    def onClick(self, sender):
+        self.host.displayWidget(blog.Blog, (self.group,))
+
+
+class GroupPanel(VerticalPanel):
+
+    def __init__(self, parent):
+        VerticalPanel.__init__(self)
+        self.setStyleName('groupPanel')
+        self._parent = parent
+        self._groups = set()
+
+    def add(self, group):
+        if group in self._groups:
+            log.warning("trying to add an already existing group")
+            return
+        _item = GroupLabel(self._parent.host, group)
+        _item.addMouseListener(self._parent)
+        DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer")
+        index = 0
+        for group_ in [child.group for child in self.getChildren()]:
+            if group_ > group:
+                break
+            index += 1
+        VerticalPanel.insert(self, _item, index)
+        self._groups.add(group)
+
+    def remove(self, group):
+        for wid in self:
+            if isinstance(wid, GroupLabel) and wid.group == group:
+                VerticalPanel.remove(self, wid)
+                self._groups.remove(group)
+                return
+        log.warning("Trying to remove a non existent group")
+
+    def getGroupBox(self, group):
+        """get the widget of a group
+
+        @param group (unicode): the group
+        @return: GroupLabel instance if present, else None"""
+        for wid in self:
+            if isinstance(wid, GroupLabel) and wid.group == group:
+                return wid
+        return None
+
+    def getGroups(self):
+        return self._groups
+
+
+class ContactTitleLabel(libervia_widget.DragLabel, Label, ClickHandler):
+
+    def __init__(self, host, text):
+        Label.__init__(self, text)  # , Element=DOM.createElement('div')
+        self.setStyleName('contactTitle')
+        libervia_widget.DragLabel.__init__(self, text, "CONTACT_TITLE", host)
+        ClickHandler.__init__(self)
+        self.addClickListener(self)
+
+    def onClick(self, sender):
+        self.host.displayWidget(blog.Blog, ())
+
+
+class ContactList(SimplePanel, QuickContactList):
+    """Manage the contacts and groups"""
+
+    def __init__(self, host, target, on_click=None, on_change=None, user_data=None, profiles=None):
+        QuickContactList.__init__(self, host, C.PROF_KEY_NONE)
+        self.contact_list = self.host.contact_list
+        SimplePanel.__init__(self)
+        self.host = host
+        self.scroll_panel = ScrollPanel()
+        self.scroll_panel.addStyleName("gwt-ScrollPanel")  # XXX: no class is set by Pyjamas
+        self.vPanel = VerticalPanel()
+        _title = ContactTitleLabel(host, 'Contacts')
+        DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer")
+
+        def on_click(contact_jid):
+            self.host.displayWidget(chat.Chat, contact_jid, type_=C.CHAT_ONE2ONE)
+            self.removeAlerts(contact_jid, True)
+
+        self._contacts_panel = contact_panel.ContactsPanel(host, contacts_click=on_click, contacts_menus=(C.MENU_JID_CONTEXT, C.MENU_ROSTER_JID_CONTEXT))
+        self._group_panel = GroupPanel(self)
+
+        self.vPanel.add(_title)
+        self.vPanel.add(self._group_panel)
+        self.vPanel.add(self._contacts_panel)
+        self.scroll_panel.add(self.vPanel)
+        self.add(self.scroll_panel)
+        self.setStyleName('contactList')
+        Window.addWindowResizeListener(self)
+
+        # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword)
+        self.avatarListener = self.onAvatarUpdate
+        host.addListener('avatar', self.avatarListener, [C.PROF_KEY_NONE])
+        self.postInit()
+
+    @property
+    def profile(self):
+        return C.PROF_KEY_NONE
+
+    def onDelete(self):
+        QuickContactList.onDelete(self)
+        self.host.removeListener('avatar', self.avatarListener)
+
+    def update(self, entities=None, type_=None, profile=None):
+        # XXX: as update is slow, we avoid many updates on profile plugs
+        # and do them all at once at the end
+        if not self.host._profile_plugged:  # FIXME: should not be necessary anymore (done in QuickFrontend)
+            return
+        ### GROUPS ###
+        _keys = self.contact_list._groups.keys()
+        try:
+            # XXX: Pyjamas doesn't do the set casting if None is present
+            _keys.remove(None)
+        except (KeyError, ValueError): # XXX: error raised depend on pyjama's compilation options
+            pass
+        current_groups = set(_keys)
+        shown_groups = self._group_panel.getGroups()
+        new_groups = current_groups.difference(shown_groups)
+        removed_groups = shown_groups.difference(current_groups)
+        for group in new_groups:
+            self._group_panel.add(group)
+        for group in removed_groups:
+            self._group_panel.remove(group)
+
+        ### JIDS ###
+        to_show = [jid_ for jid_ in self.contact_list.roster if self.contact_list.entityVisible(jid_) and jid_ != self.contact_list.whoami.bare]
+        to_show.sort()
+
+        self._contacts_panel.setList(to_show)
+
+    def onWindowResized(self, width, height):
+        ideal_height = height - DOM.getAbsoluteTop(self.getElement()) - 5
+        tab_panel = self.host.panel.tab_panel
+        if tab_panel.getWidgetCount() > 1:
+            ideal_height -= tab_panel.getTabBar().getOffsetHeight()
+        self.scroll_panel.setHeight("%s%s" % (ideal_height, "px"))
+
+    def 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 getGroups(self):
+        return set([g for g in self._groups if g is not None])
+
+    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.contact_list.getGroupData(sender.group, "jids")
+            for contact in self._contacts_panel.getBoxes():
+                if contact.jid in jids:
+                    contact.label.addStyleName("selected")
+
+    def onMouseLeave(self, sender):
+        if isinstance(sender, GroupLabel):
+            jids = self.contact_list.getGroupData(sender.group, "jids")
+            for contact in self._contacts_panel.getBoxes():
+                if contact.jid in jids:
+                    contact.label.removeStyleName("selected")
+
+    def onAvatarUpdate(self, jid_, hash_, profile):
+        """Called on avatar update events
+
+        @param jid_: jid of the entity with updated avatar
+        @param hash_: hash of the avatar
+        @param profile: %(doc_profile)s
+        """
+        box = self._contacts_panel.getContactBox(jid_)
+        if box:
+            box.update()
+
+    def onNickUpdate(self, jid_, new_nick, profile):
+        box = self._contacts_panel.getContactBox(jid_)
+        if box:
+            box.update()
+
+    def offlineContactsToShow(self):
+        """Tell if offline contacts should be visible according to the user settings
+
+        @return: boolean
+        """
+        return C.bool(self.host.getCachedParam('General', C.SHOW_OFFLINE_CONTACTS))
+
+    def emtyGroupsToShow(self):
+        """Tell if empty groups should be visible according to the user settings
+
+        @return: boolean
+        """
+        return C.bool(self.host.getCachedParam('General', C.SHOW_EMPTY_GROUPS))
+
+    def onPresenceUpdate(self, entity, show, priority, statuses, profile):
+        QuickContactList.onPresenceUpdate(self, entity, show, priority, statuses, profile)
+        box = self._contacts_panel.getContactBox(entity)
+        if box:  # box doesn't exist for MUC bare entity, don't create it
+            box.update()
+
+
+class JIDList(list):
+    """JID-friendly list implementation for Pyjamas"""
+
+    def __contains__(self, item):
+        """Tells if the list contains the given item.
+
+        @param item (object): element to check
+        @return: bool
+        """
+        # Since our JID doesn't inherit from str/unicode, without this method
+        # the test would return True only when the objects references are the
+        # same. Tests have shown that the other iterable "set" and "dict" don't
+        # need this hack to reproduce the Twisted's behavior.
+        for other in self:
+            if other == item:
+                return True
+        return False
+
+    def index(self, item):
+        i = 0
+        for other in self:
+            if other == item:
+                return i
+            i += 1
+        raise ValueError("JIDList.index(%(item)s): %(item)s not in list" % {"item": item})
+
+class JIDSet(set):
+    """JID set implementation for Pyjamas"""
+
+    def __contains__(self, item):
+        return __containsJID(self, item)
+
+
+class JIDDict(dict):
+    """JID dict implementation for Pyjamas (a dict with JID keys)"""
+
+    def __contains__(self, item):
+        return __containsJID(self, item)
+
+    def keys(self):
+        return JIDSet(dict.keys(self))
+
+
+def __containsJID(iterable, item):
+    """Tells if the given item is in the iterable, works with JID.
+
+    @param iterable(object): list, set or another iterable object
+    @param item (object): element
+    @return: bool
+    """
+    # Pyjamas JID-friendly implementation of the "in" operator. Since our JID
+    # doesn't inherit from str, without this method the test would return True
+    # only when the objects references are the same.
+    if isinstance(item, jid.JID):
+        return hash(item) in [hash(other) for other in iterable if isinstance(other, jid.JID)]
+    return super(type(iterable), iterable).__contains__(iterable, item)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/contact_panel.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,157 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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/>.
+
+""" Contacts / jids related panels """
+
+import pyjd  # this is dummy in pyjs
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat_frontends.tools import jid
+
+from pyjamas.ui.ScrollPanel import ScrollPanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+
+import contact_widget
+from constants import Const as C
+
+
+class ContactsPanel(ScrollPanel):
+    """ContactList graphic representation
+
+    Special features like popup menu panel or changing the contact states must be done in a sub-class.
+    """
+
+    def __init__(self, host, merge_resources=True, contacts_click=None,
+                 contacts_style=None, contacts_menus=None,
+                 contacts_display=C.CONTACT_DEFAULT_DISPLAY):
+        """
+
+        @param host (SatWebFrontend): host instance
+        @param merge_resources (bool): if True, the entities sharing the same
+            bare JID will also share the same contact box.
+        @param contacts_click (callable): click callback for the contact boxes
+        @param contacts_style (unicode): CSS style name for the contact boxes
+        @param contacts_menus (tuple): define the menu types that fit this
+            contact panel, with values from the menus type constants.
+        @param contacts_display (tuple): prioritize the display methods of the
+            contact's label with values in ("jid", "nick", "bare", "resource")
+        """
+        self.panel = VerticalPanel()
+        ScrollPanel.__init__(self, self.panel)
+        self.addStyleName("gwt-ScrollPanel")  # XXX: no class is set by Pyjamas
+
+        self.host = host
+        self.merge_resources = merge_resources
+        self._contacts = {}  # entity jid to ContactBox map
+        self.panel.click_listener = None
+
+        if contacts_click is not None:
+            self.panel.onClick = contacts_click
+
+        self.contacts_style = contacts_style
+        self.contacts_menus = contacts_menus
+        self.contacts_display = contacts_display
+
+    def _key(self, contact_jid):
+        """Return internal key for this contact.
+
+        @param contact_jid (jid.JID): contact JID
+        @return: jid.JID
+        """
+        return contact_jid.bare if self.merge_resources else contact_jid
+
+    def getJids(self):
+        """Return jids present in the panel
+
+        @return (list[jid.JID]): full jids or bare jids if self.merge_resources is True
+        """
+        return self._contacts.keys()
+
+    def getBoxes(self):
+        """Return ContactBox present in the panel
+
+        @return (list[ContactBox]): boxes of the contacts
+        """
+        return self._contacts.itervalues()
+
+    def clear(self):
+        """Clear all contacts."""
+        self._contacts.clear()
+        VerticalPanel.clear(self.panel)
+
+    def setList(self, jids):
+        """set all contacts in the list in one shot.
+
+        @param jids (list[jid.JID]): jids to display (the order is kept)
+        @param name (unicode): optional name of the contact
+        """
+        current_jids = [box.jid for box in self.panel.children if isinstance(box, contact_widget.ContactBox)]
+        if current_jids == jids:
+            # the display doesn't change
+            return
+        for contact_jid in set(current_jids).difference(jids):
+            self.removeContactBox(contact_jid)
+        count = 0
+        for contact_jid in jids:
+            assert isinstance(contact_jid, jid.JID)
+            self.updateContactBox(contact_jid, count)
+            count += 1
+
+    def getContactBox(self, contact_jid):
+        """Get the contact box for the given contact.
+
+        @param contact_jid (jid.JID): contact JID
+        @return: ContactBox or None
+        """
+        try:
+            return self._contacts[self._key(contact_jid)]
+        except KeyError:
+            return None
+
+    def updateContactBox(self, contact_jid, index=None):
+        """Add a contact or update it if it already exists.
+
+        @param contact_jid (jid.JID): contact JID
+        @param index (int): insertion index if the contact is added
+        @return: ContactBox
+        """
+        box = self.getContactBox(contact_jid)
+        if box:
+            box.update()
+        else:
+            entity = contact_jid.bare if self.merge_resources else contact_jid
+            box = contact_widget.ContactBox(self.host, entity,
+                                            style_name=self.contacts_style,
+                                            display=self.contacts_display,
+                                            plugin_menu_context=self.contacts_menus)
+            self._contacts[self._key(contact_jid)] = box
+            if index:
+                VerticalPanel.insert(self.panel, box, index)
+            else:
+                VerticalPanel.append(self.panel, box)
+        return box
+
+    def removeContactBox(self, contact_jid):
+        """Remove a contact.
+
+        @param contact_jid (jid.JID): contact JID
+        """
+        box = self._contacts.pop(self._key(contact_jid), None)
+        if box:
+            VerticalPanel.remove(self.panel, box)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/contact_widget.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,160 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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 import exceptions
+from sat_frontends.quick_frontend import quick_menus
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.Image import Image
+from pyjamas.ui.ClickListener import ClickHandler
+from constants import Const as C
+import html_tools
+import base_widget
+import libervia_widget
+
+unicode = str # XXX: pyjama doesn't manage unicode
+
+
+class ContactLabel(HTML):
+    """Display a contact in HTML, selecting best display (jid/nick/etc)"""
+
+    def __init__(self, host, jid_, display=C.CONTACT_DEFAULT_DISPLAY):
+        """
+
+        @param host (SatWebFrontend): host instance
+        @param jid_ (jid.JID): contact JID
+        @param display (tuple): prioritize the display methods of the contact's
+            label with values in ("jid", "nick", "bare", "resource").
+        """
+        # TODO: add a listener for nick changes
+        HTML.__init__(self)
+        self.host = host
+        self.jid = jid_
+        self.display = display
+        self.alert = False
+        self.setStyleName('contactLabel')
+
+    def update(self):
+        clist = self.host.contact_list
+        notifs = list(self.host.getNotifs(self.jid, exact_jid=False, profile=C.PROF_KEY_NONE))
+        alerts_count = len(notifs)
+        alert_html = ("<strong>(%i)</strong>&nbsp;" % alerts_count) if alerts_count else ""
+
+        contact_raw = None
+        for disp in self.display:
+            if disp == "jid":
+                contact_raw = unicode(self.jid)
+            elif disp == "nick":
+                clist = self.host.contact_list
+                contact_raw = html_tools.html_sanitize(clist.getCache(self.jid, "nick"))
+            elif disp == "bare":
+                contact_raw = unicode(self.jid.bare)
+            elif disp == "resource":
+                contact_raw = self.jid.resource
+            else:
+                raise exceptions.InternalError(u"Unknown display argument [{}]".format(disp))
+            if contact_raw:
+                break
+        if not contact_raw:
+            log.error(u"Could not find a contact display for jid {jid} (display: {display})".format(jid=self.jid, display=self.display))
+            contact_raw = "UNNAMED"
+        contact_html = html_tools.html_sanitize(contact_raw)
+
+        html = "%(alert)s%(contact)s" % {'alert': alert_html,
+                                         'contact': contact_html}
+        self.setHTML(html)
+
+
+class ContactMenuBar(base_widget.WidgetMenuBar):
+    """WidgetMenuBar displaying the contact's avatar as category."""
+
+    def onBrowserEvent(self, event):
+        base_widget.WidgetMenuBar.onBrowserEvent(self, event)
+        event.stopPropagation()  # prevent opening the chat dialog
+
+    @classmethod
+    def getCategoryHTML(cls, category):
+        """Return the HTML code for displaying contact's avatar.
+
+        @param category (quick_menus.MenuCategory): ignored
+        @return(unicode): HTML to display
+        """
+        return '<img src="%s"/>' % C.DEFAULT_AVATAR_URL
+
+    def setUrl(self, url):
+        """Set the URL of the contact avatar.
+
+        @param url (unicode): avatar URL
+        """
+        if not self.items:  # the menu is empty but we've been asked to set an avatar
+            self.addCategory("dummy")
+        self.items[0].setHTML('<img src="%s" />' % url)
+
+
+class ContactBox(VerticalPanel, ClickHandler, libervia_widget.DragLabel):
+
+    def __init__(self, host, jid_, style_name=None, display=C.CONTACT_DEFAULT_DISPLAY, plugin_menu_context=None):
+        """
+        @param host (SatWebFrontend): host instance
+        @param jid_ (jid.JID): contact JID
+        @param style_name (unicode): CSS style name
+        @param contacts_display (tuple): prioritize the display methods of the
+            contact's label with values in ("jid", "nick", "bare", "resource").
+        @param plugin_menu_context (iterable): contexts of menus to have (list of C.MENU_* constant)
+
+        """
+        self.plugin_menu_context = [] if plugin_menu_context is None else plugin_menu_context
+        VerticalPanel.__init__(self, StyleName=style_name or 'contactBox', VerticalAlignment='middle')
+        ClickHandler.__init__(self)
+        libervia_widget.DragLabel.__init__(self, jid_, "CONTACT", host)
+        self.jid = jid_
+        self.label = ContactLabel(host, self.jid, display=display)
+        self.avatar = ContactMenuBar(self, host) if plugin_menu_context else Image()
+        self.states = HTML()
+        self.add(self.avatar)
+        self.add(self.label)
+        self.add(self.states)
+        self.update()
+        self.addClickListener(self)
+
+    def update(self):
+        """Update the display.
+
+        @param with_bare (bool): if True, ignore the resource and update with bare information.
+        """
+        self.avatar.setUrl(self.host.getAvatarURL(self.jid))
+
+        self.label.update()
+        clist = self.host.contact_list
+        show = clist.getCache(self.jid, C.PRESENCE_SHOW)
+        if show is None:
+            show = C.PRESENCE_UNAVAILABLE
+        html_tools.setPresenceStyle(self.label, show)
+
+    def onClick(self, sender):
+        try:
+            self.parent.onClick(self.jid)
+        except (AttributeError, TypeError):
+            pass
+
+quick_menus.QuickMenusManager.addDataCollector(C.MENU_JID_CONTEXT, lambda caller, dummy: {'jid': unicode(caller.jid.bare)})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/dialog.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,616 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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 constants import Const as C
+from sat_frontends.tools import jid
+
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.Grid import Grid
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.PopupPanel import PopupPanel
+from pyjamas.ui.DialogBox import DialogBox
+from pyjamas.ui.ListBox import ListBox
+from pyjamas.ui.Button import Button
+from pyjamas.ui.TextBox import TextBox
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.RadioButton import RadioButton
+from pyjamas.ui import HasAlignment
+from pyjamas.ui.KeyboardListener import KEY_ESCAPE, KEY_ENTER
+from pyjamas.ui.MouseListener import MouseWheelHandler
+from pyjamas import Window
+
+import base_panel
+
+
+# List here the patterns that are not allowed in contact group names
+FORBIDDEN_PATTERNS_IN_GROUP = ()
+
+
+unicode = str # XXX: pyjama doesn't manage unicode
+
+
+class RoomChooser(Grid):
+    """Select a room from the rooms you already joined, or create a new one"""
+
+    GENERATE_MUC = "<use random name>"
+
+    def __init__(self, host, room_jid_s=None):
+        """
+
+        @param host (SatWebFrontend)
+        @param room_jid_s (unicode): room JID
+        """
+        Grid.__init__(self, 2, 2, Width='100%')
+        self.host = host
+
+        self.new_radio = RadioButton("room", "Discussion room:")
+        self.new_radio.setChecked(True)
+        self.box = TextBox(Width='95%')
+        self.box.setText(room_jid_s if room_jid_s else self.GENERATE_MUC)
+        self.exist_radio = RadioButton("room", "Already joined:")
+        self.rooms_list = ListBox(Width='95%')
+
+        self.add(self.new_radio, 0, 0)
+        self.add(self.box, 0, 1)
+        self.add(self.exist_radio, 1, 0)
+        self.add(self.rooms_list, 1, 1)
+
+        self.box.addFocusListener(self)
+        self.rooms_list.addFocusListener(self)
+
+        self.exist_radio.setVisible(False)
+        self.rooms_list.setVisible(False)
+        self.refreshOptions()
+
+    @property
+    def room(self):
+        """Get the room that has been selected or entered by the user
+
+        @return: jid.JID or None to let the backend generate a new name
+        """
+        if self.exist_radio.getChecked():
+            values = self.rooms_list.getSelectedValues()
+            return jid.JID(values[0]) if values else None
+        value = self.box.getText()
+        return None if value in ('', self.GENERATE_MUC) else jid.JID(value)
+
+    def onFocus(self, sender):
+        if sender == self.rooms_list:
+            self.exist_radio.setChecked(True)
+        elif sender == self.box:
+            if self.box.getText() == self.GENERATE_MUC:
+                self.box.setText("")
+            self.new_radio.setChecked(True)
+
+    def onLostFocus(self, sender):
+        if sender == self.box:
+            if self.box.getText() == "":
+                self.box.setText(self.GENERATE_MUC)
+
+    def refreshOptions(self):
+        """Refresh the already joined room list"""
+        contact_list = self.host.contact_list
+        muc_rooms = contact_list.getSpecials(C.CONTACT_SPECIAL_GROUP)
+        for room in muc_rooms:
+            self.rooms_list.addItem(room.bare)
+        if len(muc_rooms) > 0:
+            self.exist_radio.setVisible(True)
+            self.rooms_list.setVisible(True)
+            self.exist_radio.setChecked(True)
+
+
+class ContactsChooser(VerticalPanel):
+    """Select one or several connected contacts"""
+
+    def __init__(self, host, nb_contact=None, ok_button=None):
+        """
+        @param host: SatWebFrontend instance
+        @param nb_contact: number of contacts that have to be selected, None for no limit
+        If a tuple is given instead of an integer, nb_contact[0] is the minimal and
+        nb_contact[1] is the maximal number of contacts to be chosen.
+        """
+        self.host = host
+        if isinstance(nb_contact, tuple):
+            if len(nb_contact) == 0:
+                nb_contact = None
+            elif len(nb_contact) == 1:
+                nb_contact = (nb_contact[0], nb_contact[0])
+        elif nb_contact is not None:
+            nb_contact = (nb_contact, nb_contact)
+        if nb_contact is None:
+            log.debug("Need to select as many contacts as you want")
+        else:
+            log.debug("Need to select between %d and %d contacts" % nb_contact)
+        self.nb_contact = nb_contact
+        self.ok_button = ok_button
+        VerticalPanel.__init__(self, Width='100%')
+        self.contacts_list = ListBox()
+        self.contacts_list.setMultipleSelect(True)
+        self.contacts_list.setWidth("95%")
+        self.contacts_list.addStyleName('contactsChooser')
+        self.contacts_list.addChangeListener(self.onChange)
+        self.add(self.contacts_list)
+        self.refreshOptions()
+        self.onChange()
+
+    @property
+    def contacts(self):
+        """Return the selected contacts.
+
+        @return: list[jid.JID]
+        """
+        return [jid.JID(contact) for contact in self.contacts_list.getSelectedValues(True)]
+
+    def onChange(self, sender=None):
+        if self.ok_button is None:
+            return
+        if self.nb_contact:
+            selected = len(self.contacts_list.getSelectedValues(True))
+            if selected >= self.nb_contact[0] and selected <= self.nb_contact[1]:
+                self.ok_button.setEnabled(True)
+            else:
+                self.ok_button.setEnabled(False)
+
+    def refreshOptions(self, keep_selected=False):
+        """Fill the list with the connected contacts.
+
+        @param keep_selected (boolean): if True, keep the current selection
+        """
+        selection = self.contacts if keep_selected else []
+        self.contacts_list.clear()
+        contacts = self.host.contact_list.roster_connected
+        self.contacts_list.setVisibleItemCount(10 if len(contacts) > 5 else 5)
+        self.contacts_list.addItem("")
+        for contact in contacts:
+            self.contacts_list.addItem(contact)
+        if selection:
+            self.contacts_list.setItemTextSelection([unicode(contact) for contact in selection])
+
+
+class RoomAndContactsChooser(DialogBox):
+    """Select a room and some users to invite in"""
+
+    def __init__(self, host, callback, nb_contact=None, ok_button="OK", title="Discussion groups",
+                 title_room="Join room", title_invite="Invite contacts", visible=(True, True)):
+        DialogBox.__init__(self, centered=True)
+        self.host = host
+        self.callback = callback
+        self.title_room = title_room
+        self.title_invite = title_invite
+
+        button_panel = HorizontalPanel()
+        button_panel.addStyleName("marginAuto")
+        ok_button = Button("OK", self.onOK)
+        button_panel.add(ok_button)
+        button_panel.add(Button("Cancel", self.onCancel))
+
+        self.room_panel = RoomChooser(host, None if visible == (False, True) else host.default_muc)
+        self.contact_panel = ContactsChooser(host, nb_contact, ok_button)
+
+        self.stack_panel = base_panel.ToggleStackPanel(Width="100%")
+        self.stack_panel.add(self.room_panel, visible=visible[0])
+        self.stack_panel.add(self.contact_panel, visible=visible[1])
+        self.stack_panel.addStackChangeListener(self)
+        self.onStackChanged(self.stack_panel, 0, visible[0])
+        self.onStackChanged(self.stack_panel, 1, visible[1])
+
+        main_panel = VerticalPanel()
+        main_panel.setStyleName("room-contact-chooser")
+        main_panel.add(self.stack_panel)
+        main_panel.add(button_panel)
+
+        self.setWidget(main_panel)
+        self.setHTML(title)
+        self.show()
+
+        # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword)
+        self.presenceListener = self.refreshContactList
+        # update the contacts list when someone logged in/out
+        self.host.addListener('presence', self.presenceListener, [C.PROF_KEY_NONE])
+
+    @property
+    def room(self):
+        """Get the room that has been selected or entered by the user
+
+        @return: jid.JID or None
+        """
+        return self.room_panel.room
+
+    @property
+    def contacts(self):
+        """Return the selected contacts.
+
+        @return: list[jid.JID]
+        """
+        return self.contact_panel.contacts
+
+    def onStackChanged(self, sender, index, visible=None):
+        if visible is None:
+            visible = sender.getWidget(index).getVisible()
+        if index == 0:
+            suffix = "" if (visible or not self.room) else ": %s" % self.room
+            sender.setStackText(0, self.title_room + suffix)
+        elif index == 1:
+            suffix = "" if (visible or not self.contacts) else ": %s" % ", ".join([unicode(contact) for contact in self.contacts])
+            sender.setStackText(1, self.title_invite + suffix)
+
+    def refreshContactList(self, *args, **kwargs):
+        """Called when someone log in/out to update the list.
+
+        @param args: set by the event call but not used here
+        """
+        self.contact_panel.refreshOptions(keep_selected=True)
+
+    def onOK(self, sender):
+        room = self.room  # pyjamas issue: you need to use an intermediate variable to access a property's method
+        self.hide()
+        self.callback(room, self.contacts)
+
+    def onCancel(self, sender):
+        self.hide()
+
+    def hide(self):
+        self.host.removeListener('presence', self.presenceListener)
+        DialogBox.hide(self, autoClosed=True)
+
+
+class GenericConfirmDialog(DialogBox):
+
+    def __init__(self, widgets, callback, title='Confirmation', prompt_widgets=None, **kwargs):
+        """
+        Dialog to confirm an action
+        @param widgets (list[Widget]): widgets to attach
+        @param callback (callable): method to call when a button is pressed,
+            with the following arguments:
+                - result (bool): set to True if the dialog has been confirmed
+                - *args: a list of unicode (the values for the prompt_widgets)
+        @param title: title of the dialog
+        @param prompt_widgets (list[TextBox]): input widgets from which to retrieve
+        the string value(s) to be passed to the callback when OK button is pressed.
+        If None, OK button will return "True". Cancel button always returns "False".
+        """
+        self.callback = callback
+        added_style = kwargs.pop('AddStyleName', None)
+        DialogBox.__init__(self, centered=True, **kwargs)
+        if added_style:
+            self.addStyleName(added_style)
+
+        if prompt_widgets is None:
+            prompt_widgets = []
+
+        content = VerticalPanel()
+        content.setWidth('100%')
+        for wid in widgets:
+            content.add(wid)
+            if wid in prompt_widgets:
+                wid.setWidth('100%')
+        button_panel = HorizontalPanel()
+        button_panel.addStyleName("marginAuto")
+        self.confirm_button = Button("OK", self.onConfirm)
+        button_panel.add(self.confirm_button)
+        self.cancel_button = Button("Cancel", self.onCancel)
+        button_panel.add(self.cancel_button)
+        content.add(button_panel)
+        self.setHTML(title)
+        self.setWidget(content)
+        self.prompt_widgets = prompt_widgets
+
+    def onConfirm(self, sender):
+        self.hide()
+        result = [True]
+        result.extend([box.getText() for box in self.prompt_widgets])
+        self.callback(*result)
+
+    def onCancel(self, sender):
+        self.hide()
+        self.callback(False)
+
+    def show(self):
+        DialogBox.show(self)
+        if self.prompt_widgets:
+            self.prompt_widgets[0].setFocus(True)
+
+
+class ConfirmDialog(GenericConfirmDialog):
+
+    def __init__(self, callback, text='Are you sure ?', title='Confirmation', **kwargs):
+        GenericConfirmDialog.__init__(self, [HTML(text)], callback, title, **kwargs)
+
+
+class GenericDialog(DialogBox):
+    """Dialog which just show a widget and a close button"""
+
+    def __init__(self, title, main_widget, callback=None, options=None, **kwargs):
+        """Simple notice dialog box
+        @param title: HTML put in the header
+        @param main_widget: widget put in the body
+        @param callback: method to call on closing
+        @param options: one or more of the following options:
+                        - NO_CLOSE: don't add a close button"""
+        added_style = kwargs.pop('AddStyleName', None)
+        DialogBox.__init__(self, centered=True, **kwargs)
+        if added_style:
+            self.addStyleName(added_style)
+
+        self.callback = callback
+        if not options:
+            options = []
+        _body = VerticalPanel()
+        _body.setSize('100%', '100%')
+        _body.setSpacing(4)
+        _body.add(main_widget)
+        _body.setCellWidth(main_widget, '100%')
+        _body.setCellHeight(main_widget, '100%')
+        if 'NO_CLOSE' not in options:
+            _close_button = Button("Close", self.onClose)
+            _body.add(_close_button)
+            _body.setCellHorizontalAlignment(_close_button, HasAlignment.ALIGN_CENTER)
+        self.setHTML(title)
+        self.setWidget(_body)
+        self.panel.setSize('100%', '100%')  # Need this hack to have correct size in Gecko & Webkit
+
+    def close(self):
+        """Same effect as clicking the close button"""
+        self.onClose(None)
+
+    def onClose(self, sender):
+        self.hide()
+        if self.callback:
+            self.callback()
+
+
+class InfoDialog(GenericDialog):
+
+    def __init__(self, title, body, callback=None, options=None, **kwargs):
+        GenericDialog.__init__(self, title, HTML(body), callback, options, **kwargs)
+
+
+class PromptDialog(GenericConfirmDialog):
+
+    def __init__(self, callback, textes=None, values=None, title='User input', **kwargs):
+        """Prompt the user for one or more input(s).
+
+        @param callback (callable): method to call when a button is pressed,
+            with the following arguments:
+                - result (bool): set to True if the dialog has been confirmed
+                - *args: a list of unicode (the values entered by the user)
+        @param textes (list[unicode]): HTML textes to display before the inputs
+        @param values (list[unicode]): default values for each input
+        @param title (unicode): dialog title
+        """
+        if textes is None:
+            textes = ['']  # display a single input without any description
+        if values is None:
+            values = []
+        all_widgets = []
+        prompt_widgets = []
+        for count in xrange(len(textes)):
+            all_widgets.append(HTML(textes[count]))
+            prompt = TextBox()
+            if len(values) > count:
+                prompt.setText(values[count])
+            all_widgets.append(prompt)
+            prompt_widgets.append(prompt)
+
+        GenericConfirmDialog.__init__(self, all_widgets, callback, title, prompt_widgets, **kwargs)
+
+
+class PopupPanelWrapper(PopupPanel):
+    """This wrapper catch Escape event to avoid request cancellation by Firefox"""
+
+    def onEventPreview(self, event):
+        if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE:
+            # needed to prevent request cancellation in Firefox
+            event.preventDefault()
+        return PopupPanel.onEventPreview(self, event)
+
+
+class ExtTextBox(TextBox):
+    """Extended TextBox"""
+
+    def __init__(self, *args, **kwargs):
+        if 'enter_cb' in kwargs:
+            self.enter_cb = kwargs['enter_cb']
+            del kwargs['enter_cb']
+        TextBox.__init__(self, *args, **kwargs)
+        self.addKeyboardListener(self)
+
+    def onKeyUp(self, sender, keycode, modifiers):
+        pass
+
+    def onKeyDown(self, sender, keycode, modifiers):
+        pass
+
+    def onKeyPress(self, sender, keycode, modifiers):
+        if self.enter_cb and keycode == KEY_ENTER:
+            self.enter_cb(self)
+
+
+class GroupSelector(DialogBox):
+
+    def __init__(self, top_widgets, initial_groups, selected_groups,
+                 ok_title="OK", ok_cb=None, cancel_cb=None):
+        DialogBox.__init__(self, centered=True)
+        main_panel = VerticalPanel()
+        self.ok_cb = ok_cb
+        self.cancel_cb = cancel_cb
+
+        for wid in top_widgets:
+            main_panel.add(wid)
+
+        main_panel.add(Label('Select in which groups your contact is:'))
+        self.list_box = ListBox()
+        self.list_box.setMultipleSelect(True)
+        self.list_box.setVisibleItemCount(5)
+        self.setAvailableGroups(initial_groups)
+        self.setGroupsSelected(selected_groups)
+        main_panel.add(self.list_box)
+
+        def cb(text):
+            self.list_box.addItem(text)
+            self.list_box.setItemSelected(self.list_box.getItemCount() - 1, "selected")
+
+        main_panel.add(AddGroupPanel(initial_groups, cb))
+
+        button_panel = HorizontalPanel()
+        button_panel.addStyleName("marginAuto")
+        button_panel.add(Button(ok_title, self.onOK))
+        button_panel.add(Button("Cancel", self.onCancel))
+        main_panel.add(button_panel)
+
+        self.setWidget(main_panel)
+
+    def getSelectedGroups(self):
+        """Return a list of selected groups"""
+        return self.list_box.getSelectedValues()
+
+    def setAvailableGroups(self, groups):
+        _groups = list(set(groups))
+        _groups.sort()
+        self.list_box.clear()
+        for group in _groups:
+            self.list_box.addItem(group)
+
+    def setGroupsSelected(self, selected_groups):
+        self.list_box.setItemTextSelection(selected_groups)
+
+    def onOK(self, sender):
+        self.hide()
+        if self.ok_cb:
+            self.ok_cb(self)
+
+    def onCancel(self, sender):
+        self.hide()
+        if self.cancel_cb:
+            self.cancel_cb(self)
+
+
+class AddGroupPanel(HorizontalPanel):
+    def __init__(self, groups, cb=None):
+        """
+        @param groups: list of the already existing groups
+        """
+        HorizontalPanel.__init__(self)
+        self.groups = groups
+        self.add(Label('New group:'))
+        self.textbox = ExtTextBox(enter_cb=self.onGroupInput)
+        self.add(self.textbox)
+        self.add(Button("Add", lambda sender: self.onGroupInput(self.textbox)))
+        self.cb = cb
+
+    def onGroupInput(self, sender):
+        text = sender.getText()
+        if text == "":
+            return
+        for group in self.groups:
+            if text == group:
+                Window.alert("The group '%s' already exists." % text)
+                return
+        for pattern in FORBIDDEN_PATTERNS_IN_GROUP:
+            if pattern in text:
+                Window.alert("The pattern '%s' is not allowed in group names." % pattern)
+                return
+        sender.setText('')
+        self.groups.append(text)
+        if self.cb is not None:
+            self.cb(text)
+
+
+class WheelTextBox(TextBox, MouseWheelHandler):
+
+    def __init__(self, *args, **kwargs):
+        TextBox.__init__(self, *args, **kwargs)
+        MouseWheelHandler.__init__(self)
+
+
+class IntSetter(HorizontalPanel):
+    """This class show a bar with button to set an int value"""
+
+    def __init__(self, label, value=0, value_max=None, visible_len=3):
+        """initialize the intSetter
+        @param label: text shown in front of the setter
+        @param value: initial value
+        @param value_max: limit value, None or 0 for unlimited"""
+        HorizontalPanel.__init__(self)
+        self.value = value
+        self.value_max = value_max
+        _label = Label(label)
+        self.add(_label)
+        self.setCellWidth(_label, "100%")
+        minus_button = Button("-", self.onMinus)
+        self.box = WheelTextBox()
+        self.box.setVisibleLength(visible_len)
+        self.box.setText(unicode(value))
+        self.box.addInputListener(self)
+        self.box.addMouseWheelListener(self)
+        plus_button = Button("+", self.onPlus)
+        self.add(minus_button)
+        self.add(self.box)
+        self.add(plus_button)
+        self.valueChangedListener = []
+
+    def addValueChangeListener(self, listener):
+        self.valueChangedListener.append(listener)
+
+    def removeValueChangeListener(self, listener):
+        if listener in self.valueChangedListener:
+            self.valueChangedListener.remove(listener)
+
+    def _callListeners(self):
+        for listener in self.valueChangedListener:
+            listener(self.value)
+
+    def setValue(self, value):
+        """Change the value and fire valueChange listeners"""
+        self.value = value
+        self.box.setText(unicode(value))
+        self._callListeners()
+
+    def onMinus(self, sender, step=1):
+        self.value = max(0, self.value - step)
+        self.box.setText(unicode(self.value))
+        self._callListeners()
+
+    def onPlus(self, sender, step=1):
+        self.value += step
+        if self.value_max:
+            self.value = min(self.value, self.value_max)
+        self.box.setText(unicode(self.value))
+        self._callListeners()
+
+    def onInput(self, sender):
+        """Accept only valid integer && normalize print (no leading 0)"""
+        try:
+            self.value = int(self.box.getText()) if self.box.getText() else 0
+        except ValueError:
+            pass
+        if self.value_max:
+            self.value = min(self.value, self.value_max)
+        self.box.setText(unicode(self.value))
+        self._callListeners()
+
+    def onMouseWheel(self, sender, velocity):
+        if velocity > 0:
+            self.onMinus(sender, 10)
+        else:
+            self.onPlus(sender, 10)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/editor_widget.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,380 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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_browser import strings
+
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.TextArea import TextArea
+from pyjamas.ui import KeyboardListener as keyb
+from pyjamas.ui.FocusListener import FocusHandler
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui.MouseListener import MouseHandler
+from pyjamas.Timer import Timer
+from pyjamas import DOM
+
+import html_tools
+
+
+class MessageBox(TextArea):
+    """A basic text area for entering messages"""
+
+    def __init__(self, host):
+        TextArea.__init__(self)
+        self.host = host
+        self.size = (0, 0)
+        self.setStyleName('messageBox')
+        self.addKeyboardListener(self)
+        MouseHandler.__init__(self)
+        self.addMouseListener(self)
+
+    def onBrowserEvent(self, event):
+        # XXX: woraroung a pyjamas bug: self.currentEvent is not set
+        #     so the TextBox's cancelKey doens't work. This is a workaround
+        #     FIXME: fix the bug upstream
+        self.currentEvent = event
+        TextArea.onBrowserEvent(self, event)
+
+    def onKeyPress(self, sender, keycode, modifiers):
+        _txt = self.getText()
+
+        def history_cb(text):
+            self.setText(text)
+            Timer(5, lambda timer: self.setCursorPos(len(text)))
+
+        if keycode == keyb.KEY_ENTER:
+            if _txt:
+                self.host.selected_widget.onTextEntered(_txt)
+                self.host._updateInputHistory(_txt) # FIXME: why using a global variable ?
+            self.setText('')
+            sender.cancelKey()
+        elif keycode == keyb.KEY_UP:
+            self.host._updateInputHistory(_txt, -1, history_cb)
+        elif keycode == keyb.KEY_DOWN:
+            self.host._updateInputHistory(_txt, +1, history_cb)
+        else:
+            self._onComposing()
+
+    def _onComposing(self):
+        """Callback when the user is composing a text."""
+        self.host.selected_widget.chat_state_machine._onEvent("composing")
+
+    def onMouseUp(self, sender, x, y):
+        size = (self.getOffsetWidth(), self.getOffsetHeight())
+        if size != self.size:
+            self.size = size
+            self.host.resize()
+
+    def onSelectedChange(self, selected):
+        self._selected_cache = selected
+
+
+class BaseTextEditor(object):
+    """Basic definition of a text editor. The method edit gets a boolean parameter which
+    should be set to True when you want to edit the text and False to only display it."""
+
+    def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None):
+        """
+        Remark when inheriting this class: since the setContent method could be
+        overwritten by the child class, you should consider calling this __init__
+        after all the parameters affecting this setContent method have been set.
+        @param content: dict with at least a 'text' key
+        @param strproc: method to be applied on strings to clean the content
+        @param modifiedCb: method to be called when the text has been modified.
+            This method can return:
+                - True: the modification will be saved and afterEditCb called;
+                - False: the modification won't be saved and afterEditCb called;
+                - None: the modification won't be saved and afterEditCb not called.
+        @param afterEditCb: method to be called when the edition is done
+        """
+        if content is None:
+            content = {'text': ''}
+        assert 'text' in content
+        if strproc is None:
+            def strproc(text):
+                try:
+                    return text.strip()
+                except (TypeError, AttributeError):
+                    return text
+        self.strproc = strproc
+        self._modifiedCb = modifiedCb
+        self._afterEditCb = afterEditCb
+        self.initialized = False
+        self.edit_listeners = []
+        self.setContent(content)
+
+    def setContent(self, content=None):
+        """Set the editable content.
+        The displayed content, which is set from the child class, could differ.
+
+        @param content (dict): content data, need at least a 'text' key
+        """
+        if content is None:
+            content = {'text': ''}
+        elif not isinstance(content, dict):
+            content = {'text': content}
+        assert 'text' in content
+        self._original_content = {}
+        for key in content:
+            self._original_content[key] = self.strproc(content[key])
+
+    def getContent(self):
+        """Get the current edited or editable content.
+        @return: dict with at least a 'text' key
+        """
+        raise NotImplementedError
+
+    def setOriginalContent(self, content):
+        """Use this method with care! Content initialization should normally be
+        done with self.setContent. This method exists to let you trick the editor,
+        e.g. for self.modified to return True also when nothing has been modified.
+        @param content: dict
+        """
+        self._original_content = content
+
+    def getOriginalContent(self):
+        """
+        @return (dict): the original content before modification (i.e. content given in __init__)
+        """
+        return self._original_content
+
+    def modified(self, content=None):
+        """Check if the content has been modified.
+        Remark: we don't use the direct comparison because we want to ignore empty elements
+        @content: content to be check against the original content or None to use the current content
+        @return: True if the content has been modified.
+        """
+        if content is None:
+            content = self.getContent()
+        # the following method returns True if one non empty element exists in a but not in b
+        diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != []
+        # the following method returns True if the values for the common keys are not equals
+        diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != []
+        # finally the combination of both to return True if a difference is found
+        diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b)
+
+        return diff(content, self._original_content)
+
+    def edit(self, edit, abort=False):
+        """
+        Remark: the editor must be visible before you call this method.
+        @param edit: set to True to edit the content or False to only display it
+        @param abort: set to True to cancel the edition and loose the changes.
+        If edit and abort are both True, self.abortEdition can be used to ask for a
+        confirmation. When edit is False and abort is True, abortion is actually done.
+        """
+        if edit:
+            self.setFocus(True)
+            if abort:
+                content = self.getContent()
+                if not self.modified(content) or self.abortEdition(content):  # e.g: ask for confirmation
+                    self.edit(False, True)
+                    return
+        else:
+            if not self.initialized:
+                return
+            content = self.getContent()
+            if abort:
+                self._afterEditCb(content)
+                return
+            if self._modifiedCb and self.modified(content):
+                result = self._modifiedCb(content)  # e.g.: send a message or update something
+                if result is not None:
+                    if self._afterEditCb:
+                        self._afterEditCb(content)  # e.g.: restore the display mode
+                    if result is True:
+                        self.setContent(content)
+            elif self._afterEditCb:
+                self._afterEditCb(content)
+
+        self.initialized = True
+
+    def setFocus(self, focus):
+        """
+        @param focus: set to True to focus the editor
+        """
+        raise NotImplementedError
+
+    def abortEdition(self, content):
+        return True
+
+    def addEditListener(self, listener):
+        """Add a method to be called whenever the text is edited.
+        @param listener: method taking two arguments: sender, keycode"""
+        self.edit_listeners.append(listener)
+
+
+class SimpleTextEditor(BaseTextEditor, FocusHandler, keyb.KeyboardHandler, ClickHandler):
+    """Base class for manage a simple text editor."""
+
+    CONVERT_NEW_LINES = True
+    VALIDATE_WITH_SHIFT_ENTER = True
+
+    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
+        """
+        @param content
+        @param modifiedCb
+        @param afterEditCb
+        @param options (dict): can have the following value:
+            - no_xhtml: set to True to clean any xhtml content.
+            - enhance_display: if True, the display text will be enhanced with strings.addURLToText
+            - listen_keyboard: set to True to terminate the edition with <enter> or <escape>.
+            - listen_focus: set to True to terminate the edition when the focus is lost.
+            - listen_click: set to True to start the edition when you click on the widget.
+        """
+        self.options = {'no_xhtml': False,
+                        'enhance_display': True,
+                        'listen_keyboard': True,
+                        'listen_focus': False,
+                        'listen_click': False
+                        }
+        if options:
+            self.options.update(options)
+        if self.options['listen_focus']:
+            FocusHandler.__init__(self)
+        if self.options['listen_click']:
+            ClickHandler.__init__(self)
+        keyb.KeyboardHandler.__init__(self)
+        strproc = lambda text: html_tools.html_sanitize(html_tools.html_strip(text)) if self.options['no_xhtml'] else html_tools.html_strip(text)
+        BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb)
+        self.textarea = self.display = None
+
+    def setContent(self, content=None):
+        BaseTextEditor.setContent(self, content)
+
+    def getContent(self):
+        raise NotImplementedError
+
+    def edit(self, edit, abort=False):
+        BaseTextEditor.edit(self, edit)
+        if edit:
+            if self.options['listen_focus'] and self not in self.textarea._focusListeners:
+                self.textarea.addFocusListener(self)
+            if self.options['listen_click']:
+                self.display.clearClickListener()
+            if self not in self.textarea._keyboardListeners:
+                self.textarea.addKeyboardListener(self)
+        else:
+            self.setDisplayContent()
+            if self.options['listen_focus']:
+                try:
+                    self.textarea.removeFocusListener(self)
+                except ValueError:
+                    pass
+            if self.options['listen_click'] and self not in self.display._clickListeners:
+                self.display.addClickListener(self)
+            try:
+                self.textarea.removeKeyboardListener(self)
+            except ValueError:
+                pass
+
+    def setDisplayContent(self):
+        text = self._original_content['text']
+        if not self.options['no_xhtml']:
+            text = strings.addURLToImage(text)
+        if self.options['enhance_display']:
+            text = strings.addURLToText(text)
+        if self.CONVERT_NEW_LINES:
+            text = html_tools.convertNewLinesToXHTML(text)
+        text = strings.fixXHTMLLinks(text)
+        self.display.setHTML(text)
+
+    def setFocus(self, focus):
+        raise NotImplementedError
+
+    def onKeyDown(self, sender, keycode, modifiers):
+        for listener in self.edit_listeners:
+            listener(self.textarea, keycode, modifiers) # FIXME: edit_listeners must either be removed, or send an action instead of keycode/modifiers
+        if not self.options['listen_keyboard']:
+            return
+        if keycode == keyb.KEY_ENTER and (not self.VALIDATE_WITH_SHIFT_ENTER or modifiers & keyb.MODIFIER_SHIFT):
+            self.textarea.setFocus(False)
+            if not self.options['listen_focus']:
+                self.edit(False)
+
+    def onLostFocus(self, sender):
+        """Finish the edition when focus is lost"""
+        if self.options['listen_focus']:
+            self.edit(False)
+
+    def onClick(self, sender=None):
+        """Start the edition when the widget is clicked"""
+        if self.options['listen_click']:
+            self.edit(True)
+
+    def onBrowserEvent(self, event):
+        if self.options['listen_focus']:
+            FocusHandler.onBrowserEvent(self, event)
+        if self.options['listen_click']:
+            ClickHandler.onBrowserEvent(self, event)
+        keyb.KeyboardHandler.onBrowserEvent(self, event)
+
+
+class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, keyb.KeyboardHandler):
+    """Manage a simple text editor with the HTML 5 "contenteditable" property."""
+
+    CONVERT_NEW_LINES = False  # overwrite definition in SimpleTextEditor
+
+    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
+        HTML.__init__(self)
+        SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
+        self.textarea = self.display = self
+
+    def getContent(self):
+        text = DOM.getInnerHTML(self.getElement())
+        return {'text': self.strproc(text) if text else ''}
+
+    def edit(self, edit, abort=False):
+        if edit:
+            self.textarea.setHTML(self._original_content['text'])
+        self.getElement().setAttribute('contenteditable', 'true' if edit else 'false')
+        SimpleTextEditor.edit(self, edit, abort)
+
+    def setFocus(self, focus):
+        if focus:
+            self.getElement().focus()
+        else:
+            self.getElement().blur()
+
+
+class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, keyb.KeyboardHandler):
+    """Manage a simple text editor with a TextArea for editing, HTML for display."""
+
+    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
+        SimplePanel.__init__(self)
+        SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
+        self.textarea = TextArea()
+        self.display = HTML()
+
+    def getContent(self):
+        text = self.textarea.getText()
+        return {'text': self.strproc(text) if text else ''}
+
+    def edit(self, edit, abort=False):
+        if edit:
+            self.textarea.setText(self._original_content['text'])
+        self.setWidget(self.textarea if edit else self.display)
+        SimpleTextEditor.edit(self, edit, abort)
+
+    def setFocus(self, focus):
+        if focus and self.isAttached():
+            self.textarea.setCursorPos(len(self.textarea.getText()))
+        self.textarea.setFocus(focus)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/file_tools.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,163 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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 constants import Const as C
+from sat.core.i18n import _, D_
+from pyjamas.ui.FileUpload import FileUpload
+from pyjamas.ui.FormPanel import FormPanel
+from pyjamas import Window
+from pyjamas import DOM
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.Button import Button
+from pyjamas.ui.Label import Label
+
+
+class FilterFileUpload(FileUpload):
+
+    def __init__(self, name, max_size, types=None):
+        """
+        @param name: the input element name and id
+        @param max_size: maximum file size in MB
+        @param types: allowed types as a list of couples (x, y, z):
+        - x: MIME content type e.g. "audio/ogg"
+        - y: file extension e.g. "*.ogg"
+        - z: description for the user e.g. "Ogg Vorbis Audio"
+        If types is None, all file format are accepted
+        """
+        FileUpload.__init__(self)
+        self.setName(name)
+        while DOM.getElementById(name):
+            name = "%s_" % name
+        self.setID(name)
+        self._id = name
+        self.max_size = max_size
+        self.types = types
+
+    def getFileInfo(self):
+        from __pyjamas__ import JS
+        JS("var file = top.document.getElementById(this._id).files[0]; return [file.size, file.type]")
+
+    def check(self):
+        if self.getFilename() == "":
+            return False
+        (size, filetype) = self.getFileInfo()
+        if self.types and filetype not in [x for (x, y, z) in self.types]:
+            types = []
+            for type_ in ["- %s (%s)" % (z, y) for (x, y, z) in self.types]:
+                if type_ not in types:
+                    types.append(type_)
+            Window.alert('This file type is not accepted.\nAccepted file types are:\n\n%s' % "\n".join(types))
+            return False
+        if size > self.max_size * pow(2, 20):
+            Window.alert('This file is too big!\nMaximum file size: %d MB.' % self.max_size)
+            return False
+        return True
+
+
+class FileUploadPanel(FormPanel):
+
+    def __init__(self, action_url, input_id, max_size, texts=None, close_cb=None):
+        """Build a form panel to upload a file.
+        @param action_url: the form action URL
+        @param input_id: the input element name and id
+        @param max_size: maximum file size in MB
+        @param texts: a dict to ovewrite the default textual values
+        @param close_cb: the close button callback method
+        """
+        FormPanel.__init__(self)
+        self.texts = {'ok_button': D_('Upload file'),
+                     'cancel_button': D_('Cancel'),
+                     'body': D_('Please select a file.'),
+                     'submitting': D_('<strong>Submitting, please wait...</strong>'),
+                     'errback': D_("Your file has been rejected..."),
+                     'body_errback': D_('Please select another file.'),
+                     'callback': D_("Your file has been accepted!")}
+        if isinstance(texts, dict):
+            self.texts.update(texts)
+        self.close_cb = close_cb
+        self.setEncoding(FormPanel.ENCODING_MULTIPART)
+        self.setMethod(FormPanel.METHOD_POST)
+        self.setAction(action_url)
+        self.vPanel = VerticalPanel()
+        self.message = HTML(self.texts['body'])
+        self.vPanel.add(self.message)
+
+        hPanel = HorizontalPanel()
+        hPanel.setSpacing(5)
+        hPanel.setStyleName('marginAuto')
+        self.file_upload = FilterFileUpload(input_id, max_size)
+        self.vPanel.add(self.file_upload)
+
+        self.upload_btn = Button(self.texts['ok_button'], getattr(self, "onSubmitBtnClick"))
+        hPanel.add(self.upload_btn)
+        hPanel.add(Button(self.texts['cancel_button'], getattr(self, "onCloseBtnClick")))
+
+        self.status = Label()
+        hPanel.add(self.status)
+
+        self.vPanel.add(hPanel)
+
+        self.add(self.vPanel)
+        self.addFormHandler(self)
+
+    def setCloseCb(self, close_cb):
+        self.close_cb = close_cb
+
+    def onCloseBtnClick(self):
+        if self.close_cb:
+            self.close_cb()
+        else:
+            log.warning("no close method defined")
+
+    def onSubmitBtnClick(self):
+        if not self.file_upload.check():
+            return
+        self.message.setHTML(self.texts['submitting'])
+        self.upload_btn.setEnabled(False)
+        self.submit()
+
+    def onSubmit(self, event):
+        pass
+
+    def onSubmitComplete(self, event):
+        result = event.getResults()
+        if result == C.UPLOAD_KO:
+            Window.alert(self.texts['errback'])
+            self.message.setHTML(self.texts['body_errback'])
+            self.upload_btn.setEnabled(True)
+        elif result == C.UPLOAD_OK:
+            Window.alert(self.texts['callback'])
+            self.close_cb()
+        else:
+            Window.alert(_('Submit error: %s' % result))
+            self.upload_btn.setEnabled(True)
+
+
+class AvatarUpload(FileUploadPanel):
+    def __init__(self):
+        texts = {'ok_button': 'Upload avatar',
+                 'body': 'Please select an image to show as your avatar...<br>Your picture must be a square and will be resized to 64x64 pixels if necessary.',
+                 'errback': "Can't open image... did you actually submit an image?",
+                 'body_errback': 'Please select another image file.',
+                 'callback': "Your new profile picture has been set!"}
+        FileUploadPanel.__init__(self, 'upload_avatar', 'avatar_path', 2, texts)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/game_radiocol.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,347 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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 _, D_
+from sat_frontends.tools import host_listener
+from constants import Const as C
+
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.FlexTable import FlexTable
+from pyjamas.ui.FormPanel import FormPanel
+from pyjamas.ui.Label import Label
+from pyjamas.ui.Button import Button
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui.Hidden import Hidden
+from pyjamas.ui.CaptionPanel import CaptionPanel
+from pyjamas.media.Audio import Audio
+from pyjamas import Window
+from pyjamas.Timer import Timer
+
+import html_tools
+import file_tools
+import dialog
+
+
+unicode = str # XXX: pyjama doesn't manage unicode
+
+
+class MetadataPanel(FlexTable):
+
+    def __init__(self):
+        FlexTable.__init__(self)
+        title_lbl = Label("title:")
+        title_lbl.setStyleName('radiocol_metadata_lbl')
+        artist_lbl = Label("artist:")
+        artist_lbl.setStyleName('radiocol_metadata_lbl')
+        album_lbl = Label("album:")
+        album_lbl.setStyleName('radiocol_metadata_lbl')
+        self.title = Label("")
+        self.title.setStyleName('radiocol_metadata')
+        self.artist = Label("")
+        self.artist.setStyleName('radiocol_metadata')
+        self.album = Label("")
+        self.album.setStyleName('radiocol_metadata')
+        self.setWidget(0, 0, title_lbl)
+        self.setWidget(1, 0, artist_lbl)
+        self.setWidget(2, 0, album_lbl)
+        self.setWidget(0, 1, self.title)
+        self.setWidget(1, 1, self.artist)
+        self.setWidget(2, 1, self.album)
+        self.setStyleName("radiocol_metadata_pnl")
+
+    def setTitle(self, title):
+        self.title.setText(title)
+
+    def setArtist(self, artist):
+        self.artist.setText(artist)
+
+    def setAlbum(self, album):
+        self.album.setText(album)
+
+
+class ControlPanel(FormPanel):
+    """Panel used to show controls to add a song, or vote for the current one"""
+
+    def __init__(self, parent):
+        FormPanel.__init__(self)
+        self.setEncoding(FormPanel.ENCODING_MULTIPART)
+        self.setMethod(FormPanel.METHOD_POST)
+        self.setAction("upload_radiocol")
+        self.timer_on = False
+        self._parent = parent
+        vPanel = VerticalPanel()
+
+        types = [('audio/ogg', '*.ogg', 'Ogg Vorbis Audio'),
+                 ('video/ogg', '*.ogv', 'Ogg Vorbis Video'),
+                 ('application/ogg', '*.ogx', 'Ogg Vorbis Multiplex'),
+                 ('audio/mpeg', '*.mp3', 'MPEG-Layer 3'),
+                 ('audio/mp3', '*.mp3', 'MPEG-Layer 3'),
+                 ]
+        self.file_upload = file_tools.FilterFileUpload("song", 10, types)
+        vPanel.add(self.file_upload)
+
+        hPanel = HorizontalPanel()
+        self.upload_btn = Button("Upload song", getattr(self, "onBtnClick"))
+        hPanel.add(self.upload_btn)
+        self.status = Label()
+        self.updateStatus()
+        hPanel.add(self.status)
+        #We need to know the filename and the referee
+        self.filename_field = Hidden('filename', '')
+        hPanel.add(self.filename_field)
+        referee_field = Hidden('referee', self._parent.referee)
+        hPanel.add(self.filename_field)
+        hPanel.add(referee_field)
+        vPanel.add(hPanel)
+
+        self.add(vPanel)
+        self.addFormHandler(self)
+
+    def updateStatus(self):
+        if self.timer_on:
+            return
+        # TODO: the status should be different if a song is being played or not
+        queue = self._parent.getQueueSize()
+        queue_data = self._parent.queue_data
+        if queue < queue_data[0]:
+            left = queue_data[0] - queue
+            self.status.setText("[we need %d more song%s]" % (left, "s" if left > 1 else ""))
+        elif queue < queue_data[1]:
+            left = queue_data[1] - queue
+            self.status.setText("[%d available spot%s]" % (left, "s" if left > 1 else ""))
+        elif queue >= queue_data[1]:
+                self.status.setText("[The queue is currently full]")
+        self.status.setStyleName('radiocol_status')
+
+    def onBtnClick(self):
+        if self.file_upload.check():
+            self.status.setText('[Submitting, please wait...]')
+            self.filename_field.setValue(self.file_upload.getFilename())
+            if self.file_upload.getFilename().lower().endswith('.mp3'):
+                self._parent._parent.host.showWarning('STATUS', 'For a better support, it is recommended to submit Ogg Vorbis file instead of MP3. You can convert your files easily, ask for help if needed!', 5000)
+            self.submit()
+            self.file_upload.setFilename("")
+
+    def onSubmit(self, event):
+        pass
+
+    def blockUpload(self):
+        self.file_upload.setVisible(False)
+        self.upload_btn.setEnabled(False)
+
+    def unblockUpload(self):
+        self.file_upload.setVisible(True)
+        self.upload_btn.setEnabled(True)
+
+    def setTemporaryStatus(self, text, style):
+        self.status.setText(text)
+        self.status.setStyleName('radiocol_upload_status_%s' % style)
+        self.timer_on = True
+
+        def cb(timer):
+            self.timer_on = False
+            self.updateStatus()
+
+        Timer(5000, cb)
+
+    def onSubmitComplete(self, event):
+        result = event.getResults()
+        if result == C.UPLOAD_OK:
+            # the song can still be rejected (not readable, full queue...)
+            self.setTemporaryStatus('[Your song has been submitted to the radio]', "ok")
+        elif result == C.UPLOAD_KO:
+            self.setTemporaryStatus('[Something went wrong during your song upload]', "ko")
+            self._parent.radiocolSongRejectedHandler(_("The uploaded file has been rejected, only Ogg Vorbis and MP3 songs are accepted."))
+            # TODO: would be great to re-use the original Exception class and message
+            # but it is lost in the middle of the traceback and encapsulated within
+            # a DBusException instance --> extract the data from the traceback?
+        else:
+            Window.alert(_('Submit error: %s' % result))
+            self.status.setText('')
+
+
+class Player(Audio):
+
+    def __init__(self, player_id, metadata_panel):
+        Audio.__init__(self)
+        self._id = player_id
+        self.metadata = metadata_panel
+        self.timestamp = ""
+        self.title = ""
+        self.artist = ""
+        self.album = ""
+        self.filename = None
+        self.played = False  # True when the song is playing/has played, becomes False on preload
+        self.setAutobuffer(True)
+        self.setAutoplay(False)
+        self.setVisible(False)
+
+    def preload(self, timestamp, filename, title, artist, album):
+        """preload the song but doesn't play it"""
+        self.timestamp = timestamp
+        self.filename = filename
+        self.title = title
+        self.artist = artist
+        self.album = album
+        self.played = False
+        self.setSrc(u"radiocol/%s" % html_tools.html_sanitize(filename))
+        log.debug(u"preloading %s in %s" % (title, self._id))
+
+    def play(self, play=True):
+        """Play or pause the song
+        @param play: set to True to play or to False to pause
+        """
+        if play:
+            self.played = True
+            self.metadata.setTitle(self.title)
+            self.metadata.setArtist(self.artist)
+            self.metadata.setAlbum(self.album)
+            Audio.play(self)
+        else:
+            self.pause()
+
+
+class RadioColPanel(HorizontalPanel, ClickHandler):
+
+    def __init__(self, parent, referee, players, queue_data):
+        """
+        @param parent
+        @param referee
+        @param players
+        @param queue_data: list of integers (queue to start, queue limit)
+        """
+        # We need to set it here and not in the CSS :(
+        HorizontalPanel.__init__(self, Height="90px")
+        ClickHandler.__init__(self)
+        self._parent = parent
+        self.referee = referee
+        self.queue_data = queue_data
+        self.setStyleName("radiocolPanel")
+
+        # Now we set up the layout
+        self.metadata_panel = MetadataPanel()
+        self.add(CaptionPanel("Now playing", self.metadata_panel))
+        self.playlist_panel = VerticalPanel()
+        self.add(CaptionPanel("Songs queue", self.playlist_panel))
+        self.control_panel = ControlPanel(self)
+        self.add(CaptionPanel("Controls", self.control_panel))
+
+        self.next_songs = []
+        self.players = [Player("player_%d" % i, self.metadata_panel) for i in xrange(queue_data[1] + 1)]
+        self.current_player = None
+        for player in self.players:
+            self.add(player)
+        self.addClickListener(self)
+
+        help_msg = """Accepted file formats: Ogg Vorbis (recommended), MP3.<br />
+        Please do not submit files that are protected by copyright.<br />
+        Click <a style="color: red;">here</a> if you need some support :)"""
+        link_cb = lambda: self._parent.host.bridge.joinMUC(self._parent.host.default_muc, self._parent.nick, profile=C.PROF_KEY_NONE, callback=lambda dummy: None, errback=self._parent.host.onJoinMUCFailure)
+        # FIXME: printInfo disabled after refactoring
+        # self._parent.printInfo(help_msg, type_='link', link_cb=link_cb)
+
+    def pushNextSong(self, title):
+        """Add a song to the left panel's next songs queue"""
+        next_song = Label(title)
+        next_song.setStyleName("radiocol_next_song")
+        self.next_songs.append(next_song)
+        self.playlist_panel.append(next_song)
+        self.control_panel.updateStatus()
+
+    def popNextSong(self):
+        """Remove the first song of next songs list
+        should be called when the song is played"""
+        #FIXME: should check that the song we remove is the one we play
+        next_song = self.next_songs.pop(0)
+        self.playlist_panel.remove(next_song)
+        self.control_panel.updateStatus()
+
+    def getQueueSize(self):
+        return len(self.playlist_panel.getChildren())
+
+    def radiocolCheckPreload(self, timestamp):
+        for player in self.players:
+            if player.timestamp == timestamp:
+                return False
+        return True
+
+    def radiocolPreloadHandler(self, timestamp, filename, title, artist, album, sender):
+        if not self.radiocolCheckPreload(timestamp):
+            return  # song already preloaded
+        preloaded = False
+        for player in self.players:
+            if not player.filename or \
+               (player.played and player != self.current_player):
+                #if player has no file loaded, or it has already played its song
+                #we use it to preload the next one
+                player.preload(timestamp, filename, title, artist, album)
+                preloaded = True
+                break
+        if not preloaded:
+            log.warning("Can't preload song, we are getting too many songs to preload, we shouldn't have more than %d at once" % self.queue_data[1])
+        else:
+            self.pushNextSong(title)
+            # FIXME: printInfo disabled after refactoring
+            # self._parent.printInfo(_('%(user)s uploaded %(artist)s - %(title)s') % {'user': sender, 'artist': artist, 'title': title})
+
+    def radiocolPlayHandler(self, filename):
+        found = False
+        for player in self.players:
+            if not found and player.filename == filename:
+                player.play()
+                self.popNextSong()
+                self.current_player = player
+                found = True
+            else:
+                player.play(False)  # in case the previous player was not sync
+        if not found:
+            log.error("Song not found in queue, can't play it. This should not happen")
+
+    def radiocolNoUploadHandler(self):
+        self.control_panel.blockUpload()
+
+    def radiocolUploadOkHandler(self):
+        self.control_panel.unblockUpload()
+
+    def radiocolSongRejectedHandler(self, reason):
+        Window.alert("Song rejected: %s" % reason)
+
+
+##  Menu
+
+def hostReady(host):
+    def onCollectiveRadio(self):
+        def callback(room_jid, contacts):
+            contacts = [unicode(contact) for contact in contacts]
+            room_jid_s = unicode(room_jid) if room_jid else ''
+            host.bridge.launchRadioCollective(contacts, room_jid_s, profile=C.PROF_KEY_NONE, callback=lambda dummy: None, errback=host.onJoinMUCFailure)
+        dialog.RoomAndContactsChooser(host, callback, ok_button="Choose", title="Collective Radio", visible=(False, True))
+
+
+    def gotMenus():
+        host.menus.addMenu(C.MENU_GLOBAL, (D_(u"Groups"), D_(u"Collective radio")), callback=onCollectiveRadio)
+
+    host.addListener('gotMenus', gotMenus)
+
+host_listener.addListener(hostReady)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/game_tarot.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,410 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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 _, D_
+from sat_frontends.tools.games import TarotCard
+from sat_frontends.tools import host_listener
+
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.DockPanel import DockPanel
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.Image import Image
+from pyjamas.ui.Label import Label
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui.MouseListener import MouseHandler
+from pyjamas.ui import HasAlignment
+from pyjamas import Window
+from pyjamas import DOM
+from constants import Const as C
+
+import dialog
+import xmlui
+
+
+CARD_WIDTH = 74
+CARD_HEIGHT = 136
+CARD_DELTA_Y = 30
+MIN_WIDTH = 950  # Minimum size of the panel
+MIN_HEIGHT = 500
+
+
+unicode = str  # XXX: pyjama doesn't manage unicode
+
+
+class CardWidget(TarotCard, Image, MouseHandler):
+    """This class is used to represent a card, graphically and logically"""
+
+    def __init__(self, parent, file_):
+        """@param file: path of the PNG file"""
+        self._parent = parent
+        Image.__init__(self, file_)
+        root_name = file_[file_.rfind("/") + 1:-4]
+        suit, value = root_name.split('_')
+        TarotCard.__init__(self, (suit, value))
+        MouseHandler.__init__(self)
+        self.addMouseListener(self)
+
+    def onMouseEnter(self, sender):
+        if self._parent.state == "ecart" or self._parent.state == "play":
+            DOM.setStyleAttribute(self.getElement(), "top", "0px")
+
+    def onMouseLeave(self, sender):
+        if not self in self._parent.hand:
+            return
+        if not self in list(self._parent.selected):  # FIXME: Workaround pyjs bug, must report it
+            DOM.setStyleAttribute(self.getElement(), "top", "%dpx" % CARD_DELTA_Y)
+
+    def onMouseUp(self, sender, x, y):
+        if self._parent.state == "ecart":
+            if self not in list(self._parent.selected):
+                self._parent.addToSelection(self)
+            else:
+                self._parent.removeFromSelection(self)
+        elif self._parent.state == "play":
+            self._parent.playCard(self)
+
+
+class TarotPanel(DockPanel, ClickHandler):
+
+    def __init__(self, parent, referee, players):
+        DockPanel.__init__(self)
+        ClickHandler.__init__(self)
+        self._parent = parent
+        self._autoplay = None  # XXX: use 0 to activate fake play, None else
+        self.referee = referee
+        self.players = players
+        self.player_nick = parent.nick
+        self.bottom_nick = self.player_nick
+        idx = self.players.index(self.player_nick)
+        idx = (idx + 1) % len(self.players)
+        self.right_nick = self.players[idx]
+        idx = (idx + 1) % len(self.players)
+        self.top_nick = self.players[idx]
+        idx = (idx + 1) % len(self.players)
+        self.left_nick = self.players[idx]
+        self.bottom_nick = self.player_nick
+        self.selected = set()  # Card choosed by the player (e.g. during ecart)
+        self.hand_size = 13  # number of cards in a hand
+        self.hand = []
+        self.to_show = []
+        self.state = None
+        self.setSize("%dpx" % MIN_WIDTH, "%dpx" % MIN_HEIGHT)
+        self.setStyleName("cardPanel")
+
+        # Now we set up the layout
+        _label = Label(self.top_nick)
+        _label.setStyleName('cardGamePlayerNick')
+        self.add(_label, DockPanel.NORTH)
+        self.setCellWidth(_label, '100%')
+        self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_CENTER)
+
+        self.hand_panel = AbsolutePanel()
+        self.add(self.hand_panel, DockPanel.SOUTH)
+        self.setCellWidth(self.hand_panel, '100%')
+        self.setCellHorizontalAlignment(self.hand_panel, HasAlignment.ALIGN_CENTER)
+
+        _label = Label(self.left_nick)
+        _label.setStyleName('cardGamePlayerNick')
+        self.add(_label, DockPanel.WEST)
+        self.setCellHeight(_label, '100%')
+        self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE)
+
+        _label = Label(self.right_nick)
+        _label.setStyleName('cardGamePlayerNick')
+        self.add(_label, DockPanel.EAST)
+        self.setCellHeight(_label, '100%')
+        self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_RIGHT)
+        self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE)
+
+        self.center_panel = DockPanel()
+        self.inner_left = SimplePanel()
+        self.inner_left.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
+        self.center_panel.add(self.inner_left, DockPanel.WEST)
+        self.center_panel.setCellHeight(self.inner_left, '100%')
+        self.center_panel.setCellHorizontalAlignment(self.inner_left, HasAlignment.ALIGN_RIGHT)
+        self.center_panel.setCellVerticalAlignment(self.inner_left, HasAlignment.ALIGN_MIDDLE)
+
+        self.inner_right = SimplePanel()
+        self.inner_right.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
+        self.center_panel.add(self.inner_right, DockPanel.EAST)
+        self.center_panel.setCellHeight(self.inner_right, '100%')
+        self.center_panel.setCellVerticalAlignment(self.inner_right, HasAlignment.ALIGN_MIDDLE)
+
+        self.inner_top = SimplePanel()
+        self.inner_top.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
+        self.center_panel.add(self.inner_top, DockPanel.NORTH)
+        self.center_panel.setCellHorizontalAlignment(self.inner_top, HasAlignment.ALIGN_CENTER)
+        self.center_panel.setCellVerticalAlignment(self.inner_top, HasAlignment.ALIGN_BOTTOM)
+
+        self.inner_bottom = SimplePanel()
+        self.inner_bottom.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT)
+        self.center_panel.add(self.inner_bottom, DockPanel.SOUTH)
+        self.center_panel.setCellHorizontalAlignment(self.inner_bottom, HasAlignment.ALIGN_CENTER)
+        self.center_panel.setCellVerticalAlignment(self.inner_bottom, HasAlignment.ALIGN_TOP)
+
+        self.inner_center = SimplePanel()
+        self.center_panel.add(self.inner_center, DockPanel.CENTER)
+        self.center_panel.setCellHorizontalAlignment(self.inner_center, HasAlignment.ALIGN_CENTER)
+        self.center_panel.setCellVerticalAlignment(self.inner_center, HasAlignment.ALIGN_MIDDLE)
+
+        self.add(self.center_panel, DockPanel.CENTER)
+        self.setCellWidth(self.center_panel, '100%')
+        self.setCellHeight(self.center_panel, '100%')
+        self.setCellVerticalAlignment(self.center_panel, HasAlignment.ALIGN_MIDDLE)
+        self.setCellHorizontalAlignment(self.center_panel, HasAlignment.ALIGN_CENTER)
+
+        self.loadCards()
+        self.mouse_over_card = None  # contain the card to highlight
+        self.visible_size = CARD_WIDTH / 2  # number of pixels visible for cards
+        self.addClickListener(self)
+
+    def loadCards(self):
+        """Load all the cards in memory"""
+        def _getTarotCardsPathsCb(paths):
+            log.debug("_getTarotCardsPathsCb")
+            for file_ in paths:
+                log.debug(u"path: %s" % file_)
+                card = CardWidget(self, file_)
+                log.debug(u"card: %s" % card)
+                self.cards[(card.suit, card.value)] = card
+                self.deck.append(card)
+            self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee)
+        self.cards = {}
+        self.deck = []
+        self.cards["atout"] = {}  # As Tarot is a french game, it's more handy & logical to keep french names
+        self.cards["pique"] = {}  # spade
+        self.cards["coeur"] = {}  # heart
+        self.cards["carreau"] = {}  # diamond
+        self.cards["trefle"] = {}  # club
+        self._parent.host.bridge.call('getTarotCardsPaths', _getTarotCardsPathsCb)
+
+    def onClick(self, sender):
+        if self.state == "chien":
+            self.to_show = []
+            self.state = "wait"
+            self.updateToShow()
+        elif self.state == "wait_for_ecart":
+            self.state = "ecart"
+            self.hand.extend(self.to_show)
+            self.hand.sort()
+            self.to_show = []
+            self.updateToShow()
+            self.updateHand()
+
+    def tarotGameNewHandler(self, hand):
+        """Start a new game, with given hand"""
+        if hand is []:  # reset the display after the scores have been showed
+            self.selected.clear()
+            del self.hand[:]
+            del self.to_show[:]
+            self.state = None
+            #empty hand
+            self.updateHand()
+            #nothing on the table
+            self.updateToShow()
+            for pos in ['top', 'left', 'bottom', 'right']:
+                getattr(self, "inner_%s" % pos).setWidget(None)
+            self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee)
+            return
+        for suit, value in hand:
+            self.hand.append(self.cards[(suit, value)])
+        self.hand.sort()
+        self.state = "init"
+        self.updateHand()
+
+    def updateHand(self):
+        """Show the cards in the hand in the hand_panel (SOUTH panel)"""
+        self.hand_panel.clear()
+        self.hand_panel.setSize("%dpx" % (self.visible_size * (len(self.hand) + 1)), "%dpx" % (CARD_HEIGHT + CARD_DELTA_Y + 10))
+        x_pos = 0
+        y_pos = CARD_DELTA_Y
+        for card in self.hand:
+            self.hand_panel.add(card, x_pos, y_pos)
+            x_pos += self.visible_size
+
+    def updateToShow(self):
+        """Show cards in the center panel"""
+        if not self.to_show:
+            _widget = self.inner_center.getWidget()
+            if _widget:
+                self.inner_center.remove(_widget)
+            return
+        panel = AbsolutePanel()
+        panel.setSize("%dpx" % ((CARD_WIDTH + 5) * len(self.to_show) - 5), "%dpx" % (CARD_HEIGHT))
+        x_pos = 0
+        y_pos = 0
+        for card in self.to_show:
+            panel.add(card, x_pos, y_pos)
+            x_pos += CARD_WIDTH + 5
+        self.inner_center.setWidget(panel)
+
+    def _ecartConfirm(self, confirm):
+        if not confirm:
+            return
+        ecart = []
+        for card in self.selected:
+            ecart.append((card.suit, card.value))
+            self.hand.remove(card)
+        self.selected.clear()
+        self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, ecart)
+        self.state = "wait"
+        self.updateHand()
+
+    def addToSelection(self, card):
+        self.selected.add(card)
+        if len(self.selected) == 6:
+            dialog.ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show()
+
+    def tarotGameInvalidCardsHandler(self, phase, played_cards, invalid_cards):
+        """Invalid cards have been played
+        @param phase: phase of the game
+        @param played_cards: all the cards played
+        @param invalid_cards: cards which are invalid"""
+
+        if phase == "play":
+            self.state = "play"
+        elif phase == "ecart":
+            self.state = "ecart"
+        else:
+            log.error("INTERNAL ERROR: unmanaged game phase")  # FIXME: raise an exception here
+
+        for suit, value in played_cards:
+            self.hand.append(self.cards[(suit, value)])
+
+        self.hand.sort()
+        self.updateHand()
+        if self._autoplay == None:  # No dialog if there is autoplay
+            Window.alert('Cards played are invalid !')
+        self.__fakePlay()
+
+    def removeFromSelection(self, card):
+        self.selected.remove(card)
+        if len(self.selected) == 6:
+            dialog.ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show()
+
+    def tarotGameChooseContratHandler(self, xml_data):
+        """Called when the player has to select his contrat
+        @param xml_data: SàT xml representation of the form"""
+        body = xmlui.create(self._parent.host, xml_data, flags=['NO_CANCEL'])
+        _dialog = dialog.GenericDialog(_('Please choose your contrat'), body, options=['NO_CLOSE'])
+        body.setCloseCb(_dialog.close)
+        _dialog.show()
+
+    def tarotGameShowCardsHandler(self, game_stage, cards, data):
+        """Display cards in the middle of the game (to show for e.g. chien ou poignée)"""
+        self.to_show = []
+        for suit, value in cards:
+            self.to_show.append(self.cards[(suit, value)])
+        self.updateToShow()
+        if game_stage == "chien" and data['attaquant'] == self.player_nick:
+            self.state = "wait_for_ecart"
+        else:
+            self.state = "chien"
+
+    def getPlayerLocation(self, nick):
+        """return player location (top,bottom,left or right)"""
+        for location in ['top', 'left', 'bottom', 'right']:
+            if getattr(self, '%s_nick' % location) == nick:
+                return location
+        log.error("This line should not be reached")
+
+    def tarotGameCardsPlayedHandler(self, player, cards):
+        """A card has been played by player"""
+        if not len(cards):
+            log.warning("cards should not be empty")
+            return
+        if len(cards) > 1:
+            log.error("can't manage several cards played")
+        if self.to_show:
+            self.to_show = []
+            self.updateToShow()
+        suit, value = cards[0]
+        player_pos = self.getPlayerLocation(player)
+        player_panel = getattr(self, "inner_%s" % player_pos)
+
+        if player_panel.getWidget() != None:
+            #We have already cards on the table, we remove them
+            for pos in ['top', 'left', 'bottom', 'right']:
+                getattr(self, "inner_%s" % pos).setWidget(None)
+
+        card = self.cards[(suit, value)]
+        DOM.setElemAttribute(card.getElement(), "style", "")
+        player_panel.setWidget(card)
+
+    def tarotGameYourTurnHandler(self):
+        """Called when we have to play :)"""
+        if self.state == "chien":
+            self.to_show = []
+            self.updateToShow()
+        self.state = "play"
+        self.__fakePlay()
+
+    def __fakePlay(self):
+        """Convenience method for stupid autoplay
+        /!\ don't forgot to comment any interactive dialog for invalid card"""
+        if self._autoplay == None:
+            return
+        if self._autoplay >= len(self.hand):
+            self._autoplay = 0
+        card = self.hand[self._autoplay]
+        self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)])
+        del self.hand[self._autoplay]
+        self.state = "wait"
+        self._autoplay += 1
+
+    def playCard(self, card):
+        self.hand.remove(card)
+        self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)])
+        self.state = "wait"
+        self.updateHand()
+
+    def tarotGameScoreHandler(self, xml_data, winners, loosers):
+        """Show score at the end of a round"""
+        if not winners and not loosers:
+            title = "Draw game"
+        else:
+            if self.player_nick in winners:
+                title = "You <b>win</b> !"
+            else:
+                title = "You <b>loose</b> :("
+        body = xmlui.create(self._parent.host, xml_data, title=title, flags=['NO_CANCEL'])
+        _dialog = dialog.GenericDialog(title, body, options=['NO_CLOSE'])
+        body.setCloseCb(_dialog.close)
+        _dialog.show()
+
+
+##  Menu
+
+def hostReady(host):
+    def onTarotGame():
+        def onPlayersSelected(room_jid, other_players):
+            other_players = [unicode(contact) for contact in other_players]
+            room_jid_s = unicode(room_jid) if room_jid else ''
+            host.bridge.launchTarotGame(other_players, room_jid_s, profile=C.PROF_KEY_NONE, callback=lambda dummy: None, errback=host.onJoinMUCFailure)
+        dialog.RoomAndContactsChooser(host, onPlayersSelected, 3, title="Tarot", title_invite=_(u"Please select 3 other players"), visible=(False, True))
+
+    def gotMenus():
+        host.menus.addMenu(C.MENU_GLOBAL, (D_(u"Groups"), D_(u"Tarot")), callback=onTarotGame)
+    host.addListener('gotMenus', gotMenus)
+
+host_listener.addListener(hostReady)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/html_tools.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,81 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from sat_frontends.tools import xmltools
+
+import nativedom
+from __pyjamas__ import JS
+
+dom = nativedom.NativeDOM()
+
+
+def html_sanitize(html):
+    """Naive sanitization of HTML"""
+    return html.replace('<', '&lt;').replace('>', '&gt;')
+
+
+def html_strip(html):
+    """Strip leading/trailing white spaces, HTML line breaks and &nbsp; sequences."""
+    JS("""return html.replace(/(^(<br\/?>|&nbsp;|\s)+)|((<br\/?>|&nbsp;|\s)+$)/g, "");""")
+
+def inlineRoot(xhtml):
+    """ make root element inline """
+    doc = dom.parseString(xhtml)
+    return xmltools.inlineRoot(doc)
+
+
+def convertNewLinesToXHTML(text):
+    """Replace all the \n with <br/>"""
+    return text.replace('\n', '<br/>')
+
+
+def XHTML2Text(xhtml):
+    """Helper method to apply both html_sanitize and convertNewLinesToXHTML"""
+    return convertNewLinesToXHTML(html_sanitize(xhtml))
+
+
+def buildPresenceStyle(presence, base_style=None):
+    """Return the CSS classname to be used for displaying the given presence information.
+
+    @param presence (unicode): presence is a value in ('', 'chat', 'away', 'dnd', 'xa')
+    @param base_style (unicode): base classname
+    @return: unicode
+    """
+    if not base_style:
+        base_style = "contactLabel"
+    return '%s-%s' % (base_style, presence or 'connected')
+
+
+def setPresenceStyle(widget, presence, base_style=None):
+    """
+    Set the CSS style of a contact's element according to its presence.
+
+    @param widget (Widget): the UI element of the contact
+    @param presence (unicode): a value in ("", "chat", "away", "dnd", "xa").
+    @param base_style (unicode): the base name of the style to apply
+    """
+    if not hasattr(widget, 'presence_style'):
+        widget.presence_style = None
+    style = buildPresenceStyle(presence, base_style)
+    if style == widget.presence_style:
+        return
+    if widget.presence_style is not None:
+        widget.removeStyleName(widget.presence_style)
+    widget.addStyleName(style)
+    widget.presence_style = style
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/json.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,298 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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
+import time
+from sat_browser import main_panel
+
+from sat_browser.constants import Const as C
+import random
+
+
+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 with 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(u"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(u"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(u"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:
+            _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(u"Error %s: %s" % (errobj['message']['faultCode'], errobj['message']['faultString']))
+                else:
+                    log.error(u"%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",
+                        ["getSessionMetadata", "isConnected", "connect", "registerParams", "menusGet"])
+
+
+class BridgeCall(LiberviaJsonProxy):
+    def __init__(self):
+        LiberviaJsonProxy.__init__(self, "/json_api",
+                        ["getContacts", "addContact", "messageSend",
+                         "psNodeDelete", "psRetractItem", "psRetractItems",
+                         "mbSend", "mbRetract", "mbGet", "mbGetFromMany", "mbGetFromManyRTResult",
+                         "mbGetFromManyWithComments", "mbGetFromManyWithCommentsRTResult",
+                         "historyGet", "getPresenceStatuses", "joinMUC", "mucLeave", "mucGetRoomsJoined",
+                         "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady",
+                         "tarotGamePlayCards", "launchRadioCollective",
+                         "getWaitingSub", "subscription", "delContact", "updateContact", "avatarGet",
+                         "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction",
+                         "disconnect", "chatStateComposing", "getNewAccountDomain",
+                         "syntaxConvert", "getAccountDialogUI", "getMainResource", "getEntitiesData",
+                         "getVersion", "getLiberviaVersion", "mucGetDefaultService", "getFeatures",
+                         "namespacesGet",
+                        ])
+
+    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, callback): # FIXME
+        log.warning("isConnected is not implemeted in Libervia as for now profile is connected if session is opened")
+        callback(True)
+
+    def encryptionPluginsGet(self, callback, errback):
+        """e2e encryption have no sense if made on backend, so we ignore this call"""
+        callback([])
+
+    def bridgeConnect(self, callback, errback):
+        callback()
+
+
+class BridgeSignals(LiberviaJsonProxy):
+
+    def __init__(self, host):
+        self.host = host
+        self.retry_time = None
+        self.retry_nb = 0
+        self.retry_warning = None
+        self.retry_timer = None
+        LiberviaJsonProxy.__init__(self, "/json_signal_api",
+                        ["getSignals"])
+        self._signals = {} # key: signal name, value: callback
+
+    def onRemoteResponse(self, response, request_info):
+        if self.retry_time:
+            log.info("Connection with server restablished")
+            self.retry_nb = 0
+            self.retry_time = None
+        LiberviaJsonProxy.onRemoteResponse(self, response, request_info)
+
+    def onRemoteError(self, code, errobj, request_info):
+        if errobj['message'] == 'Empty Response':
+            log.warning(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(dummy):
+                current = time.time()
+                if current > self.retry_time:
+                    msg = "Trying to reconnect to server..."
+                    log.info(msg)
+                    self.retry_warning.showWarning("INFO", msg)
+                    self.retry_timer.cancel()
+                    self.retry_warning = self.retry_timer = None
+                    self.getSignals(callback=self.signalHandler, profile=None)
+                else:
+                    remaining = int(self.retry_time - current)
+                    msg_html = u"Connection with server lost. Retrying in <strong>{}</strong> s".format(remaining)
+                    self.retry_warning.showWarning("WARNING", msg_html, None)
+
+            if self.retry_nb < 3:
+                retry_delay = 1
+            elif self.retry_nb < 10:
+                retry_delay = random.randint(1,10)
+            else:
+                retry_delay = random.randint(1,60)
+            self.retry_nb += 1
+            log.warning(u"Lost connection, trying to reconnect in {} s (try #{})".format(retry_delay, self.retry_nb))
+            self.retry_time = time.time() + retry_delay
+            self.retry_warning = main_panel.WarningPopup()
+            self.retry_timer = Timer(notify=_timerCb)
+            self.retry_timer.scheduleRepeating(1000)
+            _timerCb(None)
+
+    def register_signal(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(u"Registering signal {}".format(name))
+        if name in self._signals:
+            log.error(u"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, profile=None)
+        if len(signal_data) == 1:
+            signal_data.append([])
+        log.debug(u"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(u"Ignoring {} signal: no handler registered !".format(name))
+            return
+        if with_profile:
+            args.append(C.PROF_KEY_NONE)
+        if not self.host._profile_plugged:
+            log.debug("Profile is not plugged, we cache the signal")
+            self.host.signals_cache[C.PROF_KEY_NONE].append((name, callback, args, {}))
+        else:
+            callback(*args)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/libervia_widget.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,811 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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/>.
+"""Libervia base widget"""
+
+import pyjd  # this is dummy in pyjs
+from sat.core.log import getLogger
+log = getLogger(__name__)
+
+from sat.core.i18n import _
+from sat.core import exceptions
+from sat_frontends.quick_frontend import quick_widgets
+
+from pyjamas.ui.FlexTable import FlexTable
+from pyjamas.ui.TabPanel import TabPanel
+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.Label import Label
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.Button import Button
+from pyjamas.ui.Widget import Widget
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui import HasAlignment
+from pyjamas.ui.DragWidget import DragWidget
+from pyjamas.ui.DropWidget import DropWidget
+from pyjamas import DOM
+from pyjamas import Window
+
+import dialog
+import base_menu
+import base_widget
+import base_panel
+
+
+unicode = str  # FIXME: pyjamas workaround
+
+
+# FIXME: we need to group several unrelated panels/widgets in this module because of isinstance tests and other references to classes (e.g. if we separate Drag n Drop classes in a separate module, we'll have cyclic import because of the references to LiberviaWidget in DropCell).
+# TODO: use a more generic method (either use duck typing, or register classes in a generic way, without hard references), then split classes in separate modules
+
+
+### Drag n Drop ###
+
+
+class DragLabel(DragWidget):
+
+    def __init__(self, text, type_, host=None):
+        """Base of Drag n Drop mecanism in Libervia
+
+        @param text: data embedded with in drag n drop operation
+        @param type_: type of data that we are dragging
+        @param host: if not None, the host will be use to highlight BorderWidgets
+        """
+        DragWidget.__init__(self)
+        self.host = host
+        self._text = text
+        self.type_ = type_
+
+    def onDragStart(self, event):
+        dt = event.dataTransfer
+        dt.setData('text/plain', "%s\n%s" % (self._text, self.type_))
+        dt.setDragImage(self.getElement(), 15, 15)
+        if self.host is not None:
+            current_panel = self.host.tab_panel.getCurrentPanel()
+            for widget in current_panel.widgets:
+                if isinstance(widget, BorderWidget):
+                    widget.addStyleName('borderWidgetOnDrag')
+
+    def onDragEnd(self, event):
+        if self.host is not None:
+            current_panel = self.host.tab_panel.getCurrentPanel()
+            for widget in current_panel.widgets:
+                if isinstance(widget, BorderWidget):
+                    widget.removeStyleName('borderWidgetOnDrag')
+
+
+class LiberviaDragWidget(DragLabel):
+    """ A DragLabel which keep the widget being dragged as class value """
+    current = None  # widget currently dragged
+
+    def __init__(self, text, type_, widget):
+        DragLabel.__init__(self, text, type_, widget.host)
+        self.widget = widget
+
+    def onDragStart(self, event):
+        LiberviaDragWidget.current = self.widget
+        DragLabel.onDragStart(self, event)
+
+    def onDragEnd(self, event):
+        DragLabel.onDragEnd(self, event)
+        LiberviaDragWidget.current = None
+
+
+class DropCell(DropWidget):
+    """Cell in the middle grid which replace itself with the dropped widget on DnD"""
+    drop_keys = {}
+
+    def __init__(self, host):
+        DropWidget.__init__(self)
+        self.host = host
+        self.setStyleName('dropCell')
+
+    @classmethod
+    def addDropKey(cls, key, cb):
+        """Add a association between a key and a class to create on drop.
+
+        @param key: key to be associated (e.g. "CONTACT", "CHAT")
+        @param cb: a callable (either a class or method) returning a
+            LiberviaWidget instance
+        """
+        DropCell.drop_keys[key] = cb
+
+    def onDragEnter(self, event):
+        if self == LiberviaDragWidget.current:
+            return
+        self.addStyleName('dragover')
+        DOM.eventPreventDefault(event)
+
+    def onDragLeave(self, event):
+        if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\
+            event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1:
+            # We check that we are inside widget's box, and we don't remove the style in this case because
+            # if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we
+            # don't want that
+            self.removeStyleName('dragover')
+
+    def onDragOver(self, event):
+        DOM.eventPreventDefault(event)
+
+    def _getCellAndRow(self, grid, event):
+        """Return cell and row index where the event is occuring"""
+        cell = grid.getEventTargetCell(event)
+        row = DOM.getParent(cell)
+        return (row.rowIndex, cell.cellIndex)
+
+    def onDrop(self, event):
+        """
+        @raise NoLiberviaWidgetException: something else than a LiberviaWidget
+            has been returned by the callback.
+        """
+        self.removeStyleName('dragover')
+        DOM.eventPreventDefault(event)
+        item, item_type = eventGetData(event)
+        if item_type == "WIDGET":
+            if not LiberviaDragWidget.current:
+                log.error("No widget registered in LiberviaDragWidget !")
+                return
+            _new_panel = LiberviaDragWidget.current
+            if self == _new_panel:  # We can't drop on ourself
+                return
+            # we need to remove the widget from the panel as it will be inserted elsewhere
+            widgets_panel = _new_panel.getParent(WidgetsPanel, expect=True)
+            wid_row = widgets_panel.getWidgetCoords(_new_panel)[0]
+            row_wids = widgets_panel.getLiberviaRowWidgets(wid_row)
+            if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]:
+                # the dropped widget is the only one in the same row
+                # as the target widget (self), we don't do anything
+                return
+            widgets_panel.removeWidget(_new_panel)
+        elif item_type in self.drop_keys:
+            _new_panel = self.drop_keys[item_type](self.host, item)
+            if not isinstance(_new_panel, LiberviaWidget):
+                raise base_widget.NoLiberviaWidgetException
+        else:
+            log.warning("unmanaged item type")
+            return
+        if isinstance(self, LiberviaWidget):
+            # self.host.unregisterWidget(self) # FIXME
+            self.onQuit()
+            if not isinstance(_new_panel, LiberviaWidget):
+                log.warning("droping an object which is not a class of LiberviaWidget")
+        _flextable = self.getParent()
+        _widgetspanel = _flextable.getParent().getParent()
+        row_idx, cell_idx = self._getCellAndRow(_flextable, event)
+        if self.host.getSelected() == self:
+            self.host.setSelected(None)
+        _widgetspanel.changeWidget(row_idx, cell_idx, _new_panel)
+        """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable))
+        _width = 90/float(len(_unempty_panels) or 1)
+        #now we resize all the cell of the column
+        for panel in _unempty_panels:
+            td_elt = panel.getElement().parentNode
+            DOM.setStyleAttribute(td_elt, "width", "%s%%" % _width)"""
+        if isinstance(self, quick_widgets.QuickWidget):
+            self.host.widgets.deleteWidget(self)
+
+
+class EmptyWidget(DropCell, SimplePanel):
+    """Empty dropable panel"""
+
+    def __init__(self, host):
+        SimplePanel.__init__(self)
+        DropCell.__init__(self, host)
+        #self.setWidget(HTML(''))
+        self.setSize('100%', '100%')
+
+
+class BorderWidget(EmptyWidget):
+    def __init__(self, host):
+        EmptyWidget.__init__(self, host)
+        self.addStyleName('borderPanel')
+
+
+class LeftBorderWidget(BorderWidget):
+    def __init__(self, host):
+        BorderWidget.__init__(self, host)
+        self.addStyleName('leftBorderWidget')
+
+
+class RightBorderWidget(BorderWidget):
+    def __init__(self, host):
+        BorderWidget.__init__(self, host)
+        self.addStyleName('rightBorderWidget')
+
+
+class BottomBorderWidget(BorderWidget):
+    def __init__(self, host):
+        BorderWidget.__init__(self, host)
+        self.addStyleName('bottomBorderWidget')
+
+
+class DropTab(Label, DropWidget):
+
+    def __init__(self, tab_panel, text):
+        Label.__init__(self, text)
+        DropWidget.__init__(self, tab_panel)
+        self.tab_panel = tab_panel
+        self.setStyleName('dropCell')
+        self.setWordWrap(False)
+
+    def _getIndex(self):
+        """ get current index of the DropTab """
+        # XXX: awful hack, but seems the only way to get index
+        return self.tab_panel.tabBar.panel.getWidgetIndex(self.getParent().getParent()) - 1
+
+    def onDragEnter(self, event):
+        #if self == LiberviaDragWidget.current:
+        #    return
+        self.parent.addStyleName('dragover')
+        DOM.eventPreventDefault(event)
+
+    def onDragLeave(self, event):
+        self.parent.removeStyleName('dragover')
+
+    def onDragOver(self, event):
+        DOM.eventPreventDefault(event)
+
+    def onDrop(self, event):
+        DOM.eventPreventDefault(event)
+        self.parent.removeStyleName('dragover')
+        if self._getIndex() == self.tab_panel.tabBar.getSelectedTab():
+            # the widget comes from the same tab, so nothing to do, we let it there
+            return
+
+        item, item_type = eventGetData(event)
+        if item_type == "WIDGET":
+            if not LiberviaDragWidget.current:
+                log.error("No widget registered in LiberviaDragWidget !")
+                return
+            _new_panel = LiberviaDragWidget.current
+        elif item_type in DropCell.drop_keys:
+            pass  # create the widget when we are sure there's a tab for it
+        else:
+            log.warning("unmanaged item type")
+            return
+
+        # XXX: when needed, new tab creation must be done exactly here to not mess up with LiberviaDragWidget.onDragEnd
+        try:
+            widgets_panel = self.tab_panel.getWidget(self._getIndex())
+        except IndexError:  # widgets panel doesn't exist, e.g. user dropped in "+" tab
+            widgets_panel = self.tab_panel.addWidgetsTab(None)
+            if widgets_panel is None:  # user cancelled
+                return
+
+        if item_type == "WIDGET":
+            _new_panel.getParent(WidgetsPanel, expect=True).removeWidget(_new_panel)
+        else:
+            _new_panel = DropCell.drop_keys[item_type](self.tab_panel.host, item)
+
+        widgets_panel.addWidget(_new_panel)
+
+
+### Libervia Widget ###
+
+
+class WidgetHeader(AbsolutePanel, LiberviaDragWidget):
+
+    def __init__(self, parent, host, title, info=None):
+        """
+        @param parent (LiberviaWidget): LiberWidget instance
+        @param host (SatWebFrontend): SatWebFrontend instance
+        @param title (Label, HTML): text widget instance
+        @param info (Widget): text widget instance
+        """
+        AbsolutePanel.__init__(self)
+        self.add(title)
+        if info:
+            # FIXME: temporary design to display the info near the menu
+            button_group_wrapper = HorizontalPanel()
+            button_group_wrapper.add(info)
+        else:
+            button_group_wrapper = SimplePanel()
+        button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper')
+        button_group = base_widget.WidgetMenuBar(parent, host)
+        button_group.addItem('<img src="media/icons/misc/settings.png"/>', True, base_menu.SimpleCmd(parent.onSetting))
+        button_group.addItem('<img src="media/icons/misc/close.png"/>', True, base_menu.SimpleCmd(parent.onClose))
+        button_group_wrapper.add(button_group)
+        self.add(button_group_wrapper)
+        self.addStyleName('widgetHeader')
+        LiberviaDragWidget.__init__(self, "", "WIDGET", parent)
+
+
+class LiberviaWidget(DropCell, VerticalPanel, ClickHandler):
+    """Libervia's widget which can replace itself with a dropped widget on DnD"""
+
+    def __init__(self, host, title='', info=None, selectable=False, plugin_menu_context=None):
+        """Init the widget
+
+        @param host (SatWebFrontend): SatWebFrontend instance
+        @param title (unicode): title shown in the header of the widget
+        @param info (unicode): info shown in the header of the widget
+        @param selectable (bool): True is widget can be selected by user
+        @param plugin_menu_context (iterable): contexts of menus to have (list of C.MENU_* constant)
+        """
+        VerticalPanel.__init__(self)
+        DropCell.__init__(self, host)
+        ClickHandler.__init__(self)
+        self._selectable = selectable
+        self._plugin_menu_context = [] if plugin_menu_context is None else plugin_menu_context
+        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:
+            self._info = HTML(info)
+            self._info.setStyleName('widgetHeader_info')
+        else:
+            self._info = None
+        header = WidgetHeader(self, host, self._title, self._info)
+        self.add(header)
+        self.setSize('100%', '100%')
+        self.addStyleName('widget')
+        if self._selectable:
+            self.addClickListener(self)
+
+    @property
+    def plugin_menu_context(self):
+        return self._plugin_menu_context
+
+    def getDebugName(self):
+        return "%s (%s)" % (self, self._title.getText())
+
+    def getParent(self, class_=None, expect=True):
+        """Return the closest ancestor of the specified class.
+
+        Note: this method overrides pyjamas.ui.Widget.getParent
+
+        @param class_: class of the ancestor to look for or None to return the first parent
+        @param expect: set to True if the parent is expected (raise an error if not found)
+        @return: the parent/ancestor or None if it has not been found
+        @raise exceptions.InternalError: expect is True and no parent is found
+        """
+        current = Widget.getParent(self)
+        if class_ is None:
+            return current  # this is the default behavior
+        while current is not None and not isinstance(current, class_):
+            current = Widget.getParent(current)
+        if current is None and expect:
+            raise exceptions.InternalError("Can't find parent %s for %s" % (class_, self))
+        return current
+
+    def onClick(self, sender):
+        self.host.setSelected(self)
+
+    def onClose(self, sender):
+        """ Called when the close button is pushed """
+        widgets_panel = self.getParent(WidgetsPanel, expect=True)
+        widgets_panel.removeWidget(self)
+        self.onQuit()
+        self.host.widgets.deleteWidget(self)
+
+    def onQuit(self):
+        """ Called when the widget is actually ending """
+        pass
+
+    def refresh(self):
+        """This can be overwritten by a child class to refresh the display when,
+        instead of creating a new one, an existing widget is found and reused.
+        """
+        pass
+
+    def onSetting(self, sender):
+        widpanel = self.getParent(WidgetsPanel, expect=True)
+        row, col = widpanel.getIndex(self)
+        body = VerticalPanel()
+
+        # colspan & rowspan
+        colspan = widpanel.getColSpan(row, col)
+        rowspan = widpanel.getRowSpan(row, col)
+
+        def onColSpanChange(value):
+            widpanel.setColSpan(row, col, value)
+
+        def onRowSpanChange(value):
+            widpanel.setRowSpan(row, col, value)
+        colspan_setter = dialog.IntSetter("Columns span", colspan)
+        colspan_setter.addValueChangeListener(onColSpanChange)
+        colspan_setter.setWidth('100%')
+        rowspan_setter = dialog.IntSetter("Rows span", rowspan)
+        rowspan_setter.addValueChangeListener(onRowSpanChange)
+        rowspan_setter.setWidth('100%')
+        body.add(colspan_setter)
+        body.add(rowspan_setter)
+
+        # size
+        width_str = self.getWidth()
+        if width_str.endswith('px'):
+            width = int(width_str[:-2])
+        else:
+            width = 0
+        height_str = self.getHeight()
+        if height_str.endswith('px'):
+            height = int(height_str[:-2])
+        else:
+            height = 0
+
+        def onWidthChange(value):
+            if not value:
+                self.setWidth('100%')
+            else:
+                self.setWidth('%dpx' % value)
+
+        def onHeightChange(value):
+            if not value:
+                self.setHeight('100%')
+            else:
+                self.setHeight('%dpx' % value)
+        width_setter = dialog.IntSetter("width (0=auto)", width)
+        width_setter.addValueChangeListener(onWidthChange)
+        width_setter.setWidth('100%')
+        height_setter = dialog.IntSetter("height (0=auto)", height)
+        height_setter.addValueChangeListener(onHeightChange)
+        height_setter.setHeight('100%')
+        body.add(width_setter)
+        body.add(height_setter)
+
+        # reset
+        def onReset(sender):
+            colspan_setter.setValue(1)
+            rowspan_setter.setValue(1)
+            width_setter.setValue(0)
+            height_setter.setValue(0)
+
+        reset_bt = Button("Reset", onReset)
+        body.add(reset_bt)
+        body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER)
+
+        _dialog = dialog.GenericDialog("Widget setting", body)
+        _dialog.show()
+
+    def setTitle(self, text):
+        """change the title in the header of the widget
+        @param text: text of the new title"""
+        self._title.setText(text)
+
+    def setHeaderInfo(self, text):
+        """change the info in the header of the widget
+        @param text: text of the new title"""
+        try:
+            self._info.setHTML(text)
+        except TypeError:
+            log.error("LiberviaWidget.setInfo: info widget has not been initialized!")
+
+    def isSelectable(self):
+        return self._selectable
+
+    def setSelectable(self, selectable):
+        if not self._selectable:
+            try:
+                self.removeClickListener(self)
+            except ValueError:
+                pass
+        if self.selectable and not self in self._clickListeners:
+            self.addClickListener(self)
+        self._selectable = selectable
+
+    def getWarningData(self):
+        """ Return exposition warning level when this widget is selected and something is sent to it
+        This method should be overriden by children
+        @return: tuple (warning level type/HTML msg). Type can be one of:
+            - PUBLIC
+            - GROUP
+            - ONE2ONE
+            - MISC
+            - NONE
+        """
+        if not self._selectable:
+            log.error("getWarningLevel must not be called for an unselectable widget")
+            raise Exception
+        # TODO: cleaner warning types (more general constants)
+        return ("NONE", None)
+
+    def setWidget(self, widget, scrollable=True):
+        """Set the widget that will be in the body of the LiberviaWidget
+        @param widget: widget to put in the body
+        @param scrollable: if true, the widget will be in a ScrollPanelWrapper"""
+        if scrollable:
+            _scrollpanelwrapper = base_panel.ScrollPanelWrapper()
+            _scrollpanelwrapper.setStyleName('widgetBody')
+            _scrollpanelwrapper.setWidget(widget)
+            body_wid = _scrollpanelwrapper
+        else:
+            body_wid = widget
+        self.add(body_wid)
+        self.setCellHeight(body_wid, '100%')
+
+    def doDetachChildren(self):
+        # We need to force the use of a panel subclass method here,
+        # for the same reason as doAttachChildren
+        VerticalPanel.doDetachChildren(self)
+
+    def doAttachChildren(self):
+        # We need to force the use of a panel subclass method here, else
+        # the event will not propagate to children
+        VerticalPanel.doAttachChildren(self)
+
+
+# XXX: WidgetsPanel and MainTabPanel are both here to avoir cyclic import
+
+
+class WidgetsPanel(base_panel.ScrollPanelWrapper):
+    """The panel wanaging the widgets indide a tab"""
+
+    def __init__(self, host, locked=False):
+        """
+
+        @param host (SatWebFrontend): host instance
+        @param locked (bool): If True, the tab containing self will not be
+            removed when there are no more widget inside self. If False, the
+            tab will be removed with self's last widget.
+        """
+        base_panel.ScrollPanelWrapper.__init__(self)
+        self.setSize('100%', '100%')
+        self.host = host
+        self.locked = locked
+        self.selected = None
+        self.flextable = FlexTable()
+        self.flextable.setSize('100%', '100%')
+        self.setWidget(self.flextable)
+        self.setStyleName('widgetsPanel')
+        _bottom = BottomBorderWidget(self.host)
+        self.flextable.setWidget(0, 0, _bottom)  # There will be always an Empty widget on the last row,
+                                                 # dropping a widget there will add a new row
+        td_elt = _bottom.getElement().parentNode
+        DOM.setStyleAttribute(td_elt, "height", "1px")  # needed so the cell adapt to the size of the border (specially in webkit)
+        self._max_cols = 1  # give the maximum number of columns in a raw
+
+    @property
+    def widgets(self):
+        return iter(self.flextable)
+
+    def isLocked(self):
+        return self.locked
+
+    def changeWidget(self, row, col, wid):
+        """Change the widget in the given location, add row or columns when necessary"""
+        log.debug(u"changing widget: %s %s %s" % (wid.getDebugName(), row, col))
+        last_row = max(0, self.flextable.getRowCount() - 1)
+        # try:  # FIXME: except without exception specified !
+        prev_wid = self.flextable.getWidget(row, col)
+        # except:
+        #     log.error("Trying to change an unexisting widget !")
+        #     return
+
+        cellFormatter = self.flextable.getFlexCellFormatter()
+
+        if isinstance(prev_wid, BorderWidget):
+            # We are on a border, we must create a row and/or columns
+            prev_wid.removeStyleName('dragover')
+
+            if isinstance(prev_wid, BottomBorderWidget):
+                # We are on the bottom border, we create a new row
+                self.flextable.insertRow(last_row)
+                self.flextable.setWidget(last_row, 0, LeftBorderWidget(self.host))
+                self.flextable.setWidget(last_row, 1, wid)
+                self.flextable.setWidget(last_row, 2, RightBorderWidget(self.host))
+                cellFormatter.setHorizontalAlignment(last_row, 2, HasAlignment.ALIGN_RIGHT)
+                row = last_row
+
+            elif isinstance(prev_wid, LeftBorderWidget):
+                if col != 0:
+                    log.error("LeftBorderWidget must be on the first column !")
+                    return
+                self.flextable.insertCell(row, col + 1)
+                self.flextable.setWidget(row, 1, wid)
+
+            elif isinstance(prev_wid, RightBorderWidget):
+                if col != self.flextable.getCellCount(row) - 1:
+                    log.error("RightBorderWidget must be on the last column !")
+                    return
+                self.flextable.insertCell(row, col)
+                self.flextable.setWidget(row, col, wid)
+
+        else:
+            prev_wid.removeFromParent()
+            self.flextable.setWidget(row, col, wid)
+
+        _max_cols = max(self._max_cols, self.flextable.getCellCount(row))
+        if _max_cols != self._max_cols:
+            self._max_cols = _max_cols
+            self._sizesAdjust()
+
+    def _sizesAdjust(self):
+        cellFormatter = self.flextable.getFlexCellFormatter()
+        width = 100.0 / max(1, self._max_cols - 2)  # we don't count the borders
+
+        for row_idx in xrange(self.flextable.getRowCount()):
+            for col_idx in xrange(self.flextable.getCellCount(row_idx)):
+                _widget = self.flextable.getWidget(row_idx, col_idx)
+                if _widget and not isinstance(_widget, BorderWidget):
+                    td_elt = _widget.getElement().parentNode
+                    DOM.setStyleAttribute(td_elt, "width", "%.2f%%" % width)
+
+        last_row = max(0, self.flextable.getRowCount() - 1)
+        cellFormatter.setColSpan(last_row, 0, self._max_cols)
+
+    def addWidget(self, wid):
+        """Add a widget to a new cell on the next to last row"""
+        last_row = max(0, self.flextable.getRowCount() - 1)
+        log.debug(u"putting widget %s at %d, %d" % (wid.getDebugName(), last_row, 0))
+        self.changeWidget(last_row, 0, wid)
+
+    def removeWidget(self, wid):
+        """Remove a widget and the cell where it is"""
+        _row, _col = self.flextable.getIndex(wid)
+        self.flextable.remove(wid)
+        self.flextable.removeCell(_row, _col)
+        if not self.getLiberviaRowWidgets(_row):  # we have no more widgets, we remove the row
+            self.flextable.removeRow(_row)
+        _max_cols = 1
+        for row_idx in xrange(self.flextable.getRowCount()):
+            _max_cols = max(_max_cols, self.flextable.getCellCount(row_idx))
+        if _max_cols != self._max_cols:
+            self._max_cols = _max_cols
+            self._sizesAdjust()
+        current = self
+
+        blank_page = self.getLiberviaWidgetsCount() == 0  # do we still have widgets on the page ?
+
+        if blank_page and not self.isLocked():
+            # we now notice the MainTabPanel that the WidgetsPanel is empty and need to be removed
+            while current is not None:
+                if isinstance(current, MainTabPanel):
+                    current.onWidgetPanelRemove(self)
+                    return
+                current = current.getParent()
+            log.error("no MainTabPanel found !")
+
+    def getWidgetCoords(self, wid):
+        return self.flextable.getIndex(wid)
+
+    def getLiberviaRowWidgets(self, row):
+        """ Return all the LiberviaWidget in the row """
+        return [wid for wid in self.getRowWidgets(row) if isinstance(wid, LiberviaWidget)]
+
+    def getRowWidgets(self, row):
+        """ Return all the widgets in the row """
+        widgets = []
+        cols = self.flextable.getCellCount(row)
+        for col in xrange(cols):
+            widgets.append(self.flextable.getWidget(row, col))
+        return widgets
+
+    def getLiberviaWidgetsCount(self):
+        """ Get count of contained widgets """
+        return len([wid for wid in self.flextable if isinstance(wid, LiberviaWidget)])
+
+    def getIndex(self, wid):
+        return self.flextable.getIndex(wid)
+
+    def getColSpan(self, row, col):
+        cellFormatter = self.flextable.getFlexCellFormatter()
+        return cellFormatter.getColSpan(row, col)
+
+    def setColSpan(self, row, col, value):
+        cellFormatter = self.flextable.getFlexCellFormatter()
+        return cellFormatter.setColSpan(row, col, value)
+
+    def getRowSpan(self, row, col):
+        cellFormatter = self.flextable.getFlexCellFormatter()
+        return cellFormatter.getRowSpan(row, col)
+
+    def setRowSpan(self, row, col, value):
+        cellFormatter = self.flextable.getFlexCellFormatter()
+        return cellFormatter.setRowSpan(row, col, value)
+
+
+class MainTabPanel(TabPanel, ClickHandler):
+    """The panel managing the tabs"""
+
+    def __init__(self, host):
+        TabPanel.__init__(self, FloatingTab=True)
+        ClickHandler.__init__(self)
+        self.host = host
+        self.setStyleName('liberviaTabPanel')
+        self.tabBar.addTab(DropTab(self, u'✚'), asHTML=False)
+        self.tabBar.setVisible(False)  # set to True when profile is logged
+        self.tabBar.addStyleDependentName('oneTab')
+
+    def onTabSelected(self, sender, tabIndex):
+        if tabIndex < self.getWidgetCount():
+            TabPanel.onTabSelected(self, sender, tabIndex)
+            self.host.selected_widget = self.getCurrentPanel().selected
+            return
+        # user clicked the "+" tab
+        self.addWidgetsTab(None, select=True)
+
+    def getCurrentPanel(self):
+        """ Get the panel of the currently selected tab
+
+        @return: WidgetsPanel
+        """
+        return self.deck.visibleWidget
+
+    def addTab(self, widget, label, select=False):
+        """Create a new tab for the given widget.
+
+        @param widget (Widget): widget to associate to the tab
+        @param label (unicode): label of the tab
+        @param select (bool): True to select the added tab
<