Mercurial > libervia-web
comparison libervia/web/pages/chat/_browser/__init__.py @ 1584:eab815e48795
browser (chat): message edition + extra menu:
- handle extra menu
- implement `quote` action
- last message correction
- show popup with last editions history when "editer" pencil icon is hovered
- up arrow let quickly edit last message
- implement input modes, `normal` being the default, `edit` or `quote` are new ones.
- [ESC] erase input and returns to `normal` mode
- fix size and set focus on message input when page is loaded
- fix identity retrieval on new messages
author | Goffi <goffi@goffi.org> |
---|---|
date | Tue, 28 Nov 2023 17:59:11 +0100 |
parents | 9ba532041a8e |
children | 9fc4120888be |
comparison
equal
deleted
inserted
replaced
1583:9865013da86c | 1584:eab815e48795 |
---|---|
1 import json | 1 import json |
2 import re | 2 import re |
3 | 3 |
4 from bridge import AsyncBridge as Bridge | 4 from bridge import AsyncBridge as Bridge |
5 from browser import DOMNode, aio, bind, console as log, document, window | 5 from browser import DOMNode, aio, bind, console as log, document, timer, window |
6 from cache import cache, identities | 6 from cache import cache, identities |
7 import dialog | 7 import dialog |
8 from file_uploader import FileUploader | 8 from file_uploader import FileUploader |
9 import jid | 9 import jid |
10 from js_modules import emoji_picker_element | 10 from js_modules import emoji_picker_element |
11 from js_modules.tippy_js import tippy | 11 from js_modules.tippy_js import tippy |
12 import popup | |
12 from template import Template, safe | 13 from template import Template, safe |
13 from tools import is_touch_device | 14 from tools import is_touch_device |
14 | 15 |
15 log.warning = log.warn | 16 log.warning = log.warn |
16 profile = window.profile or "" | 17 profile = window.profile or "" |
20 bridge = Bridge() | 21 bridge = Bridge() |
21 | 22 |
22 # Sensible value to consider that user is at the bottom | 23 # Sensible value to consider that user is at the bottom |
23 SCROLL_SENSITIVITY = 200 | 24 SCROLL_SENSITIVITY = 200 |
24 | 25 |
26 INPUT_MODES = {"normal", "edit", "quote"} | |
27 MODE_CLASS = "mode_{}" | |
28 | |
25 | 29 |
26 class LiberviaWebChat: | 30 class LiberviaWebChat: |
27 def __init__(self): | 31 def __init__(self): |
32 self._input_mode = "normal" | |
33 self.input_data = {} | |
28 self.message_tpl = Template("chat/message.html") | 34 self.message_tpl = Template("chat/message.html") |
35 self.extra_menu_tpl = Template("chat/extra_menu.html") | |
29 self.reactions_tpl = Template("chat/reactions.html") | 36 self.reactions_tpl = Template("chat/reactions.html") |
30 self.reactions_details_tpl = Template("chat/reactions_details.html") | 37 self.reactions_details_tpl = Template("chat/reactions_details.html") |
31 self.url_preview_control_tpl = Template("components/url_preview_control.html") | 38 self.url_preview_control_tpl = Template("components/url_preview_control.html") |
32 self.url_preview_tpl = Template("components/url_preview.html") | 39 self.url_preview_tpl = Template("components/url_preview.html") |
33 self.new_messages_marker_elt = Template("chat/new_messages_marker.html").get_elt() | 40 self.new_messages_marker_elt = Template("chat/new_messages_marker.html").get_elt() |
41 self.editions_tpl = Template("chat/editions.html") | |
34 | 42 |
35 self.messages_elt = document["messages"] | 43 self.messages_elt = document["messages"] |
36 | 44 |
37 # attachments | 45 # attachments |
38 self.file_uploader = FileUploader( | 46 self.file_uploader = FileUploader( |
49 observer = MutationObserver.new(lambda *__: self.update_attachments_visibility()) | 57 observer = MutationObserver.new(lambda *__: self.update_attachments_visibility()) |
50 observer.observe(self.attachments_elt, {"childList": True}) | 58 observer.observe(self.attachments_elt, {"childList": True}) |
51 | 59 |
52 # we want the message scroll to be initially at the bottom | 60 # we want the message scroll to be initially at the bottom |
53 self.messages_elt.scrollTop = self.messages_elt.scrollHeight | 61 self.messages_elt.scrollTop = self.messages_elt.scrollHeight |
62 | |
63 # listeners/dynamic updates | |
64 self.add_message_event_listeners() | |
65 self.handle_url_previews() | |
66 self.add_reactions_listeners() | |
67 | |
68 # input | |
69 self.auto_resize_message_input() | |
70 self.message_input.focus() | |
71 | |
72 @property | |
73 def input_mode(self) -> str: | |
74 return self._input_mode | |
75 | |
76 @input_mode.setter | |
77 def input_mode(self, new_mode: str) -> None: | |
78 if new_mode == self.input_mode: | |
79 return | |
80 if new_mode not in INPUT_MODES: | |
81 raise ValueError(f"Invalid input mode: {new_mode!r}") | |
82 target_elt = self.message_input | |
83 target_elt.classList.remove(MODE_CLASS.format(self._input_mode)) | |
84 self._input_mode = new_mode | |
85 target_elt.classList.add(MODE_CLASS.format(new_mode)) | |
86 self.input_data.clear() | |
54 | 87 |
55 @property | 88 @property |
56 def is_at_bottom(self): | 89 def is_at_bottom(self): |
57 return ( | 90 return ( |
58 self.messages_elt.scrollHeight | 91 self.messages_elt.scrollHeight |
59 - self.messages_elt.scrollTop | 92 - self.messages_elt.scrollTop |
60 - self.messages_elt.clientHeight | 93 - self.messages_elt.clientHeight |
61 <= SCROLL_SENSITIVITY | 94 <= SCROLL_SENSITIVITY |
62 ) | 95 ) |
63 | 96 |
64 def send_message(self): | 97 async def send_message(self): |
65 """Send message currently in input area | 98 """Send message currently in input area |
66 | 99 |
67 The message and corresponding attachment will be sent | 100 The message and corresponding attachment will be sent |
68 """ | 101 """ |
69 message = self.message_input.value.rstrip() | 102 message = self.message_input.value.rstrip() |
81 if attachments: | 114 if attachments: |
82 extra["attachments"] = attachments | 115 extra["attachments"] = attachments |
83 | 116 |
84 # now we send the message | 117 # now we send the message |
85 try: | 118 try: |
86 aio.run( | 119 if self.input_mode == "edit": |
87 bridge.message_send( | 120 message_id = self.input_data["id"] |
88 str(target_jid), {"": message}, {}, "auto", json.dumps(extra, ensure_ascii=False) | 121 edit_data = { |
122 "message": {"": message}, | |
123 "extra": extra | |
124 } | |
125 await bridge.message_edit( | |
126 message_id, json.dumps(edit_data, ensure_ascii=False) | |
127 ) | |
128 else: | |
129 await bridge.message_send( | |
130 str(target_jid), | |
131 {"": message}, {}, "auto", json.dumps(extra, ensure_ascii=False) | |
89 ) | 132 ) |
90 ) | |
91 except Exception as e: | 133 except Exception as e: |
92 dialog.notification.show(f"Can't send message: {e}", "error") | 134 dialog.notification.show(f"Can't send message: {e}", "error") |
93 else: | 135 else: |
94 self.message_input.value = "" | 136 self.message_input.value = "" |
95 self.attachments_elt.clear() | 137 self.attachments_elt.clear() |
138 self.input_mode = "normal" | |
96 | 139 |
97 def _on_message_new( | 140 def _on_message_new( |
98 self, | 141 self, |
99 uid: str, | 142 uid: str, |
100 timestamp: float, | 143 timestamp: float, |
147 log.debug(f"Message {uid} found, new reactions: {reactions}") | 190 log.debug(f"Message {uid} found, new reactions: {reactions}") |
148 reactions_elt = self.reactions_tpl.get_elt({"reactions": reactions}) | 191 reactions_elt = self.reactions_tpl.get_elt({"reactions": reactions}) |
149 reactions_wrapper_elt.clear() | 192 reactions_wrapper_elt.clear() |
150 reactions_wrapper_elt <= reactions_elt | 193 reactions_wrapper_elt <= reactions_elt |
151 self.add_reactions_listeners(reactions_elt) | 194 self.add_reactions_listeners(reactions_elt) |
195 elif type_ == "EDIT": | |
196 try: | |
197 old_message_elt = document[uid] | |
198 except KeyError: | |
199 log.debug(f"Message {uid} not found, no edition to apply") | |
200 else: | |
201 template_data = await self.message_to_template_data( | |
202 uid, | |
203 update_data["timestamp"], | |
204 jid.JID(update_data["from"]), | |
205 jid.JID(update_data["to"]), | |
206 update_data["message"], | |
207 update_data["subject"], | |
208 update_data["type"], | |
209 update_data["extra"] | |
210 ) | |
211 | |
212 new_message_elt = self.message_tpl.get_elt( | |
213 template_data | |
214 ) | |
215 old_message_elt.replaceWith(new_message_elt) | |
216 self.add_message_event_listeners(new_message_elt) | |
217 self.handle_url_previews(new_message_elt) | |
152 else: | 218 else: |
153 log.warning(f"Unsupported update type: {type_!r}") | 219 log.warning(f"Unsupported update type: {type_!r}") |
154 | 220 |
155 # If user was at the bottom, keep the scroll at the bottom | 221 # If user was at the bottom, keep the scroll at the bottom |
156 if is_at_bottom: | 222 if is_at_bottom: |
157 self.messages_elt.scrollTop = self.messages_elt.scrollHeight | 223 self.messages_elt.scrollTop = self.messages_elt.scrollHeight |
224 | |
225 async def message_to_template_data( | |
226 self, | |
227 uid: str, | |
228 timestamp: float, | |
229 from_jid: jid.JID, | |
230 to_jid: jid.JID, | |
231 message_data: dict, | |
232 subject_data: dict, | |
233 mess_type: str, | |
234 extra: dict, | |
235 ) -> dict: | |
236 """Generate template data to use with [message_tpl] | |
237 | |
238 @return: template data | |
239 """ | |
240 xhtml_data = extra.get("xhtml") | |
241 if not xhtml_data: | |
242 xhtml = None | |
243 else: | |
244 try: | |
245 xhtml = xhtml_data[""] | |
246 except KeyError: | |
247 xhtml = next(iter(xhtml_data.values())) | |
248 | |
249 if chat_type == "group": | |
250 await cache.fill_identities([str(from_jid)]) | |
251 else: | |
252 await cache.fill_identities([str(jid.JID(from_jid).bare)]) | |
253 from_jid = from_jid.bare | |
254 | |
255 return { | |
256 "own_jid": own_jid, | |
257 "msg": { | |
258 "id": uid, | |
259 "timestamp": extra.get("updated", timestamp), | |
260 "type": mess_type, | |
261 "from_": str(from_jid), | |
262 "text": message_data.get("") or next(iter(message_data.values()), ""), | |
263 "subject": subject_data.get("") or next(iter(subject_data.values()), ""), | |
264 "type": mess_type, | |
265 "thread": extra.get("thread"), | |
266 "thread_parent": extra.get("thread_parent"), | |
267 "reeceived": extra.get("received_timestamp") or timestamp, | |
268 "delay_sender": extra.get("delay_sender"), | |
269 "info_type": extra.get("info_type"), | |
270 "html": safe(xhtml) if xhtml else None, | |
271 "encrypted": extra.get("encrypted", False), | |
272 "received": extra.get("received", False), | |
273 "attachments": extra.get("attachments", []), | |
274 "extra": extra | |
275 }, | |
276 "identities": identities, | |
277 } | |
158 | 278 |
159 async def on_message_new( | 279 async def on_message_new( |
160 self, | 280 self, |
161 uid: str, | 281 uid: str, |
162 timestamp: float, | 282 timestamp: float, |
175 and self.new_messages_marker_elt.parent is None | 295 and self.new_messages_marker_elt.parent is None |
176 ): | 296 ): |
177 # the page is not visible, and we have no new messages marker yet, so we add | 297 # the page is not visible, and we have no new messages marker yet, so we add |
178 # it | 298 # it |
179 self.messages_elt <= self.new_messages_marker_elt | 299 self.messages_elt <= self.new_messages_marker_elt |
180 xhtml_data = extra.get("xhtml") | 300 |
181 if not xhtml_data: | 301 template_data = await self.message_to_template_data( |
182 xhtml = None | 302 uid, |
183 else: | 303 timestamp, |
184 try: | 304 from_jid, |
185 xhtml = xhtml_data[""] | 305 to_jid, |
186 except KeyError: | 306 message_data, |
187 xhtml = next(iter(xhtml_data.values())) | 307 subject_data, |
188 | 308 mess_type, |
189 if chat_type == "group": | 309 extra |
190 await cache.fill_identities([str(from_jid)]) | 310 ) |
191 else: | 311 |
192 await cache.fill_identities([str(jid.JID(from_jid).bare)]) | |
193 | |
194 msg_data = { | |
195 "id": uid, | |
196 "timestamp": timestamp, | |
197 "type": mess_type, | |
198 "from_": str(from_jid), | |
199 "text": message_data.get("") or next(iter(message_data.values()), ""), | |
200 "subject": subject_data.get("") or next(iter(subject_data.values()), ""), | |
201 "type": mess_type, | |
202 "thread": extra.get("thread"), | |
203 "thread_parent": extra.get("thread_parent"), | |
204 "reeceived": extra.get("received_timestamp") or timestamp, | |
205 "delay_sender": extra.get("delay_sender"), | |
206 "info_type": extra.get("info_type"), | |
207 "html": safe(xhtml) if xhtml else None, | |
208 "encrypted": extra.get("encrypted", False), | |
209 "received": extra.get("received", False), | |
210 "edited": extra.get("edited", False), | |
211 "attachments": extra.get("attachments", []), | |
212 } | |
213 message_elt = self.message_tpl.get_elt( | 312 message_elt = self.message_tpl.get_elt( |
214 { | 313 template_data |
215 "own_jid": own_jid, | |
216 "msg": msg_data, | |
217 "identities": identities, | |
218 } | |
219 ) | 314 ) |
220 | 315 |
221 # Check if user is viewing older messages or is at the bottom | 316 # Check if user is viewing older messages or is at the bottom |
222 is_at_bottom = self.is_at_bottom | 317 is_at_bottom = self.is_at_bottom |
223 | 318 |
256 if evt.keyCode == 13: # <Enter> key | 351 if evt.keyCode == 13: # <Enter> key |
257 if not window.navigator.maxTouchPoints: | 352 if not window.navigator.maxTouchPoints: |
258 # we have a non touch device, we send message on <Enter> | 353 # we have a non touch device, we send message on <Enter> |
259 if not evt.shiftKey: | 354 if not evt.shiftKey: |
260 evt.preventDefault() # Prevents line break | 355 evt.preventDefault() # Prevents line break |
261 self.send_message() | 356 aio.run(self.send_message()) |
357 elif evt.keyCode == 27: # <ESC> key | |
358 evt.preventDefault() | |
359 self.message_input.value = '' | |
360 self.input_mode = 'normal' | |
361 self.auto_resize_message_input() | |
362 elif evt.keyCode == 38: # <Up> arrow key | |
363 if self.input_mode == "normal" and self.message_input.value.strip() == "": | |
364 evt.preventDefault() | |
365 own_msgs = document.getElementsByClassName('own_msg') | |
366 if own_msgs.length > 0: | |
367 last_msg = own_msgs[own_msgs.length - 1] | |
368 aio.run(self.on_action_edit(None, last_msg)) | |
262 | 369 |
263 def update_attachments_visibility(self): | 370 def update_attachments_visibility(self): |
264 if len(self.attachments_elt.children): | 371 if len(self.attachments_elt.children): |
265 self.attachments_elt.classList.remove("is-contracted") | 372 self.attachments_elt.classList.remove("is-contracted") |
266 else: | 373 else: |
281 | 388 |
282 def on_attach_button_click(self, evt): | 389 def on_attach_button_click(self, evt): |
283 document["file_input"].click() | 390 document["file_input"].click() |
284 | 391 |
285 def on_extra_btn_click(self, evt): | 392 def on_extra_btn_click(self, evt): |
286 print("extra bouton clicked!") | 393 message_elt = evt.target.closest("div.is-chat-message") |
394 is_own = message_elt.classList.contains("own_msg") | |
395 if is_own: | |
396 own_messages = document.select('.own_msg') | |
397 # with XMPP, we can currently only edit our last message | |
398 can_edit = own_messages and message_elt is own_messages[-1] | |
399 else: | |
400 can_edit = False | |
401 | |
402 content_elt = self.extra_menu_tpl.get_elt({ | |
403 "edit": can_edit, | |
404 }) | |
405 extra_popup = popup.create_popup(evt.target, content_elt, focus_elt=message_elt) | |
406 | |
407 def on_action_click(evt, callback): | |
408 extra_popup.hide() | |
409 aio.run( | |
410 callback(evt, message_elt) | |
411 ) | |
412 | |
413 for cls_name, callback in ( | |
414 ("action_quote", self.on_action_quote), | |
415 ("action_edit", self.on_action_edit) | |
416 ): | |
417 for elt in content_elt.select(f".{cls_name}"): | |
418 elt.bind("click", lambda evt, callback=callback: on_action_click( | |
419 evt, callback | |
420 )) | |
287 | 421 |
288 def on_reaction_click(self, evt, message_elt): | 422 def on_reaction_click(self, evt, message_elt): |
289 window.evt = evt | 423 window.evt = evt |
290 aio.run( | 424 aio.run( |
291 bridge.message_reactions_set( | 425 bridge.message_reactions_set( |
292 message_elt["id"], [evt.detail["unicode"]], "toggle" | 426 message_elt["id"], [evt.detail["unicode"]], "toggle" |
293 ) | 427 ) |
294 ) | 428 ) |
295 | |
296 @bind(document["attachments"], "wheel") | |
297 def wheel_event(evt): | |
298 """Make the mouse wheel to act on horizontal scrolling for attachments | |
299 | |
300 Attachments don't have vertical scrolling, thus is makes sense to use the wheel | |
301 for horizontal scrolling | |
302 """ | |
303 if evt.deltaY != 0: | 429 if evt.deltaY != 0: |
304 document["attachments"].scrollLeft += evt.deltaY * 0.8 | 430 document["attachments"].scrollLeft += evt.deltaY * 0.8 |
305 evt.preventDefault() | 431 evt.preventDefault() |
432 | |
433 async def get_message_tuple(self, message_elt) -> tuple|None: | |
434 """Retrieve message tuple from as sent by [message_new] | |
435 | |
436 If not corresponding message data is found, an error will shown, and None is | |
437 returned. | |
438 @param message_elt: message element, it's "id" attribute will be use to retrieve | |
439 message data | |
440 @return: message data as a tuple, or None if not message with this ID is found. | |
441 """ | |
442 message_id = message_elt['id'] | |
443 history_data = await bridge.history_get( | |
444 "", "", -2, True, {"id": message_elt['id']} | |
445 ) | |
446 if not history_data: | |
447 dialog.notification.show(f"Can't find message {message_id}", "error") | |
448 return None | |
449 return history_data[0] | |
450 | |
451 async def on_action_quote(self, __, message_elt) -> None: | |
452 message_data = await self.get_message_tuple(message_elt) | |
453 if message_data is not None: | |
454 messages = message_data[4] | |
455 body = next(iter(messages.values()), "") | |
456 quote = "\n".join(f"> {l}" for l in body.split("\n")) | |
457 self.message_input.value = f"{quote}\n{self.message_input.value}" | |
458 self.input_mode = "quote" | |
459 self.input_data["id"] = message_elt["id"] | |
460 self.auto_resize_message_input() | |
461 self.message_input.focus() | |
462 | |
463 async def on_action_edit(self, __, message_elt) -> None: | |
464 message_data = await self.get_message_tuple(message_elt) | |
465 if message_data is not None: | |
466 messages = message_data[4] | |
467 body = next(iter(messages.values()), "") | |
468 if not body: | |
469 dialog.notification.show("No content found in message, nothing to edit") | |
470 return | |
471 | |
472 self.message_input.value = body | |
473 self.input_mode = "edit" | |
474 self.input_data["id"] = message_elt["id"] | |
475 self.auto_resize_message_input() | |
476 self.message_input.focus() | |
306 | 477 |
307 def get_reaction_panel(self, source_elt): | 478 def get_reaction_panel(self, source_elt): |
308 emoji_picker_elt = document.createElement("emoji-picker") | 479 emoji_picker_elt = document.createElement("emoji-picker") |
309 message_elt = source_elt.closest("div.is-chat-message") | 480 message_elt = source_elt.closest("div.is-chat-message") |
310 emoji_picker_elt.bind( | 481 emoji_picker_elt.bind( |
317 """Prepare a message to be dynamic | 488 """Prepare a message to be dynamic |
318 | 489 |
319 - make attachments dynamically clickable | 490 - make attachments dynamically clickable |
320 - make the extra button clickable | 491 - make the extra button clickable |
321 """ | 492 """ |
322 | |
323 ## attachments | 493 ## attachments |
324 # FIXME: only handle images for now, and display them in a modal | 494 # FIXME: only handle images for now, and display them in a modal |
325 if parent_elt is None: | 495 if parent_elt is None: |
326 parent_elt = document | 496 parent_elt = document |
327 img_elts = parent_elt.select(".message-attachment img") | 497 img_elts = parent_elt.select(".message-attachment img") |
340 "appendTo": document.body, | 510 "appendTo": document.body, |
341 "placement": "bottom", | 511 "placement": "bottom", |
342 "interactive": True, | 512 "interactive": True, |
343 "theme": "light", | 513 "theme": "light", |
344 "onShow": lambda __, message_elt=message_elt: ( | 514 "onShow": lambda __, message_elt=message_elt: ( |
345 message_elt.classList.add("chat-message-highlight") | 515 message_elt.classList.add("has-popup-focus") |
346 ), | 516 ), |
347 "onHide": lambda __, message_elt=message_elt: ( | 517 "onHide": lambda __, message_elt=message_elt: ( |
348 message_elt.classList.remove("chat-message-highlight") | 518 message_elt.classList.remove("has-popup-focus") |
349 ), | 519 ), |
350 }, | 520 }, |
351 ) | 521 ) |
352 | 522 |
353 ## extra button | 523 ## extra button |
354 for extra_btn in parent_elt.select(".extra-button"): | 524 for extra_btn in parent_elt.select(".extra-button"): |
355 extra_btn.bind("click", self.on_extra_btn_click) | 525 extra_btn.bind("click", self.on_extra_btn_click) |
526 | |
527 ## editions | |
528 for edition_icon_elt in parent_elt.select(".message-editions"): | |
529 message_elt = edition_icon_elt.closest("div.is-chat-message") | |
530 dataset = message_elt.dataset.to_dict() | |
531 try: | |
532 editions = json.loads(dataset["editions"]) | |
533 except (ValueError, KeyError): | |
534 log.error( | |
535 f"Internal Error: invalid or missing editions data: {message_elt['id']}" | |
536 ) | |
537 else: | |
538 for edition in editions: | |
539 edition["text"] = ( | |
540 edition["message"].get("") | |
541 or next(iter(edition["message"].values()), "") | |
542 ) | |
543 editions_elt = self.editions_tpl.get_elt({"editions": editions}) | |
544 tippy( | |
545 edition_icon_elt, | |
546 { | |
547 "content": editions_elt, | |
548 "theme": "light", | |
549 "appendTo": document.body | |
550 } | |
551 | |
552 ) | |
356 | 553 |
357 def add_reactions_listeners(self, parent_elt=None) -> None: | 554 def add_reactions_listeners(self, parent_elt=None) -> None: |
358 """Add listener on reactions to handle details and reaction toggle""" | 555 """Add listener on reactions to handle details and reaction toggle""" |
359 if parent_elt is None: | 556 if parent_elt is None: |
360 parent_elt = document | 557 parent_elt = document |
493 | 690 |
494 document["message_input"].bind( | 691 document["message_input"].bind( |
495 "input", lambda __: libervia_web_chat.auto_resize_message_input() | 692 "input", lambda __: libervia_web_chat.auto_resize_message_input() |
496 ) | 693 ) |
497 document["message_input"].bind("keydown", libervia_web_chat.on_message_keydown) | 694 document["message_input"].bind("keydown", libervia_web_chat.on_message_keydown) |
498 document["send_button"].bind("click", lambda __: libervia_web_chat.send_message()) | 695 document["send_button"].bind( |
696 "click", | |
697 lambda __: aio.run(libervia_web_chat.send_message()) | |
698 ) | |
499 document["attach_button"].bind("click", libervia_web_chat.on_attach_button_click) | 699 document["attach_button"].bind("click", libervia_web_chat.on_attach_button_click) |
500 document["file_input"].bind("change", libervia_web_chat.on_file_selected) | 700 document["file_input"].bind("change", libervia_web_chat.on_file_selected) |
501 | 701 |
502 document.bind("visibilitychange", libervia_web_chat.handle_visibility_change) | 702 document.bind("visibilitychange", libervia_web_chat.handle_visibility_change) |
503 | 703 |
504 bridge.register_signal("message_new", libervia_web_chat._on_message_new) | 704 bridge.register_signal("message_new", libervia_web_chat._on_message_new) |
505 bridge.register_signal("message_update", libervia_web_chat._on_message_update) | 705 bridge.register_signal("message_update", libervia_web_chat._on_message_update) |
506 | |
507 libervia_web_chat.add_message_event_listeners() | |
508 libervia_web_chat.handle_url_previews() | |
509 libervia_web_chat.add_reactions_listeners() |