changeset 637:7113d40533d6 frontends_multi_profiles

merged souliane changes
author Goffi <goffi@goffi.org>
date Mon, 23 Feb 2015 18:47:27 +0100
parents 86ae737da6f3 (current diff) 66a547539185 (diff)
children 63697f082e8a
files src/browser/libervia_main.py src/browser/sat_browser/base_widget.py src/browser/sat_browser/contact_list.py src/browser/sat_browser/panels.py
diffstat 12 files changed, 236 insertions(+), 159 deletions(-) [+]
line wrap: on
line diff
--- a/src/browser/libervia_main.py	Mon Feb 23 18:44:58 2015 +0100
+++ b/src/browser/libervia_main.py	Mon Feb 23 18:47:27 2015 +0100
@@ -60,7 +60,9 @@
 except ImportError:
     pass
 
-unicode = lambda s: str(s)
+
+unicode = str  # FIXME: pyjamas workaround
+
 
 MAX_MBLOG_CACHE = 500  # Max microblog entries kept in memories
 
@@ -80,7 +82,6 @@
         self.uni_box = None # FIXME: to be removed
         self.status_panel = HTML('<br />')
         self.panel = panels.MainPanel(self)
-        self.discuss_panel = self.panel.discuss_panel
         self.tab_panel = self.panel.tab_panel
         self.tab_panel.addTabListener(self)
         self._register_box = None
@@ -213,7 +214,8 @@
         @return: the URL to the avatar (str)
         """
         assert isinstance(jid_, jid.JID)
-        avatar_hash = self.contact_lists[C.PROF_KEY_NONE].getCache(jid_, 'avatar')
+        contact_list = self.contact_list  # pyjamas issue: need a temporary variable to call a property's method
+        avatar_hash = contact_list.getCache(jid_, 'avatar')
         if avatar_hash is None:
             # we have no value for avatar_hash, so we request the vcard
             self.bridge.getCard(unicode(jid_), profile=C.PROF_KEY_NONE)
@@ -239,25 +241,6 @@
             lib_wid.refresh()
         self.resize()
 
-    def addTab(self, label, wid=None, select=False):
-        """Create a new tab and eventually add a widget to it.
-
-        @param label (unicode): label of the tab
-        @param wid (LiberviaWidget): optional widget to add
-        @param select (bool): True to select the added tab
-        @return: WidgetsPanel
-        """
-        widgets_panel = base_widget.WidgetsPanel(self)
-        self.tab_panel.add(widgets_panel, label)
-        tab_index = self.tab_panel.getWidgetCount() - 1
-        if wid is not None:
-            self.addWidget(wid, tab_index)
-        if select:
-            self.tab_panel.selectTab(tab_index)
-            if wid is not None:
-                self.setSelected(wid)
-        return widgets_panel
-
     def addWidget(self, wid, tab_index=None):
         """ Add a widget at the bottom of the current or specified tab
 
@@ -376,7 +359,6 @@
 
     def addContactList(self, dummy):
         contact_list = ContactList(self)
-        self.contact_lists[C.PROF_KEY_NONE] = contact_list
         self.panel.addContactList(contact_list)
         return contact_list
 
@@ -611,7 +593,8 @@
             kwargs['on_new_widget'] = None
             kwargs['on_existing_widget'] = C.WIDGET_RECREATE
             wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs)
-            self.addTab(new_tab, wid)
+            self.tab_panel.addWidgetsTab(new_tab)
+            self.addWidget(wid)
             return wid
 
         kwargs['on_existing_widget'] = C.WIDGET_RAISE
@@ -844,7 +827,8 @@
             msg = HTML('The contact <b>%s</b> want to add you in his/her contact list, do you accept ?' % html_tools.html_sanitize(entity))
 
             def ok_cb(ignore):
-                self.bridge.call('subscription', None, "subscribed", entity, '', _dialog.getSelectedGroups())
+                self.bridge.call('subscription', None, "subscribed", entity)
+                self.bridge.updateContact(entity, '', _dialog.getSelectedGroups())
 
             def cancel_cb(ignore):
                 self.bridge.call('subscription', None, "unsubscribed", entity, '', '')
