Mercurial > libervia-web
diff libervia/web/pages/chat/_browser/__init__.py @ 1620:3a60bf3762ef
browser: threads and replies implementation:
rel 457
author | Goffi <goffi@goffi.org> |
---|---|
date | Tue, 06 May 2025 00:40:07 +0200 |
parents | a2cd4222c702 |
children |
line wrap: on
line diff
--- a/libervia/web/pages/chat/_browser/__init__.py Sat Apr 12 00:21:45 2025 +0200 +++ b/libervia/web/pages/chat/_browser/__init__.py Tue May 06 00:40:07 2025 +0200 @@ -14,7 +14,7 @@ from js_modules.tippy_js import tippy as tippy_ori import popup from template import Template, safe -from tools import is_touch_device +from tools import is_touch_device, remove_ids from loading import remove_loading_screen from jid_search import JidSearch from interpreter import Inspector @@ -34,6 +34,7 @@ INPUT_MODES = {"normal", "edit", "quote"} MODE_CLASS = "mode_{}" +SELECTED_THREAD_CLS = "has-background-info-light" # FIXME: workaround for https://github.com/brython-dev/brython/issues/2542 @@ -287,6 +288,19 @@ self.attachments_elt = document["attachments"] self.message_input = document["message_input_area"] + # reply to/thread + self.thread_panel_tpl = Template("chat/thread_panel.html") + # current thread panel, if any. + self.thread_panel = None + self._reply_to = None + self.thread_id = None + document["cancel_reply_btn"].bind( + "click", + lambda __: setattr(self, "reply_to", None) + ) + # use `thead` property to modify. + self._show_thread = None + # close_button = document.select_one(".modal-close") # close_button.bind("click", self.close_modal) @@ -356,6 +370,58 @@ <= SCROLL_SENSITIVITY ) + @property + def reply_to(self) -> dict|None: + return self._reply_to + + @reply_to.setter + def reply_to(self, reply_to: dict|None) -> None: + if reply_to == self._reply_to: + return + self._reply_to = reply_to + document["reply-to_message"].clear() + if reply_to is None: + document["reply-to"].classList.add("is-hidden") + else: + document["reply-to"].classList.remove("is-hidden") + parent_message_elt = document[reply_to["id"]] + cloned_parent_elt = parent_message_elt.cloneNode(True) + remove_ids(cloned_parent_elt) + message_actions_elt = cloned_parent_elt.select_one(".message-actions") + if message_actions_elt is not None: + message_actions_elt.remove() + document["reply-to_message"] <= cloned_parent_elt + + @property + def show_thread(self) -> str|None: + """Indicate the thread to highlight""" + return self._show_thread + + @show_thread.setter + def show_thread(self, thread_id: str|None) -> None: + """Set the thread to highlight, or None to clear view.""" + if self._show_thread == thread_id: + return + if self._show_thread is not None: + # If we have a previously selected thread, we clean the view. + for message_core_elt in self.messages_elt.select(".message-core"): + message_core_elt.classList.remove(SELECTED_THREAD_CLS) + self._show_thread = thread_id + if thread_id is not None: + for message_elt in self.messages_elt.select(".chat-message"): + try: + message_thread_id = message_elt.dataset["thread"] + except KeyError: + continue + if message_thread_id == thread_id: + message_core_elt = message_elt.select_one(".message-core") + if not message_core_elt: + log.debug( + f"Not message core found for message {message_elt['id']!r}." + ) + else: + message_core_elt.classList.add(SELECTED_THREAD_CLS) + 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. @@ -385,6 +451,12 @@ if attachments: extra["attachments"] = attachments + if self.reply_to: + extra["reply"] = self.reply_to + + if self.thread_id: + extra["thread"] = self.thread_id + # now we send the message try: if self.input_mode == "edit": @@ -405,6 +477,13 @@ dialog.notification.show(f"Can't send message: {e}", "error") else: self.message_input.value = "" + # We must not reset self.thread_id here, it is when the thread panel is + # closed. + # FIXME: Another mechanism must be used if the input panel is cloned at + # some point, with a slide-in panel for thread, as thread_id would then + # be used for the thread panel's message input, but not if the main area + # message input is used. + self.reply_to = None self.attachments_elt.clear() self.auto_resize_message_input() self.input_mode = "normal" @@ -605,6 +684,37 @@ template_data ) + + if "reply" in extra: + parent_id = extra["reply"]["id"] + try: + parent_message_elt = document[parent_id] + except KeyError: + log.info( + "Parent message of reply not found in current history: " + f"{parent_id!r}" + ) + else: + thread_id = extra.get("thread", parent_id) + if "thread" not in parent_message_elt.dataset: + parent_message_elt.dataset["thread"] = thread_id + # 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 + ): + # 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. + 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"]) + # Check if user is viewing older messages or is at the bottom is_at_bottom = self.is_at_bottom @@ -687,12 +797,21 @@ item_elt = DOMNode(target.closest(".attachment-preview")) item_elt.remove() - def on_attach_button_click(self, evt): + def on_attach_btn_click(self, evt): document["file-input"].click() + def on_reply_btn_click(self, evt) -> None: + chat_message_elt = evt.currentTarget.closest("div.chat-message") + self.reply_to = { + "id": chat_message_elt["id"], + "to": chat_message_elt.dataset["from"] + } + log.debug(f'"reply_to" set to {self.reply_to!r}') + + def on_extra_btn_click(self, evt): - message_elt = evt.target.closest("div.chat-message") - message_core_elt = evt.target.closest("div.message-core") + message_elt = evt.currentTarget.closest("div.chat-message") + message_core_elt = evt.currentTarget.closest("div.message-core") is_own = message_elt.classList.contains("own_msg") if is_own: own_messages = document.select('.own_msg') @@ -734,6 +853,53 @@ # 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 + + 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 + history_data = await bridge.history_get( + "", "", -2, True, {"thread_id": thread_id} + ) + for message_data in history_data: + uid, timestamp, from_jid, to_jid, message_data, subject_data, mess_type, extra = message_data + template_data = await self.message_to_template_data( + uid, + timestamp=timestamp, + from_jid=from_jid, + to_jid=to_jid, + message_data=message_data, + subject_data=subject_data, + mess_type=mess_type, + extra=json.loads(extra) + ) + 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. + input_panel_elt = document["input-panel"] + thread_messages_elt <= input_panel_elt + + def on_thread_panel_close(): + document["input-panel-area"].appendChild(input_panel_elt) + self.thread_id = None + self.thread_panel = None + + thread_modal = dialog.Modal( + thread_panel_elt, + closable=True, + close_cb=on_thread_panel_close + ) + thread_modal.show() + + async def get_message_tuple(self, message_elt) -> tuple|None: """Retrieve message tuple from as sent by [message_new] @@ -818,6 +984,10 @@ img_elt.bind("click", self.open_modal) img_elt.style.cursor = "pointer" + ## reply button + for reply_btn in parent_elt.select(".reply-button"): + reply_btn.bind("click", self.on_reply_btn_click) + ## reaction button i = 0 for reaction_btn in parent_elt.select(".reaction-button"): @@ -872,6 +1042,23 @@ ) + ## thread + for thread_icon_elt in parent_elt.select(".message-thread"): + message_elt = thread_icon_elt.closest("div.chat-message") + thread_id = message_elt.dataset["thread"] + thread_icon_elt.bind( + "mouseenter", + lambda __, thread_id=thread_id: setattr(self, "show_thread", thread_id) + ) + thread_icon_elt.bind( + "mouseleave", + lambda __, thread_id=thread_id: setattr(self, "show_thread", None) + ) + thread_icon_elt.bind( + "click", + lambda __, thread_id=thread_id: aio.run(self.on_show_thread(thread_id)) + ) + def add_reactions_listeners(self, parent_elt=None) -> None: """Add listener on reactions to handle details and reaction toggle""" if parent_elt is None: @@ -989,8 +1176,8 @@ def open_modal(self, evt): modal_image = document.select_one("#modal-image") - modal_image.src = evt.target.src - modal_image.alt = evt.target.alt + modal_image.src = evt.currentTarget.src + modal_image.alt = evt.currentTarget.alt modal = document.select_one("#modal") modal.classList.add("is-active") @@ -1017,11 +1204,11 @@ "input", lambda __: libervia_web_chat.auto_resize_message_input() ) document["message_input"].bind("keydown", libervia_web_chat.on_message_keydown) -# document["send_button"].bind( -# "click", -# lambda __: aio.run(libervia_web_chat.send_message()) -# ) -document["attach-button"].bind("click", libervia_web_chat.on_attach_button_click) +document["send_button"].bind( + "click", + lambda __: aio.run(libervia_web_chat.send_message()) +) +document["attach-button"].bind("click", libervia_web_chat.on_attach_btn_click) document["file-input"].bind("change", libervia_web_chat.on_file_selected) document.bind("visibilitychange", libervia_web_chat.handle_visibility_change)