diff browser/sat_browser/chat.py @ 1124:28e3eb3bb217

files reorganisation and installation rework: - files have been reorganised to follow other SàT projects and usual Python organisation (no more "/src" directory) - VERSION file is now used, as for other SàT projects - replace the overcomplicated setup.py be a more sane one. Pyjamas part is not compiled anymore by setup.py, it must be done separatly - removed check for data_dir if it's empty - installation tested working in virtual env - libervia launching script is now in bin/libervia
author Goffi <goffi@goffi.org>
date Sat, 25 Aug 2018 17:59:48 +0200
parents src/browser/sat_browser/chat.py@f2170536ba23
children 2af117bfe6cc
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/chat.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,345 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from sat.core.log import getLogger
+log = getLogger(__name__)
+
+# from sat_frontends.tools.games import SYMBOLS
+from sat_browser import strings
+from sat_frontends.tools import jid
+from sat_frontends.quick_frontend import quick_widgets, quick_games, quick_menus
+from sat_frontends.quick_frontend.quick_chat import QuickChat
+
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler
+from pyjamas.ui.HTMLPanel import HTMLPanel
+from pyjamas import DOM
+from pyjamas import Window
+
+from datetime import datetime
+
+import html_tools
+import libervia_widget
+import base_panel
+import contact_panel
+import editor_widget
+from constants import Const as C
+import plugin_xep_0085
+import game_tarot
+import game_radiocol
+
+
+unicode = str  # FIXME: pyjamas workaround
+
+
+class MessageWidget(HTMLPanel):
+
+    def __init__(self, mess_data):
+        """
+        @param mess_data(quick_chat.Message, None): message data
+            None: used only for non text widgets (e.g.: focus separator)
+        """
+        self.mess_data = mess_data
+        mess_data.widgets.add(self)
+        _msg_class = []
+        if mess_data.type == C.MESS_TYPE_INFO:
+            markup = "<span class='{msg_class}'>{msg}</span>"
+
+            if mess_data.extra.get('info_type') == 'me':
+                _msg_class.append('chatTextMe')
+            else:
+                _msg_class.append('chatTextInfo')
+            # FIXME: following code was in printInfo before refactoring
+            #        seems to be used only in radiocol
+            # elif type_ == 'link':
+            #     _wid = HTML(msg)
+            #     _wid.setStyleName('chatTextInfo-link')
+            #     if link_cb:
+            #         _wid.addClickListener(link_cb)
+        else:
+            markup = "<span class='chat_text_timestamp'>{timestamp}</span> <span class='chat_text_nick'>{nick}</span> <span class='{msg_class}'>{msg}</span>"
+            _msg_class.append("chat_text_msg")
+            if mess_data.own_mess:
+                _msg_class.append("chat_text_mymess")
+
+        xhtml = mess_data.main_message_xhtml
+        _date = datetime.fromtimestamp(float(mess_data.timestamp))
+        HTMLPanel.__init__(self, markup.format(
+                               timestamp = _date.strftime("%H:%M"),
+                               nick =  "[{}]".format(html_tools.html_sanitize(mess_data.nick)),
+                               msg_class = ' '.join(_msg_class),
+                               msg = strings.addURLToText(html_tools.html_sanitize(mess_data.main_message)) if not xhtml else html_tools.inlineRoot(xhtml)  # FIXME: images and external links must be removed according to preferences
+                           ))
+        if mess_data.type != C.MESS_TYPE_INFO:
+            self.setStyleName('chatText')
+
+
+class Chat(QuickChat, libervia_widget.LiberviaWidget, KeyboardHandler):
+
+    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, subject=None, profiles=None):
+        """Panel used for conversation (one 2 one or group chat)
+
+        @param host: SatWebFrontend instance
+        @param target: entity (jid.JID) with who we have a conversation (contact's jid for one 2 one chat, or MUC room)
+        @param type: one2one for simple conversation, group for MUC
+        """
+        QuickChat.__init__(self, host, target, type_, nick, occupants, subject, profiles=profiles)
+        self.vpanel = VerticalPanel()
+        self.vpanel.setSize('100%', '100%')
+
+        # FIXME: temporary dirty initialization to display the OTR state
+        header_info = host.plugins['otr'].getInfoTextForUser(target) if (type_ == C.CHAT_ONE2ONE and 'otr' in host.plugins) else None
+
+        libervia_widget.LiberviaWidget.__init__(self, host, title=unicode(target.bare), info=header_info, selectable=True)
+        self._body = AbsolutePanel()
+        self._body.setStyleName('chatPanel_body')
+        chat_area = HorizontalPanel()
+        chat_area.setStyleName('chatArea')
+        if type_ == C.CHAT_GROUP:
+            self.occupants_panel = contact_panel.ContactsPanel(host, merge_resources=False,
+                                                               contacts_style="muc_contact",
+                                                               contacts_menus=(C.MENU_JID_CONTEXT),
+                                                               contacts_display=('resource',))
+            chat_area.add(self.occupants_panel)
+            DOM.setAttribute(chat_area.getWidgetTd(self.occupants_panel), "className", "occupantsPanelCell")
+            # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword)
+            self.presenceListener = self.onPresenceUpdate
+            self.host.addListener('presence', self.presenceListener, [C.PROF_KEY_NONE])
+            self.avatarListener = self.onAvatarUpdate
+            host.addListener('avatar', self.avatarListener, [C.PROF_KEY_NONE])
+            Window.addWindowResizeListener(self)
+
+        else:
+            self.chat_state = None
+
+        self._body.add(chat_area)
+        self.content = AbsolutePanel()
+        self.content.setStyleName('chatContent')
+        self.content_scroll = base_panel.ScrollPanelWrapper(self.content)
+        chat_area.add(self.content_scroll)
+        chat_area.setCellWidth(self.content_scroll, '100%')
+        self.vpanel.add(self._body)
+        self.vpanel.setCellHeight(self._body, '100%')
+        self.addStyleName('chatPanel')
+        self.setWidget(self.vpanel)
+        self.chat_state_machine = plugin_xep_0085.ChatStateMachine(self.host, unicode(self.target))
+
+        self.message_box = editor_widget.MessageBox(self.host)
+        self.message_box.onSelectedChange(self)
+        self.message_box.addKeyboardListener(self)
+        self.vpanel.add(self.message_box)
+        self.postInit()
+
+    def onWindowResized(self, width=None, height=None):
+        if self.type == C.CHAT_GROUP:
+            ideal_height = self.content_scroll.getOffsetHeight()
+            self.occupants_panel.setHeight("%s%s" % (ideal_height, "px"))
+
+    @property
+    def target(self):
+        # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickChat
+        # FIXME: must remove this when either pyjamas is fixed, or we use an alternative
+        if self.type == C.CHAT_GROUP:
+            return self.current_target.bare
+        return self.current_target
+
+    @property
+    def profile(self):
+        # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickWidget
+        # FIXME: must remove this when either pyjamas is fixed, or we use an alternative
+        assert len(self.profiles) == 1 and not self.PROFILES_MULTIPLE and not self.PROFILES_ALLOW_NONE
+        return list(self.profiles)[0]
+
+    @property
+    def plugin_menu_context(self):
+        return (C.MENU_ROOM,) if self.type == C.CHAT_GROUP else (C.MENU_SINGLE,)
+
+    def onKeyDown(self, sender, keycode, modifiers):
+        if keycode == KEY_ENTER:
+            self.host.showWarning(None, None)
+        else:
+            self.host.showWarning(*self.getWarningData())
+
+    def getWarningData(self):
+        if self.type not in [C.CHAT_ONE2ONE, C.CHAT_GROUP]:
+            raise Exception("Unmanaged type !")
+        if self.type == C.CHAT_ONE2ONE:
+            msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % self.target
+        elif self.type == C.CHAT_GROUP:
+            msg = "This message will be sent to all the participants of the multi-user room <span class='warningTarget'>%s</span>" % self.target
+        return ("ONE2ONE" if self.type == C.CHAT_ONE2ONE else "GROUP", msg)
+
+    def onTextEntered(self, text):
+        self.host.messageSend(self.target,
+                              {'': text},
+                              {},
+                              C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT,
+                              {},
+                              errback=self.host.sendError,
+                              profile_key=C.PROF_KEY_NONE
+                              )
+        self.chat_state_machine._onEvent("active")
+
+    def onPresenceUpdate(self, entity, show, priority, statuses, profile):
+        """Update entity's presence status
+
+        @param entity(jid.JID): entity updated
+        @param show: availability
+        @parap priority: resource's priority
+        @param statuses: dict of statuses
+        @param profile: %(doc_profile)s
+        """
+        assert self.type == C.CHAT_GROUP
+        if entity.bare != self.target:
+            return
+        self.update(entity)
+
+    def onAvatarUpdate(self, entity, hash_, profile):
+        """Called on avatar update events
+
+        @param jid_: jid of the entity with updated avatar
+        @param hash_: hash of the avatar
+        @param profile: %(doc_profile)s
+        """
+        assert self.type == C.CHAT_GROUP
+        if entity.bare != self.target:
+            return
+        self.update(entity)
+
+    def onQuit(self):
+        libervia_widget.LiberviaWidget.onQuit(self)
+        if self.type == C.CHAT_GROUP:
+            self.host.removeListener('presence', self.presenceListener)
+            self.host.bridge.mucLeave(self.target.bare, profile=C.PROF_KEY_NONE)
+
+    def newMessage(self, from_jid, target, msg, type_, extra, profile):
+        header_info = extra.pop('header_info', None)
+        if header_info:
+            self.setHeaderInfo(header_info)
+        QuickChat.newMessage(self, from_jid, target, msg, type_, extra, profile)
+
+    def _onHistoryPrinted(self):
+        """Refresh or scroll down the focus after the history is printed"""
+        self.printMessages(clear=False)
+        super(Chat, self)._onHistoryPrinted()
+
+    def printMessages(self, clear=True):
+        """generate message widgets
+
+        @param clear(bool): clear message before printing if true
+        """
+        if clear:
+            # FIXME: clear is not handler
+            pass
+        for message in self.messages.itervalues():
+            self.appendMessage(message)
+
+    def createMessage(self, message):
+        self.appendMessage(message)
+
+    def appendMessage(self, message):
+        self.content.add(MessageWidget(message))
+        self.content_scroll.scrollToBottom()
+
+    def notify(self, contact="somebody", msg=""):
+        """Notify the user of a new message if primitivus doesn't have the focus.
+
+        @param contact (unicode): contact who wrote to the users
+        @param msg (unicode): the message that has been received
+        """
+        self.host.notification.notify(contact, msg)
+
+    # def printDayChange(self, day):
+    #     """Display the day on a new line.
+
+    #     @param day(unicode): day to display (or not if this method is not overwritten)
+    #     """
+    #     self.printInfo("* " + day)
+
+    def setTitle(self, title=None, extra=None):
+        """Refresh the title of this Chat dialog
+
+        @param title (unicode): main title or None to use default
+        @param suffix (unicode): extra title (e.g. for chat states) or None
+        """
+        if title is None:
+            title = unicode(self.target.bare)
+        if extra:
+            title += ' %s' % extra
+        libervia_widget.LiberviaWidget.setTitle(self, title)
+
+    def onChatState(self, from_jid, state, profile):
+        super(Chat, self).onChatState(from_jid, state, profile)
+        if self.type == C.CHAT_ONE2ONE:
+            self.title_dynamic = C.CHAT_STATE_ICON[state]
+
+    def update(self, entity=None):
+        """Update one or all entities.
+
+        @param entity (jid.JID): entity to update
+        """
+        if self.type == C.CHAT_ONE2ONE:  # only update the chat title
+            if self.chat_state:
+                self.setTitle(extra='({})'.format(self.chat_state))
+        else:
+            if entity is None:  # rebuild all the occupants list
+                nicks = list(self.occupants)
+                nicks.sort()
+                self.occupants_panel.setList([jid.newResource(self.target, nick) for nick in nicks])
+            else:  # add, remove or update only one occupant
+                contact_list = self.host.contact_lists[self.profile]
+                show = contact_list.getCache(entity, C.PRESENCE_SHOW)
+                if show == C.PRESENCE_UNAVAILABLE or show is None:
+                    self.occupants_panel.removeContactBox(entity)
+                else:
+                    pass
+                    # FIXME: legacy code, chat state must be checked
+                    # box = self.occupants_panel.updateContactBox(entity)
+                    # box.states.setHTML(u''.join(states.values()))
+
+        # FIXME: legacy code, chat state must be checked
+        # if 'chat_state' in states.keys():  # start/stop sending "composing" state from now
+        #     self.chat_state_machine.started = not not states['chat_state']
+
+        self.onWindowResized()  # be sure to set the good height
+
+    def addGamePanel(self, widget):
+        """Insert a game panel to this Chat dialog.
+
+        @param widget (Widget): the game panel
+        """
+        self.vpanel.insert(widget, 0)
+        self.vpanel.setCellHeight(widget, widget.getHeight())
+
+    def removeGamePanel(self, widget):
+        """Remove the game panel from this Chat dialog.
+
+        @param widget (Widget): the game panel
+        """
+        self.vpanel.remove(widget)
+
+
+quick_widgets.register(QuickChat, Chat)
+quick_widgets.register(quick_games.Tarot, game_tarot.TarotPanel)
+quick_widgets.register(quick_games.Radiocol, game_radiocol.RadioColPanel)
+libervia_widget.LiberviaWidget.addDropKey("CONTACT", lambda host, item: host.displayWidget(Chat, jid.JID(item), dropped=True))
+quick_menus.QuickMenusManager.addDataCollector(C.MENU_ROOM, {'room_jid': 'target'})
+quick_menus.QuickMenusManager.addDataCollector(C.MENU_SINGLE, {'jid': 'target'})