@@ -970,6 +954,20 @@
             self.warning_popup = panels.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()
+
 
 if __name__ == '__main__':
     app = SatWebFrontend()
--- a/src/browser/sat_browser/base_widget.py	Mon Feb 23 18:44:58 2015 +0100
+++ b/src/browser/sat_browser/base_widget.py	Mon Feb 23 18:47:27 2015 +0100
@@ -21,7 +21,9 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 from sat.core import exceptions
+from sat.core.i18n import _
 from sat_frontends.quick_frontend import quick_widgets
+
 from pyjamas.ui.SimplePanel import SimplePanel
 from pyjamas.ui.AbsolutePanel import AbsolutePanel
 from pyjamas.ui.VerticalPanel import VerticalPanel
@@ -586,10 +588,17 @@
 class WidgetsPanel(ScrollPanelWrapper):
 
     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.
+        """
         ScrollPanelWrapper.__init__(self)
         self.setSize('100%', '100%')
         self.host = host
-        self.locked = locked  # if True: tab will not be removed when there are no more widgets inside
+        self.locked = locked
         self.selected = None
         self.flextable = FlexTable()
         self.flextable.setSize('100%', '100%')
@@ -760,18 +769,18 @@
     def onDragEnter(self, event):
         #if self == LiberviaDragWidget.current:
         #    return
-        self.addStyleName('dragover')
+        self.parent.addStyleName('dragover')
         DOM.eventPreventDefault(event)
 
     def onDragLeave(self, event):
-        self.removeStyleName('dragover')
+        self.parent.removeStyleName('dragover')
 
     def onDragOver(self, event):
         DOM.eventPreventDefault(event)
 
     def onDrop(self, event):
         DOM.eventPreventDefault(event)
-        self.removeStyleName('dragover')
+        self.parent.removeStyleName('dragover')
         if self._getIndex() == self.tab_panel.tabBar.getSelectedTab():
             # the widget come from the DragTab, so nothing to do, we let it there
             return
@@ -806,16 +815,32 @@
         widgets_panel.addWidget(_new_panel)
 
 
-class MainTabPanel(TabPanel):
+class MainTabPanel(TabPanel, ClickHandler):
 
     def __init__(self, host):
         TabPanel.__init__(self)
+        ClickHandler.__init__(self)
         self.host = host
-        self.tabBar.setVisible(False)
         self.setStyleName('liberviaTabPanel')
         self.addStyleName('mainTabPanel')
         Window.addWindowResizeListener(self)
 
+        self.tabBar.addTab(u'✚', True)
+
+    def onTabSelected(self, sender, tabIndex):
+        if tabIndex < self.getWidgetCount():
+            TabPanel.onTabSelected(self, sender, tabIndex)
+            return
+        # user clicked the "+" tab
+        default_label = _(u'new tab')
+        try:
+            label = Window.prompt(_(u'Name of the new tab'), default_label)
+            if not label:
+                label = default_label
+        except:  # this happens when the user prevents the page to open the prompt dialog
+            label = default_label
+        self.addWidgetsTab(label, select=True)
+
     def getCurrentPanel(self):
         """ Get the panel of the currently selected tab
 
@@ -836,20 +861,34 @@
         self.setWidth("%s%s" % (ideal_width, "px"))
         self.setHeight("%s%s" % (ideal_height, "px"))
 
-    def add(self, widget, text=''):
-        tab = DropTab(self, text)
-        TabPanel.add(self, widget, tab, False)
-        if self.getWidgetCount() > 1:
-            self.tabBar.setVisible(True)
-            self.host.resize()
+    def 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 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
+        @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, locked=locked)
+        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.setVisible(False)
-            self.host.resize()
-            self.selectTab(0)
-        else:
-            self.selectTab(widgets_count - 1)
+        self.selectTab(widget_index if widget_index < widgets_count else widgets_count - 1)
--- a/src/browser/sat_browser/blog.py	Mon Feb 23 18:44:58 2015 +0100
+++ b/src/browser/sat_browser/blog.py	Mon Feb 23 18:47:27 2015 +0100
@@ -166,7 +166,7 @@
         is_publisher = self.author == self._blog_panel.host.whoami.bare
         if is_publisher:
             self.update_label = addIcon(u"✍", "Edit this message")
