Mercurial > libervia-web
diff 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 |
line wrap: on
line diff
--- a/libervia/web/pages/chat/_browser/__init__.py Wed May 21 15:58:56 2025 +0200 +++ b/libervia/web/pages/chat/_browser/__init__.py Fri Jun 06 11:08:05 2025 +0200 @@ -1,3 +1,4 @@ +from html import escape import json import re from typing import Callable @@ -14,7 +15,7 @@ from js_modules.tippy_js import tippy as tippy_ori import popup from template import Template, safe -from tools import is_touch_device, remove_ids +from tools import is_touch_device, remove_ids, make_placeholder from loading import remove_loading_screen from jid_search import JidSearch from interpreter import Inspector @@ -289,11 +290,15 @@ self.message_input = document["message_input_area"] # reply to/thread - self.thread_panel_tpl = Template("chat/thread_panel.html") + self.sub_messages_panel_tpl = Template("chat/sub_messages_panel.html") # current thread panel, if any. - self.thread_panel = None - self._reply_to = None - self.thread_id = None + self.sub_messages_panel = None + # Method to call to check if a new message must be displayer in sub-messages + # panel. + self.sub_messages_check_cb: Callable|None = None + self._reply_to: str|None = None + self.thread_id: str|None = None + self.keyword: str|None = None document["cancel_reply_btn"].bind( "click", lambda __: setattr(self, "reply_to", None) @@ -363,12 +368,7 @@ @property def is_at_bottom(self): - return ( - self.messages_elt.scrollHeight - - self.messages_elt.scrollTop - - self.messages_elt.clientHeight - <= SCROLL_SENSITIVITY - ) + return self.is_elt_at_bottom(self.messages_elt) @property def reply_to(self) -> dict|None: @@ -422,6 +422,15 @@ else: message_core_elt.classList.add(SELECTED_THREAD_CLS) + def is_elt_at_bottom(self, elt) -> bool: + """Tell is a scrollable elemement is scrolled down. + + There is a margin to check it set with SCROLL_SENSITIVITY. + @param element: Scrollable element to check. + @return: True if the element is as its bottom. + """ + return elt.scrollHeight - elt.scrollTop - elt.clientHeight <= SCROLL_SENSITIVITY + def open_chat(self, entity_jid: str) -> None: """Change the current chat for the given one.""" # For now we keep it simple and just load the new location. @@ -457,6 +466,9 @@ if self.thread_id: extra["thread"] = self.thread_id + if self.keyword: + extra.setdefault("keywords", []).append(self.keyword) + # now we send the message try: if self.input_mode == "edit": @@ -702,19 +714,19 @@ # TODO: Regenerate parent message, so thread icon will appear. if ( - "thread" in extra - and self.thread_panel - and self.thread_id - and extra["thread"] == self.thread_id + self.sub_messages_check_cb is not None + and self.sub_messages_panel is not None + and self.sub_messages_check_cb(extra) ): # FIXME: This is quite fragile, IDs are removed, meaning that some listener - # may not be working correctly, and it's using #input-panel as reference, - # but in the future, it may not moved from main area the thread panel like - # that anymore. + # may not be working correctly, cloned_message_elt = message_elt.cloneNode(True) remove_ids(cloned_message_elt) - thread_messages_elt = document["thread-messages"] - thread_messages_elt.insertBefore(cloned_message_elt, document["input-panel"]) + sub_messages_elt = document["sub-messages"] + is_at_bottom = self.is_elt_at_bottom(sub_messages_elt) + sub_messages_elt.appendChild(cloned_message_elt) + if is_at_bottom: + sub_messages_elt.scrollTop = sub_messages_elt.scrollHeight # Check if user is viewing older messages or is at the bottom is_at_bottom = self.is_at_bottom @@ -854,18 +866,33 @@ # document["attachments"].scrollLeft += evt.deltaY * 0.8 # evt.preventDefault() - async def on_show_thread(self, thread_id: str) -> None: - assert thread_id - self.thread_id = thread_id + async def show_filtered_messages( + self, + filters: dict, + sub_messages_panel_args: dict|None = None, + sub_messages_check_cb: Callable[[dict], bool]|None = None + ) -> None: + """Show message matching a filter in a sub-messages panel. - thread_panel_elt = self.thread_panel_tpl.get_elt({ - "messages": [] - }) - self.thread_panel = thread_panel_elt - thread_messages_elt = thread_panel_elt.select_one("#thread-messages") - assert thread_messages_elt is not None + A panel will slide-in from the right to show filtered messages. + @param filters: Filters to use in ``bridge.history_get``. + @param sub_messages_panel_args: Args to use with sub_messages_panel template. + @param sub_messages_check_cb: Method to call to check if a new message must go to + the sub-messages panel. The method must return True if the message is to be + displayed in the panel. + """ + if sub_messages_panel_args is None: + sub_messages_panel_args = {} + self.sub_messages_check_cb = sub_messages_check_cb + sub_messages_panel_elt = self.sub_messages_panel_tpl.get_elt( + sub_messages_panel_args + ) + self.sub_messages_panel = sub_messages_panel_elt + sub_messages_elt = sub_messages_panel_elt.select_one("#sub-messages") + sub_messages_input_elt = sub_messages_panel_elt.select_one("#sub-messages-input") + assert sub_messages_elt is not None history_data = await bridge.history_get( - "", "", -2, True, {"thread_id": thread_id} + "", "", -2, True, filters ) for message_data in history_data: uid, timestamp, from_jid_s, to_jid_s, message_data, subject_data, mess_type, extra_s = message_data @@ -880,25 +907,56 @@ extra=json.loads(extra_s) ) message_elt = self.message_tpl.get_elt(template_data) - thread_messages_elt <= message_elt - # FIXME: The whole input-panel is currently moved to the thread panel so listeners - # don't have to be moved. At some point, it may be better to make a clone - # (without the IDs) and to clone listeners too, this way a slide-in panel could - # be used instead of a modal. + sub_messages_elt <= message_elt + # FIXME: The whole input-panel is currently moved to the filtered messages panel + # so listeners don't have to be moved. At some point, it may be better to make a + # clone (without the IDs) and to clone listeners too, this way a non-modal panel + # could be used. input_panel_elt = document["input-panel"] - thread_messages_elt <= input_panel_elt + placeholder = make_placeholder(input_panel_elt) + sub_messages_input_elt <= input_panel_elt + document["input-panel-area"].appendChild(placeholder) + + def on_sub_messages_panel_close(): + placeholder.replaceWith(input_panel_elt) + self.thread_id = None + self.keyword = None + self.sub_messages_panel = None + self.sub_messages_check_cb = None + + sub_messages_modal = dialog.Modal( + sub_messages_panel_elt, + closable=True, + close_cb=on_sub_messages_panel_close, + position="right" + ) + sub_messages_modal.show() - def on_thread_panel_close(): - document["input-panel-area"].appendChild(input_panel_elt) - self.thread_id = None - self.thread_panel = None + async def on_show_thread(self, thread_id: str) -> None: + assert thread_id + self.thread_id = thread_id + def sub_messages_check_cb(extra: dict) -> bool: + return bool( + self.thread_id + and "thread" in extra + and extra["thread"] == self.thread_id + ) - thread_modal = dialog.Modal( - thread_panel_elt, - closable=True, - close_cb=on_thread_panel_close + await self.show_filtered_messages( + {"thread_id": thread_id}, + {"title": "Thread view"}, + sub_messages_check_cb ) - thread_modal.show() + + async def _on_keyword_click(self, keyword_elt) -> None: + self.keyword = keyword_elt.text + def sub_messages_check_cb(extra: dict) -> bool: + return self.keyword in extra.get("keywords", []) + await self.show_filtered_messages( + {"keyword": self.keyword}, + {"title": f"🏷 Label view: {self.keyword}."}, + sub_messages_check_cb + ) async def get_message_tuple(self, message_elt) -> tuple|None: @@ -1060,6 +1118,16 @@ lambda __, thread_id=thread_id: aio.run(self.on_show_thread(thread_id)) ) + ## keywords + for keyword_elt in parent_elt.select(".message-keyword"): + keyword_elt.bind( + "click", + lambda __, keyword_elt=keyword_elt: aio.run( + self._on_keyword_click(keyword_elt) + ) + ) + + def add_reactions_listeners(self, parent_elt=None) -> None: """Add listener on reactions to handle details and reaction toggle""" if parent_elt is None: