Mercurial > libervia-web
comparison 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 |
comparison
equal
deleted
inserted
replaced
1123:63a4b8fe9782 | 1124:28e3eb3bb217 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # Libervia: a Salut à Toi frontend | |
5 # Copyright (C) 2011-2018 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_browser import strings | |
25 from sat_frontends.tools import jid | |
26 from sat_frontends.quick_frontend import quick_widgets, quick_games, quick_menus | |
27 from sat_frontends.quick_frontend.quick_chat import QuickChat | |
28 | |
29 from pyjamas.ui.AbsolutePanel import AbsolutePanel | |
30 from pyjamas.ui.VerticalPanel import VerticalPanel | |
31 from pyjamas.ui.HorizontalPanel import HorizontalPanel | |
32 from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler | |
33 from pyjamas.ui.HTMLPanel import HTMLPanel | |
34 from pyjamas import DOM | |
35 from pyjamas import Window | |
36 | |
37 from datetime import datetime | |
38 | |
39 import html_tools | |
40 import libervia_widget | |
41 import base_panel | |
42 import contact_panel | |
43 import editor_widget | |
44 from constants import Const as C | |
45 import plugin_xep_0085 | |
46 import game_tarot | |
47 import game_radiocol | |
48 | |
49 | |
50 unicode = str # FIXME: pyjamas workaround | |
51 | |
52 | |
53 class MessageWidget(HTMLPanel): | |
54 | |
55 def __init__(self, mess_data): | |
56 """ | |
57 @param mess_data(quick_chat.Message, None): message data | |
58 None: used only for non text widgets (e.g.: focus separator) | |
59 """ | |
60 self.mess_data = mess_data | |
61 mess_data.widgets.add(self) | |
62 _msg_class = [] | |
63 if mess_data.type == C.MESS_TYPE_INFO: | |
64 markup = "<span class='{msg_class}'>{msg}</span>" | |
65 | |
66 if mess_data.extra.get('info_type') == 'me': | |
67 _msg_class.append('chatTextMe') | |
68 else: | |
69 _msg_class.append('chatTextInfo') | |
70 # FIXME: following code was in printInfo before refactoring | |
71 # seems to be used only in radiocol | |
72 # elif type_ == 'link': | |
73 # _wid = HTML(msg) | |
74 # _wid.setStyleName('chatTextInfo-link') | |
75 # if link_cb: | |
76 # _wid.addClickListener(link_cb) | |
77 else: | |
78 markup = "<span class='chat_text_timestamp'>{timestamp}</span> <span class='chat_text_nick'>{nick}</span> <span class='{msg_class}'>{msg}</span>" | |
79 _msg_class.append("chat_text_msg") | |
80 if mess_data.own_mess: | |
81 _msg_class.append("chat_text_mymess") | |
82 | |
83 xhtml = mess_data.main_message_xhtml | |
84 _date = datetime.fromtimestamp(float(mess_data.timestamp)) | |
85 HTMLPanel.__init__(self, markup.format( | |
86 timestamp = _date.strftime("%H:%M"), | |
87 nick = "[{}]".format(html_tools.html_sanitize(mess_data.nick)), | |
88 msg_class = ' '.join(_msg_class), | |
89 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 | |
90 )) | |
91 if mess_data.type != C.MESS_TYPE_INFO: | |
92 self.setStyleName('chatText') | |
93 | |
94 | |
95 class Chat(QuickChat, libervia_widget.LiberviaWidget, KeyboardHandler): | |
96 | |
97 def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, subject=None, profiles=None): | |
98 """Panel used for conversation (one 2 one or group chat) | |
99 | |
100 @param host: SatWebFrontend instance | |
101 @param target: entity (jid.JID) with who we have a conversation (contact's jid for one 2 one chat, or MUC room) | |
102 @param type: one2one for simple conversation, group for MUC | |
103 """ | |
104 QuickChat.__init__(self, host, target, type_, nick, occupants, subject, profiles=profiles) | |
105 self.vpanel = VerticalPanel() | |
106 self.vpanel.setSize('100%', '100%') | |
107 | |
108 # FIXME: temporary dirty initialization to display the OTR state | |
109 header_info = host.plugins['otr'].getInfoTextForUser(target) if (type_ == C.CHAT_ONE2ONE and 'otr' in host.plugins) else None | |
110 | |
111 libervia_widget.LiberviaWidget.__init__(self, host, title=unicode(target.bare), info=header_info, selectable=True) | |
112 self._body = AbsolutePanel() | |
113 self._body.setStyleName('chatPanel_body') | |
114 chat_area = HorizontalPanel() | |
115 chat_area.setStyleName('chatArea') | |
116 if type_ == C.CHAT_GROUP: | |
117 self.occupants_panel = contact_panel.ContactsPanel(host, merge_resources=False, | |
118 contacts_style="muc_contact", | |
119 contacts_menus=(C.MENU_JID_CONTEXT), | |
120 contacts_display=('resource',)) | |
121 chat_area.add(self.occupants_panel) | |
122 DOM.setAttribute(chat_area.getWidgetTd(self.occupants_panel), "className", "occupantsPanelCell") | |
123 # 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) | |
124 self.presenceListener = self.onPresenceUpdate | |
125 self.host.addListener('presence', self.presenceListener, [C.PROF_KEY_NONE]) | |
126 self.avatarListener = self.onAvatarUpdate | |
127 host.addListener('avatar', self.avatarListener, [C.PROF_KEY_NONE]) | |
128 Window.addWindowResizeListener(self) | |
129 | |
130 else: | |
131 self.chat_state = None | |
132 | |
133 self._body.add(chat_area) | |
134 self.content = AbsolutePanel() | |
135 self.content.setStyleName('chatContent') | |
136 self.content_scroll = base_panel.ScrollPanelWrapper(self.content) | |
137 chat_area.add(self.content_scroll) | |
138 chat_area.setCellWidth(self.content_scroll, '100%') | |
139 self.vpanel.add(self._body) | |
140 self.vpanel.setCellHeight(self._body, '100%') | |
141 self.addStyleName('chatPanel') | |
142 self.setWidget(self.vpanel) | |
143 self.chat_state_machine = plugin_xep_0085.ChatStateMachine(self.host, unicode(self.target)) | |
144 | |
145 self.message_box = editor_widget.MessageBox(self.host) | |
146 self.message_box.onSelectedChange(self) | |
147 self.message_box.addKeyboardListener(self) | |
148 self.vpanel.add(self.message_box) | |
149 self.postInit() | |
150 | |
151 def onWindowResized(self, width=None, height=None): | |
152 if self.type == C.CHAT_GROUP: | |
153 ideal_height = self.content_scroll.getOffsetHeight() | |
154 self.occupants_panel.setHeight("%s%s" % (ideal_height, "px")) | |
155 | |
156 @property | |
157 def target(self): | |
158 # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickChat | |
159 # FIXME: must remove this when either pyjamas is fixed, or we use an alternative | |
160 if self.type == C.CHAT_GROUP: | |
161 return self.current_target.bare | |
162 return self.current_target | |
163 | |
164 @property | |
165 def profile(self): | |
166 # FIXME: for unknow reason, pyjamas doesn't use the method inherited from QuickWidget | |
167 # FIXME: must remove this when either pyjamas is fixed, or we use an alternative | |
168 assert len(self.profiles) == 1 and not self.PROFILES_MULTIPLE and not self.PROFILES_ALLOW_NONE | |
169 return list(self.profiles)[0] | |
170 | |
171 @property | |
172 def plugin_menu_context(self): | |
173 return (C.MENU_ROOM,) if self.type == C.CHAT_GROUP else (C.MENU_SINGLE,) | |
174 | |
175 def onKeyDown(self, sender, keycode, modifiers): | |
176 if keycode == KEY_ENTER: | |
177 self.host.showWarning(None, None) | |
178 else: | |
179 self.host.showWarning(*self.getWarningData()) | |
180 | |
181 def getWarningData(self): | |
182 if self.type not in [C.CHAT_ONE2ONE, C.CHAT_GROUP]: | |
183 raise Exception("Unmanaged type !") | |
184 if self.type == C.CHAT_ONE2ONE: | |
185 msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % self.target | |
186 elif self.type == C.CHAT_GROUP: | |
187 msg = "This message will be sent to all the participants of the multi-user room <span class='warningTarget'>%s</span>" % self.target | |
188 return ("ONE2ONE" if self.type == C.CHAT_ONE2ONE else "GROUP", msg) | |
189 | |
190 def onTextEntered(self, text): | |
191 self.host.messageSend(self.target, | |
192 {'': text}, | |
193 {}, | |
194 C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, | |
195 {}, | |
196 errback=self.host.sendError, | |
197 profile_key=C.PROF_KEY_NONE | |
198 ) | |
199 self.chat_state_machine._onEvent("active") | |
200 | |
201 def onPresenceUpdate(self, entity, show, priority, statuses, profile): | |
202 """Update entity's presence status | |
203 | |
204 @param entity(jid.JID): entity updated | |
205 @param show: availability | |
206 @parap priority: resource's priority | |
207 @param statuses: dict of statuses | |
208 @param profile: %(doc_profile)s | |
209 """ | |
210 assert self.type == C.CHAT_GROUP | |
211 if entity.bare != self.target: | |
212 return | |
213 self.update(entity) | |
214 | |
215 def onAvatarUpdate(self, entity, hash_, profile): | |
216 """Called on avatar update events | |
217 | |
218 @param jid_: jid of the entity with updated avatar | |
219 @param hash_: hash of the avatar | |
220 @param profile: %(doc_profile)s | |
221 """ | |
222 assert self.type == C.CHAT_GROUP | |
223 if entity.bare != self.target: | |
224 return | |
225 self.update(entity) | |
226 | |
227 def onQuit(self): | |
228 libervia_widget.LiberviaWidget.onQuit(self) | |
229 if self.type == C.CHAT_GROUP: | |
230 self.host.removeListener('presence', self.presenceListener) | |
231 self.host.bridge.mucLeave(self.target.bare, profile=C.PROF_KEY_NONE) | |
232 | |
233 def newMessage(self, from_jid, target, msg, type_, extra, profile): | |
234 header_info = extra.pop('header_info', None) | |
235 if header_info: | |
236 self.setHeaderInfo(header_info) | |
237 QuickChat.newMessage(self, from_jid, target, msg, type_, extra, profile) | |
238 | |
239 def _onHistoryPrinted(self): | |
240 """Refresh or scroll down the focus after the history is printed""" | |
241 self.printMessages(clear=False) | |
242 super(Chat, self)._onHistoryPrinted() | |
243 | |
244 def printMessages(self, clear=True): | |
245 """generate message widgets | |
246 | |
247 @param clear(bool): clear message before printing if true | |
248 """ | |
249 if clear: | |
250 # FIXME: clear is not handler | |
251 pass | |
252 for message in self.messages.itervalues(): | |
253 self.appendMessage(message) | |
254 | |
255 def createMessage(self, message): | |
256 self.appendMessage(message) | |
257 | |
258 def appendMessage(self, message): | |
259 self.content.add(MessageWidget(message)) | |
260 self.content_scroll.scrollToBottom() | |
261 | |
262 def notify(self, contact="somebody", msg=""): | |
263 """Notify the user of a new message if primitivus doesn't have the focus. | |
264 | |
265 @param contact (unicode): contact who wrote to the users | |
266 @param msg (unicode): the message that has been received | |
267 """ | |
268 self.host.notification.notify(contact, msg) | |
269 | |
270 # def printDayChange(self, day): | |
271 # """Display the day on a new line. | |
272 | |
273 # @param day(unicode): day to display (or not if this method is not overwritten) | |
274 # """ | |
275 # self.printInfo("* " + day) | |
276 | |
277 def setTitle(self, title=None, extra=None): | |
278 """Refresh the title of this Chat dialog | |
279 | |
280 @param title (unicode): main title or None to use default | |
281 @param suffix (unicode): extra title (e.g. for chat states) or None | |
282 """ | |
283 if title is None: | |
284 title = unicode(self.target.bare) | |
285 if extra: | |
286 title += ' %s' % extra | |
287 libervia_widget.LiberviaWidget.setTitle(self, title) | |
288 | |
289 def onChatState(self, from_jid, state, profile): | |
290 super(Chat, self).onChatState(from_jid, state, profile) | |
291 if self.type == C.CHAT_ONE2ONE: | |
292 self.title_dynamic = C.CHAT_STATE_ICON[state] | |
293 | |
294 def update(self, entity=None): | |
295 """Update one or all entities. | |
296 | |
297 @param entity (jid.JID): entity to update | |
298 """ | |
299 if self.type == C.CHAT_ONE2ONE: # only update the chat title | |
300 if self.chat_state: | |
301 self.setTitle(extra='({})'.format(self.chat_state)) | |
302 else: | |
303 if entity is None: # rebuild all the occupants list | |
304 nicks = list(self.occupants) | |
305 nicks.sort() | |
306 self.occupants_panel.setList([jid.newResource(self.target, nick) for nick in nicks]) | |
307 else: # add, remove or update only one occupant | |
308 contact_list = self.host.contact_lists[self.profile] | |
309 show = contact_list.getCache(entity, C.PRESENCE_SHOW) | |
310 if show == C.PRESENCE_UNAVAILABLE or show is None: | |
311 self.occupants_panel.removeContactBox(entity) | |
312 else: | |
313 pass | |
314 # FIXME: legacy code, chat state must be checked | |
315 # box = self.occupants_panel.updateContactBox(entity) | |
316 # box.states.setHTML(u''.join(states.values())) | |
317 | |
318 # FIXME: legacy code, chat state must be checked | |
319 # if 'chat_state' in states.keys(): # start/stop sending "composing" state from now | |
320 # self.chat_state_machine.started = not not states['chat_state'] | |
321 | |
322 self.onWindowResized() # be sure to set the good height | |
323 | |
324 def addGamePanel(self, widget): | |
325 """Insert a game panel to this Chat dialog. | |
326 | |
327 @param widget (Widget): the game panel | |
328 """ | |
329 self.vpanel.insert(widget, 0) | |
330 self.vpanel.setCellHeight(widget, widget.getHeight()) | |
331 | |
332 def removeGamePanel(self, widget): | |
333 """Remove the game panel from this Chat dialog. | |
334 | |
335 @param widget (Widget): the game panel | |
336 """ | |
337 self.vpanel.remove(widget) | |
338 | |
339 | |
340 quick_widgets.register(QuickChat, Chat) | |
341 quick_widgets.register(quick_games.Tarot, game_tarot.TarotPanel) | |
342 quick_widgets.register(quick_games.Radiocol, game_radiocol.RadioColPanel) | |
343 libervia_widget.LiberviaWidget.addDropKey("CONTACT", lambda host, item: host.displayWidget(Chat, jid.JID(item), dropped=True)) | |
344 quick_menus.QuickMenusManager.addDataCollector(C.MENU_ROOM, {'room_jid': 'target'}) | |
345 quick_menus.QuickMenusManager.addDataCollector(C.MENU_SINGLE, {'jid': 'target'}) |