diff src/browser/sat_browser/chat.py @ 679:a90cc8fc9605

merged branch frontends_multi_profiles
author Goffi <goffi@goffi.org>
date Wed, 18 Mar 2015 16:15:18 +0100
parents src/browser/sat_browser/panels.py@3eb3a2c0c011 src/browser/sat_browser/panels.py@849ffb24d5bf
children e876f493dccc
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/chat.py	Wed Mar 18 16:15:18 2015 +0100
@@ -0,0 +1,347 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from sat.core.log import getLogger
+log = getLogger(__name__)
+
+# from sat_frontends.tools.games import SYMBOLS
+from sat_frontends.tools import strings
+from sat_frontends.tools import jid
+from sat_frontends.quick_frontend import quick_widgets, quick_games, quick_menus
+from sat_frontends.quick_frontend.quick_chat import QuickChat
+from sat.core.i18n import _
+
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.Label import Label
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler
+from pyjamas.ui.HTMLPanel import HTMLPanel
+from pyjamas import DOM
+
+from datetime import datetime
+from time import time
+
+import html_tools
+import libervia_widget
+import base_panel
+import contact_panel
+import editor_widget
+import contact_list
+from constants import Const as C
+import plugin_xep_0085
+import game_tarot
+import game_radiocol
+
+
+unicode = str  # FIXME: pyjamas workaround
+
+
+class ChatText(HTMLPanel):
+
+    def __init__(self, nick, mymess, msg, extra):
+        try:
+            timestamp = float(extra['timestamp'])
+        except KeyError:
+            timestamp=None
+        xhtml = extra.get('xhtml')
+        _date = datetime.fromtimestamp(float(timestamp or time()))
+        _msg_class = ["chat_text_msg"]
+        if mymess:
+            _msg_class.append("chat_text_mymess")
+        HTMLPanel.__init__(self, "<span class='chat_text_timestamp'>%(timestamp)s</span> <span class='chat_text_nick'>%(nick)s</span> <span class='%(msg_class)s'>%(msg)s</span>" %
+                           {"timestamp": _date.strftime("%H:%M"),
+                            "nick": "[%s]" % html_tools.html_sanitize(nick),
+                            "msg_class": ' '.join(_msg_class),
+                            "msg": strings.addURLToText(html_tools.html_sanitize(msg)) if not xhtml else html_tools.inlineRoot(xhtml)}  # FIXME: images and external links must be removed according to preferences
+                           )
+        self.setStyleName('chatText')
+
+
+class Chat(QuickChat, libervia_widget.LiberviaWidget, KeyboardHandler):
+
+    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
+        """Panel used for conversation (one 2 one or group chat)
+
+        @param host: SatWebFrontend instance
+        @param target: entity (jid.JID) with who we have a conversation (contact's jid for one 2 one chat, or MUC room)
+        @param type: one2one for simple conversation, group for MUC
+        """
+        QuickChat.__init__(self, host, target, type_, profiles=profiles)
+        self.vpanel = VerticalPanel()
+        self.vpanel.setSize('100%', '100%')
+
+        # FIXME: temporary dirty initialization to display the OTR state
+        header_info = host.plugins['otr'].getInfoTextForUser(target) if (type_ == C.CHAT_ONE2ONE and 'otr' in host.plugins) else None
+
+        libervia_widget.LiberviaWidget.__init__(self, host, title=unicode(target.bare), info=header_info, selectable=True)
+        self._body = AbsolutePanel()
+        self._body.setStyleName('chatPanel_body')
+        chat_area = HorizontalPanel()
+        chat_area.setStyleName('chatArea')
+        if type_ == C.CHAT_GROUP:
+            self.occupants_panel = contact_panel.ContactsPanel(host, merge_resources=False,
+                                                               contacts_style="muc_contact",
+                                                               contacts_menus=(C.MENU_JID_CONTEXT),
+                                                               contacts_display=('resource',))
+            chat_area.add(self.occupants_panel)
+            DOM.setAttribute(chat_area.getWidgetTd(self.occupants_panel), "className", "occupantsPanelCell")
+        self._body.add(chat_area)
+        self.content = AbsolutePanel()
+        self.content.setStyleName('chatContent')
+        self.content_scroll = base_panel.ScrollPanelWrapper(self.content)
+        chat_area.add(self.content_scroll)
+        chat_area.setCellWidth(self.content_scroll, '100%')
+        self.vpanel.add(self._body)
+        self.vpanel.setCellHeight(self._body, '100%')
+        self.addStyleName('chatPanel')
+        self.setWidget(self.vpanel)
+        self.state_machine = plugin_xep_0085.ChatStateMachine(self.host, unicode(self.target))
+        self._state = None
+        self.refresh()
+        if type_ == C.CHAT_ONE2ONE:
+            self.historyPrint(profile=self.profile)
+
+    @property
+    def target(self):
+        # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickChat
+        # FIXME: must remove this when either pyjamas is fixed, or we use an alternative
+        if self.type == C.CHAT_GROUP:
+            return self.current_target.bare
+        return self.current_target
+
+    @property
+    def profile(self):
+        # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickWidget
+        # FIXME: must remove this when either pyjamas is fixed, or we use an alternative
+        assert len(self.profiles) == 1 and not self.PROFILES_MULTIPLE and not self.PROFILES_ALLOW_NONE
+        return list(self.profiles)[0]
+
+    @property
+    def plugin_menu_context(self):
+        return (C.MENU_ROOM,) if self.type == C.CHAT_GROUP else (C.MENU_SINGLE,)
+
+    # @classmethod
+    # def createPanel(cls, host, item, type_=C.CHAT_ONE2ONE):
+    #     assert(item)
+    #     _contact = item if isinstance(item, jid.JID) else jid.JID(item)
+    #     host.contact_panel.setContactMessageWaiting(_contact.bare, False)
+    #     _new_panel = Chat(host, _contact, type_)  # XXX: pyjamas doesn't seems to support creating with cls directly
+    #     _new_panel.historyPrint()
+    #     host.setSelected(_new_panel)
+    #     _new_panel.refresh()
+    #     return _new_panel
+
+    def refresh(self):
+        """Refresh the display of this widget. If the unibox is disabled,
+        add a message box at the bottom of the panel"""
+        # FIXME: must be checked
+        # self.host.contact_panel.setContactMessageWaiting(self.target.bare, False)
+        # self.content_scroll.scrollToBottom()
+
+        enable_box = self.host.uni_box is None
+        if hasattr(self, 'message_box'):
+            self.message_box.setVisible(enable_box)
+        elif enable_box:
+            self.message_box = editor_widget.MessageBox(self.host)
+            self.message_box.onSelectedChange(self)
+            self.message_box.addKeyboardListener(self)
+            self.vpanel.add(self.message_box)
+
+    def onKeyDown(self, sender, keycode, modifiers):
+        if keycode == KEY_ENTER:
+            self.host.showWarning(None, None)
+        else:
+            self.host.showWarning(*self.getWarningData())
+
+    def getWarningData(self):
+        if self.type not in [C.CHAT_ONE2ONE, C.CHAT_GROUP]:
+            raise Exception("Unmanaged type !")
+        if self.type == C.CHAT_ONE2ONE:
+            msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % self.target
+        elif self.type == C.CHAT_GROUP:
+            msg = "This message will be sent to all the participants of the multi-user room <span class='warningTarget'>%s</span>" % self.target
+        return ("ONE2ONE" if self.type == C.CHAT_ONE2ONE else "GROUP", msg)
+
+    def onTextEntered(self, text):
+        self.host.sendMessage(self.target,
+                              text,
+                              mess_type=C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT,
+                              errback=self.host.sendError,
+                              profile_key=C.PROF_KEY_NONE
+                              )
+        self.state_machine._onEvent("active")
+
+    def onQuit(self):
+        libervia_widget.LiberviaWidget.onQuit(self)
+        if self.type == C.CHAT_GROUP:
+            self.host.bridge.call('mucLeave', None, unicode(self.target.bare))
+
+    def setUserNick(self, nick):
+        """Set the nick of the user, usefull for e.g. change the color of the user"""
+        self.nick = nick
+
+    def setPresents(self, nicks):
+        """Set the occupants of a group chat.
+
+        @param nicks (list[unicode]): sorted list of nicknames
+        """
+        QuickChat.setPresents(self, nicks)
+        self.occupants_panel.setList([jid.JID(u"%s/%s" % (self.target, nick)) for nick in nicks])
+
+    def replaceUser(self, nick, show_info=True):
+        """Add user if it is not in the group list"""
+        QuickChat.replaceUser(self, nick, show_info)
+        occupant_jid = jid.JID("%s/%s" % (unicode(self.target), nick))
+        self.occupants_panel.addContact(occupant_jid)
+
+    def removeUser(self, nick, show_info=True):
+        """Remove a user from the group list"""
+        QuickChat.removeUser(self, nick, show_info)
+        occupant_jid = jid.JID("%s/%s" % (unicode(self.target), nick))
+        self.occupants_panel.removeContact(occupant_jid)
+
+    def changeUserNick(self, old_nick, new_nick):
+        assert self.type == C.CHAT_GROUP
+        # FIXME
+        # self.occupants_panel.removeOccupant(old_nick)
+        # self.occupants_panel.addOccupant(new_nick)
+        self.printInfo(_("%(old_nick)s is now known as %(new_nick)s") % {'old_nick': old_nick, 'new_nick': new_nick})
+
+    # def historyPrint(self, size=C.HISTORY_LIMIT_DEFAULT):
+    #     """Print the initial history"""
+    #     def getHistoryCB(history):
+    #         # display day change
+    #         day_format = "%A, %d %b %Y"
+    #         previous_day = datetime.now().strftime(day_format)
+    #         for line in history:
+    #             timestamp, from_jid_s, to_jid_s, message, mess_type, extra = line
+    #             message_day = datetime.fromtimestamp(float(timestamp or time())).strftime(day_format)
+    #             if previous_day != message_day:
+    #                 self.printInfo("* " + message_day)
+    #                 previous_day = message_day
+    #             self.printMessage(jid.JID(from_jid_s), message, extra, timestamp)
+    #     self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, self.target.bare, size, True)
+
+    def printInfo(self, msg, type_='normal', extra=None, link_cb=None):
+        """Print general info
+        @param msg: message to print
+        @param type_: one of:
+            "normal": general info like "toto has joined the room" (will be sanitized)
+            "link": general info that is clickable like "click here to join the main room" (no sanitize done)
+            "me": "/me" information like "/me clenches his fist" ==> "toto clenches his fist" (will stay on one line)
+        @param extra (dict): message data
+        @param link_cb: method to call when the info is clicked, ignored if type_ is not 'link'
+        """
+        if extra is None:
+            extra = {}
+        if type_ == 'normal':
+            _wid = HTML(strings.addURLToText(html_tools.XHTML2Text(msg)))
+            _wid.setStyleName('chatTextInfo')
+        elif type_ == 'link':
+            _wid = HTML(msg)
+            _wid.setStyleName('chatTextInfo-link')
+            if link_cb:
+                _wid.addClickListener(link_cb)
+        elif type_ == 'me':
+            _wid = Label(msg)
+            _wid.setStyleName('chatTextMe')
+        else:
+            raise ValueError("Unknown printInfo type %s" % type_)
+        self.content.add(_wid)
+        self.content_scroll.scrollToBottom()
+
+    def printMessage(self, from_jid, msg, extra=None, profile=C.PROF_KEY_NONE):
+        if extra is None:
+            extra = {}
+        try:
+            nick, mymess = QuickChat.printMessage(self, from_jid, msg, extra, profile)
+        except TypeError:
+            # None is returned, the message is managed
+            return
+        self.content.add(ChatText(nick, mymess, msg, extra))
+        self.content_scroll.scrollToBottom()
+
+    def setState(self, state, nick=None):
+        """Set the chat state (XEP-0085) of the contact. Leave nick to None
+        to set the state for a one2one conversation, or give a nickname or
+        C.ALL_OCCUPANTS to set the state of a participant within a MUC.
+        @param state: the new chat state
+        @param nick: ignored for one2one, otherwise the MUC user nick or C.ALL_OCCUPANTS
+        """
+        return # FIXME
+        if self.type == C.CHAT_GROUP:
+            assert(nick)
+            if nick == C.ALL_OCCUPANTS:
+                occupants = self.occupants_panel.occupants_panel.keys()
+            else:
+                occupants = [nick] if nick in self.occupants_panel.occupants_panel else []
+            for occupant in occupants:
+                self.occupants_panel.occupants_panel[occupant].setState(state)
+        else:
+            self._state = state
+            self.refreshTitle()
+        self.state_machine.started = not not state  # start to send "composing" state from now
+
+    def refreshTitle(self):
+        """Refresh the title of this Chat dialog"""
+        title = unicode(self.target.bare)
+        if self._state:
+            title += " (%s)".format(self._state)
+        self.setTitle(title)
+
+    def setConnected(self, jid_s, resource, availability, priority, statuses):
+        """Set connection status
+        @param jid_s (unicode): JID userhost as unicode
+        """
+        raise Exception("should not be there") # FIXME
+        assert(jid_s == self.target.bare)
+        if self.type != C.CHAT_GROUP:
+            return
+        box = self.occupants_panel.getOccupantBox(resource)
+        if box:
+            contact_list.setPresenceStyle(box, availability)
+
+    def updateChatState(self, from_jid, state):
+        #TODO
+        pass
+
+    def addGamePanel(self, widget):
+        """Insert a game panel to this Chat dialog.
+
+        @param widget (Widget): the game panel
+        """
+        self.vpanel.insert(widget, 0)
+        self.vpanel.setCellHeight(widget, widget.getHeight())
+
+    def removeGamePanel(self, widget):
+        """Remove the game panel from this Chat dialog.
+
+        @param widget (Widget): the game panel
+        """
+        self.vpanel.remove(widget)
+
+
+quick_widgets.register(QuickChat, Chat)
+quick_widgets.register(quick_games.Tarot, game_tarot.TarotPanel)
+quick_widgets.register(quick_games.Radiocol, game_radiocol.RadioColPanel)
+libervia_widget.LiberviaWidget.addDropKey("CONTACT", lambda host, item: host.displayWidget(Chat, jid.JID(item), dropped=True))
+quick_menus.QuickMenusManager.addDataCollector(C.MENU_ROOM, {'room_jid': 'target'})
+quick_menus.QuickMenusManager.addDataCollector(C.MENU_SINGLE, {'jid': 'target'})