changeset 1963:a2bc5089c2eb

backend, frontends: message refactoring (huge commit): /!\ several features are temporarily disabled, like notifications in frontends next step in refactoring, with the following changes: - jp: updated jp message to follow changes in backend/bridge - jp: added --lang, --subject, --subject_lang, and --type options to jp message + fixed unicode handling for jid - quick_frontend (QuickApp, QuickChat): - follow backend changes - refactored chat, message are now handled in OrderedDict and uid are kept so they can be updated - Message and Occupant classes handle metadata, so frontend just have to display them - Primitivus (Chat): - follow backend/QuickFrontend changes - info & standard messages are handled in the same MessageWidget class - improved/simplified handling of messages, removed update() method - user joined/left messages are merged when next to each other - a separator is shown when message is received while widget is out of focus, so user can quickly see the new messages - affiliation/role are shown (in a basic way for now) in occupants panel - removed "/me" messages handling, as it will be done by a backend plugin - message language is displayed when available (only one language per message for now) - fixed :history and :search commands - core (constants): new constants for messages type, XML namespace, entity type - core: *Message methods renamed to follow new code sytle (e.g. sendMessageToBridge => messageSendToBridge) - core (messages handling): fixed handling of language - core (messages handling): mes_data['from'] and ['to'] are now jid.JID - core (core.xmpp): reorganised message methods, added getNick() method to client.roster - plugin text commands: fixed plugin and adapted to new messages behaviour. client is now used in arguments instead of profile - plugins: added information for cancellation reason in CancelError calls - plugin XEP-0045: various improvments, but this plugin still need work: - trigger is used to avoid message already handled by the plugin to be handled a second time - changed the way to handle history, the last message from DB is checked and we request only messages since this one, in seconds (thanks Poezio folks :)) - subject reception is waited before sending the roomJoined signal, this way we are sure that everything including history is ready - cmd_* method now follow the new convention with client instead of profile - roomUserJoined and roomUserLeft messages are removed, the events are now handled with info message with a "ROOM_USER_JOINED" info subtype - probably other forgotten stuffs :p
author Goffi <goffi@goffi.org>
date Mon, 20 Jun 2016 18:41:53 +0200
parents a45235d8dc93
children a86e41d9245d
files frontends/src/bridge/DBus.py frontends/src/jp/cmd_message.py frontends/src/primitivus/chat.py frontends/src/primitivus/constants.py frontends/src/primitivus/primitivus frontends/src/quick_frontend/quick_app.py frontends/src/quick_frontend/quick_chat.py src/bridge/DBus.py src/bridge/bridge_constructor/bridge_template.ini src/core/constants.py src/core/sat_main.py src/core/xmpp.py src/memory/memory.py src/plugins/plugin_exp_command_export.py src/plugins/plugin_exp_parrot.py src/plugins/plugin_misc_text_commands.py src/plugins/plugin_sec_otr.py src/plugins/plugin_xep_0033.py src/plugins/plugin_xep_0045.py src/plugins/plugin_xep_0048.py src/plugins/plugin_xep_0071.py src/plugins/plugin_xep_0092.py src/plugins/plugin_xep_0249.py
diffstat 23 files changed, 1011 insertions(+), 622 deletions(-) [+]
line wrap: on
line diff
--- a/frontends/src/bridge/DBus.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/frontends/src/bridge/DBus.py	Mon Jun 20 18:41:53 2016 +0200
@@ -554,7 +554,7 @@
             kwargs['error_handler'] = error_handler
         return self.db_core_iface.loadParamsTemplate(filename, **kwargs)
 
-    def messageSend(self, to_jid, message, subject='', mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None):
+    def messageSend(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None):
         if callback is None:
             error_handler = None
         else:
--- a/frontends/src/jp/cmd_message.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/frontends/src/jp/cmd_message.py	Mon Jun 20 18:41:53 2016 +0200
@@ -20,6 +20,7 @@
 from sat_frontends.jp import base
 import sys
 from sat.core.i18n import _
+from sat.core.constants import Const as C
 from sat.tools.utils import clean_ustr
 
 __commands__ = ["Message"]
@@ -31,9 +32,13 @@
         super(Send, self).__init__(host, 'send', help=_('send a message to a contact'))
 
     def add_parser_options(self):
+        self.parser.add_argument("-l", "--lang", type=str, default='', help=_("language of the message"))
         self.parser.add_argument("-s", "--separate", action="store_true", help=_("separate xmpp messages: send one message per line instead of one message alone."))
         self.parser.add_argument("-n", "--new-line", action="store_true", help=_("add a new line at the beginning of the input (usefull for ascii art ;))"))
-        self.parser.add_argument("jid", type=str, help=_("the destination jid"))
+        self.parser.add_argument("-S", "--subject", type=base.unicode_decoder, help=_("subject of the message"))
+        self.parser.add_argument("-L", "--subject_lang", type=str, default='', help=_("language of subject"))
+        self.parser.add_argument("-t", "--type", choices=C.MESS_TYPE_STANDARD + (C.MESS_TYPE_AUTO,), default=C.MESS_TYPE_AUTO, help=_("type of the message"))
+        self.parser.add_argument("jid", type=base.unicode_decoder, help=_("the destination jid"))
 
     def start(self):
         jids = self.host.check_jids([self.args.jid])
@@ -46,21 +51,25 @@
         @param dest_jid: destination jid
         """
         header = "\n" if self.args.new_line else ""
+        if self.args.subject is None:
+            subject = {}
+        else:
+            subject = {self.args.subject_lang: self.args.subject}
 
         if self.args.separate:  #we send stdin in several messages
 
             if header:
-                self.host.bridge.sendMessage(dest_jid, header, profile_key=self.profile, callback=lambda: None, errback=lambda ignore: ignore)
+                self.host.bridge.messageSend(dest_jid, {self.args.lang: header}, subject, self.args.type, profile_key=self.profile, callback=lambda: None, errback=lambda ignore: ignore)
 
             while (True):
                 line = clean_ustr(sys.stdin.readline().decode('utf-8','ignore'))
                 if not line:
                     break
-                self.host.bridge.sendMessage(dest_jid, line.replace("\n",""), profile_key=self.host.profile, callback=lambda: None, errback=lambda ignore: ignore)
+                self.host.bridge.messageSend(dest_jid, {self.args.lang: line.replace("\n","")}, subject, self.args.type, profile_key=self.host.profile, callback=lambda: None, errback=lambda ignore: ignore)
 
         else:
             msg = header + clean_ustr(u"".join([stream.decode('utf-8','ignore') for stream in sys.stdin.readlines()]))
-            self.host.bridge.sendMessage(dest_jid, msg, profile_key=self.host.profile, callback=lambda: None, errback=lambda ignore: ignore)
+            self.host.bridge.messageSend(dest_jid, {self.args.lang: msg}, subject, self.args.type, profile_key=self.host.profile, callback=lambda: None, errback=lambda ignore: ignore)
 
 
 class Message(base.CommandBase):
--- a/frontends/src/primitivus/chat.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/frontends/src/primitivus/chat.py	Mon Jun 20 18:41:53 2016 +0200
@@ -23,7 +23,7 @@
 import urwid
 from urwid_satext import sat_widgets
 from sat_frontends.quick_frontend import quick_widgets
-from sat_frontends.quick_frontend.quick_chat import QuickChat
+from sat_frontends.quick_frontend import quick_chat
 from sat_frontends.quick_frontend import quick_games
 from sat_frontends.primitivus import game_tarot
 from sat_frontends.primitivus.constants import Const as C
@@ -31,19 +31,63 @@
 from sat_frontends.primitivus.widget import PrimitivusWidget
 import time
 from sat_frontends.tools import jid
+from functools import total_ordering
+import bisect
 
 
-class ChatText(urwid.FlowWidget):
-    """Manage the printing of chat message"""
+class MessageWidget(urwid.WidgetWrap):
+
+    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)
+        self.timestamp = time.localtime(mess_data.timestamp)
+        super(MessageWidget, self).__init__(urwid.Text(self.markup))
+
+    @property
+    def markup(self):
+        return self._generateInfoMarkup() if self.mess_data.type == C.MESS_TYPE_INFO else self._generateMarkup()
+
+    @property
+    def info_type(self):
+        return self.mess_data.info_type
+
+    @property
+    def parent(self):
+        return self.mess_data.parent
 
-    def __init__(self, parent, timestamp, nick, my_mess, message, align='left', is_info=False):
-        self.parent = parent
-        self.timestamp = time.localtime(timestamp)
-        self.nick = nick
-        self.my_mess = my_mess
-        self.message = unicode(message)
-        self.align = align
-        self.is_info = is_info
+    @property
+    def message(self):
+        """Return currently displayed message"""
+        message = self.mess_data.message
+        if self.parent.lang in message:
+            self.selected_lang = self.parent.lang
+            return message[self.parent.lang]
+        try:
+            self.selected_lang = ''
+            return message['']
+        except KeyError:
+            try:
+                lang, mess = message.iteritems().next()
+                self.selected_lang = lang
+                return mess
+            except StopIteration:
+                log.error(u"Can't find message for uid {}".format(self.mess_data.uid))
+
+    @message.setter
+    def message(self, value):
+        self.mess_data.message = {'':value}
+        self._w.set_text(self.markup)
+
+    @property
+    def type(self):
+        try:
+            return self.mess_data.type
+        except AttributeError:
+            return C.MESS_TYPE_INFO
 
     def selectable(self):
         return True
@@ -51,42 +95,103 @@
     def keypress(self, size, key):
         return key
 
-    def rows(self, size, focus=False):
-        return self.display_widget(size, focus).rows(size, focus)
+    def get_cursor_coords(self, size):
+        return 0, 0
 
     def render(self, size, focus=False):
-        canvas = urwid.CompositeCanvas(self.display_widget(size, focus).render(size, focus))
+        # Text widget doesn't render cursor, but we want one
+        # so we add it here
+        canvas = urwid.CompositeCanvas(self._w.render(size, focus))
         if focus:
             canvas.set_cursor(self.get_cursor_coords(size))
         return canvas
 
+    def _generateInfoMarkup(self):
+        return ('info_msg', self.message)
+
+    def _generateMarkup(self):
+        """Generate text markup according to message data and Widget options"""
+        markup = []
+        d = self.mess_data
+
+        # timestamp
+        if self.parent.show_timestamp:
+            # if the message was sent before today, we print the full date
+            time_format = "%c" if self.timestamp < self.parent.day_change else "%H:%M"
+            markup.append(('date', "[{}]".format(time.strftime(time_format, self.timestamp).decode('utf-8'))))
+
+        # nickname
+        if self.parent.show_short_nick:
+            markup.append(('my_nick' if d.own_mess else 'other_nick', "**" if d.own_mess else "*"))
+        else:
+            markup.append(('my_nick' if d.own_mess else 'other_nick', u"[{}] ".format(d.nick or '')))
+
+        msg = self.message  # needed to generate self.selected_lang
+
+        if self.selected_lang:
+            markup.append(("msg_lang", u"[{}] ".format(self.selected_lang)))
+
+        # message body
+        markup.append(msg)
+
+        return markup
+
+@total_ordering
+class OccupantWidget(urwid.WidgetWrap):
+
+    def __init__(self, occupant_data):
+        self.occupant_data = occupant_data
+        occupant_data.widgets.add(self)
+        markup = self._generateMarkup()
+        super(OccupantWidget, self).__init__(urwid.Text(markup))
+
+    def __eq__(self, other):
+        return self.occupant_data.nick == other.occupant_data.nick
+
+    def __lt__(self, other):
+        return self.occupant_data.nick.lower() < other.occupant_data.nick.lower()
+
+    @property
+    def parent(self):
+        return self.mess_data.parent
+
+    def selectable(self):
+        return True
+
+    def keypress(self, size, key):
+        return key
+
     def get_cursor_coords(self, size):
         return 0, 0
 
-    def display_widget(self, size, focus):
-        render_txt = []
-        if not self.is_info:
-            if self.parent.show_timestamp:
-                time_format = "%c" if self.timestamp < self.parent.day_change else "%H:%M"  # if the message was sent before today, we print the full date
-                render_txt.append(('date', "[%s]" % time.strftime(time_format, self.timestamp).decode('utf-8')))
-            if self.parent.show_short_nick:
-                render_txt.append(('my_nick' if self.my_mess else 'other_nick', "**" if self.my_mess else "*"))
-            else:
-                render_txt.append(('my_nick' if self.my_mess else 'other_nick', "[%s] " % (self.nick or '')))
-        render_txt.append(self.message)
-        txt_widget = urwid.Text(render_txt, align=self.align)
-        if self.is_info:
-            return urwid.AttrMap(txt_widget, 'info_msg')
-        return txt_widget
+    def render(self, size, focus=False):
+        # Text widget doesn't render cursor, but we want one
+        # so we add it here
+        canvas = urwid.CompositeCanvas(self._w.render(size, focus))
+        if focus:
+            canvas.set_cursor(self.get_cursor_coords(size))
+        return canvas
+
+    def _generateMarkup(self):
+        # TODO: role and affiliation are shown in a Q&D way
+        #       should be more intuitive and themable
+        o = self.occupant_data
+        markup = []
+        markup.append(('info_msg', '{}{} '.format(
+            o.role[0].upper(),
+            o.affiliation[0].upper(),
+            )))
+        markup.append(o.nick)
+        return markup
 
 
-class Chat(PrimitivusWidget, QuickChat):
+class Chat(PrimitivusWidget, quick_chat.QuickChat):
 
-    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
-        QuickChat.__init__(self, host, target, type_, profiles=profiles)
-        self.content = urwid.SimpleListWalker([])
-        self.text_list = urwid.ListBox(self.content)
-        self.chat_widget = urwid.Frame(self.text_list)
+    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, occupants=None, subject=None, profiles=None):
+        quick_chat.QuickChat.__init__(self, host, target, type_, occupants, subject, profiles=profiles)
+        self.mess_walker = urwid.SimpleListWalker([])
+        self.mess_widgets = urwid.ListBox(self.mess_walker)
+        self.chat_widget = urwid.Frame(self.mess_widgets)
         self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)])
         self.pile = urwid.Pile([self.chat_colums])
         PrimitivusWidget.__init__(self, self.pile, self.target)
@@ -94,16 +199,28 @@
         # we must adapt the behaviour with the type
         if type_ == C.CHAT_GROUP:
             if len(self.chat_colums.contents) == 1:
-                self.occupants_list = sat_widgets.GenericList([], option_type=sat_widgets.ClickableText, on_click=self._occupantsClicked)
-                self.occupants_panel = sat_widgets.VerticalSeparator(self.occupants_list)
+                self.occupants_walker = urwid.SimpleListWalker([])
+                # TODO: put a real ContactPanel class here, based on FocusWidget ?
+                self.occupants_widgets = urwid.ListBox(self.occupants_walker)
+                # FIXME
+                # , option_type=sat_widgets.ClickableText, on_click=self._occupantsClicked)
+                self.occupants_panel = sat_widgets.VerticalSeparator(self.occupants_widgets)
                 self._appendOccupantsPanel()
+                occupants_list = sorted(self.occupants.keys(), key=lambda o:o.lower())
+                for occupant in occupants_list:
+                    occupant_data = self.occupants[occupant]
+                    self.occupants_walker.append(OccupantWidget(occupant_data))
+
                 self.host.addListener('presence', self.presenceListener, [profiles])
 
+        # focus marker is a separator indicated last visible message before focus was lost
+        self.focus_marker = None  # link to current marker
+        self.focus_marker_set = None  # True if a new marker has been inserted
         self.day_change = time.strptime(time.strftime("%a %b %d 00:00:00  %Y"))  # struct_time of day changing time
         self.show_timestamp = True
         self.show_short_nick = False
         self.show_title = 1  # 0: clip title; 1: full title; 2: no title
-        self.subject = None
+        self.postInit()
 
     def keypress(self, size, key):
         if key == a_key['OCCUPANTS_HIDE']:  # user wants to (un)hide the occupants panel
@@ -115,11 +232,11 @@
                     self._appendOccupantsPanel()
         elif key == a_key['TIMESTAMP_HIDE']:  # user wants to (un)hide timestamp
             self.show_timestamp = not self.show_timestamp
-            for wid in self.content:
+            for wid in self.mess_walker:
                 wid._invalidate()
         elif key == a_key['SHORT_NICKNAME']:  # user wants to (not) use short nick
             self.show_short_nick = not self.show_short_nick
-            for wid in self.content:
+            for wid in self.mess_walker:
                 wid._invalidate()
         elif key == a_key['SUBJECT_SWITCH']:  # user wants to (un)hide group's subject or change its apperance
             if self.subject:
@@ -160,76 +277,87 @@
         @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)
+        # FIXME: disable for refactoring, need to be checked and re-enabled
+        return
+        # assert self.type == C.CHAT_GROUP
+        # if entity.bare != self.target:
+        #     return
+        # self.update(entity)
 
-    def update(self, entity=None):
-        """Update one or all entities.
+    def createMessage(self, message):
+        self.appendMessage(message)
+
+    def _user_moved(self, message):
+        """return true if message is a user left/joined message
 
-        @param entity (jid.JID): entity to update
+        @param message(quick_chat.Message): message to add
         """
-        contact_list = self.host.contact_lists[self.profile]
+        if message.type != C.MESS_TYPE_INFO:
+            return False
+        try:
+            info_type = message.extra['info_type']
+        except KeyError:
+            return False
+        else:
+            return info_type in quick_chat.ROOM_USER_MOVED
 
-        if self.type == C.CHAT_ONE2ONE:  # only update the chat title
-            states = self.getEntityStates(self.target)
-            self.title_dynamic = ' '.join([u'({})'.format(state) for state in states.values()])
-            self.host.redraw()
-            return
+    def appendMessage(self, message):
+        """Create a MessageWidget and append it
 
-        nicks = list(self.occupants)
-        if entity is None:  # rebuild all the occupants list
-            values = []
-            nicks.sort()
-            for nick in nicks:
-                values.append(self._buildOccupantMarkup(jid.newResource(self.target, nick)))
-            self.occupants_list.changeValues(values)
-        else:  # add, remove or update only one occupant
-            nick = entity.resource
-            show = contact_list.getCache(entity, C.PRESENCE_SHOW)
-            if show == C.PRESENCE_UNAVAILABLE or show is None:
-                try:
-                    self.occupants_list.deleteValue(nick)
-                except ValueError:
-                    pass
-            else:
-                values = self.occupants_list.getAllValues()
-                markup = self._buildOccupantMarkup(entity)
-                if not values:  # room has just been created
-                    values = [markup]
-                else:  # add or update the occupant, keep the list sorted
-                    index = 0
-                    for entry in values:
-                        order = cmp(entry.value if hasattr(entry, 'value') else entry, nick)
-                        if order < 0:
-                            index += 1
-                            continue
-                        if order > 0:  # insert the occupant
-                            values.insert(index, markup)
-                        else:  # update an existing occupant
-                            values[index] = markup
-                        break
-                    if index == len(values):  # add to the end of the list
-                        values.append(markup)
-                self.occupants_list.changeValues(values)
-        self.host.redraw()
+        Can merge messages together is desirable (e.g.: multiple joined/leave)
+        @param message(quick_chat.Message): message to add
+        """
+        if self._user_moved(message):
+            for wid in reversed(self.mess_walker):
+                # we merge in/out messages if no message was sent meanwhile
+                if not isinstance(wid, MessageWidget):
+                    continue
+                if wid.mess_data.type != C.MESS_TYPE_INFO:
+                    break
+                if wid.info_type in quick_chat.ROOM_USER_MOVED and wid.mess_data.nick == message.nick:
+                    try:
+                        count = wid.reentered_count
+                    except AttributeError:
+                        count = wid.reentered_count = 1
+                    nick = wid.mess_data.nick
+                    if message.info_type == quick_chat.ROOM_USER_LEFT:
+                        wid.message = _(u"<= {nick} has left the room ({count})").format(nick=nick, count=count)
+                    else:
+                        wid.message = _(u"<=> {nick} re-entered the room ({count})") .format(nick=nick, count=count)
+                        wid.reentered_count+=1
+                    return
 
-    def _buildOccupantMarkup(self, entity):
-        """Return the option attributes for a MUC occupant.
+        if ((self.host.selected_widget != self or not self.host.x_notify.hasFocus())
+            and self.focus_marker_set is not None):
+            if not self.focus_marker_set and not self._locked and self.mess_walker:
+                if self.focus_marker is not None:
+                    self.mess_walker.remove(self.focus_marker)
+                self.focus_marker = urwid.Divider('—')
+                self.mess_walker.append(self.focus_marker)
+                self.focus_marker_set = True
+        else:
+            if self.focus_marker_set:
+                self.focus_marker_set = False
 
-        @param nick (unicode): occupant nickname
-        """
-        # TODO: for now it's not a markup but a simple text, the problem is that ListOption is unicode and not urwid.Text
-        contact_list = self.host.contact_lists[self.profile]
-        show = contact_list.getCache(entity, C.PRESENCE_SHOW)
-        states = self.getEntityStates(entity)
-        nick = entity.resource
-        show_icon, entity_attr = C.PRESENCE.get(show, (u'', u'default'))  # TODO: use entity_attr and return (nick, markup)
-        text = "%s%s %s" % (u''.join(states.values()), show_icon, nick)
-        return (nick, text)
+        if not message.message:
+            log.error(u"Received an empty message for uid {}".format(message.uid))
+        else:
+            self.mess_walker.append(MessageWidget(message))
+            self.mess_widgets.focus_position = len(self.mess_walker) - 1  # scroll down
+            self.host.redraw()  # FIXME: should not be necessary
+
+    def addUser(self, nick):
+        occupant = super(Chat, self).addUser(nick)
+        bisect.insort(self.occupants_walker, OccupantWidget(occupant))
+
+    def removeUser(self, occupant_data):
+        occupant = super(Chat, self).removeUser(occupant_data)
+        if occupant is not None:
+            for widget in occupant.widgets:
+                self.occupants_walker.remove(widget)
 
     def _occupantsClicked(self, list_wid, clicked_wid):
+        # FIXME: not called anymore after refactoring
         assert self.type == C.CHAT_GROUP
         nick = clicked_wid.getValue().value
         if nick == self.nick:
@@ -274,55 +402,32 @@
 
     def setSubject(self, subject, wrap='space'):
         """Set title for a group chat"""
-        QuickChat.setSubject(self, subject)
-        self.subject = subject
+        quick_chat.QuickChat.setSubject(self, subject)
         self.subj_wid = urwid.Text(unicode(subject.replace('\n', '|') if wrap == 'clip' else subject),
                                    align='left' if wrap == 'clip' else 'center', wrap=wrap)
         self.chat_widget.header = urwid.AttrMap(self.subj_wid, 'title')
         self.host.redraw()
 
-    def clearHistory(self):
-        """Clear the content of this chat."""
-        del self.content[:]
+    ## Messages
 
-    def afterHistoryPrint(self):
+    def updateHistory(self, size=C.HISTORY_LIMIT_DEFAULT, search='', profile='@NONE@'):
+        del self.mess_walker[:]
+        if search:
+            self.mess_walker.append(urwid.Text(_(u"Results for searching the globbing pattern: {}").format(search)))
+            self.mess_walker.append(urwid.Text(_(u"Type ':history <lines>' to reset the chat history").format(search)))
+        super(Chat, self).updateHistory(size, search, profile)
+
+    def _onHistoryPrinted(self):
         """Refresh or scroll down the focus after the history is printed"""
-        if len(self.content):
-            self.text_list.focus_position = len(self.content) - 1  # scroll down
-        self.host.redraw()
+        for message in self.messages.itervalues():
+            self.appendMessage(message)
+        super(Chat, self)._onHistoryPrinted()
 
     def onPrivateCreated(self, widget):
         self.host.contact_lists[widget.profile].specialResourceVisible(widget.target)
 
-    def printMessage(self, nick, my_message, message, timestamp, extra=None, profile=C.PROF_KEY_NONE):
-        """Print message in chat window.
-
-        @param nick (unicode): author nick
-        @param my_message (boolean): True if profile is the author
-        @param message (unicode): message content
-        @param extra (dict): extra data
-        """
-        new_text = ChatText(self, timestamp, nick, my_message, message)
-        self.content.append(new_text)
-        QuickChat.printMessage(self, nick, my_message, message, timestamp, extra, profile)
-
-    def printInfo(self, msg, type_='normal', extra=None):
-        """Print general info
-        @param msg: message to print
-        @type_: one of:
-            normal: general info like "toto has joined the room"
-            me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist"
-        @param timestamp (float): number of seconds since epoch
-        """
-        if extra is None:
-            extra = {}
-        try:
-            timestamp = float(extra['timestamp'])
-        except KeyError:
-            timestamp = None
-        _widget = ChatText(self, timestamp, None, False, msg, is_info=True)
-        self.content.append(_widget)
-        QuickChat.printInfo(self, msg, type_, extra)
+    def onSelected(self):
+        self.focus_marker_set = False
 
     def notify(self, contact="somebody", msg=""):
         """Notify the user of a new message if primitivus doesn't have the focus.
@@ -330,12 +435,13 @@
         @param contact (unicode): contact who wrote to the users
         @param msg (unicode): the message that has been received
         """
+        # FIXME: not called anymore after refactoring
         if msg == "":
             return
-        if self.text_list.get_focus()[1] == len(self.content) - 2:
+        if self.mess_widgets.get_focus()[1] == len(self.mess_walker) - 2:
             # we don't change focus if user is not at the bottom
             # as that mean that he is probably watching discussion history
-            self.text_list.focus_position = len(self.content) - 1
+            self.mess_widgets.focus_position = len(self.mess_walker) - 1
         self.host.redraw()
         if not self.host.x_notify.hasFocus():
             if self.type == C.CHAT_ONE2ONE:
@@ -354,10 +460,11 @@
     # MISC EVENTS #
 
     def onDelete(self):
-        QuickChat.onDelete(self)
+        # FIXME: to be checked after refactoring
+        quick_chat.QuickChat.onDelete(self)
         if self.type == C.CHAT_GROUP:
             self.host.removeListener('presence', self.presenceListener)
 
 
-quick_widgets.register(QuickChat, Chat)
+quick_widgets.register(quick_chat.QuickChat, Chat)
 quick_widgets.register(quick_games.Tarot, game_tarot.TarotGame)
--- a/frontends/src/primitivus/constants.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/frontends/src/primitivus/constants.py	Mon Jun 20 18:41:53 2016 +0200
@@ -32,10 +32,13 @@
                ('default_focus', 'default,bold', 'default'),
                ('alert', 'default,underline', 'default'),
                ('alert_focus', 'default,bold,underline', 'default'),
+               # Messages
                ('date', 'light gray', 'default'),
                ('my_nick', 'dark red,bold', 'default'),
                ('other_nick', 'dark cyan,bold', 'default'),
                ('info_msg', 'yellow', 'default', 'bold'),
+               ('msg_lang', 'dark cyan', 'default'),
+
                ('menubar', 'light gray,bold', 'dark red'),
                ('menubar_focus', 'light gray,bold', 'dark green'),
                ('selected_menu', 'light gray,bold', 'dark green'),
--- a/frontends/src/primitivus/primitivus	Sun Jun 19 22:22:13 2016 +0200
+++ b/frontends/src/primitivus/primitivus	Mon Jun 20 18:41:53 2016 +0200
@@ -99,7 +99,7 @@
                 self.host.messageSend(
                     chat_widget.target,
                     {'': editBar.get_edit_text()}, # TODO: handle language
-                    mess_type = "groupchat" if chat_widget.type == 'group' else "chat", # TODO: put this in QuickChat
+                    mess_type = C.MESS_TYPE_GROUPCHAT if chat_widget.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, # TODO: put this in QuickChat
                     errback=lambda failure: self.host.notify(_("Error while sending message ({})").format(failure)),
                     profile_key=chat_widget.profile
                     )
@@ -139,19 +139,15 @@
                     limit = int(args[0])
                 except (IndexError, ValueError):
                     limit = 50
-                widget.clearHistory()
-                if limit > 0:
-                    widget.historyPrint(size=limit, profile=widget.profile)
+                widget.updateHistory(size=limit, profile=widget.profile)
         elif command == 'search':
             widget = self.host.selected_widget
             if isinstance(widget, quick_chat.QuickChat):
                 pattern = " ".join(args)
                 if not pattern:
                     self.host.notif_bar.addMessage(D_("Please specify the globbing pattern to search for"))
-                widget.clearHistory()
-                widget.printInfo(D_("Results for searching the globbing pattern: %s") % pattern, extra={'timestamp': 0})
-                widget.historyPrint(size=C.HISTORY_LIMIT_NONE, search=pattern, profile=widget.profile)
-                widget.printInfo(D_("Type ':history <lines>' to reset the chat history"))
+                else:
+                    widget.updateHistory(size=C.HISTORY_LIMIT_NONE, search=pattern, profile=widget.profile)
         else:
             return
         self.set_edit_text('')
@@ -540,7 +536,8 @@
         self.redraw()
 
     def newWidget(self, widget):
-        self.selectWidget(widget)
+        if self.selected_widget is None:
+            self.selectWidget(widget)
 
     def selectWidget(self, widget):
         """Display a widget if possible,
@@ -556,6 +553,12 @@
         except KeyError:
             log.debug("No menu to delete")
         self.selected_widget = widget
+        try:
+            onSelected = self.selected_widget.onSelected
+        except AttributeError:
+            pass
+        else:
+            onSelected()
         self._visible_widgets = set([widget]) # XXX: we can only have one widget visible at the time for now
         self.contact_lists.select(None)
 
@@ -722,11 +725,12 @@
             log.error (_("FIXME FIXME FIXME: type [%s] not implemented") % type_)
             raise NotImplementedError
 
-    def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, profile):
-        super(PrimitivusApp, self).roomJoinedHandler(room_jid_s, room_nicks, user_nick, profile)
-        for contact_list in self.widgets.getWidgets(ContactList):
-            if profile in contact_list.profiles:
-                contact_list.setFocus(jid.JID(room_jid_s), True)
+    def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, subject, profile):
+        super(PrimitivusApp, self).roomJoinedHandler(room_jid_s, room_nicks, user_nick, subject, profile)
+        # if self.selected_widget is None:
+        #     for contact_list in self.widgets.getWidgets(ContactList):
+        #         if profile in contact_list.profiles:
+        #             contact_list.setFocus(jid.JID(room_jid_s), True)
 
     def progressStartedHandler(self, pid, metadata, profile):
         super(PrimitivusApp, self).progressStartedHandler(pid, metadata, profile)
@@ -745,7 +749,7 @@
     def onJoinRoom(self, button, edit):
         self.removePopUp()
         room_jid = jid.JID(edit.get_edit_text())
-        self.bridge.joinMUC(room_jid, self.profiles[self.current_profile].whoami.node, {}, self.current_profile, callback=lambda dummy: None, errback=self.dialogFailure)
+        self.bridge.mucJoin(room_jid, self.profiles[self.current_profile].whoami.node, {}, self.current_profile, callback=lambda dummy: None, errback=self.dialogFailure)
 
     #MENU EVENTS#
     def onConnectRequest(self, menu):
--- a/frontends/src/quick_frontend/quick_app.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/frontends/src/quick_frontend/quick_app.py	Mon Jun 20 18:41:53 2016 +0200
@@ -122,13 +122,6 @@
         #Now we open the MUC window where we already are:
         for room_args in rooms_args:
             self.host.roomJoinedHandler(*room_args, profile=self.profile)
-
-        self.bridge.getRoomsSubjects(self.profile, callback=self._plug_profile_gotRoomsSubjects)
-
-    def _plug_profile_gotRoomsSubjects(self, subjects_args):
-        for subject_args in subjects_args:
-            self.host.roomNewSubjectHandler(*subject_args, profile=self.profile)
-
         #Presence must be requested after rooms are filled
         self.host.bridge.getPresenceStatuses(self.profile, callback=self._plug_profile_gotPresences)
 
@@ -263,8 +256,6 @@
         self.registerSignal("actionResultExt", self.actionResultHandler)
         self.registerSignal("roomJoined", iface="plugin")
         self.registerSignal("roomLeft", iface="plugin")
-        self.registerSignal("roomUserJoined", iface="plugin")
-        self.registerSignal("roomUserLeft", iface="plugin")
         self.registerSignal("roomUserChangedNick", iface="plugin")
         self.registerSignal("roomNewSubject", iface="plugin")
         self.registerSignal("chatStateReceived", iface="plugin")
@@ -490,11 +481,8 @@
 
         from_me = from_jid.bare == self.profiles[profile].whoami.bare
         target = to_jid if from_me else from_jid
-
-        chat_type = C.CHAT_GROUP if type_ == C.MESS_TYPE_GROUPCHAT else C.CHAT_ONE2ONE
         contact_list = self.contact_lists[profile]
-
-        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, target, type_=chat_type, on_new_widget=None, profile=profile)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, target, type_=C.CHAT_ONE2ONE, on_new_widget=None, profile=profile)
 
         self.current_action_ids = set() # FIXME: to be removed
         self.current_action_ids_cb = {} # FIXME: to be removed
@@ -560,14 +548,14 @@
 
         self.callListeners('presence', entity, show, priority, statuses, profile=profile)
 
-    def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, profile):
+    def roomJoinedHandler(self, room_jid_s, occupants, user_nick, subject, profile):
         """Called when a MUC room is joined"""
-        log.debug(u"Room [%(room_jid)s] joined by %(profile)s, users presents:%(users)s" % {'room_jid': room_jid_s, 'profile': profile, 'users': room_nicks})
+        log.debug(u"Room [{room_jid}] joined by {profile}, users presents:{users}".format(room_jid=room_jid_s, profile=profile, users=occupants.keys()))
         room_jid = jid.JID(room_jid_s)
-        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, occupants=occupants, subject=subject, profile=profile)
         chat_widget.setUserNick(unicode(user_nick))
         self.contact_lists[profile].setSpecial(room_jid, C.CONTACT_SPECIAL_GROUP)
-        chat_widget.update()
+        # chat_widget.update()
 
     def roomLeftHandler(self, room_jid_s, profile):
         """Called when a MUC room is left"""
@@ -578,20 +566,6 @@
             self.widgets.deleteWidget(chat_widget)
         self.contact_lists[profile].removeContact(room_jid)
 
-    def roomUserJoinedHandler(self, room_jid_s, user_nick, user_data, profile):
-        """Called when an user joined a MUC room"""
-        room_jid = jid.JID(room_jid_s)
-        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
-        chat_widget.addUser(user_nick)
-        log.debug(u"user [%(user_nick)s] joined room [%(room_jid)s]" % {'user_nick': user_nick, 'room_jid': room_jid})
-
-    def roomUserLeftHandler(self, room_jid_s, user_nick, user_data, profile):
-        """Called when an user joined a MUC room"""
-        room_jid = jid.JID(room_jid_s)
-        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
-        chat_widget.removeUser(user_nick)
-        log.debug(u"user [%(user_nick)s] left room [%(room_jid)s]" % {'user_nick': user_nick, 'room_jid': room_jid})
-
     def roomUserChangedNickHandler(self, room_jid_s, old_nick, new_nick, profile):
         """Called when an user joined a MUC room"""
         room_jid = jid.JID(room_jid_s)
@@ -613,20 +587,20 @@
         @param state (unicode): new state
         @param profile (unicode): current profile
         """