-        if is_publisher or str(self.node).endswith(self._blog_panel.host.whoami.bare):
+        if is_publisher or str(self.node).endswith(unicode(self._blog_panel.host.whoami.bare)):
             self.delete_label = addIcon(u"✗", "Delete this message")
 
     def updateAvatar(self, new_avatar):
@@ -374,7 +374,10 @@
         self.vpanel = VerticalPanel()
         self.vpanel.setStyleName('microblogPanel')
         self.setWidget(self.vpanel)
-        host.addListerner('avatar', self.onAvatarUpdate)
+
+        # 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])
 
     def __str__(self):
         return u"Blog Widget [target: {}, profile: {}]".format(self.target, self.profile)
@@ -385,17 +388,16 @@
 
     def onDelete(self):
         quick_widgets.QuickWidget.onDelete(self)
-        self.host.removeListener('avatar', self.onAvatarUpdate)
+        self.host.removeListener('avatar', self.avatarListener)
 
-    def onAvatarUpdate(self, jid_, hash_, profile):
+    def onAvatarUpdate(self, jid_, hash_):
         """Called on avatar update events
 
         @param jid_: jid of the entity with updated avatar
         @param hash_: hash of the avatar
-        @param profile: should be C.PROF_KEY_NONE
         """
-        whoami = self.host.profiles[self.profile].whoami.bare
-        if self.isJidAccepted(jid_) or jid_.bare  == whoami.bare:
+        whoami = self.host.profiles[self.profile].whoami
+        if self.isJidAccepted(jid_) or jid_.bare == whoami.bare:
             self.updateValue('avatar', jid_, hash_)
 
     def refresh(self):
@@ -567,7 +569,7 @@
         assert isinstance(sender, jid.JID) # FIXME temporary
         if (mblog_entry.type == "comment"
             or self.isJidAccepted(sender)
-            or (groups == None and sender == self.host.profiles[self.profile].whoami.bare)
+            or (groups is None and sender == self.host.profiles[self.profile].whoami.bare)
             or (groups and groups.intersection(self.accepted_groups))):
             self.addEntry(mblog_entry)
 
@@ -580,7 +582,7 @@
         _entry = MicroblogEntry(self, data)
         if _entry.type == "comment":
             comments_hash = (_entry.service, _entry.node)
-            if not comments_hash in self.comments:
+            if comments_hash not in self.comments:
                 # The comments node is not known in this panel
                 return None
             parent = self.comments[comments_hash]
--- a/src/browser/sat_browser/chat.py	Mon Feb 23 18:44:58 2015 +0100
+++ b/src/browser/sat_browser/chat.py	Mon Feb 23 18:47:27 2015 +0100
@@ -49,6 +49,9 @@
 import plugin_xep_0085
 
 
+unicode = str  # FIXME: pyjamas workaround
+
+
 class ChatText(HTMLPanel):
 
     def __init__(self, nick, mymess, msg, extra):
@@ -88,7 +91,7 @@
             host.plugins['otr'].infoTextCallback(target, cb)
         header_info = header_info_cb if (type_ == C.CHAT_ONE2ONE and 'otr' in host.plugins) else None
 
-        base_widget.LiberviaWidget.__init__(self, host, title=target.bare, info=header_info, selectable=True)
+        base_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()
@@ -206,7 +209,7 @@
     def onQuit(self):
         base_widget.LiberviaWidget.onQuit(self)
         if self.type == C.CHAT_GROUP:
-            self.host.bridge.call('mucLeave', None, self.target.bare)
+            self.host.bridge.call('mucLeave', None, unicode(self.target.bare))
 
     def setUserNick(self, nick):
         """Set the nick of the user, usefull for e.g. change the color of the user"""
@@ -338,10 +341,10 @@
 
     def refreshTitle(self):
         """Refresh the title of this Chat dialog"""
