changeset 1216:b2d067339de3

python 3 port: /!\ Python 3.6+ is now needed to use libervia /!\ instability may occur and features may not be working anymore, this will improve with time /!\ TxJSONRPC dependency has been removed The same procedure as in backend has been applied (check backend commit ab2696e34d29 logs for details). Removed now deprecated code (Pyjamas compiled browser part, legacy blog, JSON RPC related code). Adapted code to work without `html` and `themes` dirs.
author Goffi <goffi@goffi.org>
date Tue, 13 Aug 2019 19:12:31 +0200
parents f14ab8a25e8b
children fe9782391f63
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 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/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/blog.py libervia/server/constants.py libervia/server/pages.py libervia/server/pages_tools.py libervia/server/server.py libervia/server/session_iface.py libervia/server/tasks.py libervia/server/utils.py libervia/server/websockets.py setup.py themes/__init__.py themes/default/images/atom/Feed-icon.svg themes/default/images/atom/README themes/default/images/flaticon/COPYING themes/default/images/flaticon/README themes/default/images/flaticon/clock104.png themes/default/images/flaticon/comment33.png themes/default/images/flaticon/tag67.png themes/default/images/sat_logo_16.png themes/default/images/tpl/COPYING themes/default/images/tpl/README themes/default/images/tpl/page-gradient.png themes/default/static_blog.html themes/default/static_blog_error.html themes/default/styles/blog.css twisted/plugins/libervia_server.py
diffstat 107 files changed, 1134 insertions(+), 14725 deletions(-) [+]
line wrap: on
line diff
--- a/bin/libervia	Tue Aug 13 09:39:33 2019 +0200
+++ b/bin/libervia	Tue Aug 13 19:12:31 2019 +0200
@@ -2,7 +2,7 @@
 
 DEBUG=""
 DAEMON=""
