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