+        title = unicode(self.target.bare)
         if self._state:
-            self.setTitle(self.target.bare + " (" + self._state + ")")
-        else:
-            self.setTitle(self.target.bare)
+            title += " (%s)".format(self._state)
+        self.setTitle(title)
 
     def setConnected(self, jid_s, resource, availability, priority, statuses):
         """Set connection status
--- a/src/browser/sat_browser/contact_list.py	Mon Feb 23 18:44:58 2015 +0100
+++ b/src/browser/sat_browser/contact_list.py	Mon Feb 23 18:47:27 2015 +0100
@@ -358,7 +358,10 @@
         self.add(self.scroll_panel)
         self.setStyleName('contactList')
         Window.addWindowResizeListener(self)
-        host.addListerner('avatar', self.onAvatarUpdate)
+
+        # 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])
 
     @property
     def profile(self):
@@ -366,7 +369,7 @@
 
     def onDelete(self):
         QuickContactList.onDelete(self)
-        self.host.removeListener('avatar', self.onAvatarUpdate)
+        self.host.removeListener('avatar', self.avatarListener)
 
     def update(self):
         ### GROUPS ###
@@ -477,10 +480,6 @@
     #         # case 2: update (or confirm) with the values of the resource which takes precedence
     #         self._contacts_panel.setState(jid_s, "availability", availability)
 
-    #     # update the connected contacts chooser live
-    #     if hasattr(self.host, "room_contacts_chooser") and self.host.room_contacts_chooser is not None:
-    #         self.host.room_contacts_chooser.resetContacts()
-
     #     self.updateVisibility([jid_s], self.getContactGroups(jid_s))
 
     def setContactMessageWaiting(self, jid, waiting):
@@ -563,12 +562,11 @@
         """
         self._contacts_panel.updateAvatar(jid_s, url)
 
-    def onAvatarUpdate(self, jid_, hash_, profile):
+    def onAvatarUpdate(self, jid_, hash_):
         """Called on avatar update events
 
         @param jid_: jid of the entity with updated avatar
         @param hash_: hash of the avatar
-        @param profile: should be C.PROF_KEY_NONE
         """
         self._contacts_panel.updateAvatar(jid_, self.host.getAvatarURL(jid_))
 
--- a/src/browser/sat_browser/dialog.py	Mon Feb 23 18:44:58 2015 +0100
+++ b/src/browser/sat_browser/dialog.py	Mon Feb 23 18:47:27 2015 +0100
@@ -19,7 +19,10 @@
 
 from sat.core.log import getLogger
 log = getLogger(__name__)
+
+from constants import Const as C
 from sat_frontends.tools.misc import DEFAULT_MUC
+from sat_frontends.tools import jid
 
 from pyjamas.ui.VerticalPanel import VerticalPanel
 from pyjamas.ui.Grid import Grid
@@ -44,12 +47,19 @@
 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, default_room=DEFAULT_MUC):
+        """
+
+        @param host (SatWebFrontend)
+        """
         Grid.__init__(self, 2, 2, Width='100%')
         self.host = host
 
@@ -70,7 +80,19 @@
 
         self.exist_radio.setVisible(False)
         self.rooms_list.setVisible(False)
-        self.setRooms()
+        self.refreshOptions()
+
+    @property
+    def room(self):
+        """Get the room that has been selected or entered by the user
+
+        @return: jid.JID or None
+        """
+        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 == self.GENERATE_MUC else jid.JID(value)
 
     def onFocus(self, sender):
         if sender == self.rooms_list:
@@ -85,21 +107,17 @@
             if self.box.getText() == "":
                 self.box.setText(self.GENERATE_MUC)
 
-    def setRooms(self):
-        for room in self.host.room_list:
+    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(self.host.room_list) > 0:
+        if len(muc_rooms) > 0:
             self.exist_radio.setVisible(True)
             self.rooms_list.setVisible(True)
             self.exist_radio.setChecked(True)
 
-    def getRoom(self):
-        if self.exist_radio.getChecked():
-            values = self.rooms_list.getSelectedValues()
-            return "" if values == [] else values[0]
-        value = self.box.getText()
-        return "" if value == self.GENERATE_MUC else value
-
 
 class ContactsChooser(VerticalPanel):
     """Select one or several connected contacts"""
