comparison 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
comparison
equal deleted inserted replaced
585:bade589dbd5a 589:a5019e62c3e9
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # Libervia: a Salut à Toi frontend
5 # Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.log import getLogger
21 log = getLogger(__name__)
22
23 from sat_frontends.tools.games import SYMBOLS
24 from sat_frontends.tools import strings
25 from sat_frontends.tools import jid
26 from sat_frontends.quick_frontend import quick_widgets
27 from sat_frontends.quick_frontend.quick_chat import QuickChat
28 from sat.core.i18n import _
29
30 from pyjamas.ui.AbsolutePanel import AbsolutePanel
31 from pyjamas.ui.VerticalPanel import VerticalPanel
32 from pyjamas.ui.HorizontalPanel import HorizontalPanel
33 from pyjamas.ui.Label import Label
34 from pyjamas.ui.HTML import HTML
35 from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler
36 from pyjamas.ui.HTMLPanel import HTMLPanel
37
38 from datetime import datetime
39 from time import time
40
41 import html_tools
42 import base_panels
43 import panels
44 import card_game
45 import radiocol
46 import base_widget
47 import contact_list
48 from constants import Const as C
49 import plugin_xep_0085
50
51
52 class ChatText(HTMLPanel):
53
54 def __init__(self, nick, mymess, msg, extra):
55 try:
56 timestamp = float(extra['timestamp'])
57 except KeyError:
58 timestamp=None
59 xhtml = extra.get('xhtml')
60 _date = datetime.fromtimestamp(float(timestamp or time()))
61 _msg_class = ["chat_text_msg"]
62 if mymess:
63 _msg_class.append("chat_text_mymess")
64 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>" %
65 {"timestamp": _date.strftime("%H:%M"),
66 "nick": "[%s]" % html_tools.html_sanitize(nick),
67 "msg_class": ' '.join(_msg_class),
68 "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
69 )
70 self.setStyleName('chatText')
71
72
73 class Chat(QuickChat, base_widget.LiberviaWidget, KeyboardHandler):
74
75 def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
76 """Panel used for conversation (one 2 one or group chat)
77
78 @param host: SatWebFrontend instance
79 @param target: entity (jid.JID) with who we have a conversation (contact's jid for one 2 one chat, or MUC room)
80 @param type: one2one for simple conversation, group for MUC"""
81 QuickChat.__init__(self, host, target, type_, profiles=profiles)
82 self.vpanel = VerticalPanel()
83 self.vpanel.setSize('100%', '100%')
84
85 # FIXME: temporary dirty initialization to display the OTR state
86 def header_info_cb(cb):
87 host.plugins['otr'].infoTextCallback(target, cb)
88 header_info = header_info_cb if (type_ == C.CHAT_ONE2ONE and 'otr' in host.plugins) else None
89
90 base_widget.LiberviaWidget.__init__(self, host, title=target.bare, info=header_info, selectable=True)
91 self._body = AbsolutePanel()
92 self._body.setStyleName('chatPanel_body')
93 chat_area = HorizontalPanel()
94 chat_area.setStyleName('chatArea')
95 if type_ == C.CHAT_GROUP:
96 self.occupants_list = base_panels.OccupantsList()
97 self.occupants_initialised = False
98 chat_area.add(self.occupants_list)
99 self._body.add(chat_area)
100 self.content = AbsolutePanel()
101 self.content.setStyleName('chatContent')
102 self.content_scroll = base_widget.ScrollPanelWrapper(self.content)
103 chat_area.add(self.content_scroll)
104 chat_area.setCellWidth(self.content_scroll, '100%')
105 self.vpanel.add(self._body)
106 self.vpanel.setCellHeight(self._body, '100%')
107 self.addStyleName('chatPanel')
108 self.setWidget(self.vpanel)
109 self.state_machine = plugin_xep_0085.ChatStateMachine(self.host, str(self.target))
110 self._state = None
111 self.refresh()
112 if type_ == C.CHAT_ONE2ONE:
113 self.historyPrint(profile=self.profile)
114
115 @property
116 def target(self):
117 # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickChat
118 # FIXME: must remove this when either pyjamas is fixed, or we use an alternative
119 if self.type == C.CHAT_GROUP:
120 return self.current_target.bare
121 return self.current_target
122
123 @property
124 def profile(self):
125 # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickWidget
126 # FIXME: must remove this when either pyjamas is fixed, or we use an alternative
127 assert len(self.profiles) == 1 and not self.PROFILES_MULTIPLE and not self.PROFILES_ALLOW_NONE
128 return list(self.profiles)[0]
129
130 @classmethod
131 def registerClass(cls):
132 base_widget.LiberviaWidget.addDropKey("CONTACT", cls.createPanel)
133
134 @classmethod
135 def createPanel(cls, host, item, type_=C.CHAT_ONE2ONE):
136 assert(item)
137 _contact = item if isinstance(item, jid.JID) else jid.JID(item)
138 host.contact_panel.setContactMessageWaiting(_contact.bare, False)
139 _new_panel = Chat(host, _contact, type_) # XXX: pyjamas doesn't seems to support creating with cls directly
140 _new_panel.historyPrint()
141 host.setSelected(_new_panel)
142 _new_panel.refresh()
143 return _new_panel
144
145 def refresh(self):
146 """Refresh the display of this widget. If the unibox is disabled,
147 add a message box at the bottom of the panel"""
148 # FIXME: must be checked
149 # self.host.contact_panel.setContactMessageWaiting(self.target.bare, False)
150 # self.content_scroll.scrollToBottom()
151
152 enable_box = self.host.uni_box is None
153 if hasattr(self, 'message_box'):
154 self.message_box.setVisible(enable_box)
155 elif enable_box:
156 self.message_box = panels.MessageBox(self.host)
157 self.message_box.onSelectedChange(self)
158 self.message_box.addKeyboardListener(self)
159 self.vpanel.add(self.message_box)
160
161 def onKeyDown(self, sender, keycode, modifiers):
162 if keycode == KEY_ENTER:
163 self.host.showWarning(None, None)
164 else:
165 self.host.showWarning(*self.getWarningData())
166
167 def matchEntity(self, item, type_=None):
168 """
169 @param entity: target jid as a string or jid.JID instance.
170 @return: True if self matches the given entity
171 """
172 if type_ is None:
173 type_ = self.type
174 entity = item if isinstance(item, jid.JID) else jid.JID(item)
175 try:
176 return self.target.bare == entity.bare and self.type == type_
177 except AttributeError as e:
178 e.include_traceback()
179 return False
180
181 def addMenus(self, menu_bar):
182 """Add cached menus to the header.
183
184 @param menu_bar (GenericMenuBar): menu bar of the widget's header
185 """
186 if self.type == C.CHAT_GROUP:
187 menu_bar.addCachedMenus(C.MENU_ROOM, {'room_jid': self.target.bare})
188 elif self.type == C.CHAT_ONE2ONE:
189 menu_bar.addCachedMenus(C.MENU_SINGLE, {'jid': self.target})
190
191 def getWarningData(self):
192 if self.type not in [C.CHAT_ONE2ONE, C.CHAT_GROUP]:
193 raise Exception("Unmanaged type !")
194 if self.type == C.CHAT_ONE2ONE:
195 msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % self.target
196 elif self.type == C.CHAT_GROUP:
197 msg = "This message will be sent to all the participants of the multi-user room <span class='warningTarget'>%s</span>" % self.target
198 return ("ONE2ONE" if self.type == C.CHAT_ONE2ONE else "GROUP", msg)
199
200 def onTextEntered(self, text):
201 self.host.sendMessage(str(self.target),
202 text,
203 mess_type = C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT,
204 errback=self.host.sendError,
205 profile_key=C.PROF_KEY_NONE
206 )
207 self.state_machine._onEvent("active")
208
209 def onQuit(self):
210 base_widget.LiberviaWidget.onQuit(self)
211 if self.type == C.CHAT_GROUP:
212 self.host.bridge.call('mucLeave', None, self.target.bare)
213
214 def setUserNick(self, nick):
215 """Set the nick of the user, usefull for e.g. change the color of the user"""
216 self.nick = nick
217
218 def setPresents(self, nicks):
219 """Set the users presents in this room
220 @param occupants: list of nicks (string)"""
221 for nick in nicks:
222 self.occupants_list.addOccupant(nick)
223 self.occupants_initialised = True
224
225 # def userJoined(self, nick, data):
226 # if self.occupants_list.getOccupantBox(nick):
227 # return # user is already displayed
228 # self.occupants_list.addOccupant(nick)
229 # if self.occupants_initialised:
230 # self.printInfo("=> %s has joined the room" % nick)
231
232 # def userLeft(self, nick, data):
233 # self.occupants_list.removeOccupant(nick)
234 # self.printInfo("<= %s has left the room" % nick)
235
236 def changeUserNick(self, old_nick, new_nick):
237 assert(self.type == C.CHAT_GROUP)
238 self.occupants_list.removeOccupant(old_nick)
239 self.occupants_list.addOccupant(new_nick)
240 self.printInfo(_("%(old_nick)s is now known as %(new_nick)s") % {'old_nick': old_nick, 'new_nick': new_nick})
241
242 # def historyPrint(self, size=C.HISTORY_LIMIT_DEFAULT):
243 # """Print the initial history"""
244 # def getHistoryCB(history):
245 # # display day change
246 # day_format = "%A, %d %b %Y"
247 # previous_day = datetime.now().strftime(day_format)
248 # for line in history:
249 # timestamp, from_jid_s, to_jid_s, message, mess_type, extra = line
250 # message_day = datetime.fromtimestamp(float(timestamp or time())).strftime(day_format)
251 # if previous_day != message_day:
252 # self.printInfo("* " + message_day)
253 # previous_day = message_day
254 # self.printMessage(jid.JID(from_jid_s), message, extra, timestamp)
255 # self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, self.target.bare, size, True)
256
257 def printInfo(self, msg, type_='normal', extra=None, link_cb=None):
258 """Print general info
259 @param msg: message to print
260 @param type_: one of:
261 "normal": general info like "toto has joined the room" (will be sanitized)
262 "link": general info that is clickable like "click here to join the main room" (no sanitize done)
263 "me": "/me" information like "/me clenches his fist" ==> "toto clenches his fist" (will stay on one line)
264 @param extra (dict): message data
265 @param link_cb: method to call when the info is clicked, ignored if type_ is not 'link'
266 """
267 if extra is None:
268 extra = {}
269 if type_ == 'normal':
270 _wid = HTML(strings.addURLToText(html_tools.XHTML2Text(msg)))
271 _wid.setStyleName('chatTextInfo')
272 elif type_ == 'link':
273 _wid = HTML(msg)
274 _wid.setStyleName('chatTextInfo-link')
275 if link_cb:
276 _wid.addClickListener(link_cb)
277 elif type_ == 'me':
278 _wid = Label(msg)
279 _wid.setStyleName('chatTextMe')
280 else:
281 raise ValueError("Unknown printInfo type %s" % type_)
282 self.content.add(_wid)
283 self.content_scroll.scrollToBottom()
284
285 def printMessage(self, from_jid, msg, extra=None, profile=C.PROF_KEY_NONE):
286 if extra is None:
287 extra = {}
288 try:
289 nick, mymess = QuickChat.printMessage(self, from_jid, msg, extra, profile)
290 except TypeError:
291 # None is returned, the message is managed
292 return
293 self.content.add(ChatText(nick, mymess, msg, extra))
294 self.content_scroll.scrollToBottom()
295
296 def startGame(self, game_type, waiting, referee, players, *args):
297 """Configure the chat window to start a game"""
298 classes = {"Tarot": card_game.CardPanel, "RadioCol": radiocol.RadioColPanel}
299 if game_type not in classes.keys():
300 return # unknown game
301 attr = game_type.lower()
302 self.occupants_list.updateSpecials(players, SYMBOLS[attr])
303 if waiting or not self.nick in players:
304 return # waiting for player or not playing
305 attr = "%s_panel" % attr
306 if hasattr(self, attr):
307 return
308 log.info("%s Game Started \o/" % game_type)
309 panel = classes[game_type](self, referee, self.nick, players, *args)
310 setattr(self, attr, panel)
311 self.vpanel.insert(panel, 0)
312 self.vpanel.setCellHeight(panel, panel.getHeight())
313
314 def getGame(self, game_type):
315 """Return class managing the game type"""
316 # TODO: check that the game is launched, and manage errors
317 if game_type == "Tarot":
318 return self.tarot_panel
319 elif game_type == "RadioCol":
320 return self.radiocol_panel
321
322 def setState(self, state, nick=None):
323 """Set the chat state (XEP-0085) of the contact. Leave nick to None
324 to set the state for a one2one conversation, or give a nickname or
325 C.ALL_OCCUPANTS to set the state of a participant within a MUC.
326 @param state: the new chat state
327 @param nick: ignored for one2one, otherwise the MUC user nick or C.ALL_OCCUPANTS
328 """
329 if self.type == C.CHAT_GROUP:
330 assert(nick)
331 if nick == C.ALL_OCCUPANTS:
332 occupants = self.occupants_list.occupants_list.keys()
333 else:
334 occupants = [nick] if nick in self.occupants_list.occupants_list else []
335 for occupant in occupants:
336 self.occupants_list.occupants_list[occupant].setState(state)
337 else:
338 self._state = state
339 self.refreshTitle()
340 self.state_machine.started = not not state # start to send "composing" state from now
341
342 def refreshTitle(self):
343 """Refresh the title of this ChatPanel dialog"""
344 if self._state:
345 self.setTitle(self.target.bare + " (" + self._state + ")")
346 else:
347 self.setTitle(self.target.bare)
348
349 def setConnected(self, jid_s, resource, availability, priority, statuses):
350 """Set connection status
351 @param jid_s (str): JID userhost as unicode
352 """
353 assert(jid_s == self.target.bare)
354 if self.type != C.CHAT_GROUP:
355 return
356 box = self.occupants_list.getOccupantBox(resource)
357 if box:
358 contact_list.setPresenceStyle(box, availability)
359
360 def updateChatState(self, from_jid, state):
361 #TODO
362 pass
363
364 quick_widgets.register(QuickChat, Chat)