Mercurial > libervia-web
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) |