Use a component to display image link previews
This commit is contained in:
@@ -105,6 +105,7 @@ class UISettings(JSONConfigFile):
|
||||
async def default_data(self) -> JsonData:
|
||||
return {
|
||||
"alertOnMessageForMsec": 4000,
|
||||
"messageImageMaxThumbnailSize": 256,
|
||||
"theme": "Default.qpl",
|
||||
"writeAliases": {},
|
||||
"keys": {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import re
|
||||
|
||||
import mistune
|
||||
from lxml.html import HtmlElement, etree # nosec
|
||||
from lxml.html import HtmlElement # nosec
|
||||
|
||||
import html_sanitizer.sanitizer as sanitizer
|
||||
from html_sanitizer.sanitizer import Sanitizer
|
||||
@@ -66,39 +66,22 @@ class HtmlFilter:
|
||||
if outgoing:
|
||||
return html
|
||||
|
||||
tree = etree.fromstring(html, parser=etree.HTMLParser())
|
||||
|
||||
if tree is None:
|
||||
return ""
|
||||
|
||||
for el in tree.iter("img"):
|
||||
el = self._wrap_img_in_a(el)
|
||||
|
||||
for el in tree.iter("a"):
|
||||
el = self._append_img_to_a(el)
|
||||
|
||||
result = b"".join((etree.tostring(el, encoding="utf-8")
|
||||
for el in tree[0].iterchildren()))
|
||||
|
||||
text = str(result, "utf-8").strip("\n")
|
||||
|
||||
return re.sub(
|
||||
r"<(p|br/?)>(\s*>.*)(!?</?(?:br|p)/?>)",
|
||||
r'<\1><span class="quote">\2</span>\3',
|
||||
text,
|
||||
html,
|
||||
)
|
||||
|
||||
|
||||
def sanitize_settings(self, inline: bool = False) -> dict:
|
||||
# https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes
|
||||
# TODO: mx-reply, audio, video, the new hidden thing
|
||||
# TODO: mx-reply and the new hidden thing
|
||||
|
||||
inline_tags = {"font", "a", "sup", "sub", "b", "i", "s", "u", "code"}
|
||||
tags = inline_tags | {
|
||||
"h1", "h2", "h3", "h4", "h5", "h6","blockquote",
|
||||
"p", "ul", "ol", "li", "hr", "br",
|
||||
"table", "thead", "tbody", "tr", "th", "td",
|
||||
"pre", "img",
|
||||
"table", "thead", "tbody", "tr", "th", "td", "pre",
|
||||
}
|
||||
|
||||
inlines_attributes = {
|
||||
@@ -107,7 +90,6 @@ class HtmlFilter:
|
||||
"code": {"class"},
|
||||
}
|
||||
attributes = {**inlines_attributes, **{
|
||||
"img": {"width", "height", "alt", "title", "src"},
|
||||
"ol": {"start"},
|
||||
"hr": {"width"},
|
||||
}}
|
||||
@@ -115,7 +97,7 @@ class HtmlFilter:
|
||||
return {
|
||||
"tags": inline_tags if inline else tags,
|
||||
"attributes": inlines_attributes if inline else attributes,
|
||||
"empty": {} if inline else {"hr", "br", "img"},
|
||||
"empty": {} if inline else {"hr", "br"},
|
||||
"separate": {"a"} if inline else {
|
||||
"a", "p", "li", "table", "tr", "th", "td", "br", "hr",
|
||||
},
|
||||
@@ -139,6 +121,7 @@ class HtmlFilter:
|
||||
sanitizer.tag_replacer("caption", "p"),
|
||||
sanitizer.target_blank_noopener,
|
||||
self._process_span_font,
|
||||
self._img_to_a,
|
||||
],
|
||||
"element_postprocessors": [],
|
||||
"is_mergeable": lambda e1, e2: e1.attrib == e2.attrib,
|
||||
@@ -159,42 +142,13 @@ class HtmlFilter:
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _wrap_img_in_a(el: HtmlElement) -> HtmlElement:
|
||||
link = el.attrib.get("src", "")
|
||||
width = el.attrib.get("width", "256")
|
||||
height = el.attrib.get("height", "256")
|
||||
def _img_to_a(el: HtmlElement) -> HtmlElement:
|
||||
if el.tag == "img":
|
||||
el.tag = "a"
|
||||
el.attrib["href"] = el.attrib.pop("src", "")
|
||||
el.text = el.attrib.pop("alt", None) or el.attrib["href"]
|
||||
|
||||
if el.getparent().tag == "a" or el.tag != "img":
|
||||
return el
|
||||
|
||||
el.tag = "a"
|
||||
el.attrib.clear()
|
||||
el.attrib["href"] = link
|
||||
el.append(etree.Element("img", src=link, width=width, height=height))
|
||||
return el
|
||||
|
||||
|
||||
def _append_img_to_a(self, el: HtmlElement) -> HtmlElement:
|
||||
link = el.attrib.get("href", "")
|
||||
|
||||
if not (el.tag == "a" and self._is_image_path(link)):
|
||||
return el
|
||||
|
||||
for _ in el.iter("img"): # if the <a> already has an <img> child
|
||||
return el
|
||||
|
||||
el.append(etree.Element("br"))
|
||||
el.append(etree.Element("img", src=link, width="256", height="256"))
|
||||
return el
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _is_image_path(link: str) -> bool:
|
||||
return bool(re.match(
|
||||
r"(https?|s?ftp)://.+/.+\.(jpg|jpeg|png|gif|bmp|webp|tiff|svg)$",
|
||||
link,
|
||||
re.IGNORECASE,
|
||||
))
|
||||
|
||||
|
||||
HTML_FILTER = HtmlFilter()
|
||||
|
@@ -2,6 +2,9 @@ import re
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import lxml # nosec
|
||||
|
||||
import nio
|
||||
|
||||
@@ -145,6 +148,27 @@ class Event(ModelItem):
|
||||
def event_type(self) -> str:
|
||||
return self.local_event_type or type(self.source).__name__
|
||||
|
||||
@property
|
||||
def preview_links(self) -> List[Tuple[str, str]]:
|
||||
if not self.content.strip():
|
||||
return []
|
||||
|
||||
return [
|
||||
(self._get_preview_type(link[0], link[2]), link[2])
|
||||
for link in lxml.html.iterlinks(self.content)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _get_preview_type(el: lxml.html.HtmlElement, link: str) -> str:
|
||||
path = urlparse(link).path.lower()
|
||||
|
||||
for ext in ("jpg", "jpeg", "png", "gif", "bmp", "webp", "tiff", "svg"):
|
||||
if el.tag == "img" or path.endswith(ext):
|
||||
return "image"
|
||||
|
||||
return "page"
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class Device(ModelItem):
|
||||
|
Reference in New Issue
Block a user