Mercurial > libervia-web
diff 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 diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/web/pages/chat/_browser/__init__.py Wed Jun 28 10:05:13 2023 +0200 @@ -0,0 +1,275 @@ +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()