@@ -132,34 +150,41 @@
         self.contacts_list.addStyleName('contactsChooser')
         self.contacts_list.addChangeListener(self.onChange)
         self.add(self.contacts_list)
-        self.setContacts()
+        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]:
+            if selected >= self.nb_contact[0] and selected <= self.nb_contact[1]:
                 self.ok_button.setEnabled(True)
             else:
                 self.ok_button.setEnabled(False)
 
-    def setContacts(self, selected=[]):
-        """Fill the list with the connected contacts
-        @param select: list of the contacts to select by default
+    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_panel.getConnected(filter_muc=True)
+        contacts = self.host.contact_list.roster_entities_connected
         self.contacts_list.setVisibleItemCount(10 if len(contacts) > 5 else 5)
         self.contacts_list.addItem("")
         for contact in contacts:
-            if contact not in [room.bare for room in self.host.room_list]:
-                self.contacts_list.addItem(contact)
-        self.contacts_list.setItemTextSelection(selected)
-
-    def getContacts(self):
-        return self.contacts_list.getSelectedValues(True)
+            self.contacts_list.addItem(contact)
+        if selection:
+            self.contacts_list.setItemTextSelection([unicode(contact) for contact in selection])
 
 
 class RoomAndContactsChooser(DialogBox):
@@ -198,60 +223,72 @@
         self.setHTML(title)
         self.show()
 
-        # needed to update the contacts list when someone logged in/out
-        self.host.room_contacts_chooser = 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.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
 
-    def getRoom(self, asSuffix=False):
-        room = self.room_panel.getRoom()
-        if asSuffix:
-            return room if room == "" else ": %s" % room
-        else:
-            return room
+        @return: jid.JID or None
+        """
+        return self.room_panel.room
 
-    def getContacts(self, asSuffix=False):
-        contacts = self.contact_panel.getContacts()
-        if asSuffix:
-            return "" if contacts == [] else ": %s" % ", ".join(contacts)
-        else:
-            return contacts
+    @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:
-            sender.setStackText(0, self.title_room + ("" if visible else self.getRoom(True)))
+            suffix = "" if (visible or not self.room) else ": %s" % self.room
+            sender.setStackText(0, self.title_room + suffix)
         elif index == 1:
-            sender.setStackText(1, self.title_invite + ("" if visible else self.getContacts(True)))
+            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 resetContacts(self):
-        """Called when someone log in/out to update the list"""
-        self.contact_panel.setContacts(self.getContacts())
+    def refreshContactList(self, *args):
+        """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_jid = self.getRoom()
-        if room_jid != "" and "@" not in room_jid:
+        room = self.room  # pyjamas issue: you need to use an intermediate variable to access a property's method
+        if room and not room.is_valid():
             Window.alert('You must enter a room jid in the form room@chat.%s' % self.host._defaultDomain)
             return
         self.hide()
-        self.callback(room_jid, self.getContacts())
+        self.callback(room, self.contacts)
 
     def onCancel(self, sender):
         self.hide()
 
     def hide(self):
-        self.host.room_contacts_chooser = None
+        self.host.removeListener('presence', self.presenceListener)
         DialogBox.hide(self, autoClosed=True)
 
 
 class GenericConfirmDialog(DialogBox):
 
-    def __init__(self, widgets, callback, title='Confirmation', prompt=None, **kwargs):
+    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: method to call when a button is clicked
+        @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 (TextBox, list[TextBox]): input widgets from which to retrieve
+        @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".
         """
@@ -261,16 +298,14 @@
         if added_style:
             self.addStyleName(added_style)
 
-        if prompt is None:
-            prompt = []
-        elif isinstance(prompt, TextBox):
-            prompt = [prompt]
+        if prompt_widgets is None:
+            prompt_widgets = []
 
         content = VerticalPanel()
         content.setWidth('100%')
         for wid in widgets:
             content.add(wid)
