Mercurial > libervia-web
comparison libervia/web/pages/chat/_browser/__init__.py @ 1625:698eaabfca0e
browser (chat): side/panel and keyword handling:
- `dialog.Modal` can now be used with a `position` argument. The default `center` keeps
the old behaviour of modal in the middle of the screen, while using one of the
direction makes the modal appear from top/bottom/right/left with an animation. The
closing cross has been removed in favor of clicking/touching any part outside of the
modal.
- A method to create mockup clones of elements is now used to make placeholders. It is
used for the input panel in the main area when it is moved to a sub-panel modal, this
way there is not element disappearing.
- Clicking on a keyword now shows a sub-messages panel with all messages with this
keyword, in a similar way as for threads. Writing a messages there will add the keyword
automatically.
- Sub-messages panel will auto-scroll when messages are added.
rel 458
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 06 Jun 2025 11:08:05 +0200 |
parents | fdb5689fb826 |
children | 84e287565fab |
comparison
equal
deleted
inserted
replaced
1624:fd421f1be8f5 | 1625:698eaabfca0e |
---|---|
1 from html import escape | |
1 import json | 2 import json |
2 import re | 3 import re |
3 from typing import Callable | 4 from typing import Callable |
4 import errors | 5 import errors |
5 | 6 |
12 from javascript import pyobj2jsobj | 13 from javascript import pyobj2jsobj |
13 from js_modules import emoji_picker_element | 14 from js_modules import emoji_picker_element |
14 from js_modules.tippy_js import tippy as tippy_ori | 15 from js_modules.tippy_js import tippy as tippy_ori |
15 import popup | 16 import popup |
16 from template import Template, safe | 17 from template import Template, safe |
17 from tools import is_touch_device, remove_ids | 18 from tools import is_touch_device, remove_ids, make_placeholder |
18 from loading import remove_loading_screen | 19 from loading import remove_loading_screen |
19 from jid_search import JidSearch | 20 from jid_search import JidSearch |
20 from interpreter import Inspector | 21 from interpreter import Inspector |
21 from components import init_collapsible_cards | 22 from components import init_collapsible_cards |
22 | 23 |
287 ) | 288 ) |
288 self.attachments_elt = document["attachments"] | 289 self.attachments_elt = document["attachments"] |
289 self.message_input = document["message_input_area"] | 290 self.message_input = document["message_input_area"] |
290 | 291 |
291 # reply to/thread | 292 # reply to/thread |
292 self.thread_panel_tpl = Template("chat/thread_panel.html") | 293 self.sub_messages_panel_tpl = Template("chat/sub_messages_panel.html") |
293 # current thread panel, if any. | 294 # current thread panel, if any. |
294 self.thread_panel = None | 295 self.sub_messages_panel = None |
295 self._reply_to = None | 296 # Method to call to check if a new message must be displayer in sub-messages |
296 self.thread_id = None | 297 # panel. |
298 self.sub_messages_check_cb: Callable|None = None | |
299 self._reply_to: str|None = None | |
300 self.thread_id: str|None = None | |
301 self.keyword: str|None = None | |
297 document["cancel_reply_btn"].bind( | 302 document["cancel_reply_btn"].bind( |
298 "click", | 303 "click", |
299 lambda __: setattr(self, "reply_to", None) | 304 lambda __: setattr(self, "reply_to", None) |
300 ) | 305 ) |
301 # use `thead` property to modify. | 306 # use `thead` property to modify. |
361 target_elt.classList.add(MODE_CLASS.format(new_mode)) | 366 target_elt.classList.add(MODE_CLASS.format(new_mode)) |
362 self.input_data.clear() | 367 self.input_data.clear() |
363 | 368 |
364 @property | 369 @property |
365 def is_at_bottom(self): | 370 def is_at_bottom(self): |
366 return ( | 371 return self.is_elt_at_bottom(self.messages_elt) |
367 self.messages_elt.scrollHeight | |
368 - self.messages_elt.scrollTop | |
369 - self.messages_elt.clientHeight | |
370 <= SCROLL_SENSITIVITY | |
371 ) | |
372 | 372 |
373 @property | 373 @property |
374 def reply_to(self) -> dict|None: | 374 def reply_to(self) -> dict|None: |
375 return self._reply_to | 375 return self._reply_to |
376 | 376 |
420 f"Not message core found for message {message_elt['id']!r}." | 420 f"Not message core found for message {message_elt['id']!r}." |
421 ) | 421 ) |
422 else: | 422 else: |
423 message_core_elt.classList.add(SELECTED_THREAD_CLS) | 423 message_core_elt.classList.add(SELECTED_THREAD_CLS) |
424 | 424 |
425 def is_elt_at_bottom(self, elt) -> bool: | |
426 """Tell is a scrollable elemement is scrolled down. | |
427 | |
428 There is a margin to check it set with SCROLL_SENSITIVITY. | |
429 @param element: Scrollable element to check. | |
430 @return: True if the element is as its bottom. | |
431 """ | |
432 return elt.scrollHeight - elt.scrollTop - elt.clientHeight <= SCROLL_SENSITIVITY | |
433 | |
425 def open_chat(self, entity_jid: str) -> None: | 434 def open_chat(self, entity_jid: str) -> None: |
426 """Change the current chat for the given one.""" | 435 """Change the current chat for the given one.""" |
427 # For now we keep it simple and just load the new location. | 436 # For now we keep it simple and just load the new location. |
428 window.location = f"{chat_url}/{entity_jid}" | 437 window.location = f"{chat_url}/{entity_jid}" |
429 | 438 |
454 if self.reply_to: | 463 if self.reply_to: |
455 extra["reply"] = self.reply_to | 464 extra["reply"] = self.reply_to |
456 | 465 |
457 if self.thread_id: | 466 if self.thread_id: |
458 extra["thread"] = self.thread_id | 467 extra["thread"] = self.thread_id |
468 | |
469 if self.keyword: | |
470 extra.setdefault("keywords", []).append(self.keyword) | |
459 | 471 |
460 # now we send the message | 472 # now we send the message |
461 try: | 473 try: |
462 if self.input_mode == "edit": | 474 if self.input_mode == "edit": |
463 message_id = self.input_data["id"] | 475 message_id = self.input_data["id"] |
700 if "thread" not in parent_message_elt.dataset: | 712 if "thread" not in parent_message_elt.dataset: |
701 parent_message_elt.dataset["thread"] = thread_id | 713 parent_message_elt.dataset["thread"] = thread_id |
702 # TODO: Regenerate parent message, so thread icon will appear. | 714 # TODO: Regenerate parent message, so thread icon will appear. |
703 | 715 |
704 if ( | 716 if ( |
705 "thread" in extra | 717 self.sub_messages_check_cb is not None |
706 and self.thread_panel | 718 and self.sub_messages_panel is not None |
707 and self.thread_id | 719 and self.sub_messages_check_cb(extra) |
708 and extra["thread"] == self.thread_id | |
709 ): | 720 ): |
710 # FIXME: This is quite fragile, IDs are removed, meaning that some listener | 721 # FIXME: This is quite fragile, IDs are removed, meaning that some listener |
711 # may not be working correctly, and it's using #input-panel as reference, | 722 # may not be working correctly, |
712 # but in the future, it may not moved from main area the thread panel like | |
713 # that anymore. | |
714 cloned_message_elt = message_elt.cloneNode(True) | 723 cloned_message_elt = message_elt.cloneNode(True) |
715 remove_ids(cloned_message_elt) | 724 remove_ids(cloned_message_elt) |
716 thread_messages_elt = document["thread-messages"] | 725 sub_messages_elt = document["sub-messages"] |
717 thread_messages_elt.insertBefore(cloned_message_elt, document["input-panel"]) | 726 is_at_bottom = self.is_elt_at_bottom(sub_messages_elt) |
727 sub_messages_elt.appendChild(cloned_message_elt) | |
728 if is_at_bottom: | |
729 sub_messages_elt.scrollTop = sub_messages_elt.scrollHeight | |
718 | 730 |
719 # Check if user is viewing older messages or is at the bottom | 731 # Check if user is viewing older messages or is at the bottom |
720 is_at_bottom = self.is_at_bottom | 732 is_at_bottom = self.is_at_bottom |
721 | 733 |
722 self.messages_elt <= message_elt | 734 self.messages_elt <= message_elt |
852 ) | 864 ) |
853 # if evt.deltaY != 0: | 865 # if evt.deltaY != 0: |
854 # document["attachments"].scrollLeft += evt.deltaY * 0.8 | 866 # document["attachments"].scrollLeft += evt.deltaY * 0.8 |
855 # evt.preventDefault() | 867 # evt.preventDefault() |
856 | 868 |
857 async def on_show_thread(self, thread_id: str) -> None: | 869 async def show_filtered_messages( |
858 assert thread_id | 870 self, |
859 self.thread_id = thread_id | 871 filters: dict, |
860 | 872 sub_messages_panel_args: dict|None = None, |
861 thread_panel_elt = self.thread_panel_tpl.get_elt({ | 873 sub_messages_check_cb: Callable[[dict], bool]|None = None |
862 "messages": [] | 874 ) -> None: |
863 }) | 875 """Show message matching a filter in a sub-messages panel. |
864 self.thread_panel = thread_panel_elt | 876 |
865 thread_messages_elt = thread_panel_elt.select_one("#thread-messages") | 877 A panel will slide-in from the right to show filtered messages. |
866 assert thread_messages_elt is not None | 878 @param filters: Filters to use in ``bridge.history_get``. |
879 @param sub_messages_panel_args: Args to use with sub_messages_panel template. | |
880 @param sub_messages_check_cb: Method to call to check if a new message must go to | |
881 the sub-messages panel. The method must return True if the message is to be | |
882 displayed in the panel. | |
883 """ | |
884 if sub_messages_panel_args is None: | |
885 sub_messages_panel_args = {} | |
886 self.sub_messages_check_cb = sub_messages_check_cb | |
887 sub_messages_panel_elt = self.sub_messages_panel_tpl.get_elt( | |
888 sub_messages_panel_args | |
889 ) | |
890 self.sub_messages_panel = sub_messages_panel_elt | |
891 sub_messages_elt = sub_messages_panel_elt.select_one("#sub-messages") | |
892 sub_messages_input_elt = sub_messages_panel_elt.select_one("#sub-messages-input") | |
893 assert sub_messages_elt is not None | |
867 history_data = await bridge.history_get( | 894 history_data = await bridge.history_get( |
868 "", "", -2, True, {"thread_id": thread_id} | 895 "", "", -2, True, filters |
869 ) | 896 ) |
870 for message_data in history_data: | 897 for message_data in history_data: |
871 uid, timestamp, from_jid_s, to_jid_s, message_data, subject_data, mess_type, extra_s = message_data | 898 uid, timestamp, from_jid_s, to_jid_s, message_data, subject_data, mess_type, extra_s = message_data |
872 template_data = await self.message_to_template_data( | 899 template_data = await self.message_to_template_data( |
873 uid, | 900 uid, |
878 subject_data=subject_data, | 905 subject_data=subject_data, |
879 mess_type=mess_type, | 906 mess_type=mess_type, |
880 extra=json.loads(extra_s) | 907 extra=json.loads(extra_s) |
881 ) | 908 ) |
882 message_elt = self.message_tpl.get_elt(template_data) | 909 message_elt = self.message_tpl.get_elt(template_data) |
883 thread_messages_elt <= message_elt | 910 sub_messages_elt <= message_elt |
884 # FIXME: The whole input-panel is currently moved to the thread panel so listeners | 911 # FIXME: The whole input-panel is currently moved to the filtered messages panel |
885 # don't have to be moved. At some point, it may be better to make a clone | 912 # so listeners don't have to be moved. At some point, it may be better to make a |
886 # (without the IDs) and to clone listeners too, this way a slide-in panel could | 913 # clone (without the IDs) and to clone listeners too, this way a non-modal panel |
887 # be used instead of a modal. | 914 # could be used. |
888 input_panel_elt = document["input-panel"] | 915 input_panel_elt = document["input-panel"] |
889 thread_messages_elt <= input_panel_elt | 916 placeholder = make_placeholder(input_panel_elt) |
890 | 917 sub_messages_input_elt <= input_panel_elt |
891 def on_thread_panel_close(): | 918 document["input-panel-area"].appendChild(placeholder) |
892 document["input-panel-area"].appendChild(input_panel_elt) | 919 |
920 def on_sub_messages_panel_close(): | |
921 placeholder.replaceWith(input_panel_elt) | |
893 self.thread_id = None | 922 self.thread_id = None |
894 self.thread_panel = None | 923 self.keyword = None |
895 | 924 self.sub_messages_panel = None |
896 thread_modal = dialog.Modal( | 925 self.sub_messages_check_cb = None |
897 thread_panel_elt, | 926 |
927 sub_messages_modal = dialog.Modal( | |
928 sub_messages_panel_elt, | |
898 closable=True, | 929 closable=True, |
899 close_cb=on_thread_panel_close | 930 close_cb=on_sub_messages_panel_close, |
900 ) | 931 position="right" |
901 thread_modal.show() | 932 ) |
933 sub_messages_modal.show() | |
934 | |
935 async def on_show_thread(self, thread_id: str) -> None: | |
936 assert thread_id | |
937 self.thread_id = thread_id | |
938 def sub_messages_check_cb(extra: dict) -> bool: | |
939 return bool( | |
940 self.thread_id | |
941 and "thread" in extra | |
942 and extra["thread"] == self.thread_id | |
943 ) | |
944 | |
945 await self.show_filtered_messages( | |
946 {"thread_id": thread_id}, | |
947 {"title": "Thread view"}, | |
948 sub_messages_check_cb | |
949 ) | |
950 | |
951 async def _on_keyword_click(self, keyword_elt) -> None: | |
952 self.keyword = keyword_elt.text | |
953 def sub_messages_check_cb(extra: dict) -> bool: | |
954 return self.keyword in extra.get("keywords", []) | |
955 await self.show_filtered_messages( | |
956 {"keyword": self.keyword}, | |
957 {"title": f"🏷 Label view: {self.keyword}."}, | |
958 sub_messages_check_cb | |
959 ) | |
902 | 960 |
903 | 961 |
904 async def get_message_tuple(self, message_elt) -> tuple|None: | 962 async def get_message_tuple(self, message_elt) -> tuple|None: |
905 """Retrieve message tuple from as sent by [message_new] | 963 """Retrieve message tuple from as sent by [message_new] |
906 | 964 |
1058 thread_icon_elt.bind( | 1116 thread_icon_elt.bind( |
1059 "click", | 1117 "click", |
1060 lambda __, thread_id=thread_id: aio.run(self.on_show_thread(thread_id)) | 1118 lambda __, thread_id=thread_id: aio.run(self.on_show_thread(thread_id)) |
1061 ) | 1119 ) |
1062 | 1120 |
1121 ## keywords | |
1122 for keyword_elt in parent_elt.select(".message-keyword"): | |
1123 keyword_elt.bind( | |
1124 "click", | |
1125 lambda __, keyword_elt=keyword_elt: aio.run( | |
1126 self._on_keyword_click(keyword_elt) | |
1127 ) | |
1128 ) | |
1129 | |
1130 | |
1063 def add_reactions_listeners(self, parent_elt=None) -> None: | 1131 def add_reactions_listeners(self, parent_elt=None) -> None: |
1064 """Add listener on reactions to handle details and reaction toggle""" | 1132 """Add listener on reactions to handle details and reaction toggle""" |
1065 if parent_elt is None: | 1133 if parent_elt is None: |
1066 parent_elt = document | 1134 parent_elt = document |
1067 | 1135 |