-        log.debug(_(u"Received new chat state {} from {} [{}]").format(state, from_jid_s, profile))
-        from_jid = jid.JID(from_jid_s) if from_jid_s != C.ENTITY_ALL else C.ENTITY_ALL
-        contact_list = self.contact_lists[profile]
-        for widget in self.widgets.getWidgets(quick_chat.QuickChat):
-            if profile != widget.profile:
-                continue
-            to_display = C.USER_CHAT_STATES[state] if (state and widget.type == C.CHAT_GROUP) else state
-            if widget.type == C.CHAT_GROUP and from_jid_s == C.ENTITY_ALL:
-                for occupant in [jid.newResource(widget.target, nick) for nick in widget.occupants]:
-                    contact_list.setCache(occupant, 'chat_state', to_display)
-                    widget.update(occupant)
-            elif from_jid.bare == widget.target.bare:  # roster contact or MUC occupant
-                contact_list.setCache(from_jid, 'chat_state', to_display)
-                widget.update(from_jid)
+        # log.debug(_(u"Received new chat state {} from {} [{}]").format(state, from_jid_s, profile))
+        # from_jid = jid.JID(from_jid_s) if from_jid_s != C.ENTITY_ALL else C.ENTITY_ALL
+        # contact_list = self.contact_lists[profile]
+        # for widget in self.widgets.getWidgets(quick_chat.QuickChat):
+        #     if profile != widget.profile:
+        #         continue
+        #     to_display = C.USER_CHAT_STATES[state] if (state and widget.type == C.CHAT_GROUP) else state
+        #     if widget.type == C.CHAT_GROUP and from_jid_s == C.ENTITY_ALL:
+        #         for occupant in [jid.newResource(widget.target, nick) for nick in widget.occupants]:
+        #             contact_list.setCache(occupant, 'chat_state', to_display)
+        #             widget.update(occupant)
+        #     elif from_jid.bare == widget.target.bare:  # roster contact or MUC occupant
+        #         contact_list.setCache(from_jid, 'chat_state', to_display)
+        #         widget.update(from_jid)
 
     def psEventHandler(self, category, service_s, node, event_type, data, profile):
         """Called when a PubSub event is received.
--- a/frontends/src/quick_frontend/quick_chat.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/frontends/src/quick_frontend/quick_chat.py	Mon Jun 20 18:41:53 2016 +0200
@@ -20,11 +20,17 @@
 from sat.core.i18n import _
 from sat.core.log import getLogger
 log = getLogger(__name__)
-from sat_frontends.tools import jid
+from sat.core import exceptions
 from sat_frontends.quick_frontend import quick_widgets
 from sat_frontends.quick_frontend.constants import Const as C
 from collections import OrderedDict
-from datetime import datetime
+from sat_frontends.tools import jid
+
+ROOM_USER_JOINED = 'ROOM_USER_JOINED'
+ROOM_USER_LEFT = 'ROOM_USER_LEFT'
+ROOM_USER_MOVED = (ROOM_USER_JOINED, ROOM_USER_LEFT)
+
+# from datetime import datetime
 
 try:
     # FIXME: to be removed when an acceptable solution is here
@@ -32,26 +38,109 @@
 except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options
     unicode = str
 
+# FIXME: day_format need to be settable (i18n)
+
+class Message(object):
+    """Message metadata"""
+
+    def __init__(self, parent, uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile):
+        self.parent = parent
+        self.profile = profile
+        self.uid = uid
+        self.timestamp = timestamp
+        self.from_jid = from_jid
+        self.to_jid = to_jid
+        self.message = msg
+        self.subject = subject
+        self.type = type_
+        self.extra = extra
+        self.nick = self.getNick(from_jid)
+        # own_mess is True if message was sent by profile's jid
+        self.own_mess = (from_jid.resource == self.parent.nick) if self.parent.type == C.CHAT_GROUP else (from_jid.bare == self.host.profiles[profile].whoami.bare)
+        self.widgets = set()  # widgets linked to this message
+
+    @property
+    def host(self):
+        return self.parent.host
+
+    @property
+    def info_type(self):
+        return self.extra.get('info_type')
+
+    def getNick(self, entity):
+        """Return nick of an entity when possible"""
+        contact_list = self.host.contact_lists[self.profile]
+        if self.type == C.MESS_TYPE_INFO and self.info_type in ROOM_USER_MOVED:
+            try:
+                return self.extra['user_nick']
+            except KeyError:
+                log.error(u"extra data is missing user nick for uid {}".format(self.uid))
+                return ""
+        if self.parent.type == C.CHAT_GROUP or entity in contact_list.getSpecialExtras(C.CONTACT_SPECIAL_GROUP):
+            return entity.resource or ""
+        if entity.bare in contact_list:
+            return contact_list.getCache(entity, 'nick') or contact_list.getCache(entity, 'name') or entity.node or entity
+        return entity.node or entity
+
+
+class Occupant(object):
+    """Occupant metadata"""
+
+    def __init__(self, parent, data, profile):
+        self.parent = parent
+        self.profile = profile
+        self.nick = data['nick']
+        self.entity = data.get('entity')
+        if not self.entity:
+            self.entity = jid.JID(u"{}/{}".format(parent.target.bare, self.nick)),
+        self.affiliation = data['affiliation']
+        self.role = data['role']
+        self.widgets = set()  # widgets linked to this occupant
+
+    @property
+    def host(self):
+        return self.parent.host
+
 
 class QuickChat(quick_widgets.QuickWidget):
 
     visible_states = ['chat_state']
 
-    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
+    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, occupants=None, subject=None, profiles=None):
         """
         @param type_: can be C.CHAT_ONE2ONE for single conversation or C.CHAT_GROUP for chat à la IRC
         """
+        self.lang = ''  # default language to use for messages
         quick_widgets.QuickWidget.__init__(self, host, target, profiles=profiles)
+        self._locked = False  # True when we are waiting for history/search
+                              # messageNew signals are cached when locked
+        self._cache = []
         assert type_ in (C.CHAT_ONE2ONE, C.CHAT_GROUP)
-        if type_ == C.CHAT_GROUP and target.resource:
-            raise ValueError("A group chat entity can't have a resource")
         self.current_target = target
         self.type = type_
-        self.id = "" # FIXME: to be removed
-        self.nick = None
+        if type_ == C.CHAT_GROUP:
+            if target.resource:
+                raise exceptions.InternalError(u"a group chat entity can't have a resource")
+            self.nick = None
+            self.occupants = {}
+            self.setOccupants(occupants)
+        else:
+            if occupants is not None:
+                raise exceptions.InternalError(u"only group chat can have occupants")
+        self.messages = OrderedDict()  # key: uid, value: Message instance
         self.games = {}  # key=game name (unicode), value=instance of quick_games.RoomGame
+        self.subject = subject
 
+    def postInit(self):
+        """Method to be called by frontend after widget is initialised
+
+        handle the display of history and subject
+        """
         self.historyPrint(profile=self.profile)
+        if self.subject is not None:
+            self.setSubject(self.subject)
+
+    ## Widget management ##
 
     def __str__(self):
         return u"Chat Widget [target: {}, type: {}, profile: {}]".format(self.target, self.type, self.profile)
@@ -74,109 +163,6 @@
         if target.resource:
             self.current_target = target # FIXME: tmp, must use resource priority throught contactList instead
 
-    @property
-    def target(self):
-        if self.type == C.CHAT_GROUP:
-            return self.current_target.bare
-        return self.current_target
-
-    @property
-    def occupants(self):
-        """Return the occupants of a group chat (nicknames).
-
-        @return: set(unicode)
-        """
-        if self.type != C.CHAT_GROUP:
-            return set()
-        contact_list = self.host.contact_lists[self.profile]
-        return contact_list.getCache(self.target, C.CONTACT_RESOURCES).keys()
-
-    def manageMessage(self, entity, mess_type):
-        """Tell if this chat widget manage an entity and message type couple
-
-        @param entity (jid.JID): (full) jid of the sending entity
-        @param mess_type (str): message type as given by messageNew
-        @return (bool): True if this Chat Widget manage this couple
-        """
-        if self.type == C.CHAT_GROUP:
-            if mess_type == C.MESS_TYPE_GROUPCHAT and self.target == entity.bare:
-                return True
-        else:
-            if mess_type != C.MESS_TYPE_GROUPCHAT and entity in self.targets:
-                return True
-        return False
-
-    def addUser(self, nick):
-        """Add user if it is not in the group list"""
-        self.printInfo("=> %s has joined the room" % nick)
-
-    def removeUser(self, nick):
-        """Remove a user from the group list"""
-        self.printInfo("<= %s has left the room" % nick)
-
-    def setUserNick(self, nick):
-        """Set the nick of the user, usefull for e.g. change the color of the user"""
-        self.nick = nick
-
-    def changeUserNick(self, old_nick, new_nick):
-        """Change nick of a user in group list"""
-        self.printInfo("%s is now known as %s" % (old_nick, new_nick))
-
-    def setSubject(self, subject):
-        """Set title for a group chat"""
-        log.debug(_("Setting subject to %s") % subject)
-        if self.type != C.CHAT_GROUP:
-            log.error (_("[INTERNAL] trying to set subject for a non group chat window"))
-            raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
-
-    def afterHistoryPrint(self):
-        """Refresh or scroll down the focus after the history is printed"""
-        pass
-
-    def historyPrint(self, size=C.HISTORY_LIMIT_DEFAULT, search='', profile='@NONE@'):
-        """Print the current history
-
-        @param size (int): number of messages
-        @param search (str): pattern to filter the history results
-        @param profile (str): %(doc_profile)s
-        """
-        log_msg = _(u"now we print the history")
-        if size != C.HISTORY_LIMIT_DEFAULT:
-            log_msg += _(u" (%d messages)" % size)
-        log.debug(log_msg)
-
-        target = self.target.bare
-
-        def _historyGetCb(history):
-            day_format = "%A, %d %b %Y"  # to display the day change
-            previous_day = datetime.now().strftime(day_format)
-            for data in history:
-                uid, timestamp, from_jid, to_jid, message, subject, type_, extra = data  # FIXME: extra is unused !
-                if ((self.type == C.CHAT_GROUP and type_ != C.MESS_TYPE_GROUPCHAT) or
-                   (self.type == C.CHAT_ONE2ONE and type_ == C.MESS_TYPE_GROUPCHAT)):
-                    continue
-                message_day = datetime.fromtimestamp(timestamp).strftime(day_format)
-                if previous_day != message_day:
-                    self.printDayChange(message_day)
-                    previous_day = message_day
-                extra["timestamp"] = timestamp
-                self.messageNew(uid, timestamp, jid.JID(from_jid), target, message, subject, type_, extra, profile)
-            self.afterHistoryPrint()
-
-        def _historyGetEb(err):
-            log.error(_("Can't get history"))
-
-        self.host.bridge.historyGet(unicode(self.host.profiles[profile].whoami.bare), unicode(target), size, True, search, profile, callback=_historyGetCb, errback=_historyGetEb)
-
-    def _get_nick(self, entity):
-        """Return nick of this entity when possible"""
-        contact_list = self.host.contact_lists[self.profile]
-        if self.type == C.CHAT_GROUP or entity in contact_list.getSpecialExtras(C.CONTACT_SPECIAL_GROUP):
-            return entity.resource or ""
-        if entity.bare in contact_list:
-            return contact_list.getCache(entity, 'nick') or contact_list.getCache(entity, 'name') or entity.node or entity
-        return entity.node or entity
-
     def onPrivateCreated(self, widget):
         """Method called when a new widget for private conversation (MUC) is created"""
         raise NotImplementedError
@@ -188,57 +174,171 @@
         """
         return self.host.widgets.getOrCreateWidget(QuickChat, entity, type_=C.CHAT_ONE2ONE, force_hash=self.getPrivateHash(self.profile, entity), on_new_widget=self.onPrivateCreated, profile=self.profile) # we force hash to have a new widget, not this one again
 
-    def messageNew(self, uid, timestamp, from_jid, target, msg, subject, type_, extra, profile):
+    @property
+    def target(self):
+        if self.type == C.CHAT_GROUP:
+            return self.current_target.bare
+        return self.current_target
+
+    ## occupants ##
+
+    def setOccupants(self, occupants):
+        """set the whole list of occupants"""
+        assert len(self.occupants) == 0
+        for nick, data in occupants.iteritems():
+            self.occupants[nick] = Occupant(
+                self,
+                data,
+                self.profile
+                )
+
+    def addUser(self, occupant_data):
+        """Add user if it is not in the group list"""
+        occupant = Occupant(
+            self,
+            occupant_data,
+            self.profile
+            )
+        self.occupants[occupant.nick] = occupant
+        return occupant
+
+    def removeUser(self, occupant_data):
+        """Remove a user from the group list"""
+        nick = occupant_data['nick']
         try:
-            msg = msg.itervalues().next() # FIXME: tmp fix until message refactoring is finished (msg is now a dict)
-        except StopIteration:
-            log.warning(u"No message found (uid: {})".format(uid))
-            msg = ''
-        if self.type == C.CHAT_GROUP and target.resource and type_ != C.MESS_TYPE_GROUPCHAT:
-            # we have a private message, we forward it to a private conversation widget
-            chat_widget = self.getOrCreatePrivateWidget(target)
-            chat_widget.messageNew(uid, timestamp, from_jid, target, msg, subject, type_, extra, profile)
-            return
+            occupant = self.occupants.pop(nick)
+        except KeyError:
+            log.warning(u"Trying to remove an unknown occupant: {}".format(nick))
+        else:
+            return occupant
 