-            if wid in prompt:
+            if wid in prompt_widgets:
                 wid.setWidth('100%')
         button_panel = HorizontalPanel()
         button_panel.addStyleName("marginAuto")
@@ -281,11 +316,12 @@
         content.add(button_panel)
         self.setHTML(title)
         self.setWidget(content)
-        self.prompt = prompt
+        self.prompt_widgets = prompt_widgets
 
     def onConfirm(self, sender):
         self.hide()
-        result = [box.getText() for box in self.prompt] if self.prompt else [True]
+        result = [True]
+        result.extend([box.getText() for box in self.prompt_widgets])
         self.callback(*result)
 
     def onCancel(self, sender):
@@ -294,8 +330,8 @@
 
     def show(self):
         DialogBox.show(self)
-        if self.prompt:
-            self.prompt[0].setFocus(True)
+        if self.prompt_widgets:
+            self.prompt_widgets[0].setFocus(True)
 
 
 class ConfirmDialog(GenericConfirmDialog):
@@ -328,7 +364,7 @@
         _body.add(main_widget)
         _body.setCellWidth(main_widget, '100%')
         _body.setCellHeight(main_widget, '100%')
-        if not 'NO_CLOSE' in options:
+        if 'NO_CLOSE' not in options:
             _close_button = Button("Close", self.onClose)
             _body.add(_close_button)
             _body.setCellHorizontalAlignment(_close_button, HasAlignment.ALIGN_CENTER)
@@ -357,19 +393,18 @@
     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 clicking OK
-        @param textes (str, list[str]): HTML textes to display before the inputs
-        @param values (str, list[str]): default values for each input
-        @param title (str): dialog title
+        @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
-        elif not isinstance(textes, list):
-            textes = [textes]  # allow to pass a single string instead of a list
         if values is None:
             values = []
-        elif not isinstance(values, list):
-            values = [values]  # allow to pass a single string instead of a list
         all_widgets = []
         prompt_widgets = []
         for count in xrange(len(textes)):
@@ -388,7 +423,7 @@
 
     def onEventPreview(self, event):
         if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE:
-            #needed to prevent request cancellation in Firefox
+            # needed to prevent request cancellation in Firefox
             event.preventDefault()
         return PopupPanel.onEventPreview(self, event)
 
--- a/src/browser/sat_browser/menu.py	Mon Feb 23 18:44:58 2015 +0100
+++ b/src/browser/sat_browser/menu.py	Mon Feb 23 18:47:27 2015 +0100
@@ -23,8 +23,6 @@
 
 from sat.core.i18n import _
 
-from sat_frontends.tools import jid
-
 from pyjamas.ui.SimplePanel import SimplePanel
 from pyjamas.ui.HTML import HTML
 from pyjamas.ui.Frame import Frame
@@ -41,6 +39,9 @@
 from base_menu import MenuCmd
 
 
+unicode = str  # FIXME: pyjamas workaround
+
+
 class MainMenuBar(base_menu.GenericMenuBar):
     """The main menu bar which is displayed on top of the document"""
 
@@ -149,28 +150,31 @@
 
         def invite(room_jid, contacts):
             for contact in contacts:
-                self.host.bridge.call('inviteMUC', None, contact, room_jid)
+                self.host.bridge.call('inviteMUC', None, unicode(contact), unicode(room_jid))
 
         def join(room_jid, contacts):
             if self.host.whoami:
                 nick = self.host.whoami.node
-                if room_jid not in [room.bare for room in self.host.room_list]:
-                    self.host.bridge.call('joinMUC', lambda room_jid: invite(room_jid, contacts), room_jid, nick)
+                contact_list = self.host.contact_list
+                if room_jid not in contact_list.getSpecials(C.CONTACT_SPECIAL_GROUP):
+                    self.host.bridge.call('joinMUC', lambda room_jid: invite(room_jid, contacts), unicode(room_jid), nick)
                 else:
-                    self.host.displayWidget(chat.Chat, room_jid, type_="group", new_tab=jid.JID(room_jid).bare)
+                    self.host.displayWidget(chat.Chat, room_jid, type_="group", new_tab=room_jid)
                     invite(room_jid, contacts)
 
         dialog.RoomAndContactsChooser(self.host, join, ok_button="Join", visible=(True, False))
 
     def onCollectiveRadio(self):
         def callback(room_jid, contacts):
