comparison libervia/web/pages/chat/_browser/__init__.py @ 1619:a2cd4222c702

browser: Updates for new design: This patch add code to handle the new design for chat. New bridge method are used to invite users to MUC or get list of occupants. A new modules is used for components, with a first one for collapsible cards. rel 457
author Goffi <goffi@goffi.org>
date Sat, 12 Apr 2025 00:21:45 +0200
parents c4407befc52a
children 3a60bf3762ef
comparison
equal deleted inserted replaced
1618:5d9889f14012 1619:a2cd4222c702
1 import json 1 import json
2 import re 2 import re
3 from typing import Callable
4 import errors
3 5
4 from bridge import AsyncBridge as Bridge 6 from bridge import AsyncBridge as Bridge
5 from browser import DOMNode, aio, console as log, document, window 7 from browser import DOMNode, aio, console as log, document, window
6 from cache import cache, identities 8 from cache import cache, identities, roster
7 import dialog 9 import dialog
8 from file_uploader import FileUploader 10 from file_uploader import FileUploader
9 import jid 11 import jid
12 from javascript import pyobj2jsobj
10 from js_modules import emoji_picker_element 13 from js_modules import emoji_picker_element
11 from js_modules.tippy_js import tippy 14 from js_modules.tippy_js import tippy as tippy_ori
12 import popup 15 import popup
13 from template import Template, safe 16 from template import Template, safe
14 from tools import is_touch_device 17 from tools import is_touch_device
18 from loading import remove_loading_screen
19 from jid_search import JidSearch
20 from interpreter import Inspector
21 from components import init_collapsible_cards
15 22
16 log.warning = log.warn 23 log.warning = log.warn
17 profile = window.profile or "" 24 profile = window.profile or ""
18 # JID used in the local chat (real JID for one2one, room JID otherwise) 25 # JID used in the local chat (real JID for one2one, room JID otherwise)
19 own_local_jid = jid.JID(window.own_local_jid) 26 own_local_jid = jid.JID(window.own_local_jid)
20 target_jid = jid.JID(window.target_jid) 27 target_jid = jid.JID(window.target_jid)
21 chat_type = window.chat_type 28 chat_type = window.chat_type
29 chat_url = window.chat_url
22 bridge = Bridge() 30 bridge = Bridge()
23 31
24 # Sensible value to consider that user is at the bottom 32 # Sensible value to consider that user is at the bottom
25 SCROLL_SENSITIVITY = 200 33 SCROLL_SENSITIVITY = 200
26 34
27 INPUT_MODES = {"normal", "edit", "quote"} 35 INPUT_MODES = {"normal", "edit", "quote"}
28 MODE_CLASS = "mode_{}" 36 MODE_CLASS = "mode_{}"
37
38
39 # FIXME: workaround for https://github.com/brython-dev/brython/issues/2542
40 def tippy(target, data):
41 return tippy_ori(target, pyobj2jsobj(data))
42
43
44 class NewChatDialog:
45
46 def __init__(self, on_select: Callable[[str], None]|None = None) -> None:
47 self.on_select = on_select
48 self.new_chat_dialog_tpl = Template("chat/new_chat_dialog.html")
49 self.dialog_elt = self.new_chat_dialog_tpl.get_elt()
50 self.modal = dialog.Modal(self.dialog_elt, is_card=True)
51
52 # direct chat
53 self.direct_search_input_elt = self.dialog_elt.select_one(
54 "div.direct-content input.search-input"
55 )
56 self.direct_items_container = self.dialog_elt.select_one(".direct-items")
57 self.direct_count_elt = self.dialog_elt.select_one(".direct-count")
58 self.start_chat_btn = self.dialog_elt.select_one(".action_ok")
59 assert self.start_chat_btn is not None
60 self.start_chat_btn.bind("click", self.on_start_chat_btn)
61 if not self.direct_count_elt or not self.start_chat_btn:
62 log.error('"direct-count" or "action_ok" element is missing.')
63 self.selected_entities = set()
64 self.jid_search = JidSearch(
65 self.direct_search_input_elt,
66 self.direct_items_container,
67 click_cb = self.on_search_item_click,
68 template = "chat/search_item.html",
69 )
70 for elt in self.dialog_elt.select(".action_close"):
71 elt.bind("click", lambda __: self.close())
72 for elt in self.dialog_elt.select("div.direct-content .action_clear_search"):
73 elt.bind("click", lambda __: self.clear_search_input())
74
75 # groups
76 self.groups_search_input_elt = self.dialog_elt.select_one(
77 "div.groups-content input.search-input"
78 )
79 self.groups_items_container = self.dialog_elt.select_one(".groups-items")
80
81 self.selected_entities = set()
82
83 self.groups_jid_search = JidSearch(
84 self.groups_search_input_elt,
85 self.groups_items_container,
86 click_cb = self.on_group_search_item_click,
87 options={"type": "group"},
88 template = "chat/groups_search_item.html",
89 )
90 for elt in self.dialog_elt.select(".action_close"):
91 elt.bind("click", lambda __: self.close())
92 for elt in self.dialog_elt.select("div.groups-content .action_clear_search"):
93 elt.bind("click", lambda __: self.clear_groups_search_input())
94
95 self.new_room_btn = self.dialog_elt.select_one(".action_new_room")
96 assert self.new_room_btn is not None
97 self.new_room_btn.bind("click", self.on_new_room_btn)
98 self.panel_new_room = self.dialog_elt.select_one(".panel_new_room")
99 assert self.panel_new_room is not None
100 self.create_room_btn = self.dialog_elt.select_one(".action_create_room")
101 assert self.create_room_btn is not None
102 self.create_room_btn.bind("click", self._on_create_room_btn)
103
104 self.error_message_elt = self.dialog_elt.select_one("div.error-message")
105 assert self.error_message_elt is not None
106 self.error_message_elt.select_one("button.delete").bind(
107 "click",
108 lambda __: self.error_message_elt.classList.add("is-hidden")
109 )
110
111 # tabs
112 self.tabs = {}
113 self.selected_tab_elt = None
114 for tab_elt in self.dialog_elt.select('div.tabs>ul>li'):
115 if tab_elt.classList.contains("is-active"):
116 self.selected_tab_elt = tab_elt
117 tab_name = tab_elt.dataset.tab
118 tab_content_elt = self.dialog_elt.select_one(f".{tab_name}-content")
119 assert tab_content_elt is not None
120 self.tabs[tab_elt] = tab_content_elt
121 tab_elt.bind(
122 'click',
123 lambda __, tab_elt=tab_elt: self.set_active_tab(tab_elt)
124 )
125
126 def set_active_tab(self, selected_tab_elt) -> None:
127 """Display a tab."""
128 self.selected_tab_elt = selected_tab_elt
129 for tab_elt, tab_content_elt in self.tabs.items():
130 if tab_elt == selected_tab_elt:
131 tab_elt.classList.add("is-active")
132 tab_content_elt.classList.remove("is-hidden")
133 else:
134 tab_elt.classList.remove("is-active")
135 tab_content_elt.classList.add("is-hidden")
136 self.update()
137
138 def clear_search_input(self) -> None:
139 """Clear search input, and update dialog."""
140 self.direct_search_input_elt.value = ""
141 self.direct_search_input_elt.dispatchEvent(window.Event.new("input"))
142 self.update()
143
144 def clear_groups_search_input(self) -> None:
145 """Clear search input, and update dialog."""
146 self.groups_search_input_elt.value = ""
147 self.groups_search_input_elt.dispatchEvent(window.Event.new("input"))
148 self.groups_update()
149
150 def on_search_item_click(self, event, item) -> None:
151 """A search item has been clicked"""
152 search_item_elt = event.currentTarget
153 search_item_elt.classList.toggle("is-selected")
154 self.update()
155
156 def on_group_search_item_click(self, event, item) -> None:
157 """A search item has been clicked"""
158 for item_elt in self.groups_items_container.select(".search-item"):
159 if item_elt == event.currentTarget:
160 item_elt.classList.add("is-selected")
161 else:
162 item_elt.classList.remove("is-selected")
163 self.update()
164
165 def update(self) -> None:
166 """Update dialog elements (counter, button) when search items change."""
167 assert self.selected_tab_elt is not None
168 current_tab = self.selected_tab_elt.dataset.tab
169 match current_tab:
170 case "direct":
171 self.selected_entities = {
172 item_elt.dataset.entity for item_elt in
173 self.direct_items_container.select(".search-item.is-selected")
174 }
175 self.direct_count_elt.text = str(len(self.selected_entities))
176 case "groups":
177 self.selected_entities = {
178 item_elt.dataset.entity for item_elt in
179 self.groups_items_container.select(".search-item.is-selected")
180 }
181 case _:
182 raise ValueError(f"Unknown tab: {current_tab!r}.")
183
184 self.start_chat_btn.disabled = not bool(self.selected_entities)
185
186 def groups_update(self) -> None:
187 """Update dialog elements when groups search items change."""
188 self.selected_entities = {
189 item_elt.dataset.entity for item_elt in
190 self.direct_items_container.select(".search-item.is-selected")
191 }
192 self.start_chat_btn.disabled = not bool(self.selected_entities)
193
194 def on_new_room_btn(self, evt) -> None:
195 self.panel_new_room.classList.toggle("is-hidden")
196
197 def _on_create_room_btn(self, evt) -> None:
198 aio.run(self.on_create_room_btn())
199
200 async def on_create_room_btn(self) -> None:
201 assert self.on_select is not None
202 input_elt = self.dialog_elt.select_one(".input-room-name")
203 assert input_elt is not None
204 try:
205 joined_data = await bridge.muc_join(input_elt.value.strip(), "", {})
206 except Exception as e:
207 msg = f"Can't create room: {e}"
208 log.error(msg)
209 self.error_message_elt.select_one("p").text = msg
210 self.error_message_elt.classList.remove("is-hidden")
211 return
212
213 joined, room_jid_s, occupants, user_nick, subject, statuses, profile = joined_data
214 self.on_select(room_jid_s)
215
216 def on_start_chat_btn(self, evt) -> None:
217 evt.stopPropagation()
218 if self.on_select is None:
219 return
220 if not self.selected_entities:
221 raise errors.InternalError(
222 "Start button should never be called when no entity is selected."
223 )
224 if len(self.selected_entities) == 1:
225 selected_entity = next(iter(self.selected_entities))
226 self.on_select(selected_entity)
227 else:
228 aio.run(self.create_room_selected_jids())
229
230 async def create_room_selected_jids(self) -> None:
231 assert self.on_select is not None
232 joined_data = await bridge.muc_join("", "", {})
233 joined, room_jid_s, occupants, user_nick, subject, statuses, profile = joined_data
234 if not self.selected_entities:
235 Inspector()
236 for entity_jid in self.selected_entities:
237 print(f"inviting {entity_jid=}")
238 await bridge.muc_invite(entity_jid, room_jid_s, {})
239 self.on_select(room_jid_s)
240
241
242 def show(self) -> None:
243 """Show the dialog."""
244 # We want ot be sure to have the elements correctly set when dialog is shown
245 self.update()
246 self.modal.show()
247
248 def close(self) -> None:
249 """Close the dialog."""
250 self.modal.close()
29 251
30 252
31 class LiberviaWebChat: 253 class LiberviaWebChat:
32 def __init__(self): 254 def __init__(self):
33 self._input_mode = "normal" 255 self._input_mode = "normal"
34 self.input_data = {} 256 self.input_data = {}
257 self.direct_messages_tpl = Template("chat/direct_messages.html")
35 self.message_tpl = Template("chat/message.html") 258 self.message_tpl = Template("chat/message.html")
36 self.extra_menu_tpl = Template("chat/extra_menu.html") 259 self.extra_menu_tpl = Template("chat/extra_menu.html")
37 self.reactions_tpl = Template("chat/reactions.html") 260 self.reactions_tpl = Template("chat/reactions.html")
38 self.reactions_details_tpl = Template("chat/reactions_details.html") 261 self.reactions_details_tpl = Template("chat/reactions_details.html")
39 self.url_preview_control_tpl = Template("components/url_preview_control.html") 262 self.url_preview_control_tpl = Template("components/url_preview_control.html")
40 self.url_preview_tpl = Template("components/url_preview.html") 263 self.url_preview_tpl = Template("components/url_preview.html")
41 self.new_messages_marker_elt = Template("chat/new_messages_marker.html").get_elt() 264 self.new_messages_marker_elt = Template("chat/new_messages_marker.html").get_elt()
42 self.editions_tpl = Template("chat/editions.html") 265 self.editions_tpl = Template("chat/editions.html")
266 self.occupant_item_tpl = Template("chat/occupant_item.html")
267
268 # panels and their toggle buttons
269
270 self.left_panel = document["left_panel"]
271 self.left_toggle = document["left_panel-toggle"]
272 self.left_toggle.bind("click", self.on_left_panel_toggle_click)
273 self.main_panel = document["main_panel"]
274 self.right_panel = document["right_panel"]
275 self.right_toggle = document["right_panel-toggle"]
276 self.right_toggle.bind("click", self.on_right_panel_toggle_click)
43 277
44 self.messages_elt = document["messages"] 278 self.messages_elt = document["messages"]
279
280 # right-panel internal buttons
281 init_collapsible_cards(self.right_panel)
45 282
46 # attachments 283 # attachments
47 self.file_uploader = FileUploader( 284 self.file_uploader = FileUploader(
48 "", "chat/attachment_preview.html", on_delete_cb=self.on_attachment_delete 285 "", "chat/attachment_preview.html", on_delete_cb=self.on_attachment_delete
49 ) 286 )
50 self.attachments_elt = document["attachments"] 287 self.attachments_elt = document["attachments"]
51 self.message_input = document["message_input"] 288 self.message_input = document["message_input_area"]
52 289
53 close_button = document.select_one(".modal-close") 290 # close_button = document.select_one(".modal-close")
54 close_button.bind("click", self.close_modal) 291 # close_button.bind("click", self.close_modal)
55 292
56 # hide/show attachments 293 # hide/show attachments
57 MutationObserver = window.MutationObserver 294 MutationObserver = window.MutationObserver
58 observer = MutationObserver.new(lambda *__: self.update_attachments_visibility()) 295 observer = MutationObserver.new(lambda *__: self.update_attachments_visibility())
59 observer.observe(self.attachments_elt, {"childList": True}) 296 observer.observe(self.attachments_elt, {"childList": True})
67 self.add_reactions_listeners() 304 self.add_reactions_listeners()
68 305
69 # input 306 # input
70 self.auto_resize_message_input() 307 self.auto_resize_message_input()
71 self.message_input.focus() 308 self.message_input.focus()
309
310 # direct messages
311 direct_messages_elt = self.direct_messages_tpl.get_elt(
312 {
313 "roster": roster,
314 "identities": identities,
315 "chat_url": chat_url
316 }
317 )
318 document["direct-messages"] <= direct_messages_elt
319
320 async def post_init(self) -> None:
321 if chat_type == "group":
322 occupants = await bridge.muc_occupants_get(
323 str(target_jid)
324 )
325 document["occupants-count"].text = str(len(occupants))
326 for occupant, occupant_data in occupants.items():
327 occupant_elt = self.occupant_item_tpl.get_elt({
328 "nick": occupant,
329 "item": occupant_data,
330 "identities": identities,
331 })
332 document["group-occupants"] <= occupant_elt
72 333
73 @property 334 @property
74 def input_mode(self) -> str: 335 def input_mode(self) -> str:
75 return self._input_mode 336 return self._input_mode
76 337
92 self.messages_elt.scrollHeight 353 self.messages_elt.scrollHeight
93 - self.messages_elt.scrollTop 354 - self.messages_elt.scrollTop
94 - self.messages_elt.clientHeight 355 - self.messages_elt.clientHeight
95 <= SCROLL_SENSITIVITY 356 <= SCROLL_SENSITIVITY
96 ) 357 )
358
359 def open_chat(self, entity_jid: str) -> None:
360 """Change the current chat for the given one."""
361 # For now we keep it simple and just load the new location.
362 window.location = f"{chat_url}/{entity_jid}"
363
364 async def on_new_chat(self) -> None:
365 new_chat_dialog = NewChatDialog(on_select = self.open_chat)
366 new_chat_dialog.show()
97 367
98 async def send_message(self): 368 async def send_message(self):
99 """Send message currently in input area 369 """Send message currently in input area
100 370
101 The message and corresponding attachment will be sent 371 The message and corresponding attachment will be sent
134 except Exception as e: 404 except Exception as e:
135 dialog.notification.show(f"Can't send message: {e}", "error") 405 dialog.notification.show(f"Can't send message: {e}", "error")
136 else: 406 else:
137 self.message_input.value = "" 407 self.message_input.value = ""
138 self.attachments_elt.clear() 408 self.attachments_elt.clear()
409 self.auto_resize_message_input()
139 self.input_mode = "normal" 410 self.input_mode = "normal"
140 411
141 def _on_message_new( 412 def _on_message_new(
142 self, 413 self,
143 uid: str, 414 uid: str,
346 if is_at_bottom: 617 if is_at_bottom:
347 self.messages_elt.scrollTop = self.messages_elt.scrollHeight 618 self.messages_elt.scrollTop = self.messages_elt.scrollHeight
348 619
349 def auto_resize_message_input(self): 620 def auto_resize_message_input(self):
350 """Resize the message input field according to content.""" 621 """Resize the message input field according to content."""
351
352 is_at_bottom = self.is_at_bottom 622 is_at_bottom = self.is_at_bottom
353 623
354 # The textarea's height is first reset to 'auto' to ensure it's not influenced by 624 # The textarea's height is first reset to 'auto' to ensure it's not influenced by
355 # the previous content. 625 # the previous content.
356 self.message_input.style.height = "auto" 626 self.message_input.style.height = "auto"
361 self.message_input.style.height = f"{self.message_input.scrollHeight + 2}px" 631 self.message_input.style.height = f"{self.message_input.scrollHeight + 2}px"
362 632
363 if is_at_bottom: 633 if is_at_bottom:
364 # we want the message are to still display the last message 634 # we want the message are to still display the last message
365 self.messages_elt.scrollTop = self.messages_elt.scrollHeight 635 self.messages_elt.scrollTop = self.messages_elt.scrollHeight
636
637 def on_left_panel_toggle_click(self, evt) -> None:
638 """Show/Hide side bar."""
639 self.left_panel.classList.toggle("is-collapsed")
640 self.main_panel.classList.toggle("is-expanded-left")
641
642 def on_right_panel_toggle_click(self, evt) -> None:
643 """Show/Hide side bar."""
644 self.right_panel.classList.toggle("is-collapsed")
645 self.main_panel.classList.toggle("is-expanded-right")
366 646
367 def on_message_keydown(self, evt): 647 def on_message_keydown(self, evt):
368 """Handle the 'keydown' event of the message input field 648 """Handle the 'keydown' event of the message input field
369 649
370 @param evt: The event object. 'target' refers to the textarea element. 650 @param evt: The event object. 'target' refers to the textarea element.
406 target = evt.currentTarget 686 target = evt.currentTarget
407 item_elt = DOMNode(target.closest(".attachment-preview")) 687 item_elt = DOMNode(target.closest(".attachment-preview"))
408 item_elt.remove() 688 item_elt.remove()
409 689
410 def on_attach_button_click(self, evt): 690 def on_attach_button_click(self, evt):
411 document["file_input"].click() 691 document["file-input"].click()
412 692
413 def on_extra_btn_click(self, evt): 693 def on_extra_btn_click(self, evt):
414 message_elt = evt.target.closest("div.is-chat-message") 694 message_elt = evt.target.closest("div.chat-message")
695 message_core_elt = evt.target.closest("div.message-core")
415 is_own = message_elt.classList.contains("own_msg") 696 is_own = message_elt.classList.contains("own_msg")
416 if is_own: 697 if is_own:
417 own_messages = document.select('.own_msg') 698 own_messages = document.select('.own_msg')
418 # with XMPP, we can currently only edit our last message 699 # with XMPP, we can currently only edit our last message
419 can_edit = own_messages and message_elt is own_messages[-1] 700 can_edit = own_messages and message_elt is own_messages[-1]
422 703
423 content_elt = self.extra_menu_tpl.get_elt({ 704 content_elt = self.extra_menu_tpl.get_elt({
424 "edit": can_edit, 705 "edit": can_edit,
425 "retract": is_own, 706 "retract": is_own,
426 }) 707 })
427 extra_popup = popup.create_popup(evt.target, content_elt, focus_elt=message_elt) 708 extra_popup = popup.create_popup(evt.target, content_elt, focus_elt=message_core_elt)
428 709
429 def on_action_click(evt, callback): 710 def on_action_click(evt, callback):
430 extra_popup.hide() 711 extra_popup.hide()
431 aio.run( 712 aio.run(
432 callback(evt, message_elt) 713 callback(evt, message_elt)
447 aio.run( 728 aio.run(
448 bridge.message_reactions_set( 729 bridge.message_reactions_set(
449 message_elt["id"], [evt.detail["unicode"]], "toggle" 730 message_elt["id"], [evt.detail["unicode"]], "toggle"
450 ) 731 )
451 ) 732 )
452 if evt.deltaY != 0: 733 # if evt.deltaY != 0:
453 document["attachments"].scrollLeft += evt.deltaY * 0.8 734 # document["attachments"].scrollLeft += evt.deltaY * 0.8
454 evt.preventDefault() 735 # evt.preventDefault()
455 736
456 async def get_message_tuple(self, message_elt) -> tuple|None: 737 async def get_message_tuple(self, message_elt) -> tuple|None:
457 """Retrieve message tuple from as sent by [message_new] 738 """Retrieve message tuple from as sent by [message_new]
458 739
459 If not corresponding message data is found, an error will shown, and None is 740 If not corresponding message data is found, an error will shown, and None is
460 returned. 741 returned.
461 @param message_elt: message element, it's "id" attribute will be use to retrieve 742 @param message_elt: message element, it's "id" attribute will be use to retrieve
462 message data 743 message data
463 @return: message data as a tuple, or None if not message with this ID is found. 744 @return: message data as a tuple, or None if not message with this ID is found.
464 """ 745 """
746 print(f"{message_elt=}")
465 message_id = message_elt['id'] 747 message_id = message_elt['id']
466 history_data = await bridge.history_get( 748 history_data = await bridge.history_get(
467 "", "", -2, True, {"id": message_elt['id']} 749 "", "", -2, True, {"id": message_elt['id']}
468 ) 750 )
469 if not history_data: 751 if not history_data:
512 else: 794 else:
513 log.info(f"Retraction of message {message_elt['id']} cancelled by user.") 795 log.info(f"Retraction of message {message_elt['id']} cancelled by user.")
514 796
515 def get_reaction_panel(self, source_elt): 797 def get_reaction_panel(self, source_elt):
516 emoji_picker_elt = document.createElement("emoji-picker") 798 emoji_picker_elt = document.createElement("emoji-picker")
517 message_elt = source_elt.closest("div.is-chat-message") 799 message_elt = source_elt.closest("div.chat-message")
518 emoji_picker_elt.bind( 800 emoji_picker_elt.bind(
519 "emoji-click", lambda evt: self.on_reaction_click(evt, message_elt) 801 "emoji-click", lambda evt: self.on_reaction_click(evt, message_elt)
520 ) 802 )
521 803
522 return emoji_picker_elt 804 return emoji_picker_elt
535 for img_elt in img_elts: 817 for img_elt in img_elts:
536 img_elt.bind("click", self.open_modal) 818 img_elt.bind("click", self.open_modal)
537 img_elt.style.cursor = "pointer" 819 img_elt.style.cursor = "pointer"
538 820
539 ## reaction button 821 ## reaction button
822 i = 0
540 for reaction_btn in parent_elt.select(".reaction-button"): 823 for reaction_btn in parent_elt.select(".reaction-button"):
541 message_elt = reaction_btn.closest("div.is-chat-message") 824 i+=1
825 message_elt = reaction_btn.closest("div.message-core")
542 tippy( 826 tippy(
543 reaction_btn, 827 reaction_btn,
544 { 828 {
545 "trigger": "click", 829 "trigger": "click",
546 "content": self.get_reaction_panel, 830 "content": self.get_reaction_panel,
552 message_elt.classList.add("has-popup-focus") 836 message_elt.classList.add("has-popup-focus")
553 ), 837 ),
554 "onHide": lambda __, message_elt=message_elt: ( 838 "onHide": lambda __, message_elt=message_elt: (
555 message_elt.classList.remove("has-popup-focus") 839 message_elt.classList.remove("has-popup-focus")
556 ), 840 ),
557 }, 841 }
558 ) 842 )
559 843
560 ## extra button 844 ## extra button
561 for extra_btn in parent_elt.select(".extra-button"): 845 for extra_btn in parent_elt.select(".extra-button"):
562 extra_btn.bind("click", self.on_extra_btn_click) 846 extra_btn.bind("click", self.on_extra_btn_click)
563 847
564 ## editions 848 ## editions
565 for edition_icon_elt in parent_elt.select(".message-editions"): 849 for edition_icon_elt in parent_elt.select(".message-editions"):
566 message_elt = edition_icon_elt.closest("div.is-chat-message") 850 message_elt = edition_icon_elt.closest("div.chat-message")
567 dataset = message_elt.dataset.to_dict() 851 dataset = message_elt.dataset.to_dict()
568 try: 852 try:
569 editions = json.loads(dataset["editions"]) 853 editions = json.loads(dataset["editions"])
570 except (ValueError, KeyError): 854 except (ValueError, KeyError):
571 log.error( 855 log.error(
615 tippy(reaction_elt, tippy_config) 899 tippy(reaction_elt, tippy_config)
616 900
617 # Toggle reaction when clicked/touched 901 # Toggle reaction when clicked/touched
618 emoji_elt = reaction_elt.select_one(".emoji") 902 emoji_elt = reaction_elt.select_one(".emoji")
619 emoji = emoji_elt.html.strip() 903 emoji = emoji_elt.html.strip()
620 message_elt = reaction_elt.closest("div.is-chat-message") 904 message_elt = reaction_elt.closest("div.chat-message")
621 msg_id = message_elt["id"] 905 msg_id = message_elt["id"]
622 906
623 def toggle_reaction(event, msg_id=msg_id, emoji=emoji): 907 def toggle_reaction(event, msg_id=msg_id, emoji=emoji):
624 # Prevent default if it's a touch device to distinguish from long press 908 # Prevent default if it's a touch device to distinguish from long press
625 if is_touch: 909 if is_touch:
681 user click, or directly the previews, or nothing at all. 965 user click, or directly the previews, or nothing at all.
682 """ 966 """
683 967
684 if parent_elt is None: 968 if parent_elt is None:
685 parent_elt = document 969 parent_elt = document
686 chat_message_elts = parent_elt.select(".is-chat-message") 970 chat_message_elts = parent_elt.select(".chat-message")
687 else: 971 else:
688 chat_message_elts = [parent_elt] 972 chat_message_elts = [parent_elt]
689 for message_elt in chat_message_elts: 973 for message_elt in chat_message_elts:
690 urls = self.find_links(message_elt) 974 urls = self.find_links(message_elt)
691 if urls: 975 if urls:
723 self.new_messages_marker_elt.remove() 1007 self.new_messages_marker_elt.remove()
724 1008
725 1009
726 libervia_web_chat = LiberviaWebChat() 1010 libervia_web_chat = LiberviaWebChat()
727 1011
1012 document["new_chat_btn"].bind(
1013 "click", lambda __: aio.run(libervia_web_chat.on_new_chat())
1014 )
1015
728 document["message_input"].bind( 1016 document["message_input"].bind(
729 "input", lambda __: libervia_web_chat.auto_resize_message_input() 1017 "input", lambda __: libervia_web_chat.auto_resize_message_input()
730 ) 1018 )
731 document["message_input"].bind("keydown", libervia_web_chat.on_message_keydown) 1019 document["message_input"].bind("keydown", libervia_web_chat.on_message_keydown)
732 document["send_button"].bind( 1020 # document["send_button"].bind(
733 "click", 1021 # "click",
734 lambda __: aio.run(libervia_web_chat.send_message()) 1022 # lambda __: aio.run(libervia_web_chat.send_message())
735 ) 1023 # )
736 document["attach_button"].bind("click", libervia_web_chat.on_attach_button_click) 1024 document["attach-button"].bind("click", libervia_web_chat.on_attach_button_click)
737 document["file_input"].bind("change", libervia_web_chat.on_file_selected) 1025 document["file-input"].bind("change", libervia_web_chat.on_file_selected)
738 1026
739 document.bind("visibilitychange", libervia_web_chat.handle_visibility_change) 1027 document.bind("visibilitychange", libervia_web_chat.handle_visibility_change)
740 1028
741 bridge.register_signal("message_new", libervia_web_chat._on_message_new) 1029 bridge.register_signal("message_new", libervia_web_chat._on_message_new)
742 bridge.register_signal("message_update", libervia_web_chat._on_message_update) 1030 bridge.register_signal("message_update", libervia_web_chat._on_message_update)
1031 aio.run(libervia_web_chat.post_init())
1032 remove_loading_screen()