-        if type_ == C.MESS_TYPE_INFO:
-            self.printInfo(msg, extra=extra)
+    def setUserNick(self, nick):
+        """Set the nick of the user, usefull for e.g. change the color of the user"""
+        self.nick = nick
+
+    def changeUserNick(self, old_nick, new_nick):
+        """Change nick of a user in group list"""
+        self.printInfo("%s is now known as %s" % (old_nick, new_nick))
+
+    ## Messages ##
+
+    def manageMessage(self, entity, mess_type):
+        """Tell if this chat widget manage an entity and message type couple
+
+        @param entity (jid.JID): (full) jid of the sending entity
+        @param mess_type (str): message type as given by messageNew
+        @return (bool): True if this Chat Widget manage this couple
+        """
+        if self.type == C.CHAT_GROUP:
+            if mess_type in (C.MESS_TYPE_GROUPCHAT, C.MESS_TYPE_INFO) and self.target == entity.bare:
+                return True
         else:
-            nick = self._get_nick(from_jid)
-            if msg.startswith('/me '):
-                self.printInfo('* {} {}'.format(nick, msg[4:]), type_='me', extra=extra)
-            else:
-                # my_message is True if message comes from local user
-                my_message = (from_jid.resource == self.nick) if self.type == C.CHAT_GROUP else (from_jid.bare == self.host.profiles[profile].whoami.bare)
-                self.printMessage(nick, my_message, msg, timestamp, extra, profile)
-        # FIXME: to be checked/removed after message refactoring
-        # if timestamp:
-        self.afterHistoryPrint()
+            if mess_type != C.MESS_TYPE_GROUPCHAT and entity in self.targets:
+                return True
+        return False
+
+    def updateHistory(self, size=C.HISTORY_LIMIT_DEFAULT, search='', profile='@NONE@'):
+        """Called when history need to be recreated
+
+        Remove all message from history then call historyPrint
+        Must probably be overriden by frontend to clear widget
+        @param size (int): number of messages
+        @param search (str): pattern to filter the history results
+        @param profile (str): %(doc_profile)s
+        """
+        self._locked = True
+        self.messages.clear()
+        self.historyPrint(size, search, profile)
+
+    def _onHistoryPrinted(self):
+        """Method called when history is printed (or failed)
 
-    def printMessage(self, nick, my_message, message, timestamp, extra=None, profile=C.PROF_KEY_NONE):
-        """Print message in chat window.
+        unlock the widget, and can be used to refresh or scroll down
+        the focus after the history is printed
+        """
+        self._locked = False
+        for data in self._cache:
+            self.messageNew(*data)
 
-        @param nick (unicode): author nick
-        @param my_message (boolean): True if profile is the author
-        @param message (unicode): message content
-        @param extra (dict): extra data
+    def historyPrint(self, size=C.HISTORY_LIMIT_DEFAULT, search='', profile='@NONE@'):
+        """Print the current history
+
+        @param size (int): number of messages
+        @param search (str): pattern to filter the history results
+        @param profile (str): %(doc_profile)s
         """
-        # FIXME: check/remove this if necessary (message refactoring)
-        # if not timestamp:
-        #     # XXX: do not send notifications for each line of the history being displayed
-        #     # FIXME: this must be changed in the future if the timestamp is passed with
-        #     # all messages and not only with the messages coming from the history.
-        self.notify(nick, message)
+        if size == 0:
+            log.debug(u"Empty history requested, skipping")
+            self._onHistoryPrinted()
+            return
+        log_msg = _(u"now we print the history")
+        if size != C.HISTORY_LIMIT_DEFAULT:
+            log_msg += _(u" ({} messages)".format(size))
+        log.debug(log_msg)
+
+        target = self.target.bare
+
+        def _historyGetCb(history):
+            # day_format = "%A, %d %b %Y"  # to display the day change
+            # previous_day = datetime.now().strftime(day_format)
+            # message_day = datetime.fromtimestamp(timestamp).strftime(self.day_format)
+            # if previous_day != message_day:
+            #     self.printDayChange(message_day)
+            #     previous_day = message_day
+            for data in history:
+                uid, timestamp, from_jid, to_jid, message, subject, type_, extra = data
+                from_jid = jid.JID(from_jid)
+                to_jid = jid.JID(to_jid)
+                # if ((self.type == C.CHAT_GROUP and type_ != C.MESS_TYPE_GROUPCHAT) or
+                #    (self.type == C.CHAT_ONE2ONE and type_ == C.MESS_TYPE_GROUPCHAT)):
+                #     continue
+                self.messages[uid] = Message(self, uid, timestamp, from_jid, to_jid, message, subject, type_, extra, profile)
+            self._onHistoryPrinted()
+
+        def _historyGetEb(err):
+            log.error(_(u"Can't get history"))
+            self._onHistoryPrinted()
+
+        self.host.bridge.historyGet(unicode(self.host.profiles[profile].whoami.bare), unicode(target), size, True, search, profile, callback=_historyGetCb, errback=_historyGetEb)
 
-    def printInfo(self, msg, type_='normal', extra=None):
-        """Print general info.
+    def messageNew(self, uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile):
+        log.debug(u"messageNew ==> {}".format((uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile)))
+        if self._locked:
+            self._cache.append(uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile)
+            return
+        if self.type == C.CHAT_GROUP:
+            if to_jid.resource and type_ != C.MESS_TYPE_GROUPCHAT:
+                # we have a private message, we forward it to a private conversation widget
+                chat_widget = self.getOrCreatePrivateWidget(to_jid)
+                chat_widget.messageNew(uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile)
+                return
+            if type_ == C.MESS_TYPE_INFO:
+                try:
+                    info_type = extra['info_type']
+                except KeyError:
+                    pass
+                else:
+                    user_data = {k[5:]:v for k,v in extra.iteritems() if k.startswith('user_')}
+                    if info_type == ROOM_USER_JOINED:
+                        self.addUser(user_data)
+                    elif info_type == ROOM_USER_LEFT:
+                        self.removeUser(user_data)
 
-        @param msg (unicode): message to print
-        @param type_ (unicode):
-            - 'normal': general info like "toto has joined the room"
-            - 'me': "/me" information like "/me clenches his fist" ==> "toto clenches his fist"
-        @param extra (dict): message data
+        message = Message(self, uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile)
+        self.messages[uid] = message
+
+        if 'received_timestamp' in extra:
+            log.warning(u"Delayed message received after history, this should not happen")
+        self.createMessage(message)
+
+    def createMessage(self, message, append=False):
+        """Must be implemented by frontend to create and show a new message widget
+
+        This is only called on messageNew, not on history.
+        You need to override historyPrint to handle the later
+        @param message(Message): message data
         """
-        self.notify(msg=msg)
+        raise NotImplementedError
 
     def notify(self, contact="somebody", msg=""):
         """Notify the user of a new message if the frontend doesn't have the focus.
@@ -246,6 +346,7 @@
         @param contact (unicode): contact who wrote to the users
         @param msg (unicode): the message that has been received
         """
+        # FIXME: not called anymore after refactoring
         raise NotImplemented
 
     def printDayChange(self, day):
@@ -253,21 +354,16 @@
 
         @param day(unicode): day to display (or not if this method is not overwritten)
         """
+        # FIXME: not called anymore after refactoring
         pass
 
-    def getEntityStates(self, entity):
-        """Retrieve states for an entity.
+    ## Room ##
 
-        @param entity (jid.JID): entity
-        @return: OrderedDict{unicode: unicode}
-        """
-        states = OrderedDict()
-        clist = self.host.contact_lists[self.profile]
-        for key in self.visible_states:
-            value = clist.getCache(entity, key)
-            if value:
-                states[key] = value
-        return states
+    def setSubject(self, subject):
+        """Set title for a group chat"""
+        self.subject = subject
+        if self.type != C.CHAT_GROUP:
+            raise exceptions.InternalError("trying to set subject for a non group chat window")
 
     def addGamePanel(self, widget):
         """Insert a game panel to this Chat dialog.
--- a/src/bridge/DBus.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/src/bridge/DBus.py	Mon Jun 20 18:41:53 2016 +0200
@@ -425,7 +425,7 @@
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='sa{ss}a{ss}sa{ss}s', out_signature='',
                          async_callbacks=('callback', 'errback'))
-    def messageSend(self, to_jid, message, subject='', mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None):
+    def messageSend(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None):
         return self._callback("messageSend", unicode(to_jid), message, subject, unicode(mess_type), extra, unicode(profile_key), callback=callback, errback=errback)
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
--- a/src/bridge/bridge_constructor/bridge_template.ini	Sun Jun 19 22:22:13 2016 +0200
+++ b/src/bridge/bridge_constructor/bridge_template.ini	Mon Jun 20 18:41:53 2016 +0200
@@ -51,6 +51,7 @@
   - thread_parent: id of the parent of the current thread
   - received_timestamp: date of receiption for delayed messages
   - delay_sender: entity which has originally sent or which has delayed the message
+  - info_type: subtype for info messages
 doc_param_8=%(doc_profile)s
 
 [newAlert]
@@ -447,7 +448,7 @@
 category=core
 sig_in=sa{ss}a{ss}sa{ss}s
 sig_out=
-param_2_default=''
+param_2_default={}
 param_3_default="auto"
 param_4_default={}
 param_5_default="@NONE@"
--- a/src/core/constants.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/src/core/constants.py	Mon Jun 20 18:41:53 2016 +0200
@@ -82,6 +82,7 @@
     ENTITY_ALL_RESOURCES = '@ALL_RESOURCES@'
     ENTITY_MAIN_RESOURCE = '@MAIN_RESOURCE@'
     ENTITY_CAP_HASH = 'CAP_HASH'
+    ENTITY_TYPE = 'TYPE'
 
 
     ## Roster jids selection ##
@@ -98,6 +99,11 @@
     MESS_TYPE_GROUPCHAT = 'groupchat'
     MESS_TYPE_HEADLINE = 'headline'
     MESS_TYPE_NORMAL = 'normal'
+    MESS_TYPE_AUTO = 'auto'  # magic value to let the backend guess the type
+    MESS_TYPE_STANDARD = (MESS_TYPE_CHAT, MESS_TYPE_ERROR, MESS_TYPE_GROUPCHAT, MESS_TYPE_HEADLINE, MESS_TYPE_NORMAL)
+    MESS_TYPE_ALL = MESS_TYPE_STANDARD + (MESS_TYPE_INFO, MESS_TYPE_AUTO)
+
+    MESS_EXTRA_INFO = "info_type"
 
 
     ## Presence ##
@@ -113,6 +119,7 @@
 
 
     ## Common namespaces ##
+    NS_XML = 'http://www.w3.org/XML/1998/namespace'
     NS_CLIENT = 'jabber:client'
     NS_FORWARD = 'urn:xmpp:forward:0'
     NS_DELAY = 'urn:xmpp:delay'
--- a/src/core/sat_main.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/src/core/sat_main.py	Mon Jun 20 18:41:53 2016 +0200
@@ -95,7 +95,7 @@
         self.bridge.register("getParamsUI", self.memory.getParamsUI)
         self.bridge.register("getParamsCategories", self.memory.getParamsCategories)
         self.bridge.register("paramsRegisterApp", self.memory.paramsRegisterApp)
-        self.bridge.register("historyGet", self.memory.historyGet)
+        self.bridge.register("historyGet", self.memory._historyGet)
         self.bridge.register("setPresence", self._setPresence)
         self.bridge.register("subscription", self.subscription)
         self.bridge.register("addContact", self._addContact)
@@ -553,11 +553,11 @@
         for lang, subject in data["subject"].iteritems():
             subject_elt = message_elt.addElement("subject", content=subject)
             if lang:
-                subject_elt['xml:lang'] = lang
+                subject_elt[(C.NS_XML, 'lang')] = lang
         for lang, message in data["message"].iteritems():
             body_elt = message_elt.addElement("body", content=message)
             if lang:
-                body_elt['xml:lang'] = lang
+                body_elt[(C.NS_XML, 'lang')] = lang
         try:
             thread = data['extra']['thread']
         except KeyError:
@@ -646,21 +646,21 @@
 
         pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(data))
         pre_xml_treatments.chainDeferred(post_xml_treatments)
-        post_xml_treatments.addCallback(self._sendMessageToStream, client)
+        post_xml_treatments.addCallback(self.messageSendToStream, client)
         if send_only:
             log.debug(_("Triggers, storage and echo have been inhibited by the 'send_only' parameter"))
         else:
-            post_xml_treatments.addCallback(self._storeMessage, client)
-            post_xml_treatments.addCallback(self.sendMessageToBridge, client)
+            post_xml_treatments.addCallback(self.messageAddToHistory, client)
+            post_xml_treatments.addCallback(self.messageSendToBridge, client)
             post_xml_treatments.addErrback(self._cancelErrorTrap)
         pre_xml_treatments.callback(data)
         return pre_xml_treatments
 
-    def _cancelErrorTrap(failure):
+    def _cancelErrorTrap(self, failure):
         """A message sending can be cancelled by a plugin treatment"""
         failure.trap(exceptions.CancelError)
 
-    def _sendMessageToStream(self, data, client):
+    def messageSendToStream(self, data, client):
         """Actualy send the message to the server
 
         @param data: message data dictionnary
@@ -669,7 +669,7 @@
         client.xmlstream.send(data['xml'])
         return data
 
-    def _storeMessage(self, data, client):
+    def messageAddToHistory(self, data, client):
         """Store message into database (for local history)
 
         @param data: message data dictionnary
@@ -678,13 +678,13 @@
         if data["type"] != C.MESS_TYPE_GROUPCHAT:
             # we don't add groupchat message to history, as we get them back
             # and they will be added then
-            if data['message']: # we need a message to save something
+            if data['message'] or data['subject']: # we need a message to store
                 self.memory.addToHistory(client, data)
             else:
                log.warning(u"No message found") # empty body should be managed by plugins before this point
         return data
 
-    def sendMessageToBridge(self, data, client):
+    def messageSendToBridge(self, data, client):
         """Send message to bridge, so frontends can display it
 
         @param data: message data dictionnary
@@ -693,7 +693,7 @@
         if data["type"] != C.MESS_TYPE_GROUPCHAT:
             # we don't send groupchat message to bridge, as we get them back
             # and they will be added the
-            if data['message']: # we need a message to send something
+            if data['message'] or data['subject']: # we need a message to send something
                 # We send back the message, so all frontends are aware of it
                 self.bridge.messageNew(data['uid'], data['timestamp'], data['from'].full(), data['to'].full(), data['message'], data['subject'], data['type'], data['extra'], profile=client.profile)
             else:
@@ -1039,6 +1039,7 @@
 
     def importMenu(self, path, callback, security_limit=C.NO_SECURITY_LIMIT, help_string="", type_=C.MENU_GLOBAL):
         """register a new menu for frontends
+
         @param path: path to go to the menu (category/subcategory/.../item), must be an iterable (e.g.: ("File", "Open"))
             /!\ use D_() instead of _() for translations (e.g. (D_("File"), D_("Open")))
         @param callback: method to be called when menuitem is selected, callable or a callback id (string) as returned by [registerCallback]
--- a/src/core/xmpp.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/src/core/xmpp.py	Mon Jun 20 18:41:53 2016 +0200
@@ -25,7 +25,7 @@
 from twisted.words.protocols.jabber import error
 from twisted.words.protocols.jabber import jid
 from twisted.python import failure
-from wokkel import client, disco, xmppim, generic, iwokkel
+from wokkel import client as wokkel_client, disco, xmppim, generic, iwokkel
 from wokkel import delay
 from sat.core.log import getLogger
 log = getLogger(__name__)
@@ -36,13 +36,13 @@
 import uuid
 
 
-class SatXMPPClient(client.XMPPClient):
+class SatXMPPClient(wokkel_client.XMPPClient):
     implements(iwokkel.IDisco)
 
     def __init__(self, host_app, profile, user_jid, password, host=None, port=C.XMPP_C2S_PORT, max_retries=C.XMPP_MAX_RETRIES):
         # XXX: DNS SRV records are checked when the host is not specified.
         # If no SRV record is found, the host is directly extracted from the JID.
-        client.XMPPClient.__init__(self, user_jid, password, host or None, port or C.XMPP_C2S_PORT)
+        wokkel_client.XMPPClient.__init__(self, user_jid, password, host or None, port or C.XMPP_C2S_PORT)
         self.factory.clientConnectionLost = self.connectionLost
         self.factory.maxRetries = max_retries
         self.__connected = False