-PYTHON="python2"
+PYTHON="python3"
 TWISTD="$(which twistd)"
 
 kill_process() {
@@ -25,17 +25,13 @@
 eval `"$PYTHON" << PYTHONEND
 from libervia.server.constants import Const as C
 from sat.memory.memory import fixLocalDir
-from ConfigParser import SafeConfigParser
+from configparser import ConfigParser
 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)
+config = ConfigParser(defaults=C.DEFAULT_CONFIG)
 try:
     config.read(C.CONFIG_FILES)
 except:
@@ -48,7 +44,7 @@
 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)
+print (";".join(env))
 PYTHONEND
 `
 APP_NAME="$APP_NAME"
--- a/browser/collections.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,149 +0,0 @@
-#!/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
--- a/browser/libervia_main.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,714 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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_browser import json
-# XXX: workaround for incomplete json.dumps in pyjamas
-import json as json_pyjs
-dumps_old = json_pyjs.dumps
-json_pyjs.dumps = lambda obj, *args, **kwargs: dumps_old(obj)
-
-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 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)
--- a/browser/libervia_test.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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()
--- a/browser/otr.min.js	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-/*
-
-  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}});
-
-};
--- a/browser/otr.min.js_README	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-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.
--- a/browser/public/contrat_social.html	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,110 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
-<html><head>
-  
-  <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type">
-  <title>Salut  Toi: Contrat Social</title>
-
-  
-</head><body>
-Le projet  Salut  Toi  est n d'un besoin de protection de nos
-liberts, de notre vie prive et de notre indpendance. Il se veut
-garant des droits et liberts qu'un utilisateur a vis  vis de ses
-propres informations, des informations numriques sur sa vie ou celles
-de ses connaissances, des donnes qu'il manipule; et se veut galement
-un point de contact humain, ne se substituant pas aux rapports rels,
-mais au contraire les facilitant.<br>
-
-Salut  Toi lutte et luttera toujours contre toute forme de main mise
-sur les technologies par des intrts privs. Le rseau global doit
-appartenir  tous, et tre un point d'expression et de libert pour
-l'Humanit.<br>
-
-<br>
-
- ce titre,  Salut  Toi  et ceux qui y participent se basent sur un
-contrat social, un engagement vis  vis de ceux qui l'utilisent. Ce
-contrat consiste en les points suivants:<br>
-
-<ul>
-
-  <li>nous plaons la <span style="font-style: italic;">Libert</span> en tte de nos priorits: libert de
-l'utilisateur, libert vis  vis de ses donnes. Pour cela,  Salut 
-Toi  est un logiciel Libre - condition essentielle -, et son
-infrastructure se base galement sur des logiciels Libres, c'est  dire
-des logiciels qui respectent ces 4 liberts fondamentales
-    <ul>
-
-    <li>la libert d'excuter le programme, pour tous les usages,</li>
-  
-    </ul>
-    <ul>
-
-    <li>la libert d'tudier le fonctionnement du programme et de
-l'adapter  ses besoins,</li>
-  
-    </ul>
-    <ul>
-
-    <li>la libert de redistribuer des copies du programme,</li>
-  
-    </ul>
-    <ul>
-
-    <li>la libert d'amliorer le programme et de distribuer ces
-amliorations au public.<br>
-</li>
-  
-    </ul>
-</li>
-  
-  
-  
-  
-
-Vous avez ainsi la possibilit d'installer votre propre version de 
-Salut  Toi  sur votre propre machine, d'en vrifier - et de
-comprendre - ainsi son fonctionnement, de l'adapter  vos besoins, d'en
-faire profiter vos amis.
-
-  <li>Les informations vous concernant vous appartiennent, et nous
-n'aurons pas la prtention - et l'indcence ! - de considrer le
-contenu que vous produisez ou faites circuler via  Salut  Toi  comme
-nous appartenant. De mme, nous nous engageons  ne jamais faire de
-profit en revendant vos informations personnelles.</li>
-  <li>Nous incitons fortement  la <span style="text-decoration: underline;">dcentralisation gnralise</span>. 
-Salut  Toi  tant bas sur un protocole dcentralis (XMPP), il l'est
-lui-mme par nature. La dcentralisation est essentielle pour une
-meilleure protection de vos informations, une meilleure rsistance  la
-censure ou aux pannes, et pour viter les drives autoritaires.</li>
-  <li>Luttant contre les tentatives de contrle priv et les abus
-commerciaux du rseau global, et afin de garder notre indpendance,
-nous nous refusons  toute forme de publicit: vous ne verrez <span style="font-weight: bold;">jamais</span>
-de forme de rclame commerciale de notre fait.</li>
-  <li>L'<span style="font-style: italic;">galit</span> des utilisateurs est essentielle pour nous, nous
-refusons toute forme de discrimination, que ce soit pour une zone
-gographique, une catgorie de la population, ou tout autre raison.</li>
-  <li>Nous ferons tout notre possible pour lutter contre toute
-tentative de censure. Le rseau global doit tre un moyen d'expression
-pour tous.</li>
-  <li>Nous refusons toute ide d'autorit absolue en ce qui concerne
-les dcisions prises pour  Salut  Toi  et son fonctionnement, et le
-choix de la dcentralisation et l'utilisation de logiciel Libre permet
-de lutter contre toute forme de hirarchie.</li>
-  
-  <li>L'ide de <span style="font-style: italic;">Fraternit</span> est essentielle, aussi:
-    <ul>
-      <li>nous ferons notre
-possible pour aider les utilisateurs, quel que soit leur niveau</li>
-      <li>de mme, des efforts seront fait quant 
-l'accessibilit 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>
--- a/browser/public/favico.min.js	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-/**
- * 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}();
--- a/browser/public/libervia.css	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1687 +0,0 @@
-/*
-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;
-}
--- a/browser/public/libervia.html	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-<!--
-Libervia: a Salut à Toi frontend
-Copyright (C) 2011  Jérôme Poisson (goffi@goffi.org)
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
--->
-
-<html>
-<head profile="http://www.w3.org/2005/10/profile">
-<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-<meta name="pygwt:module" content="libervia_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>
--- a/browser/public/robots.txt	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-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
--- a/browser/sat_browser/base_menu.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,183 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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
--- a/browser/sat_browser/base_panel.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,236 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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)
--- a/browser/sat_browser/base_widget.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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
--- a/browser/sat_browser/blog.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,559 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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 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 ''}
-        content['tags'] = self.item.tags
-
-        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 = content['tags']
-        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)
--- a/browser/sat_browser/chat.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,345 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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'})
--- a/browser/sat_browser/constants.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a SAT frontend
-# Copyright (C) 2009-2019 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')
--- a/browser/sat_browser/contact_group.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,245 +0,0 @@
-#!/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_entities)
-        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()
--- a/browser/sat_browser/contact_list.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,319 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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)
--- a/browser/sat_browser/contact_panel.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,157 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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)
--- a/browser/sat_browser/contact_widget.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,160 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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)})
--- a/browser/sat_browser/dialog.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,616 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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)
--- a/browser/sat_browser/editor_widget.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,383 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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:
-            if isinstance(content[key], list):
-                self._original_content[key] = [self.strproc(s) for s in content[key]]
-            else:
-                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)
--- a/browser/sat_browser/file_tools.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,163 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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)
--- a/browser/sat_browser/game_radiocol.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,347 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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)
--- a/browser/sat_browser/game_tarot.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,410 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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)
--- a/browser/sat_browser/html_tools.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,80 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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
--- a/browser/sat_browser/json.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,298 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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)
--- a/browser/sat_browser/libervia_widget.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,811 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# Libervia: a Salut à Toi frontend
-# Copyright (C) 2011-2019 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
-        """
-        TabPanel.add(self, widget, DropTab(self, label), False)
-        if self.getWidgetCount() > 1:
-            self.tabBar.removeStyleDependentName('oneTab')
-            self.host.resize()
-        if select:
-            self.selectTab(self.getWidgetCount() - 1)
-
-    def addWidgetsTab(self, label, select=False, locked=False):
-        """Create a new tab for containing LiberviaWidgets.
-
-        @param label (unicode): label of the tab (None or '' for user prompt)
-        @param select (bool): True to select the added tab
-        @param locked (bool): If True, the tab will not be removed when there
-            are no more widget inside. If False, the tab will be removed with
-            the last widget.
-        @return: WidgetsPanel
-        """
-        widgets_panel = WidgetsPanel(self.host, locked=locked)
-
-        if not label:
-            default_label = _(u'new tab')
-            try:
-                label = Window.prompt(_(u'Name of the new tab'), default_label)
-                if not label:  # empty label or user pressed "cancel"
-                    return None
-            except:  # this happens when the user prevents the page to open the prompt dialog
-                label = default_label
-
-        self.addTab(widgets_panel, label, select)
-        return widgets_panel
-
-    def onWidgetPanelRemove(self, panel):
-        """ Called when a child WidgetsPanel is empty and need to be removed """
-        widget_index = self.getWidgetIndex(panel)
-        self.remove(panel)
-        widgets_count = self.getWidgetCount()
-        if widgets_count == 1:
-            self.tabBar.addStyleDependentName('oneTab')
-            self.host.resize()
-        self.selectTab(widget_index if widget_index < widgets_count else widgets_count - 1)
-
-
-def eventGetData(event):
-    """Retrieve the event data.
-
-    @param event(EventObject)
-    @return tuple: (event_text, event_type)
-    """
-    dt = event.dataTransfer
-    # 'text', 'text/plain', and 'Text' are equivalent.
-    try:
-        item, item_type = dt.getData("text/plain").split('\n')  # Workaround for webkit, only text/plain seems to be managed
-        if item_type and item_type[-1] == '\0':  # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
-            item_type = item_type[:-1]           # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
-        # item_type = dt.getData("type")
-        log.debug(u"event data: %s (type %s)" % (item, item_type))
-    except:
-        log.debug("event data not found")
-        item = '&nbsp;'
-        item_type = None
-    return item, item_type
--- a/browser/sat_browser/list_manager.py	Tue Aug 13 09:39:33 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,516 +0,0 @@
-#!/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 sat.core.log import getLogger
-log = getLogger(__name__)
-from sat.core.i18n import _
-
-from pyjamas.ui.ClickListener import ClickHandler
-from pyjamas.ui.FocusListener import FocusHandler
-from pyjamas.ui.ChangeListener import ChangeHandler
-from pyjamas.ui.DragHandler import DragHandler
-from pyjamas.ui.KeyboardListener import KeyboardHandler, KEY_ENTER
-from pyjamas.ui.DragWidget import DragWidget
-from pyjamas.ui.ListBox import ListBox
-from pyjamas.ui.Button import Button
-from pyjamas.ui.FlowPanel import FlowPanel
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.FlexTable import FlexTable
-from pyjamas.ui.AutoComplete import AutoCompleteTextBox
-
-import base_panel
-import base_widget
-import libervia_widget
-
-from sat_frontends.quick_frontend import quick_list_manager
-
-
-unicode = str  # FIXME: pyjamas workaround
-
-
-class ListItem(HorizontalPanel):
-    """This class implements a list item with auto-completion and a delete button."""
-
-    STYLE = {"listItem": "listItem",
-             "listItem-box": "listItem-box",
-             "listItem-box-invalid": "listItem-box-invalid",
-             "listItem-button": "listItem-button",
-             }
-
-    VALID = 1
-    INVALID = 2
-    DUPLICATE = 3
-
-    def __init__(self, listener=None, taglist=None, validate=None):
-        """
-
-        @param listener (ListItemHandler): handler for the UI events
-        @param taglist (quick_list_manager.QuickTagList): list manager
-        @param validate (callable): method returning a bool to validate the entry
-        """
-        HorizontalPanel.__init__(self)
-        self.addStyleName(self.STYLE["listItem"])
-
-        self.box = AutoCompleteTextBox(StyleName=self.STYLE["listItem-box"])
-        self.remove_btn = Button('<span>x</span>', Visible=False)
-        self.remove_btn.setStyleName(self.STYLE["listItem-button"])
-        self.add(self.box)
-        self.add(self.remove_btn)
-
-        if listener:
-            self.box.addFocusListener(listener)
-            self.box.addChangeListener(listener)
-            self.box.addKeyboardListener(listener)
-            self.box.choices.addClickListener(listener)
-            self.remove_btn.addClickListener(listener)
-
-        self.taglist = taglist
-        self.validate = validate
-        self.last_checked_value = ""
-        self.last_validity = self.VALID
-
-    @property
-    def text(self):
-        return self.box.getText()
-
-    def setText(self, text):
-        """
-        Set the text and refresh the Widget.
-        
-        @param text (unicode): text to set
-        """
-        self.box.setText(text)
-        self.refresh()
-
-    def refresh(self):
-        if self.last_checked_value == self.text:
-            return
-
-        if self.taglist and self.last_checked_value:
-            self.taglist.untag([self.last_checked_value])
-
-        if self.validate:  # if None, the state is always valid
-            self.last_validity = self.validate(self.text)
-
-        if self.last_validity == self.VALID:
-            self.box.removeStyleName(self.STYLE["listItem-box-invalid"])
-            self.box.setVisibleLength(max(len(self.text), 10))
-        elif self.last_validity == self.INVALID:
-            self.box.addStyleName(self.STYLE["listItem-box-invalid"])
-        elif self.last_validity == self.DUPLICATE:
-            self.remove_btn.click()  # this may do more stuff then self.remove()
-            return
-        
-        if self.taglist and self.text:
-            self.taglist.tag([self.text])
-        self.last_checked_value = self.text
-        self.box.setSelectionRange(len(self.text), 0)  
-        self.remove_btn.setVisible(len(self.text) > 0)
-                     
-    def setFocus(self, focused):
-        self.box.setFocus(focused)
-
-    def remove(self):
-        """Remove the list item from its parent."""
-        self.removeFromParent()
-
-        if self.taglist and self.text:  # this must be done after the widget has been removed
-            self.taglist.untag([self.text])
-
-
-class DraggableListItem(ListItem, DragWidget):
-    """This class is like ListItem, but in addition it can be dragged."""
-
-    def __init__(self, listener=None, taglist=None, validate=None):
-        """
-    
-        @param listener (ListItemHandler): handler for the UI events
-        @param taglist (quick_list_manager.QuickTagList): list manager
-        @param validate (callable): method returning a bool to validate the entry
-        """
-        ListItem.__init__(self, listener, taglist, validate)
-        DragWidget.__init__(self)
-        self.addDragListener(listener)
-
-
-    def onDragStart(self, event):
-        """The user starts dragging the item."""
-        dt = event.dataTransfer
-        dt.setData('text/plain', "%s\n%s" % (self.text, "CONTACT_TEXTBOX"))
-        dt.setDragImage(self.box.getElement(), 15, 15)
-
-
-class ListItemHandler(ClickHandler, FocusHandler, KeyboardHandler, ChangeHandler):
-    """Implements basic handlers for the ListItem events."""
-
-    last_item = None  # the last item is an empty text box for user input
-
-    def __init__(self, taglist):
-        """
-        
-        @param taglist (quick_list_manager.QuickTagList): list manager
-        """
-        ClickHandler.__init__(self)
-        FocusHandler.__init__(self)
-        ChangeHandler.__init__(self)
-        KeyboardHandler.__init__(self)
-        self.taglist = taglist
-
-    def addItem(self, item):
-        raise NotImplementedError
-
-    def removeItem(self, item):
-        raise NotImplementedError
-
-    def onClick(self, sender):
-        """The remove button or a suggested completion item has been clicked."""
-        #log.debug("onClick sender type: %s" % type(sender))
-        if isinstance(sender, Button):
-            item = sender.getParent()
-            self.removeItem(item)
-        elif isinstance(sender, ListBox):
-            # this is called after onChange when you click a suggested item, and now we get the final value
-            textbox = sender._clickListeners[0]
-            self.checkValue(textbox)
-        else:
-            raise AssertionError
-
-    def onFocus(self, sender):
-        """The text box has the focus."""
-        #log.debug("onFocus sender type:  %s" % type(sender))
-        assert isinstance(sender, AutoCompleteTextBox)
-        sender.setCompletionItems(self.taglist.untagged)
-
-    def onKeyUp(self, sender, keycode, modifiers):
-        """The text box is being modified - or ENTER key has been pressed."""
-        # this is called after onChange when you press ENTER, and now we get the final value
-        #log.debug("onKeyUp sender type:  %s" % type(sender))
-        assert isinstance(sender, AutoCompleteTextBox)
-        if keycode == KEY_ENTER:
-            self.checkValue(sender)
-
-    def onChange(self, sender):
-        """The text box has been changed by the user."""
-        # this is called before the completion when you press ENTER or click a suggest item
-        #log.debug("onChange sender type:  %s" % type(sender))
-        assert isinstance(sender, AutoCompleteTextBox)
-        self.checkValue(sender)
-
-    def checkValue(self, textbox):
-        """Internal handler to call when a new value is submitted by the user."""
-        item = textbox.getParent()
-        if item.text == item.last_checked_value:
-            # this method has already been called (by self.onChange) and there's nothing new
-            return
-        item.refresh()
-        if item == self.last_item and item.last_validity == ListItem.VALID and item.text:
-            self.addItem()
-
-class DraggableListItemHandler(ListItemHandler, DragHandler):
-    """Implements basic handlers for the DraggableListItem events."""
-
-    def __init__(self, manager):
-        """
-        
-        @param manager (ListManager): list manager
-        """
-        ListItemHandler.__init__(self, manager)
-        DragHandler.__init__(self)
-