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'})