@@ -80,7 +80,7 @@
     def _authd(self, xmlstream):
         if not self.host_app.trigger.point("XML Initialized", xmlstream, self.profile):
             return
-        client.XMPPClient._authd(self, xmlstream)
+        wokkel_client.XMPPClient._authd(self, xmlstream)
         self.__connected = True
         log.info(_("********** [%s] CONNECTED **********") % self.profile)
         self.streamInitialized()
@@ -112,7 +112,7 @@
         log.error(_(u"ERROR: XMPP connection failed for profile '%(profile)s': %(reason)s" % {'profile': self.profile, 'reason': reason}))
         self.conn_deferred.errback(reason.value)
         try:
-            client.XMPPClient.initializationFailed(self, reason)
+            wokkel_client.XMPPClient.initializationFailed(self, reason)
         except:
             # we already chained an errback, no need to raise an exception
             pass
@@ -152,8 +152,8 @@
         message = {}
         subject = {}
         extra = {}
-        data = {"from": message_elt['from'],
-                "to": message_elt['to'],
+        data = {"from": jid.JID(message_elt['from']),
+                "to": jid.JID(message_elt['to']),
                 "uid": message_elt.getAttribute('uid', unicode(uuid.uuid4())), # XXX: uid is not a standard attribute but may be added by plugins
                 "message": message,
                 "subject": subject,
@@ -169,11 +169,11 @@
 
         # message
         for e in message_elt.elements(C.NS_CLIENT, 'body'):
-            message[e.getAttribute('xml:lang','')] = unicode(e)
+            message[e.getAttribute((C.NS_XML,'lang'),'')] = unicode(e)
 
         # subject
         for e in message_elt.elements(C.NS_CLIENT, 'subject'):
-            subject[e.getAttribute('xml:lang','')] = unicode(e)
+            subject[e.getAttribute((C.NS_XML, 'lang'),'')] = unicode(e)
 
         # delay and timestamp
         try:
@@ -187,41 +187,38 @@
             if parsed_delay.sender:
                 data['delay_sender'] = parsed_delay.sender.full()
 
-        def skipEmptyMessage(data):
-            if not data['message'] and not data['extra']:
-                raise failure.Failure(exceptions.CancelError())
-            return data
+
+        post_treat.addCallback(self.skipEmptyMessage)
+        post_treat.addCallback(self.addToHistory, client)
+        post_treat.addErrback(self.treatmentsEb)
+        post_treat.addCallback(self.bridgeSignal, client, data)
+        post_treat.addErrback(self.cancelErrorTrap)
+        post_treat.callback(data)
 
-        def bridgeSignal(data):
-            try:
-                data['extra']['received_timestamp'] = data['received_timestamp']
-                data['extra']['delay_sender'] = data['delay_sender']
-            except KeyError:
-                pass
-            if data is not None:
-                self.host.bridge.messageNew(data['uid'], data['timestamp'], data['from'].full(), data['to'].full(), data['message'], data['subject'], data['type'], data['extra'], profile=client.profile)
-            return data
+    def skipEmptyMessage(self, data):
+        if not data['message'] and not data['extra'] and not data['subject']:
+            raise failure.Failure(exceptions.CancelError("Cancelled empty message"))
+        return data
+
+    def addToHistory(self, data, client):
+        return self.host.memory.addToHistory(client, data)
 
-        def addToHistory(data):
-            data['from'] = jid.JID(data['from'])
-            data['to'] = jid.JID(data['to'])
-            self.host.memory.addToHistory(client, data)
-            return data
-
-        def treatmentsEb(failure_):
-            failure_.trap(exceptions.SkipHistory)
-            return data
+    def treatmentsEb(self, failure_):
+        failure_.trap(exceptions.SkipHistory)
 
-        def cancelErrorTrap(failure_):
-            """A message sending can be cancelled by a plugin treatment"""
-            failure_.trap(exceptions.CancelError)
+    def bridgeSignal(self, dummy, client, data):
+        try:
+            data['extra']['received_timestamp'] = data['received_timestamp']
+            data['extra']['delay_sender'] = data['delay_sender']
+        except KeyError:
+            pass
+        if data is not None:
+            self.host.bridge.messageNew(data['uid'], data['timestamp'], data['from'].full(), data['to'].full(), data['message'], data['subject'], data['type'], data['extra'], profile=client.profile)
+        return data
 
-        post_treat.addCallback(skipEmptyMessage)
-        post_treat.addCallback(addToHistory)
-        post_treat.addErrback(treatmentsEb)
-        post_treat.addCallback(bridgeSignal)
-        post_treat.addErrback(cancelErrorTrap)
-        post_treat.callback(data)
+    def cancelErrorTrap(self, failure_):
+        """A message sending can be cancelled by a plugin treatment"""
+        failure_.trap(exceptions.CancelError)
 
 
 class SatRosterProtocol(xmppim.RosterClientProtocol):
@@ -283,8 +280,10 @@
 
     def getAttributes(self, item):
         """Return dictionary of attributes as used in bridge from a RosterItem
+
         @param item: RosterItem
-        @return: dictionary of attributes"""
+        @return: dictionary of attributes
+        """
         item_attr = {'to': unicode(item.subscriptionTo),
                      'from': unicode(item.subscriptionFrom),
                      'ask': unicode(item.ask)
@@ -339,8 +338,9 @@
     def getItem(self, entity_jid):
         """Return RosterItem for a given jid
 
-        @param entity_jid: jid of the contact
-        @return: RosterItem or None if contact is not in cache
+        @param entity_jid(jid.JID): jid of the contact
+        @return(RosterItem, None): RosterItem instance
+            None if contact is not in cache
         """
         return self._jids.get(entity_jid, None)
 
@@ -384,6 +384,18 @@
         else:
             raise ValueError(u'Unexpected type_ {}'.format(type_))
 
+    def getNick(self, entity_jid):
+        """Return a nick name for an entity
+
+        return nick choosed by user if available
+        else return user part of entity_jid
+        """
+        item = self.getItem(entity_jid)
+        if item is None:
+            return entity_jid.user
+        else:
+            return item.name or entity_jid.user
+
 
 class SatPresenceProtocol(xmppim.PresenceClientProtocol):
 
@@ -474,7 +486,9 @@
             return
         self.send(presence_elt)
 
+    @defer.inlineCallbacks
     def subscribed(self, entity):
+        yield self.parent.roster.got_roster
         xmppim.PresenceClientProtocol.subscribed(self, entity)
         self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile)
         item = self.parent.roster.getItem(entity)
@@ -494,8 +508,10 @@
         log.debug(_(u"unsubscription confirmed for [%s]") % entity.userhost())
         self.host.bridge.subscribe('unsubscribed', entity.userhost(), self.parent.profile)
 
+    @defer.inlineCallbacks
     def subscribeReceived(self, entity):
         log.debug(_(u"subscription request from [%s]") % entity.userhost())
+        yield self.parent.roster.got_roster
         item = self.parent.roster.getItem(entity)
         if item and item.subscriptionTo:
             # We automatically accept subscription if we are already subscribed to contact presence
@@ -505,8 +521,10 @@
             self.host.memory.addWaitingSub('subscribe', entity.userhost(), self.parent.profile)
             self.host.bridge.subscribe('subscribe', entity.userhost(), self.parent.profile)
 
+    @defer.inlineCallbacks
     def unsubscribeReceived(self, entity):
         log.debug(_(u"unsubscription asked for [%s]") % entity.userhost())
+        yield self.parent.roster.got_roster
         item = self.parent.roster.getItem(entity)
         if item and item.subscriptionFrom:  # we automatically remove contact
             log.debug(_('automatic contact deletion'))
--- a/src/memory/memory.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/src/memory/memory.py	Mon Jun 20 18:41:53 2016 +0200
@@ -513,6 +513,9 @@
     def addToHistory(self, client, data):
         return self.storage.addToHistory(data, client.profile)
 
+    def _historyGet(self, from_jid_s, to_jid_s, limit=C.HISTORY_LIMIT_NONE, between=True, search=None, profile=C.PROF_KEY_NONE):
+        return self.historyGet(jid.JID(from_jid_s), jid.JID(to_jid_s), limit, between, search, profile)
+
     def historyGet(self, from_jid, to_jid, limit=C.HISTORY_LIMIT_NONE, between=True, search=None, profile=C.PROF_KEY_NONE):
         """Retrieve messages in history
 
@@ -525,7 +528,7 @@
         @param between (bool): confound source and dest (ignore the direction)
         @param search (str): pattern to filter the history results
         @param profile (str): %(doc_profile)s
-        @return: list of message data as in [messageNew]
+        @return (D(list)): list of message data as in [messageNew]
         """
         assert profile != C.PROF_KEY_NONE
         if limit == C.HISTORY_LIMIT_DEFAULT:
@@ -534,7 +537,7 @@
             limit = None
         if limit == 0:
             return defer.succeed([])
-        return self.storage.historyGet(jid.JID(from_jid), jid.JID(to_jid), limit, between, search, profile)
+        return self.storage.historyGet(from_jid, to_jid, limit, between, search, profile)
 
     ## Statuses ##
 
--- a/src/plugins/plugin_exp_command_export.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/src/plugins/plugin_exp_command_export.py	Mon Jun 20 18:41:53 2016 +0200
@@ -103,14 +103,14 @@
         except ValueError:
             pass
 
-    def MessageReceivedTrigger(self, client, message, post_treat):
+    def MessageReceivedTrigger(self, client, message_elt, post_treat):
         """ Check if source is linked and repeat message, else do nothing  """
-        from_jid = jid.JID(message["from"])
+        from_jid = jid.JID(message_elt["from"])
         spawned_key = (from_jid.userhostJID(), client.profile)
 
         if spawned_key in self.spawned:
             try:
-                body = message.elements(C.NS_CLIENT, 'body').next()
+                body = message_elt.elements(C.NS_CLIENT, 'body').next()
             except StopIteration:
                 # do not block message without body (chat state notification...)
                 return True
--- a/src/plugins/plugin_exp_parrot.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/src/plugins/plugin_exp_parrot.py	Mon Jun 20 18:41:53 2016 +0200
@@ -67,12 +67,12 @@
     #        log.debug("Parrot link detected, skipping other triggers")
     #        raise trigger.SkipOtherTriggers
 
-    def MessageReceivedTrigger(self, client, message, post_treat):
+    def MessageReceivedTrigger(self, client, message_elt, post_treat):
         """ Check if source is linked and repeat message, else do nothing  """
         # TODO: many things are not repeated (subject, thread, etc)
         profile = client.profile
         client = self.host.getClient(profile)
-        from_jid = jid.JID(message["from"])
+        from_jid = message_elt["from"]
 
         try:
             _links = client.parrot_links
@@ -83,7 +83,7 @@
             return True
 
         message = {}
-        for e in message.elements(C.NS_CLIENT, 'body'):
+        for e in message_elt.elements(C.NS_CLIENT, 'body'):
             body = unicode(e)
             lang = e.getAttribute('lang') or ''
 
@@ -106,12 +106,12 @@
 
         return True
 
-    def addParrot(self, source_jid, dest_jid, profile):
+    def addParrot(self, client, source_jid, dest_jid):
         """Add a parrot link from one entity to another one
+
         @param source_jid: entity from who messages will be repeated
         @param dest_jid: entity where the messages will be repeated
-        @param profile: %(doc_profile_key)s"""
-        client = self.host.getClient(profile)
+        """
         try:
             _links = client.parrot_links
         except AttributeError:
@@ -120,17 +120,17 @@
         _links[source_jid.userhostJID()] = dest_jid
         log.info(u"Parrot mode: %s will be repeated to %s" % (source_jid.userhost(), unicode(dest_jid)))
 
-    def removeParrot(self, source_jid, profile):
+    def removeParrot(self, client, source_jid):
         """Remove parrot link
+
         @param source_jid: this entity will no more be repeated
-        @param profile: %(doc_profile_key)s"""
-        client = self.host.getClient(profile)
+        """
         try:
             del client.parrot_links[source_jid.userhostJID()]
         except (AttributeError, KeyError):
             pass
 
-    def cmd_parrot(self, mess_data, profile):
+    def cmd_parrot(self, client, mess_data):
         """activate Parrot mode between 2 entities, in both directions."""
         log.debug("Catched parrot command")
         txt_cmd = self.host.plugins[C.TEXT_CMDS]
@@ -140,19 +140,19 @@
             if not link_left_jid.user or not link_left_jid.host:
                 raise jid.InvalidFormat
         except (RuntimeError, jid.InvalidFormat, AttributeError):
-            txt_cmd.feedBack("Can't activate Parrot mode for invalid jid", mess_data, profile)
+            txt_cmd.feedBack(client, "Can't activate Parrot mode for invalid jid", mess_data)
             return False
 
         link_right_jid = mess_data['to']
 
-        self.addParrot(link_left_jid, link_right_jid, profile)
-        self.addParrot(link_right_jid, link_left_jid, profile)
+        self.addParrot(client, link_left_jid, link_right_jid)
+        self.addParrot(client, link_right_jid, link_left_jid)
 
-        txt_cmd.feedBack("Parrot mode activated for %s" % (unicode(link_left_jid), ), mess_data, profile)
+        txt_cmd.feedBack(client, "Parrot mode activated for {}".format(unicode(link_left_jid)), mess_data)
 
         return False
 
-    def cmd_unparrot(self, mess_data, profile):
+    def cmd_unparrot(self, client, mess_data):
         """remove Parrot mode between 2 entities, in both directions."""
         log.debug("Catched unparrot command")
         txt_cmd = self.host.plugins[C.TEXT_CMDS]
@@ -162,14 +162,14 @@
             if not link_left_jid.user or not link_left_jid.host:
                 raise jid.InvalidFormat
         except jid.InvalidFormat:
-            txt_cmd.feedBack("Can't deactivate Parrot mode for invalid jid", mess_data, profile)
+            txt_cmd.feedBack(client, u"Can't deactivate Parrot mode for invalid jid", mess_data)
             return False
 
         link_right_jid = mess_data['to']
 
-        self.removeParrot(link_left_jid, profile)
-        self.removeParrot(link_right_jid, profile)
+        self.removeParrot(client, link_left_jid)
+        self.removeParrot(client, link_right_jid)
 
-        txt_cmd.feedBack("Parrot mode deactivated for %s and %s" % (unicode(link_left_jid), unicode(link_right_jid)), mess_data, profile)
+        txt_cmd.feedBack(client, u"Parrot mode deactivated for {} and {}".format(unicode(link_left_jid), unicode(link_right_jid)), mess_data)
 
         return False
--- a/src/plugins/plugin_misc_text_commands.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/src/plugins/plugin_misc_text_commands.py	Mon Jun 20 18:41:53 2016 +0200
@@ -46,6 +46,7 @@
 
 CMD_KEY = "@command"
 CMD_TYPES = ('group', 'one2one', 'all')
+FEEDBACK_INFO_TYPE = "TEXT_CMD"
 
 
 class TextCommands(object):
@@ -148,7 +149,7 @@
                     while (cmd_name + str(suff)) in self._commands:
                         suff+=1
                     new_name = cmd_name + str(suff)
-                    log.warning(_(u"Conflict for command [%(old_name)s], renaming it to [%(new_name)s]") % {'old_name': cmd_name, 'new_name': new_name})
+                    log.warning(_(u"Conflict for command [{old_name}], renaming it to [{new_name}]").format(old_name=cmd_name, new_name=new_name))
                     cmd_name = new_name
                 self._commands[cmd_name] = cmd_data = OrderedDict({'callback':cmd}) # We use an Ordered dict to keep documenation order
                 cmd_data.update(self._parseDocString(cmd, cmd_name))
@@ -182,7 +183,6 @@
         @param mess_data(dict): data comming from messageSend trigger
         @param profile: %(doc_profile)s
         """
-        profile = client.profile
         try:
             msg = mess_data["message"]['']
             msg_lang = ''
@@ -217,7 +217,7 @@
                 if ret:
                     return mess_data
                 else:
-                    log.debug("text commands took over")
+                    log.debug(u"text command detected ({})".format(command))
                     raise failure.Failure(exceptions.CancelError())
 
             def genericErrback(failure):
@@ -225,26 +225,26 @@
                     msg = u"with condition {}".format(failure.value.condition)
                 except AttributeError:
                     msg = u"with error {}".format(failure.value)
-                self.feedBack(u"Command failed {}".format(msg), mess_data, profile)
+                self.feedBack(client, u"Command failed {}".format(msg), mess_data)
                 return False
 
             mess_data["unparsed"] = msg[1 + len(command):]  # part not yet parsed of the message
             try:
                 cmd_data = self._commands[command]
             except KeyError:
-                self.feedBack(_("Unknown command /%s. ") % command + self.HELP_SUGGESTION, mess_data, profile)
-                log.debug("text commands took over")
+                self.feedBack(client, _("Unknown command /%s. ") % command + self.HELP_SUGGESTION, mess_data)
+                log.debug("text command help message")
                 raise failure.Failure(exceptions.CancelError())
             else:
                 if not self._contextValid(mess_data, cmd_data):
                     # The command is not launched in the right context, we throw a message with help instructions
                     context_txt = _("group discussions") if cmd_data["type"] == "group" else _("one to one discussions")
                     feedback = _("/{command} command only applies in {context}.").format(command=command, context=context_txt)
-                    self.feedBack(u"{} {}".format(feedback, self.HELP_SUGGESTION), mess_data, profile)
-                    log.debug("text commands took over")
+                    self.feedBack(client, u"{} {}".format(feedback, self.HELP_SUGGESTION), mess_data)
+                    log.debug("text command invalid message")
                     raise failure.Failure(exceptions.CancelError())
                 else:
-                    d = defer.maybeDeferred(cmd_data["callback"], mess_data, profile)
+                    d = defer.maybeDeferred(cmd_data["callback"], client, mess_data)
                     d.addErrback(genericErrback)
                     d.addCallback(retHandling)
 
@@ -276,16 +276,22 @@
             return jid.JID(arg + service_jid)
         return jid.JID(u"%s@%s" % (arg, service_jid))
 
-    def feedBack(self, message, mess_data, profile):
+    def feedBack(self, client, message, mess_data, info_type=FEEDBACK_INFO_TYPE):
         """Give a message back to the user"""
         if mess_data["type"] == 'groupchat':
-            _from = mess_data["to"].userhostJID()
+            to_ = mess_data["to"].userhostJID()
         else:
-            _from = self.host.getJidNStream(profile)[0]
+            to_ = client.jid
 
-        self.host.bridge.messageNew(unicode(mess_data["to"]), {'': message}, {}, C.MESS_TYPE_INFO, unicode(_from), {}, profile=profile)
+        # we need to invert send message back, so sender need to original destinee
+        mess_data["from"] = mess_data["to"]
+        mess_data["to"] = to_
+        mess_data["type"] = C.MESS_TYPE_INFO
+        mess_data["message"] = {'': message}
+        mess_data["extra"]["info_type"] = info_type
+        self.host.messageSendToBridge(mess_data, client)
 
-    def cmd_whois(self, mess_data, profile):
+    def cmd_whois(self, client, mess_data):
         """show informations on entity
 
         @command: [JID|ROOM_NICK]
@@ -299,7 +305,7 @@
         if mess_data['type'] == "groupchat":
             room = mess_data["to"].userhostJID()
             try:
-                if self.host.plugins["XEP-0045"].isNickInRoom(room, entity, profile):
+                if self.host.plugins["XEP-0045"].isNickInRoom(room, entity, client.profile):
                     entity = u"%s/%s" % (room, entity)
             except KeyError:
                 log.warning("plugin XEP-0045 is not present")
@@ -312,20 +318,20 @@
                 if not target_jid.user or not target_jid.host:
                     raise jid.InvalidFormat
             except (RuntimeError, jid.InvalidFormat, AttributeError):
-                self.feedBack(_("Invalid jid, can't whois"), mess_data, profile)
+                self.feedBack(client, _("Invalid jid, can't whois"), mess_data)
                 return False
 
         if not target_jid.resource:
-            target_jid.resource = self.host.memory.getMainResource(target_jid, profile)
+            target_jid.resource = self.host.memory.getMainResource(target_jid, client.profile)
 
         whois_msg = [_(u"whois for %(jid)s") % {'jid': target_jid}]
 
         d = defer.succeed(None)
         for ignore, callback in self._whois:
-            d.addCallback(lambda ignore: callback(whois_msg, mess_data, target_jid, profile))
+            d.addCallback(lambda ignore: callback(client, whois_msg, mess_data, target_jid))
 
         def feedBack(ignore):
-            self.feedBack(u"\n".join(whois_msg), mess_data, profile)
+            self.feedBack(client, u"\n".join(whois_msg), mess_data)
             return False
 
         d.addCallback(feedBack)
@@ -345,7 +351,7 @@
 
         return strings
 
-    def cmd_help(self, mess_data, profile):
+    def cmd_help(self, client, mess_data):
         """show help on available commands
 
         @command: [cmd_name]
@@ -355,7 +361,7 @@
         if cmd_name and cmd_name[0] == "/":
             cmd_name = cmd_name[1:]
         if cmd_name and cmd_name not in self._commands:
-            self.feedBack(_(u"Invalid command name [{}]\n".format(cmd_name)), mess_data, profile)
+            self.feedBack(client, _(u"Invalid command name [{}]\n".format(cmd_name)), mess_data)
             cmd_name = ""
         if not cmd_name:
             # we show the global help
@@ -384,13 +390,4 @@
                 syntax=_(" "*4+"syntax: {}\n").format(syntax) if syntax else "",
                 args_help=u'\n'.join([u" "*8+"{}".format(line) for line in self._getArgsHelp(cmd_data)]))
 
-        self.feedBack(help_mess, mess_data, profile)
-
-    def cmd_me(self, mess_data, profile):
-        """Display a message at third person
-
-        @command: message
-            - message: message to display at the third person
-        """
-        # We just catch the method and continue it as the frontends should manage /me display
-        return True
+        self.feedBack(client, help_mess, mess_data)
--- a/src/plugins/plugin_sec_otr.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/src/plugins/plugin_sec_otr.py	Mon Jun 20 18:41:53 2016 +0200
@@ -209,7 +209,7 @@
         self.skipped_profiles = set()
         host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000)
         host.trigger.add("messageSend", self.messageSendTrigger, priority=100000)
