Mercurial > libervia-web
view libervia/web/pages/chat/_browser/__init__.py @ 1536:dc81403a5b2f
browser: chat page:
since the move to Brython, the chat was really basic and not really usable. Now that
dynamism has been re-implemented correctly in the new frontend, a real advanced chat page
can be done. This is the first draft in this direction.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 28 Jun 2023 10:05:13 +0200 |
parents | |
children | b4342176fa0a |
line wrap: on
line source
import json import dialog import jid from bridge import AsyncBridge as Bridge from browser import aio, console as log, document, DOMNode, window, bind from template import Template, safe from file_uploader import FileUploader from cache import cache, identities log.warning = log.warn profile = window.profile or "" own_jid = jid.JID(window.own_jid) target_jid = jid.JID(window.target_jid) bridge = Bridge() # Sensible value to consider that user is at the bottom SCROLL_SENSITIVITY = 200 class LiberviaWebChat: def __init__(self): self.message_tpl = Template("chat/message.html") self.messages_elt = document["messages"] # attachments self.file_uploader = FileUploader( "", "chat/attachment_preview.html", on_delete_cb=self.on_attachment_delete ) self.attachments_elt = document["attachments"] self.message_input = document["message_input"] # hide/show attachments MutationObserver = window.MutationObserver observer = MutationObserver.new(lambda *__: self.update_attachments_visibility()) observer.observe(self.attachments_elt, {"childList": True}) # we want the message scroll to be initially at the bottom self.messages_elt.scrollTop = self.messages_elt.scrollHeight @property def is_at_bottom(self): return ( self.messages_elt.scrollHeight - self.messages_elt.scrollTop - self.messages_elt.clientHeight <= SCROLL_SENSITIVITY ) def send_message(self): """Send message currently in input area The message and corresponding attachment will be sent """ message = self.message_input.value.rstrip() log.info(f"{message=}") # attachments attachments = [] for attachment_elt in self.attachments_elt.children: file_data = json.loads(attachment_elt.getAttribute("data-file")) attachments.append(file_data) if message or attachments: extra = {} if attachments: extra["attachments"] = attachments # now we send the message try: aio.run( bridge.message_send( str(target_jid), {"": message}, {}, "auto", json.dumps(extra) ) ) except Exception as e: dialog.notification.show(f"Can't send message: {e}", "error") else: self.message_input.value = "" self.attachments_elt.clear() def _on_message_new( self, uid: str, timestamp: float, from_jid: str, to_jid: str, message: dict, subject: dict, mess_type: str, extra_s: str, profile: str, ) -> None: if ( jid.JID(from_jid).bare == window.target_jid or jid.JID(to_jid).bare == window.target_jid ): aio.run( self.on_message_new( uid, timestamp, from_jid, to_jid, message, subject, mess_type, json.loads(extra_s), profile, ) ) async def on_message_new( self, uid: str, timestamp: float, from_jid: str, to_jid: str, message_data: dict, subject_data: dict, mess_type: str, extra: dict, profile: str, ) -> None: log.info(f"on_message_new: [{from_jid} -> {to_jid}] {message_data}, {extra=}") xhtml_data = extra.get("xhtml") if not xhtml_data: xhtml = None else: try: xhtml = xhtml_data[""] except KeyError: xhtml = next(iter(xhtml_data.values())) await cache.fill_identities([from_jid]) msg_data = { "id": uid, "timestamp": timestamp, "type": mess_type, "from_": from_jid, "text": message_data.get("") or next(iter(message_data.values()), ""), "subject": subject_data.get("") or next(iter(subject_data.values()), ""), "type": mess_type, "thread": extra.get("thread"), "thread_parent": extra.get("thread_parent"), "reeceived": extra.get("received_timestamp") or timestamp, "delay_sender": extra.get("delay_sender"), "info_type": extra.get("info_type"), "html": safe(xhtml) if xhtml else None, "encrypted": extra.get("encrypted", False), "received": extra.get("received", False), "edited": extra.get("edited", False), "attachments": extra.get("attachments", []), } message_elt = self.message_tpl.get_elt( { "own_jid": own_jid, "msg": msg_data, "identities": identities, } ) # Check if user is viewing older messages or is at the bottom is_at_bottom = self.is_at_bottom self.messages_elt <= message_elt self.make_attachments_dynamic(message_elt) # If user was at the bottom, keep the scroll at the bottom if is_at_bottom: self.messages_elt.scrollTop = self.messages_elt.scrollHeight def auto_resize_message_input(self): """Resize the message input field according to content.""" is_at_bottom = self.is_at_bottom # The textarea's height is first reset to 'auto' to ensure it's not influenced by # the previous content. self.message_input.style.height = "auto" # Then the height is set to the scrollHeight of the textarea (which is the height # of the content), plus the vertical border, resulting in a textarea that grows as # more lines of text are added. self.message_input.style.height = f"{self.message_input.scrollHeight + 2}px" if is_at_bottom: # we want the message are to still display the last message self.messages_elt.scrollTop = self.messages_elt.scrollHeight def on_message_keydown(self, evt): """Handle the 'keydown' event of the message input field @param evt: The event object. 'target' refers to the textarea element. """ if evt.keyCode == 13: # <Enter> key if not window.navigator.maxTouchPoints: # we have a non touch device, we send message on <Enter> if not evt.shiftKey: evt.preventDefault() # Prevents line break self.send_message() def update_attachments_visibility(self): if len(self.attachments_elt.children): self.attachments_elt.classList.remove("is-contracted") else: self.attachments_elt.classList.add("is-contracted") def on_file_selected(self, evt): """Handle file selection""" log.info("file selected") files = evt.currentTarget.files self.file_uploader.upload_files(files, self.attachments_elt) self.message_input.focus() def on_attachment_delete(self, evt): evt.stopPropagation() target = evt.currentTarget item_elt = DOMNode(target.closest('.attachment-preview')) item_elt.remove() def on_attach_button_click(self, evt): document["file_input"].click() @bind(document["attachments"], 'wheel') def wheel_event(evt): """Make the mouse wheel to act on horizontal scrolling for attachments Attachments don't have vertical scrolling, thus is makes sense to use the wheel for horizontal scrolling """ if evt.deltaY != 0: document['attachments'].scrollLeft += evt.deltaY * 0.8 evt.preventDefault() def make_attachments_dynamic(self, parent_elt = None): """Make attachments dynamically clickable""" # FIXME: only handle images for now, and display them in a modal if parent_elt is None: parent_elt = document img_elts = parent_elt.select('.message-attachment img') for img_elt in img_elts: img_elt.bind('click', self.open_modal) img_elt.style.cursor = 'pointer' close_button = document.select_one('.modal-close') close_button.bind('click', self.close_modal) 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 = document.select_one('#modal') modal.classList.add('is-active') def close_modal(self, evt): modal = document.select_one('#modal') modal.classList.remove('is-active') libervia_web_chat = LiberviaWebChat() document["message_input"].bind( "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 __: libervia_web_chat.send_message()) document["attach_button"].bind( "click", libervia_web_chat.on_attach_button_click ) document["file_input"].bind("change", libervia_web_chat.on_file_selected) bridge.register_signal("message_new", libervia_web_chat._on_message_new) libervia_web_chat.make_attachments_dynamic()