Mercurial > libervia-web
diff src/browser/sat_browser/chat.py @ 589:a5019e62c3e9 frontends_multi_profiles
browser side: big refactoring to base Libervia on QuickFrontend, first draft:
/!\ not finished, partially working and highly instable
- add collections module with an OrderedDict like class
- SatWebFrontend inherit from QuickApp
- general sat_frontends tools.jid module is used
- bridge/json methods have moved to json module
- UniBox is partially removed (should be totally removed before merge to trunk)
- Signals are now register with the generic registerSignal method (which is called mainly in QuickFrontend)
- the generic getOrCreateWidget method from QuickWidgetsManager is used instead of Libervia's specific methods
- all Widget are now based more or less directly on QuickWidget
- with the new QuickWidgetsManager.getWidgets method, it's no more necessary to check all widgets which are instance of a particular class
- ChatPanel and related moved to chat module
- MicroblogPanel and related moved to blog module
- global and overcomplicated send method has been disabled: each class should manage its own sending
- for consistency with other frontends, former ContactPanel has been renamed to ContactList and vice versa
- for the same reason, ChatPanel has been renamed to Chat
- for compatibility with QuickFrontend, a fake profile is used in several places, it is set to C.PROF_KEY_NONE (real profile is managed server side for obvious security reasons)
- changed default url for web panel to SàT website, and contact address to generic SàT contact address
- ContactList is based on QuickContactList, UI changes are done in update method
- bride call (now json module) have been greatly improved, in particular call can be done in the same way as for other frontends (bridge.method_name(arg1, arg2, ..., callback=cb, errback=eb). Blocking method must be called like async methods due to javascript architecture
- in bridge calls, a callback can now exists without errback
- hard reload on BridgeSignals remote error has been disabled, a better option should be implemented
- use of constants where that make sens, some style improvments
- avatars are temporarily disabled
- lot of code disabled, will be fixed or removed before merge
- various other changes, check diff for more details
server side: manage remote exception on getEntityData, removed getProfileJid call, added getWaitingConf, added getRoomsSubjects
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 24 Jan 2015 01:45:39 +0100 |
parents | src/browser/sat_browser/panels.py@bade589dbd5a |
children | ed6d8f7c6026 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/chat.py Sat Jan 24 01:45:39 2015 +0100 @@ -0,0 +1,364 @@ +#!/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 +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 datetime import datetime +from time import time + +import html_tools +import base_panels +import panels +import card_game +import radiocol +import base_widget +import contact_list +from constants import Const as C +import plugin_xep_0085 + + +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, base_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 + def header_info_cb(cb): + host.plugins['otr'].infoTextCallback(target, cb) + header_info = header_info_cb if (type_ == C.CHAT_ONE2ONE and 'otr' in host.plugins) else None + + base_widget.LiberviaWidget.__init__(self, host, title=target.bare, info=header_info, selectable=True) + self._body = AbsolutePanel() + self._body.setStyleName('chatPanel_body') + chat_area = HorizontalPanel() + chat_area.setStyleName('chatArea') + if type_ == C.CHAT_GROUP: + self.occupants_list = base_panels.OccupantsList() + self.occupants_initialised = False + chat_area.add(self.occupants_list) + self._body.add(chat_area) + self.content = AbsolutePanel() + self.content.setStyleName('chatContent') + self.content_scroll = base_widget.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, str(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] + + @classmethod + def registerClass(cls): + base_widget.LiberviaWidget.addDropKey("CONTACT", cls.createPanel) + + @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 = panels.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 matchEntity(self, item, type_=None): + """ + @param entity: target jid as a string or jid.JID instance. + @return: True if self matches the given entity + """ + if type_ is None: + type_ = self.type + entity = item if isinstance(item, jid.JID) else jid.JID(item) + try: + return self.target.bare == entity.bare and self.type == type_ + except AttributeError as e: + e.include_traceback() + return False + + def addMenus(self, menu_bar): + """Add cached menus to the header. + + @param menu_bar (GenericMenuBar): menu bar of the widget's header + """ + if self.type == C.CHAT_GROUP: + menu_bar.addCachedMenus(C.MENU_ROOM, {'room_jid': self.target.bare}) + elif self.type == C.CHAT_ONE2ONE: + menu_bar.addCachedMenus(C.MENU_SINGLE, {'jid': self.target}) + + 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(str(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): + base_widget.LiberviaWidget.onQuit(self) + if self.type == C.CHAT_GROUP: + self.host.bridge.call('mucLeave', None, 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 users presents in this room + @param occupants: list of nicks (string)""" + for nick in nicks: + self.occupants_list.addOccupant(nick) + self.occupants_initialised = True + + # def userJoined(self, nick, data): + # if self.occupants_list.getOccupantBox(nick): + # return # user is already displayed + # self.occupants_list.addOccupant(nick) + # if self.occupants_initialised: + # self.printInfo("=> %s has joined the room" % nick) + + # def userLeft(self, nick, data): + # self.occupants_list.removeOccupant(nick) + # self.printInfo("<= %s has left the room" % nick) + + def changeUserNick(self, old_nick, new_nick): + assert(self.type == C.CHAT_GROUP) + self.occupants_list.removeOccupant(old_nick) + self.occupants_list.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 startGame(self, game_type, waiting, referee, players, *args): + """Configure the chat window to start a game""" + classes = {"Tarot": card_game.CardPanel, "RadioCol": radiocol.RadioColPanel} + if game_type not in classes.keys(): + return # unknown game + attr = game_type.lower() + self.occupants_list.updateSpecials(players, SYMBOLS[attr]) + if waiting or not self.nick in players: + return # waiting for player or not playing + attr = "%s_panel" % attr + if hasattr(self, attr): + return + log.info("%s Game Started \o/" % game_type) + panel = classes[game_type](self, referee, self.nick, players, *args) + setattr(self, attr, panel) + self.vpanel.insert(panel, 0) + self.vpanel.setCellHeight(panel, panel.getHeight()) + + def getGame(self, game_type): + """Return class managing the game type""" + # TODO: check that the game is launched, and manage errors + if game_type == "Tarot": + return self.tarot_panel + elif game_type == "RadioCol": + return self.radiocol_panel + + 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 + """ + if self.type == C.CHAT_GROUP: + assert(nick) + if nick == C.ALL_OCCUPANTS: + occupants = self.occupants_list.occupants_list.keys() + else: + occupants = [nick] if nick in self.occupants_list.occupants_list else [] + for occupant in occupants: + self.occupants_list.occupants_list[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 ChatPanel dialog""" + if self._state: + self.setTitle(self.target.bare + " (" + self._state + ")") + else: + self.setTitle(self.target.bare) + + def setConnected(self, jid_s, resource, availability, priority, statuses): + """Set connection status + @param jid_s (str): JID userhost as unicode + """ + assert(jid_s == self.target.bare) + if self.type != C.CHAT_GROUP: + return + box = self.occupants_list.getOccupantBox(resource) + if box: + contact_list.setPresenceStyle(box, availability) + + def updateChatState(self, from_jid, state): + #TODO + pass + +quick_widgets.register(QuickChat, Chat)