-        host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR)
+        host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR)  # FIXME: must be removed, must be done on per-message basis
         host.importMenu((MAIN_MENU, D_("Start/Refresh")), self._startRefresh, security_limit=0, help_string=D_("Start or refresh an OTR session"), type_=C.MENU_SINGLE)
         host.importMenu((MAIN_MENU, D_("End session")), self._endSession, security_limit=0, help_string=D_("Finish an OTR session"), type_=C.MENU_SINGLE)
         host.importMenu((MAIN_MENU, D_("Authenticate")), self._authenticate, security_limit=0, help_string=D_("Authenticate user/see your fingerprint"), type_=C.MENU_SINGLE)
@@ -237,6 +237,9 @@
 
         @param profile (str): %(doc_profile)s
         """
+        # FIXME: should not be done per profile but per message, using extra data
+        #        for message received, profile wide hook may be need, but client
+        #        should be used anyway instead of a class attribute
         self.skipped_profiles.add(profile)
 
     @defer.inlineCallbacks
@@ -406,7 +409,7 @@
         return {'xmlui': confirm.toXml()}
 
     def _receivedTreatment(self, data, profile):
-        from_jid = jid.JID(data['from'])
+        from_jid = data['from']
         log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid)
         otrctx = self.context_managers[profile].getContextForUser(from_jid)
         encrypted = True
@@ -437,7 +440,7 @@
                 data['message'] = {'':res[0].decode('utf-8')} # FIXME: Q&D fix for message refactoring, message is now a dict
                 raise failure.Failure(exceptions.SkipHistory()) # we send the decrypted message to frontends, but we don't want it in history
             else:
-                raise failure.Failure(exceptions.CancelError()) # no message at all (no history, no signal)
+                raise failure.Failure(exceptions.CancelError('Cancelled by OTR')) # no message at all (no history, no signal)
 
     def _receivedTreatmentForSkippedProfiles(self, data, profile):
         """This profile must be skipped because the frontend manages OTR itself,
@@ -450,7 +453,7 @@
             raise failure.Failure(exceptions.SkipHistory())
         return data
 
-    def MessageReceivedTrigger(self, client, message, post_treat):
+    def MessageReceivedTrigger(self, client, message_elt, post_treat):
         profile = client.profile
         if profile in self.skipped_profiles:
             post_treat.addCallback(self._receivedTreatmentForSkippedProfiles, profile)
@@ -478,7 +481,7 @@
                         log.warning(u"No message found")
                         return False
                 otrctx.sendMessage(0, msg.encode('utf-8'))
-                self.host.sendMessageToBridge(mess_data, client)
+                self.host.messageSendToBridge(mess_data, client)
             else:
                 feedback = D_("Your message was not sent because your correspondent closed the encrypted conversation on his/her side. Either close your own side, or refresh the session.")
                 self.host.bridge.messageNew(to_jid.full(),
--- a/src/plugins/plugin_xep_0033.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/src/plugins/plugin_xep_0033.py	Mon Jun 20 18:41:53 2016 +0200
@@ -86,7 +86,7 @@
             def discoCallback(entities):
                 if not entities:
                     log.warning(_("XEP-0033 is being used but the server doesn't support it!"))
-                    raise failure.Failure(exceptions.CancelError())
+                    raise failure.Failure(exceptions.CancelError(u'Cancelled by XEP-0033'))
                 if mess_data["to"] not in entities:
                     expected = _(' or ').join([entity.userhost() for entity in entities])
                     log.warning(_(u"Stanzas using XEP-0033 should be addressed to %(expected)s, not %(current)s!") % {'expected': expected, 'current': mess_data["to"]})
@@ -99,7 +99,7 @@
                 # when the prosody plugin is completed, we can immediately return mess_data from here
                 self.sendAndStoreMessage(mess_data, entries, profile)
                 log.debug("XEP-0033 took over")
-                raise failure.Failure(exceptions.CancelError())
+                raise failure.Failure(exceptions.CancelError(u'Cancelled by XEP-0033'))
             d = self.host.findFeaturesSet([NS_ADDRESS], profile=profile)
             d.addCallbacks(discoCallback, lambda dummy: discoCallback(None))
             return d
@@ -124,9 +124,9 @@
             client = self.host.profiles[profile]
             d = defer.Deferred()
             if not skip_send:
-                d.addCallback(self.host._sendMessageToStream, client)
-            d.addCallback(self.host._storeMessage, client)
-            d.addCallback(self.host.sendMessageToBridge, client)
+                d.addCallback(self.host.messageSendToStream, client)
+            d.addCallback(self.host.messageAddToHistory, client)
+            d.addCallback(self.host.messageSendToBridge, client)
             d.addErrback(lambda failure: failure.trap(exceptions.CancelError))
             return d.callback(mess_data)
 
--- a/src/plugins/plugin_xep_0045.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/src/plugins/plugin_xep_0045.py	Mon Jun 20 18:41:53 2016 +0200
@@ -23,10 +23,13 @@
 log = getLogger(__name__)
 from twisted.internet import defer
 from twisted.words.protocols.jabber import jid
+from dateutil.tz import tzutc
 
 from sat.core import exceptions
 from sat.memory import memory
 
+import calendar
+import time
 import uuid
 import copy
 
@@ -50,6 +53,10 @@
 
 NS_MUC = 'http://jabber.org/protocol/muc'
 AFFILIATIONS = ('owner', 'admin', 'member', 'none', 'outcast')
+ROOM_USER_JOINED = 'ROOM_USER_JOINED'
+ROOM_USER_LEFT = 'ROOM_USER_LEFT'
+OCCUPANT_KEYS = ('nick', 'entity', 'affiliation', 'role')
+ENTITY_TYPE_MUC = "MUC"
 
 CONFIG_SECTION = u'plugin muc'
 
@@ -73,20 +80,18 @@
     def __init__(self, host):
         log.info(_("Plugin XEP_0045 initialization"))
         self.host = host
-        self.clients = {}
+        self.clients = {}  # FIXME: should be moved to profile's client
         self._sessions = memory.Sessions()
-        host.bridge.addMethod("joinMUC", ".plugin", in_sign='ssa{ss}s', out_sign='s', method=self._join, async=True)
+        host.bridge.addMethod("mucJoin", ".plugin", in_sign='ssa{ss}s', out_sign='s', method=self._join, async=True)
         host.bridge.addMethod("mucNick", ".plugin", in_sign='sss', out_sign='', method=self.mucNick)
         host.bridge.addMethod("mucLeave", ".plugin", in_sign='ss', out_sign='', method=self.mucLeave, async=True)
-        host.bridge.addMethod("getRoomsJoined", ".plugin", in_sign='s', out_sign='a(sass)', method=self.getRoomsJoined)
+        host.bridge.addMethod("getRoomsJoined", ".plugin", in_sign='s', out_sign='a(sa{sa{ss}}ss)', method=self.getRoomsJoined)
         host.bridge.addMethod("getRoomsSubjects", ".plugin", in_sign='s', out_sign='a(ss)', method=self.getRoomsSubjects)
         host.bridge.addMethod("getUniqueRoomName", ".plugin", in_sign='ss', out_sign='s', method=self._getUniqueName)
         host.bridge.addMethod("configureRoom", ".plugin", in_sign='ss', out_sign='s', method=self._configureRoom, async=True)
         host.bridge.addMethod("getDefaultMUC", ".plugin", in_sign='', out_sign='s', method=self.getDefaultMUC)
-        host.bridge.addSignal("roomJoined", ".plugin", signature='sasss')  # args: room_jid, room_nicks, user_nick, profile
+        host.bridge.addSignal("roomJoined", ".plugin", signature='sa{sa{ss}}sss')  # args: room_jid, occupants, user_nick, subject, profile
         host.bridge.addSignal("roomLeft", ".plugin", signature='ss')  # args: room_jid, profile
-        host.bridge.addSignal("roomUserJoined", ".plugin", signature='ssa{ss}s')  # args: room_jid, user_nick, user_data, profile
-        host.bridge.addSignal("roomUserLeft", ".plugin", signature='ssa{ss}s')  # args: room_jid, user_nick, user_data, profile
         host.bridge.addSignal("roomUserChangedNick", ".plugin", signature='ssss')  # args: room_jid, old_nick, new_nick, profile
         host.bridge.addSignal("roomNewSubject", ".plugin", signature='sss')  # args: room_jid, subject, profile
         self.__submit_conf_id = host.registerCallback(self._submitConfiguration, with_data=True)
@@ -98,6 +103,7 @@
             log.info(_("Text commands not available"))
 
         host.trigger.add("presence_available", self.presenceTrigger)
+        host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=1000000)
 
     def profileConnected(self, profile):
         def assign_service(service):
@@ -105,6 +111,23 @@
             client.muc_service = service
         return self.getMUCService(profile=profile).addCallback(assign_service)
 