-            self.host.bridge.call('launchRadioCollective', None, contacts, room_jid)
+            contacts = [unicode(contact) for contact in contacts]
+            self.host.bridge.call('launchRadioCollective', None, contacts, unicode(room_jid))
         dialog.RoomAndContactsChooser(self.host, callback, ok_button="Choose", title="Collective Radio", visible=(False, True))
 
     #Game menu
     def onTarotGame(self):
         def onPlayersSelected(room_jid, other_players):
-            self.host.bridge.call('launchTarotGame', None, other_players, room_jid)
+            other_players = [unicode(contact) for contact in other_players]
+            self.host.bridge.call('launchTarotGame', None, other_players, unicode(room_jid))
         dialog.RoomAndContactsChooser(self.host, onPlayersSelected, 3, title="Tarot", title_invite="Please select 3 other players", visible=(False, True))
 
     def onXiangqiGame(self):
--- a/src/browser/sat_browser/panels.py	Mon Feb 23 18:44:58 2015 +0100
+++ b/src/browser/sat_browser/panels.py	Mon Feb 23 18:47:27 2015 +0100
@@ -21,6 +21,7 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 
+from sat.core.i18n import _
 from sat_frontends.tools.strings import addURLToText
 
 from pyjamas.ui.AbsolutePanel import AbsolutePanel
@@ -509,9 +510,7 @@
 
         # tabs
         self.tab_panel = base_widget.MainTabPanel(host)
-        self.discuss_panel = base_widget.WidgetsPanel(self.host, locked=True)
-        self.tab_panel.add(self.discuss_panel, "Discussions")
-        self.tab_panel.selectTab(0)
+        self.tab_panel.addWidgetsTab(_(u"Discussions"), select=True, locked=True)
 
         self.header = AbsolutePanel()
         self.header.add(self.menu)
--- a/src/browser/sat_browser/plugin_sec_otr.py	Mon Feb 23 18:44:58 2015 +0100
+++ b/src/browser/sat_browser/plugin_sec_otr.py	Mon Feb 23 18:47:27 2015 +0100
@@ -264,20 +264,20 @@
         title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer)
         if type_ == 'question':
             if act == 'asked':
-                def cb(question, answer=None):
-                    if question is False or not answer:  # dialog cancelled or the answer is empty
+                def cb(result, question, answer=None):
+                    if not result or not answer:  # dialog cancelled or the answer is empty
                         return
                     self.smpAuthSecret(answer, question)
                 text = (AUTH_INFO_TXT + "<i>" + AUTH_QUEST_DEFINE_TXT + "</i>" + AUTH_QUEST_DEFINE).format(eol=DIALOG_EOL)
                 dialog.PromptDialog(cb, [text, AUTH_SECRET_INPUT.format(eol=DIALOG_EOL)], title=title, AddStyleName="maxWidthLimit").show()
             else:
-                def cb(answer):
-                    if not answer:  # dialog cancelled or the answer is empty
+                def cb(result, answer):
+                    if not result or not answer:  # dialog cancelled or the answer is empty
                         self.smpAuthAbort('answered')
                         return
                     self.smpAuthSecret(answer)
                 text = (AUTH_INFO_TXT + "<i>" + AUTH_QUEST_ANSWER_TXT + "</i>" + AUTH_QUEST_ANSWER).format(eol=DIALOG_EOL, question=data)
-                dialog.PromptDialog(cb, text + AUTH_SECRET_INPUT.format(eol=DIALOG_EOL), title=title, AddStyleName="maxWidthLimit").show()
+                dialog.PromptDialog(cb, [text + AUTH_SECRET_INPUT.format(eol=DIALOG_EOL)], title=title, AddStyleName="maxWidthLimit").show()
         elif type_ == 'trust':
             self.setCurrentTrust('smp' if data else '', act)
         elif type_ == 'abort':
