diff libervia/web/pages/chat/_browser/__init__.py @ 1541:3c384b51b2a1

browser (chat): URL previews implementation
author Goffi <goffi@goffi.org>
date Wed, 28 Jun 2023 10:09:14 +0200
parents b4342176fa0a
children fb31d3dba0c3
line wrap: on
line diff
--- a/libervia/web/pages/chat/_browser/__init__.py	Wed Jun 28 10:06:54 2023 +0200
+++ b/libervia/web/pages/chat/_browser/__init__.py	Wed Jun 28 10:09:14 2023 +0200
@@ -1,12 +1,13 @@
 import json
+import re
 
-import dialog
-import jid
 from bridge import AsyncBridge as Bridge
-from browser import aio, console as log, document, DOMNode, window, bind
+from browser import DOMNode, aio, bind, console as log, document, window, html
+from cache import cache, identities
+import dialog
+from file_uploader import FileUploader
+import jid
 from template import Template, safe
-from file_uploader import FileUploader
-from cache import cache, identities
 
 log.warning = log.warn
 profile = window.profile or ""
@@ -21,6 +22,9 @@
 class LiberviaWebChat:
     def __init__(self):
         self.message_tpl = Template("chat/message.html")
+        self.url_preview_control_tpl = Template("components/url_preview_control.html")
+        self.url_preview_tpl = Template("components/url_preview.html")
+
         self.messages_elt = document["messages"]
 
         # attachments
@@ -166,6 +170,8 @@
 
         self.messages_elt <= message_elt
         self.make_attachments_dynamic(message_elt)
+        # we add preview in parallel on purpose, as they can be slow to get
+        self.handle_url_previews(message_elt)
 
         # If user was at the bottom, keep the scroll at the bottom
         if is_at_bottom:
@@ -247,6 +253,80 @@
         close_button = document.select_one(".modal-close")
         close_button.bind("click", self.close_modal)
 
+    def find_links(self, message_elt):
+        """Find all http and https links within the body of a message."""
+        msg_body_elt = message_elt.select_one(".msg_body")
+        if not msg_body_elt:
+            return
+
+        # Extracting links from text content
+        text = msg_body_elt.text
+        raw_urls = re.findall(
+            r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F])|#)+",
+            text,
+        )
+
+        # Extracting links from <a> elements
+        a_elements = msg_body_elt.select("a")
+        for a_elt in a_elements:
+            href = a_elt.attrs.get("href", "")
+            if href.startswith("http://") or href.startswith("https://"):
+                raw_urls.append(href)
+
+        # we remove duplicates
+        urls = list(dict.fromkeys(raw_urls))
+
+        return urls
+
+    async def add_url_previews(self, url_previews_elt, urls) -> None:
+        """Add URL previews to the .url-previews element of a message."""
+        url_previews_elt.clear()
+        for url in urls:
+            try:
+                url_preview_data_s = await bridge.url_preview_get(url, "")
+            except Exception as e:
+                log.warning(f"Couldn't get URL preview for {url}: {e}")
+                continue
+
+            if not url_preview_data_s:
+                log.warning(f"No preview could be found for URL: {url}")
+                continue
+
+            url_preview_data = json.loads(url_preview_data_s)
+
+            url_preview_elt = self.url_preview_tpl.get_elt(
+                {"url_preview": url_preview_data}
+            )
+            url_previews_elt <= url_preview_elt
+
+
+    def handle_url_previews(self, parent_elt=None):
+        """Check if URL are presents in a message and show appropriate element
+
+        According to settings, either a preview control panel will be shown to wait for
+        user click, or directly the previews, or nothing at all.
+        """
+
+        if parent_elt is None:
+            parent_elt = document
+            chat_message_elts = parent_elt.select(".is-chat-message")
+        else:
+            chat_message_elts = [parent_elt]
+        for message_elt in chat_message_elts:
+            urls = self.find_links(message_elt)
+            if urls:
+                url_previews_elt = message_elt.select_one(".url-previews")
+                url_previews_elt.classList.remove("is-hidden")
+                preview_control_elt = self.url_preview_control_tpl.get_elt()
+                fetch_preview_btn = preview_control_elt.select_one(".action_fetch_preview")
+                fetch_preview_btn.bind(
+                    "click",
+                    lambda __, previews_elt=url_previews_elt, preview_urls=urls: aio.run(
+                        self.add_url_previews(previews_elt, preview_urls)
+                    )
+                )
+                url_previews_elt <= preview_control_elt
+
     def open_modal(self, evt):
         modal_image = document.select_one("#modal-image")
         modal_image.src = evt.target.src
@@ -271,3 +351,4 @@
 bridge.register_signal("message_new", libervia_web_chat._on_message_new)
 
 libervia_web_chat.make_attachments_dynamic()
+libervia_web_chat.handle_url_previews()