+    def MessageReceivedTrigger(self, client, message_elt, post_treat):
+        if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT:
+            if message_elt.subject or message_elt.delay:
+                return False
+            from_jid = jid.JID(message_elt['from'])
+            room_jid = from_jid.userhostJID()
+            if room_jid in self.clients[client.profile].joined_rooms:
+                room = self.clients[client.profile].joined_rooms[room_jid]
+                if not room._room_ok:
+                    log.warning(u"Received non delayed message in a room before its initialisation: {}".format(message_elt.toXml()))
+                    room._cache.append(message_elt)
+                    return False
+            else:
+                log.warning(u"Received groupchat message for a room which has not been joined, ignoring it: {}".format(message_elt.toXml()))
+                return False
+        return True
+
     def checkClient(self, profile):
         """Check if the profile is connected and has used the MUC feature.
 
@@ -132,29 +155,23 @@
             raise UnknownRoom("This room has not been joined")
         return profile
 
-    def __room_joined(self, room, profile):
+    def _joinCb(self, room, profile):
         """Called when the user is in the requested room"""
-
-        def _sendBridgeSignal(ignore=None):
-            self.host.bridge.roomJoined(room.roomJID.userhost(), [user.nick for user in room.roster.values()], room.nick, profile)
-
-        self.clients[profile].joined_rooms[room.roomJID] = room
         if room.locked:
             # FIXME: the current behaviour is to create an instant room
             # and send the signal only when the room is unlocked
             # a proper configuration management should be done
             print "room locked !"
-            self.clients[profile].configure(room.roomJID, {}).addCallbacks(_sendBridgeSignal, lambda x: log.error(_(u'Error while configuring the room')))
-        else:
-            _sendBridgeSignal()
+            d = self.clients[profile].configure(room.roomJID, {})
+            d.addErrback(lambda dummy: log.error(_(u'Error while configuring the room')))
         return room
 
-    def __err_joining_room(self, failure, room_jid, nick, history_options, password, profile):
+    def _joinEb(self, failure, room_jid, nick, password, profile):
         """Called when something is going wrong when joining the room"""
         if hasattr(failure.value, "condition") and failure.value.condition == 'conflict':
             # we have a nickname conflict, we try again with "_" suffixed to current nickname
             nick += '_'
-            return self.clients[profile].join(room_jid, nick, history_options, password).addCallbacks(self.__room_joined, self.__err_joining_room, callbackKeywords={'profile': profile}, errbackArgs=[room_jid, nick, history_options, password, profile])
+            return self.clients[profile].join(room_jid, nick, password).addCallbacks(self._joinCb, self._joinEb, callbackKeywords={'profile': profile}, errbackArgs=[room_jid, nick, password, profile])
         mess = D_("Error while joining the room %s" % room_jid.userhost())
         try:
             mess += " with condition '%s'" % failure.value.condition
@@ -164,6 +181,11 @@
         self.host.bridge.newAlert(mess, D_("Group chat error"), "ERROR", profile)
         raise failure
 
+    @staticmethod
+    def _getOccupants(room):
+        """Get occupants of a room in a form suitable for bridge"""
+        return {u.nick: {k:unicode(getattr(u,k) or '') for k in OCCUPANT_KEYS} for u in room.roster.values()}
+
     def isRoom(self, entity_bare, profile_key):
         """Tell if a bare entity is a MUC room.
 
@@ -181,7 +203,8 @@
         if not self.checkClient(profile):
             return result
         for room in self.clients[profile].joined_rooms.values():
-            result.append((room.roomJID.userhost(), [user.nick for user in room.roster.values()], room.nick))
+            if room._room_ok:
+                result.append((room.roomJID.userhost(), self._getOccupants(room), room.nick, room.subject))
         return result
 
     def getRoomNick(self, room_jid, profile_key=C.PROF_KEY_NONE):
@@ -301,6 +324,7 @@
 
     def getRoomsSubjects(self, profile_key=C.PROF_KEY_NONE):
         """Return received subjects of rooms"""
+        # FIXME: to be removed
         profile = self.host.memory.getProfileName(profile_key)
         if not self.checkClient(profile):
             return []
@@ -355,45 +379,31 @@
         """
         return self.host.memory.getConfig(CONFIG_SECTION, 'default_muc', default_conf['default_muc'])
 
-    def join(self, room_jid, nick, options, profile_key=C.PROF_KEY_NONE):
+    def join(self, client, room_jid, nick, options):
         def _errDeferred(exc_obj=Exception, txt='Error while joining room'):
             d = defer.Deferred()
             d.errback(exc_obj(txt))
             return d
 
-        profile = self.host.memory.getProfileName(profile_key)
-        if not self.checkClient(profile):
-            return _errDeferred()
-        if room_jid in self.clients[profile].joined_rooms:
-            log.warning(_(u'%(profile)s is already in room %(room_jid)s') % {'profile': profile, 'room_jid': room_jid.userhost()})
+        if room_jid in self.clients[client.profile].joined_rooms:
+            log.warning(_(u'%(profile)s is already in room %(room_jid)s') % {'profile': client.profile, 'room_jid': room_jid.userhost()})
             return _errDeferred(AlreadyJoinedRoom, D_(u"The room has already been joined"))
-        log.info(_(u"[%(profile)s] is joining room %(room)s with nick %(nick)s") % {'profile': profile, 'room': room_jid.userhost(), 'nick': nick})
+        log.info(_(u"[%(profile)s] is joining room %(room)s with nick %(nick)s") % {'profile': client.profile, 'room': room_jid.userhost(), 'nick': nick})
 
-        if "history" in options:
-            history_limit = int(options["history"])
-        else:
-            history_limit = int(self.host.memory.getParamA(C.HISTORY_LIMIT, 'General', profile_key=profile))
-        # http://xmpp.org/extensions/xep-0045.html#enter-managehistory
-        history_options = muc.HistoryOptions(maxStanzas=history_limit)
         password = options["password"] if "password" in options else None
 
-        return self.clients[profile].join(room_jid, nick, history_options, password).addCallbacks(self.__room_joined, self.__err_joining_room, callbackKeywords={'profile': profile}, errbackArgs=[room_jid, nick, history_options, password, profile])
-        # FIXME: how to set the cancel method on the Deferred created by wokkel?
-        # This happens when the room is not reachable, e.g. no internet connection:
-        # > /usr/local/lib/python2.7/dist-packages/twisted/internet/defer.py(480)_startRunCallbacks()
-        # -> raise AlreadyCalledError(extra)
+        return self.clients[client.profile].join(room_jid, nick, password).addCallbacks(self._joinCb, self._joinEb, callbackKeywords={'profile': client.profile}, errbackArgs=[room_jid, nick, password, client.profile])
 
     def _join(self, room_jid_s, nick, options=None, profile_key=C.PROF_KEY_NONE):
-        """join method used by bridge: use the join method, but doesn't return any deferred
+        """join method used by bridge
+
         @return: unicode (the room bare)
         """
+        client = self.host.getClient(profile_key)
         if options is None:
             options = {}
-        profile = self.host.memory.getProfileName(profile_key)
-        if not self.checkClient(profile):
-            return
         if room_jid_s:
-            muc_service = self.host.getClient(profile).muc_service
+            muc_service = self.host.getClient(client.profile).muc_service
             try:
                 room_jid = jid.JID(room_jid_s)
             except (RuntimeError, jid.InvalidFormat, AttributeError):
@@ -401,9 +411,9 @@
             if not room_jid.user:
                 room_jid.user, room_jid.host = room_jid.host, muc_service
         else:
-            room_jid = self.getUniqueName(profile_key=profile_key)
+            room_jid = self.getUniqueName(profile_key=client.profile)
         # TODO: error management + signal in bridge
-        d = self.join(room_jid, nick, options, profile)
+        d = self.join(client, room_jid, nick, options)
         return d.addCallback(lambda room: room.roomJID.userhost())
 
     def nick(self, room_jid, nick, profile_key):
@@ -477,7 +487,7 @@
 
     # Text commands #
 
-    def cmd_nick(self, mess_data, profile):
+    def cmd_nick(self, client, mess_data):
         """change nickname
 
         @command (group): new_nick
@@ -486,11 +496,11 @@
         nick = mess_data["unparsed"].strip()
         if nick:
             room = mess_data["to"]
-            self.nick(room, nick, profile)
+            self.nick(room, nick, client.profile)
 
         return False
 
-    def cmd_join(self, mess_data, profile):
+    def cmd_join(self, client, mess_data):
         """join a new room
 
         @command (all): JID
@@ -498,13 +508,13 @@
         """
         if mess_data["unparsed"].strip():
             room_jid = self.host.plugins[C.TEXT_CMDS].getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host)
-            nick = (self.getRoomNick(room_jid, profile) or
-                    self.host.getClient(profile).jid.user)
-            self.join(room_jid, nick, {}, profile)
+            nick = (self.getRoomNick(room_jid, client.profile) or
+                    self.host.getClient(client.profile).jid.user)
+            self.join(client, room_jid, nick, {})
 
         return False
 
-    def cmd_leave(self, mess_data, profile):
+    def cmd_leave(self, client, mess_data):
         """quit a room
 
         @command (group): [ROOM_JID]
@@ -515,19 +525,19 @@
         else:
             room = mess_data["to"]
 
-        self.leave(room, profile)
+        self.leave(room, client.profile)
 
         return False
 
-    def cmd_part(self, mess_data, profile):
+    def cmd_part(self, client, mess_data):
         """just a synonym of /leave
 
         @command (group): [ROOM_JID]
             - ROOM_JID: jid of the room to live (current room if not specified)
         """
-        return self.cmd_leave(mess_data, profile)
+        return self.cmd_leave(client, mess_data)
 
-    def cmd_kick(self, mess_data, profile):
+    def cmd_kick(self, client, mess_data):
         """kick a room member
 
         @command (group): ROOM_NICK
@@ -536,24 +546,24 @@
         options = mess_data["unparsed"].strip().split()
         try:
             nick = options[0]
-            assert(self.isNickInRoom(mess_data["to"], nick, profile))
+            assert(self.isNickInRoom(mess_data["to"], nick, client.profile))
         except (IndexError, AssertionError):
             feedback = _(u"You must provide a member's nick to kick.")
-            self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile)
+            self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data)
             return False
 
-        d = self.kick(nick, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, profile)
+        d = self.kick(nick, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, client.profile)
 
         def cb(dummy):
             feedback_msg = _(u'You have kicked {}').format(nick)
             if len(options) > 1:
                 feedback_msg += _(u' for the following reason: {}').format(options[1])
-            self.host.plugins[C.TEXT_CMDS].feedBack(feedback_msg, mess_data, profile)
+            self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback_msg, mess_data)
             return True
         d.addCallback(cb)
         return d
 
-    def cmd_ban(self, mess_data, profile):
+    def cmd_ban(self, client, mess_data):
         """ban an entity from the room
 
         @command (group): (JID) [reason]
@@ -568,21 +578,21 @@
             assert(entity_jid.host)
         except (RuntimeError, jid.InvalidFormat, AttributeError, IndexError, AssertionError):
             feedback = _(u"You must provide a valid JID to ban, like in '/ban contact@example.net'")
-            self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile)
+            self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data)
             return False
 
-        d = self.ban(entity_jid, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, profile)
+        d = self.ban(entity_jid, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, client.profile)
 
         def cb(dummy):
             feedback_msg = _(u'You have banned {}').format(entity_jid)
             if len(options) > 1:
                 feedback_msg += _(u' for the following reason: {}').format(options[1])
-            self.host.plugins[C.TEXT_CMDS].feedBack(feedback_msg, mess_data, profile)
+            self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback_msg, mess_data)
             return True
         d.addCallback(cb)
         return d
 
-    def cmd_affiliate(self, mess_data, profile):
+    def cmd_affiliate(self, client, mess_data):
         """affiliate an entity to the room
 
         @command (group): (JID) [owner|admin|member|none|outcast]
@@ -601,25 +611,25 @@
             assert(entity_jid.host)
         except (RuntimeError, jid.InvalidFormat, AttributeError, IndexError, AssertionError):
             feedback = _(u"You must provide a valid JID to affiliate, like in '/affiliate contact@example.net member'")
-            self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile)
+            self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data)
             return False
 
         affiliation = options[1] if len(options) > 1 else 'none'
         if affiliation not in AFFILIATIONS:
             feedback = _(u"You must provide a valid affiliation: %s") % ' '.join(AFFILIATIONS)
-            self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile)
+            self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data)
             return False
 
-        d = self.affiliate(entity_jid, mess_data["to"], {'affiliation': affiliation}, profile)
+        d = self.affiliate(entity_jid, mess_data["to"], {'affiliation': affiliation}, client.profile)
 
         def cb(dummy):
             feedback_msg = _(u'New affiliation for %(entity)s: %(affiliation)s').format(entity=entity_jid, affiliation=affiliation)
-            self.host.plugins[C.TEXT_CMDS].feedBack(feedback_msg, mess_data, profile)
+            self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback_msg, mess_data)
             return True
         d.addCallback(cb)
         return d
 
-    def cmd_title(self, mess_data, profile):
+    def cmd_title(self, client, mess_data):
         """change room's subject
 
         @command (group): title
@@ -629,28 +639,28 @@
 
         if subject:
             room = mess_data["to"]
-            self.subject(room, subject, profile)
+            self.subject(room, subject, client.profile)
 
         return False
 
-    def cmd_topic(self, mess_data, profile):
+    def cmd_topic(self, client, mess_data):
         """just a synonym of /title
 
         @command (group): title
             - title: new room subject
         """
-        return self.cmd_title(mess_data, profile)
+        return self.cmd_title(client, mess_data)
 
-    def _whois(self, whois_msg, mess_data, target_jid, profile):
+    def _whois(self, client, whois_msg, mess_data, target_jid):
         """ Add MUC user information to whois """
         if mess_data['type'] != "groupchat":
             return
-        if target_jid.userhostJID() not in self.clients[profile].joined_rooms:
+        if target_jid.userhostJID() not in self.clients[client.profile].joined_rooms:
             log.warning(_("This room has not been joined"))
             return
         if not target_jid.resource:
             return
-        user = self.clients[profile].joined_rooms[target_jid.userhostJID()].getUser(target_jid.resource)
+        user = self.clients[client.profile].joined_rooms[target_jid.userhostJID()].getUser(target_jid.resource)
         whois_msg.append(_("Nickname: %s") % user.nick)
         if user.entity:
             whois_msg.append(_("Entity: %s") % user.entity)
@@ -681,16 +691,40 @@
         self.host = plugin_parent.host
         muc.MUCClient.__init__(self)
         self.rec_subjects = {}
-        self.__changing_nicks = set()  # used to keep trace of who is changing nick,
-                                       # and to discard userJoinedRoom signal in this case
+        self._changing_nicks = set()  # used to keep trace of who is changing nick,
+                                      # and to discard userJoinedRoom signal in this case
         print "init SatMUCClient OK"
 
     @property
     def joined_rooms(self):
         return self._rooms
 
-    def subject(self, room, subject):
-        return muc.MUCClientProtocol.subject(self, room, subject)
+    def _addRoom(self, room):
+        super(SatMUCClient, self)._addRoom(room)
+        room._roster_ok = False
+        room._room_ok = None  # False when roster, history and subject are available
+                              # True when new messages are saved to database
+        room._history_d = defer.Deferred()  # use to send bridge signal once backlog are written in history
+        room._history_d.callback(None)
+        room._cache = []
+
+    def _gotLastDbHistory(self, mess_data_list, room_jid, nick, password):
+        if mess_data_list:
+            timestamp = mess_data_list[0][1]
+            # we use seconds since last message to get backlog without duplicates
+            # and we remove 1 second to avoid getting the last message again
+            seconds = int(time.time() - timestamp) - 1
+        else:
+            seconds = None
+        d = super(SatMUCClient, self).join(room_jid, nick, muc.HistoryOptions(seconds=seconds), password)
+        return d
+
+    def join(self, room_jid, nick, password=None):
+        d = self.host.memory.historyGet(self.parent.jid.userhostJID(), room_jid, 1, True, profile=self.parent.profile)
+        d.addCallback(self._gotLastDbHistory, room_jid, nick, password)
+        return d
+
+    ## presence/roster ##
 
     def availableReceived(self, presence):
         """
@@ -698,7 +732,6 @@
         """
         # XXX: we override MUCClient.availableReceived to fix bugs
         # (affiliation and role are not set)
-        # FIXME: propose a patch upstream
 
         room, user = self._getRoomUser(presence)
 
@@ -720,9 +753,9 @@
         else:
             room.addUser(user)
             self.userJoinedRoom(room, user)
+
     def unavailableReceived(self, presence):
         # XXX: we override this method to manage nickname change