@@ -344,7 +344,7 @@
         log.debug(u"getContextForUser [%s]" % other_jid)
         if not other_jid.resource:
             log.error("getContextForUser called with a bare jid")
-            running_sessions = [jid_.bareJID() for jid_ in self.contexts.keys() if self.contexts[jid_].state == otr.context.STATE_ENCRYPTED]
+            running_sessions = [jid_.bare for jid_ in self.contexts.keys() if self.contexts[jid_].state == otr.context.STATE_ENCRYPTED]
             if start or (other_jid in running_sessions):
                 users_ml = DIALOG_USERS_ML.format(subject=D_("OTR issue in Libervia: getContextForUser called with a bare jid in an encrypted context"))
                 text = RESOURCE_ISSUE.format(eol=DIALOG_EOL, jid=other_jid, users_ml=users_ml)
@@ -485,7 +485,7 @@
         return False  # interrupt the main process
 
     def presenceReceivedTrigger(self, entity, show, priority, statuses):
-        if show == "unavailable":
+        if show == C.PRESENCE_UNAVAILABLE:
             self.endSession(entity, finish=True)
         return True
 
@@ -542,7 +542,8 @@
 
         try:
             other_jid = menu_data['jid']
-            if other_jid.bare not in self.host.contact_panel.connected:
+            contact_list = self.host.contact_list
+            if contact_list.getCache(other_jid.bare, C.PRESENCE_SHOW) is None:
                 dialog.InfoDialog(ACTION_NA_TITLE, ACTION_NA, AddStyleName="maxWidthLimit").show()
                 return
             self.fixResource(other_jid, cb)
--- a/src/browser/sat_browser/richtext.py	Mon Feb 23 18:44:58 2015 +0100
+++ b/src/browser/sat_browser/richtext.py	Mon Feb 23 18:47:27 2015 +0100
@@ -442,7 +442,7 @@
             setText()
             return True
         if recipients is None:
-            recipients = self.recipient.getContacts()
+            recipients = self.recipient.getItemsByKey()
         target = ""
         # we could eventually allow more in the future
         allowed = 1
@@ -487,7 +487,7 @@
 
     def __sendMessage(self):
         """Send the message."""
-        recipients = self.recipient.getContacts()
+        recipients = self.recipient.getItemsByKey()
         targets = []
         for addr in recipients:
             for recipient in recipients[addr]:
@@ -518,7 +518,7 @@
         list_ = []
         list_.append("@@")
         list_.extend("@%s" % group for group in parent.host.contact_panel.getGroups())
-        list_.extend(contact for contact in parent.host.contact_panel.getContacts())
+        list_.extend(contact for contact in parent.host.contact_list.roster_entities)
         list_manager.ListManager.__init__(self, parent, composition.RECIPIENT_TYPES, list_, {'y': y_offset})
 
         self.registerPopupMenuPanel(entries=composition.RECIPIENT_TYPES,
--- a/src/common/constants.py	Mon Feb 23 18:44:58 2015 +0100
+++ b/src/common/constants.py	Mon Feb 23 18:47:27 2015 +0100
@@ -18,7 +18,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from sat.core.i18n import D_
-from sat_frontends import constants
+from sat_frontends.quick_frontend import constants
 import os.path
 
 
--- a/src/server/server.py	Mon Feb 23 18:44:58 2015 +0100
+++ b/src/server/server.py	Mon Feb 23 18:47:27 2015 +0100
@@ -212,13 +212,11 @@
         profile = ISATSession(self.session).profile
         self.sat_host.bridge.updateContact(entity, name, groups, profile)
 
-    def jsonrpc_subscription(self, sub_type, entity, name, groups):
+    def jsonrpc_subscription(self, sub_type, entity):
         """Confirm (or infirm) subscription,
         and setup user roster in case of subscription"""
         profile = ISATSession(self.session).profile
         self.sat_host.bridge.subscription(sub_type, entity, profile)
-        if sub_type == 'subscribed':
-            self.sat_host.bridge.updateContact(entity, name, groups, profile)
 
     def jsonrpc_getWaitingSub(self):
         """Return list of room already joined by user"""