-        # TODO: feed this back to Wokkel
         """
         Unavailable presence was received.
 
@@ -737,22 +770,49 @@
         room.removeUser(user)
 
         if muc.STATUS_CODE.NEW_NICK in presence.mucStatuses:
-            self.__changing_nicks.add(presence.nick)
+            self._changing_nicks.add(presence.nick)
             self.userChangedNick(room, user, presence.nick)
         else:
-            self.__changing_nicks.discard(presence.nick)
+            self._changing_nicks.discard(presence.nick)
             self.userLeftRoom(room, user)
 
     def userJoinedRoom(self, room, user):
-        self.host.memory.updateEntityData(room.roomJID, "type", "chatroom", profile_key=self.parent.profile)
-        if user.nick in self.__changing_nicks:
-            self.__changing_nicks.remove(user.nick)
-        else:
-            log.debug(_(u"user %(nick)s has joined room (%(room_id)s)") % {'nick': user.nick, 'room_id': room.occupantJID.userhost()})
-            if not self.host.trigger.point("MUC user joined", room, user, self.parent.profile):
-                return
-            user_data = {'entity': user.entity.full() if user.entity else '', 'affiliation': user.affiliation, 'role': user.role}
-            self.host.bridge.roomUserJoined(room.roomJID.userhost(), user.nick, user_data, self.parent.profile)
+        if user.nick == room.nick:
+            # we have received our own nick, this mean that the full room roster was received
+            room._roster_ok = True
+            log.debug(u"room {room} joined with nick {nick}".format(room=room.occupantJID.userhost(), nick=user.nick))
+            # We set type so we don't have use a deferred with disco to check entity type
+            self.host.memory.updateEntityData(room.roomJID, C.ENTITY_TYPE, ENTITY_TYPE_MUC, profile_key=self.parent.profile)
+
+        elif room._roster_ok:
+            try:
+                self._changing_nicks.remove(user.nick)
+            except KeyError:
+                # this is a new user
+                log.debug(_(u"user {nick} has joined room {room_id}").format(nick=user.nick, room_id=room.occupantJID.userhost()))
+                if not self.host.trigger.point("MUC user joined", room, user, self.parent.profile):
+                    return
+
+                extra = {'info_type': ROOM_USER_JOINED,
+                         'user_affiliation': user.affiliation,
+                         'user_role': user.role,
+                         'user_nick': user.nick
+                         }
+                if user.entity is not None:
+                    extra['user_entity'] = user.entity.full()
+                mess_data = {  # dict is similar to the one used in client.onMessage
+                    "from": room.roomJID,
+                    "to": self.parent.jid,
+                    "uid": unicode(uuid.uuid4()),
+                    "message": {'': D_(u"=> {} has joined the room").format(user.nick)},
+                    "subject": {},
+                    "type": C.MESS_TYPE_INFO,
+                    "extra": extra,
+                    "timestamp": time.time(),
+                }
+                self.host.messageAddToHistory(mess_data, self.parent)
+                self.host.messageSendToBridge(mess_data, self.parent)
+
 
     def userLeftRoom(self, room, user):
         if not self.host.trigger.point("MUC user left", room, user, self.parent.profile):
@@ -760,14 +820,31 @@
         if user.nick == room.nick:
             # we left the room
             room_jid_s = room.roomJID.userhost()
-            log.info(_(u"Room [%(room)s] left (%(profile)s))") % {"room": room_jid_s,
-                                                                 "profile": self.parent.profile})
+            log.info(_(u"Room ({room}) left ({profile})").format(
+                room = room_jid_s, profile = self.parent.profile))
             self.host.memory.delEntityCache(room.roomJID, profile_key=self.parent.profile)
             self.host.bridge.roomLeft(room.roomJID.userhost(), self.parent.profile)
         else:
-            log.debug(_(u"user %(nick)s left room (%(room_id)s)") % {'nick': user.nick, 'room_id': room.occupantJID.userhost()})
-            user_data = {'entity': user.entity.full() if user.entity else '', 'affiliation': user.affiliation, 'role': user.role}
-            self.host.bridge.roomUserLeft(room.roomJID.userhost(), user.nick, user_data, self.parent.profile)
+            log.debug(_(u"user {nick} left room {room_id}").format(nick=user.nick, room_id=room.occupantJID.userhost()))
+            extra = {'info_type': ROOM_USER_LEFT,
+                     'user_affiliation': user.affiliation,
+                     'user_role': user.role,
+                     'user_nick': user.nick
+                     }
+            if user.entity is not None:
+                extra['user_entity'] = user.entity.full()
+            mess_data = {  # dict is similar to the one used in client.onMessage
+                "from": room.roomJID,
+                "to": self.parent.jid,
+                "uid": unicode(uuid.uuid4()),
+                "message": {'': D_(u"<= {} has left the room").format(user.nick)},
+                "subject": {},
+                "type": C.MESS_TYPE_INFO,
+                "extra": extra,
+                "timestamp": time.time(),
+            }
+            self.host.messageAddToHistory(mess_data, self.parent)
+            self.host.messageSendToBridge(mess_data, self.parent)
 
     def userChangedNick(self, room, user, new_nick):
         self.host.bridge.roomUserChangedNick(room.roomJID.userhost(), user.nick, new_nick, self.parent.profile)
@@ -775,19 +852,111 @@
     def userUpdatedStatus(self, room, user, show, status):
         self.host.bridge.presenceUpdate(room.roomJID.userhost() + '/' + user.nick, show or '', 0, {C.PRESENCE_STATUSES_DEFAULT: status or ''}, self.parent.profile)
 
+    ## messages ##
+
     def receivedGroupChat(self, room, user, body):
         log.debug(u'receivedGroupChat: room=%s user=%s body=%s' % (room.roomJID.full(), user, body))
 
+    def _addToHistory(self, dummy, user, message):
+        # we check if message is not in history
+        # and raise ConflictError else
+        stamp = message.delay.stamp.astimezone(tzutc()).timetuple()
+        timestamp = float(calendar.timegm(stamp))
+        data = {  # dict is similar to the one used in client.onMessage
+            "from": message.sender,
+            "to": message.recipient,
+            "uid": unicode(uuid.uuid4()),
+            "type": C.MESS_TYPE_GROUPCHAT,
+            "extra": {},
+            "timestamp": timestamp,
+            "received_timestamp": unicode(time.time()),
+        }
+        # FIXME: message and subject don't handle xml:lang
+        data['message'] = {'': message.body} if message.body is not None else {}
+        data['subject'] = {'': message.subject} if message.subject is not None else {}
+
+        if data['message'] or data['subject']:
+            return self.host.memory.addToHistory(self.parent, data)
+        else:
+            return defer.succeed(None)
+
+    def _addToHistoryEb(self, failure):
+        failure.trap(exceptions.CancelError)
+
     def receivedHistory(self, room, user, message):
-        # http://xmpp.org/extensions/xep-0045.html#enter-history
-        # log.debug(u'receivedHistory: room=%s user=%s body=%s' % (room.roomJID.full(), user, message))
-        pass
+        """Called when history (backlog) message are received
+
+        we check if message is not already in our history
+        and add it if needed
+        @param room(muc.Room): room instance
+        @param user(muc.User, None): the user that sent the message
+            None if the message come from the room
+        @param message(muc.GroupChat): the parsed message
+        """
+        room._history_d.addCallback(self._addToHistory, user, message)
+        room._history_d.addErrback(self._addToHistoryEb)
+
+    ## subject ##
+
+    def groupChatReceived(self, message):
+        """
+        A group chat message has been received from a MUC room.
+
+        There are a few event methods that may get called here.
+        L{receivedGroupChat}, L{receivedSubject} or L{receivedHistory}.
+        """
+        # We override this method to fix subject handling
+        # FIXME: remove this merge fixed upstream
+        room, user = self._getRoomUser(message)
+
+        if room is None:
+            return
+
+        if message.subject is not None:
+            self.receivedSubject(room, user, message.subject)
+        elif message.delay is None:
+            self.receivedGroupChat(room, user, message)
+        else:
+            self.receivedHistory(room, user, message)
+
+    def subject(self, room, subject):
+        return muc.MUCClientProtocol.subject(self, room, subject)
+
+    def _historyCb(self, dummy, room):
+        self.host.bridge.roomJoined(
+            room.roomJID.userhost(),
+            XEP_0045._getOccupants(room),
+            room.nick,
+            room.subject,
+            self.parent.profile)
+        del room._history_d
+        cache = room._cache
+        del room._cache
+        room._room_ok = True
+        for elem in cache:
+            self.parent.xmlstream.dispatch(elem)
+
+
+    def _historyEb(self, failure_, room):
+        log.error(u"Error while managing history: {}".format(failure_))
+        self._historyCb(None, room)
 
     def receivedSubject(self, room, user, subject):
-        # http://xmpp.org/extensions/xep-0045.html#enter-subject
-        log.debug(_(u"New subject for room (%(room_id)s): %(subject)s") % {'room_id': room.roomJID.full(), 'subject': subject})
+        # when subject is received, we know that we have whole roster and history
+        # cf. http://xmpp.org/extensions/xep-0045.html#enter-subject
+        room.subject = subject  # FIXME: subject doesn't handle xml:lang
         self.rec_subjects[room.roomJID.userhost()] = (room.roomJID.userhost(), subject)
-        self.host.bridge.roomNewSubject(room.roomJID.userhost(), subject, self.parent.profile)
+        if room._room_ok is None:
+            # this is the first subject we receive
+            # that mean that we have received everything we need
+            room._room_ok = False
+            room._history_d.addCallbacks(self._historyCb, self._historyEb, [room], errbackArgs=[room])
+        else:
+            # the subject has been changed
+            log.debug(_(u"New subject for room ({room_id}): {subject}").format(room_id = room.roomJID.full(), subject = subject))
+            self.host.bridge.roomNewSubject(room.roomJID.userhost(), subject, self.parent.profile)
+
+    ## disco ##
 
     def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
         return [disco.DiscoFeature(NS_MUC)]
--- a/src/plugins/plugin_xep_0048.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/src/plugins/plugin_xep_0048.py	Mon Jun 20 18:41:53 2016 +0200
@@ -88,7 +88,7 @@
                 for (room_jid, data) in bookmarks[XEP_0048.MUC_TYPE].items():
                     if data.get('autojoin', 'false') == 'true':
                         nick = data.get('nick', client.jid.user)
-                        self.host.plugins['XEP-0045'].join(room_jid, nick, {}, profile_key=client.profile)
+                        self.host.plugins['XEP-0045'].join(client, room_jid, nick, {})
 
     @defer.inlineCallbacks
     def _getServerBookmarks(self, storage_type, profile):
@@ -219,9 +219,10 @@
             log.warning(_("No room jid selected"))
             return {}
 
-        d = self.host.plugins['XEP-0045'].join(room_jid, nick, {}, profile_key=profile)
+        client = self.host.getClient(profile)
+        d = self.host.plugins['XEP-0045'].join(client, room_jid, nick, {})
         def join_eb(failure):
-            log.warning(u"Error while trying to join room: %s" % failure)
+            log.warning(u"Error while trying to join room: {}".format(failure))
             # FIXME: failure are badly managed in plugin XEP-0045. Plugin XEP-0045 need to be fixed before managing errors correctly here
             return {}
         d.addCallbacks(lambda dummy: {}, join_eb)
@@ -417,33 +418,32 @@
             location = jid.JID(location)
         return self.addBookmark(type_, location, data, storage_type, profile_key)
 
-    def cmd_bookmark(self, mess_data, profile):
+    def cmd_bookmark(self, client, mess_data):
         """(Un)bookmark a MUC room
 
         @command (group): [autojoin | remove]
             - autojoin: join room automatically on connection
             - remove: remove bookmark(s) for this room
         """
-        client = self.host.getClient(profile)
         txt_cmd = self.host.plugins[C.TEXT_CMDS]
 
         options = mess_data["unparsed"].strip().split()
         if options and options[0] not in ('autojoin', 'remove'):
-            txt_cmd.feedBack(_("Bad arguments"), mess_data, profile)
+            txt_cmd.feedBack(client, _("Bad arguments"), mess_data)
             return False
 
         room_jid = mess_data["to"].userhostJID()
 
         if "remove" in options:
-            self.removeBookmark(XEP_0048.MUC_TYPE, room_jid, profile_key = profile)
-            txt_cmd.feedBack(_("All [%s] bookmarks are being removed") % room_jid.full(), mess_data, profile)
+            self.removeBookmark(XEP_0048.MUC_TYPE, room_jid, profile_key = client.profile)
+            txt_cmd.feedBack(client, _("All [%s] bookmarks are being removed") % room_jid.full(), mess_data)
             return False
 
         data = { "name": room_jid.user,
                  "nick": client.jid.user,
                  "autojoin": "true" if "autojoin" in options else "false",
                }
-        self.addBookmark(XEP_0048.MUC_TYPE, room_jid, data, profile_key=profile)
-        txt_cmd.feedBack(_("Bookmark added"), mess_data, profile)
+        self.addBookmark(XEP_0048.MUC_TYPE, room_jid, data, profile_key=client.profile)
+        txt_cmd.feedBack(client, _("Bookmark added"), mess_data)
 
         return False
--- a/src/plugins/plugin_xep_0071.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/src/plugins/plugin_xep_0071.py	Mon Jun 20 18:41:53 2016 +0200
@@ -18,6 +18,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from sat.core.i18n import _
+from sat.core.constants import Const as C
 from sat.core import exceptions
 from sat.core.log import getLogger
 log = getLogger(__name__)
@@ -91,7 +92,7 @@
         @return: the data with the extra parameter updated
         """
         # TODO: check if text only body is empty, then try to convert XHTML-IM to pure text and show a warning message
-        def converted(xhtml):
+        def converted(xhtml, lang):
             if lang:
                 data['extra']['xhtml_{}'.format(lang)] = xhtml
             else:
@@ -99,7 +100,7 @@
 
         defers = []
         for body_elt in body_elts:
-            lang = body_elt.getAttribute('xml:lang', '')
+            lang = body_elt.getAttribute((C.NS_XML, 'lang'), '')
             d = self._s.convert(body_elt.toXml(), self.SYNTAX_XHTML_IM, safe=True)
             d.addCallback(converted, lang)
             defers.append(d)
@@ -121,7 +122,7 @@
         def syntax_converted(xhtml_im, lang):
             body_elt = html_elt.addElement((NS_XHTML, 'body'))
             if lang:
-                body_elt['xml:lang'] = lang
+                body_elt[(C.NS_XML, 'lang')] = lang
                 data['extra']['xhtml_{}'.format(lang)] = xhtml_im
             else:
                 data['extra']['xhtml'] = xhtml_im
--- a/src/plugins/plugin_xep_0092.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/src/plugins/plugin_xep_0092.py	Mon Jun 20 18:41:53 2016 +0200
@@ -99,7 +99,7 @@
         return tuple(ret)
 
 
-    def _whois(self, whois_msg, mess_data, target_jid, profile):
+    def _whois(self, client, whois_msg, mess_data, target_jid):
         """ Add software/OS information to whois """
         def versionCb(version_data):
             name, version, os = version_data
@@ -116,7 +116,7 @@
             else:
                 whois_msg.append(_("Client software version request timeout"))
 
-        d = self.getVersion(target_jid, profile)
+        d = self.getVersion(target_jid, client.profile)
         d.addCallbacks(versionCb, versionEb)
         return d
 
--- a/src/plugins/plugin_xep_0249.py	Sun Jun 19 22:22:13 2016 +0200
+++ b/src/plugins/plugin_xep_0249.py	Mon Jun 20 18:41:53 2016 +0200
@@ -125,13 +125,9 @@
 
         @param room (jid.JID): JID of the room
         """
-        profile = self.host.memory.getProfileName(profile_key)
-        if not profile:
-            log.error(_("Profile doesn't exists !"))
-            return
-        log.info(_(u'Invitation accepted for room %(room)s [%(profile)s]') % {'room': room_jid.userhost(), 'profile': profile})
-        _jid, xmlstream = self.host.getJidNStream(profile)
-        d = self.host.plugins["XEP-0045"].join(room_jid, _jid.user, {}, profile)
+        client = self.host.getClient(profile_key)
+        log.info(_(u'Invitation accepted for room %(room)s [%(profile)s]') % {'room': room_jid.userhost(), 'profile': client.profile})
+        d = self.host.plugins["XEP-0045"].join(client, room_jid, client.jid.user, {})
         return d
 
     def onInvitation(self, message, profile):
@@ -165,23 +161,23 @@
             data = {"message": _("You have been invited by %(user)s to join the room %(room)s. Do you accept?") % {'user': from_jid_s, 'room': room_jid_s}, "title": _("MUC invitation")}
             self.host.askConfirmation(room_jid_s, "YES/NO", data, accept_cb, profile)
 
-    def cmd_invite(self, mess_data, profile):
+    def cmd_invite(self, client, mess_data):
         """invite someone in the room
 
         @command (group): JID
             - JID: the JID of the person to invite
         """
         contact_jid_s = mess_data["unparsed"].strip()
-        my_host = self.host.profiles[profile].jid.host
+        my_host = client.jid.host
         try:
             contact_jid = jid.JID(contact_jid_s)
         except (RuntimeError, jid.InvalidFormat, AttributeError):
             feedback = _(u"You must provide a valid JID to invite, like in '/invite contact@{host}'").format(host=my_host)
-            self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile)
+            self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data)
             return False
         if not contact_jid.user:
             contact_jid.user, contact_jid.host = contact_jid.host, my_host
-        self.invite(contact_jid, mess_data["to"], {}, profile)
+        self.invite(contact_jid, mess_data["to"], {}, client.profile)
         return False