Use a component to display image link previews
This commit is contained in:
parent
3c4ca7d433
commit
5674d0c7b7
6
TODO.md
6
TODO.md
|
@ -11,6 +11,7 @@
|
||||||
- When qml syntax highlighting supports ES6 string interpolation, use them
|
- When qml syntax highlighting supports ES6 string interpolation, use them
|
||||||
|
|
||||||
- Fixes
|
- Fixes
|
||||||
|
- Scroll to begin/end
|
||||||
- `minutesBetween()` for 13:13:58 and 14:15:07
|
- `minutesBetween()` for 13:13:58 and 14:15:07
|
||||||
- `# > quote` doesn't color
|
- `# > quote` doesn't color
|
||||||
- Pressing backspace in composer sometimes doesn't work
|
- Pressing backspace in composer sometimes doesn't work
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
- Verify big avatars aren't downloaded uselessly
|
- Verify big avatars aren't downloaded uselessly
|
||||||
|
|
||||||
- UI
|
- UI
|
||||||
|
- Esc in sidepane to focus chat again
|
||||||
- Set an explicit placeholder text color for text field/area
|
- Set an explicit placeholder text color for text field/area
|
||||||
- Change typing bar background
|
- Change typing bar background
|
||||||
- Show error if uploading avatar fails or file is corrupted
|
- Show error if uploading avatar fails or file is corrupted
|
||||||
|
@ -38,8 +40,8 @@
|
||||||
- Message selection
|
- Message selection
|
||||||
- Make scroll wheel usable
|
- Make scroll wheel usable
|
||||||
- Copy to X11 selection
|
- Copy to X11 selection
|
||||||
- Make events copiable
|
- Link previews
|
||||||
- Images don't load correctly in TextEdit
|
- Take the previews into account to calculate delegate min height
|
||||||
|
|
||||||
- Just use Shortcut onHeld instead of analyzing the current velocity
|
- Just use Shortcut onHeld instead of analyzing the current velocity
|
||||||
in `smartVerticalFlick()`
|
in `smartVerticalFlick()`
|
||||||
|
|
|
@ -105,6 +105,7 @@ class UISettings(JSONConfigFile):
|
||||||
async def default_data(self) -> JsonData:
|
async def default_data(self) -> JsonData:
|
||||||
return {
|
return {
|
||||||
"alertOnMessageForMsec": 4000,
|
"alertOnMessageForMsec": 4000,
|
||||||
|
"messageImageMaxThumbnailSize": 256,
|
||||||
"theme": "Default.qpl",
|
"theme": "Default.qpl",
|
||||||
"writeAliases": {},
|
"writeAliases": {},
|
||||||
"keys": {
|
"keys": {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import mistune
|
import mistune
|
||||||
from lxml.html import HtmlElement, etree # nosec
|
from lxml.html import HtmlElement # nosec
|
||||||
|
|
||||||
import html_sanitizer.sanitizer as sanitizer
|
import html_sanitizer.sanitizer as sanitizer
|
||||||
from html_sanitizer.sanitizer import Sanitizer
|
from html_sanitizer.sanitizer import Sanitizer
|
||||||
|
@ -66,39 +66,22 @@ class HtmlFilter:
|
||||||
if outgoing:
|
if outgoing:
|
||||||
return html
|
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(
|
return re.sub(
|
||||||
r"<(p|br/?)>(\s*>.*)(!?</?(?:br|p)/?>)",
|
r"<(p|br/?)>(\s*>.*)(!?</?(?:br|p)/?>)",
|
||||||
r'<\1><span class="quote">\2</span>\3',
|
r'<\1><span class="quote">\2</span>\3',
|
||||||
text,
|
html,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def sanitize_settings(self, inline: bool = False) -> dict:
|
def sanitize_settings(self, inline: bool = False) -> dict:
|
||||||
# https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes
|
# 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"}
|
inline_tags = {"font", "a", "sup", "sub", "b", "i", "s", "u", "code"}
|
||||||
tags = inline_tags | {
|
tags = inline_tags | {
|
||||||
"h1", "h2", "h3", "h4", "h5", "h6","blockquote",
|
"h1", "h2", "h3", "h4", "h5", "h6","blockquote",
|
||||||
"p", "ul", "ol", "li", "hr", "br",
|
"p", "ul", "ol", "li", "hr", "br",
|
||||||
"table", "thead", "tbody", "tr", "th", "td",
|
"table", "thead", "tbody", "tr", "th", "td", "pre",
|
||||||
"pre", "img",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inlines_attributes = {
|
inlines_attributes = {
|
||||||
|
@ -107,7 +90,6 @@ class HtmlFilter:
|
||||||
"code": {"class"},
|
"code": {"class"},
|
||||||
}
|
}
|
||||||
attributes = {**inlines_attributes, **{
|
attributes = {**inlines_attributes, **{
|
||||||
"img": {"width", "height", "alt", "title", "src"},
|
|
||||||
"ol": {"start"},
|
"ol": {"start"},
|
||||||
"hr": {"width"},
|
"hr": {"width"},
|
||||||
}}
|
}}
|
||||||
|
@ -115,7 +97,7 @@ class HtmlFilter:
|
||||||
return {
|
return {
|
||||||
"tags": inline_tags if inline else tags,
|
"tags": inline_tags if inline else tags,
|
||||||
"attributes": inlines_attributes if inline else attributes,
|
"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 {
|
"separate": {"a"} if inline else {
|
||||||
"a", "p", "li", "table", "tr", "th", "td", "br", "hr",
|
"a", "p", "li", "table", "tr", "th", "td", "br", "hr",
|
||||||
},
|
},
|
||||||
|
@ -139,6 +121,7 @@ class HtmlFilter:
|
||||||
sanitizer.tag_replacer("caption", "p"),
|
sanitizer.tag_replacer("caption", "p"),
|
||||||
sanitizer.target_blank_noopener,
|
sanitizer.target_blank_noopener,
|
||||||
self._process_span_font,
|
self._process_span_font,
|
||||||
|
self._img_to_a,
|
||||||
],
|
],
|
||||||
"element_postprocessors": [],
|
"element_postprocessors": [],
|
||||||
"is_mergeable": lambda e1, e2: e1.attrib == e2.attrib,
|
"is_mergeable": lambda e1, e2: e1.attrib == e2.attrib,
|
||||||
|
@ -159,42 +142,13 @@ class HtmlFilter:
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _wrap_img_in_a(el: HtmlElement) -> HtmlElement:
|
def _img_to_a(el: HtmlElement) -> HtmlElement:
|
||||||
link = el.attrib.get("src", "")
|
if el.tag == "img":
|
||||||
width = el.attrib.get("width", "256")
|
el.tag = "a"
|
||||||
height = el.attrib.get("height", "256")
|
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
|
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()
|
HTML_FILTER = HtmlFilter()
|
||||||
|
|
|
@ -2,6 +2,9 @@ import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import lxml # nosec
|
||||||
|
|
||||||
import nio
|
import nio
|
||||||
|
|
||||||
|
@ -145,6 +148,27 @@ class Event(ModelItem):
|
||||||
def event_type(self) -> str:
|
def event_type(self) -> str:
|
||||||
return self.local_event_type or type(self.source).__name__
|
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
|
@dataclass
|
||||||
class Device(ModelItem):
|
class Device(ModelItem):
|
||||||
|
|
|
@ -5,10 +5,10 @@ import QtQuick.Layouts 1.12
|
||||||
Button {
|
Button {
|
||||||
id: button
|
id: button
|
||||||
spacing: theme.spacing
|
spacing: theme.spacing
|
||||||
leftPadding: spacing / (circle ? 1.5 : 1)
|
|
||||||
rightPadding: leftPadding
|
|
||||||
topPadding: spacing / (circle ? 1.75 : 1.5)
|
topPadding: spacing / (circle ? 1.75 : 1.5)
|
||||||
bottomPadding: topPadding
|
bottomPadding: topPadding
|
||||||
|
leftPadding: spacing / (circle ? 1.5 : 1)
|
||||||
|
rightPadding: leftPadding
|
||||||
|
|
||||||
iconItem.svgName: loading ? "hourglass" : icon.name
|
iconItem.svgName: loading ? "hourglass" : icon.name
|
||||||
icon.color: theme.icons.colorize
|
icon.color: theme.icons.colorize
|
||||||
|
|
|
@ -3,6 +3,7 @@ import QtGraphicalEffects 1.12
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: image
|
id: image
|
||||||
|
autoTransform: true
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
cache: true
|
cache: true
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
|
|
|
@ -7,7 +7,6 @@ Row {
|
||||||
id: eventContent
|
id: eventContent
|
||||||
spacing: theme.spacing / 2
|
spacing: theme.spacing / 2
|
||||||
|
|
||||||
|
|
||||||
readonly property string eventText: Utils.processedEventText(model)
|
readonly property string eventText: Utils.processedEventText(model)
|
||||||
readonly property string eventTime: Utils.formatTime(model.date)
|
readonly property string eventTime: Utils.formatTime(model.date)
|
||||||
readonly property int eventTimeSpaces: 2
|
readonly property int eventTimeSpaces: 2
|
||||||
|
@ -29,8 +28,8 @@ Row {
|
||||||
HoverHandler { id: hover }
|
HoverHandler { id: hover }
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: hideAvatar ? 0 : 48
|
width: hideAvatar ? 0 : 58
|
||||||
height: hideAvatar ? 0 : collapseAvatar ? 1 : smallAvatar ? 28 : 48
|
height: hideAvatar ? 0 : collapseAvatar ? 1 : smallAvatar ? 28 : 58
|
||||||
opacity: hideAvatar || collapseAvatar ? 0 : 1
|
opacity: hideAvatar || collapseAvatar ? 0 : 1
|
||||||
visible: width > 0
|
visible: width > 0
|
||||||
|
|
||||||
|
@ -39,8 +38,8 @@ Row {
|
||||||
userId: model.sender_id
|
userId: model.sender_id
|
||||||
displayName: model.sender_name
|
displayName: model.sender_name
|
||||||
avatarUrl: model.sender_avatar
|
avatarUrl: model.sender_avatar
|
||||||
width: hideAvatar ? 0 : 48
|
width: hideAvatar ? 0 : 58
|
||||||
height: hideAvatar ? 0 : collapseAvatar ? 1 : 48
|
height: hideAvatar ? 0 : collapseAvatar ? 1 : 58
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,15 +54,18 @@ Row {
|
||||||
theme.fontSize.normal * 0.5 * 75, // 600 with 16px font
|
theme.fontSize.normal * 0.5 * 75, // 600 with 16px font
|
||||||
Math.max(
|
Math.max(
|
||||||
nameLabel.visible ? nameLabel.implicitWidth : 0,
|
nameLabel.visible ? nameLabel.implicitWidth : 0,
|
||||||
contentLabel.implicitWidth
|
contentLabel.implicitWidth,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
height: (nameLabel.visible ? nameLabel.height : 0) +
|
height: childrenRect.height
|
||||||
contentLabel.implicitHeight
|
|
||||||
y: parent.height / 2 - height / 2
|
y: parent.height / 2 - height / 2
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.fill: parent
|
id: mainColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: theme.spacing / 1.75
|
||||||
|
topPadding: theme.spacing / 1.75
|
||||||
|
bottomPadding: topPadding
|
||||||
|
|
||||||
HSelectableLabel {
|
HSelectableLabel {
|
||||||
id: nameLabel
|
id: nameLabel
|
||||||
|
@ -71,6 +73,8 @@ Row {
|
||||||
visible: ! hideNameLine
|
visible: ! hideNameLine
|
||||||
container: selectableLabelContainer
|
container: selectableLabelContainer
|
||||||
selectable: ! unselectableNameLine
|
selectable: ! unselectableNameLine
|
||||||
|
leftPadding: eventContent.spacing
|
||||||
|
rightPadding: leftPadding
|
||||||
|
|
||||||
// This is +0.1 and content is +0 instead of the opposite,
|
// This is +0.1 and content is +0 instead of the opposite,
|
||||||
// because the eventList is reversed
|
// because the eventList is reversed
|
||||||
|
@ -81,10 +85,6 @@ Row {
|
||||||
// elide: Text.ElideRight
|
// elide: Text.ElideRight
|
||||||
horizontalAlignment: onRight ? Text.AlignRight : Text.AlignLeft
|
horizontalAlignment: onRight ? Text.AlignRight : Text.AlignLeft
|
||||||
|
|
||||||
leftPadding: theme.spacing
|
|
||||||
rightPadding: leftPadding
|
|
||||||
topPadding: theme.spacing / 2
|
|
||||||
|
|
||||||
function selectAllTextPlus() {
|
function selectAllTextPlus() {
|
||||||
contentLabel.selectAllTextPlus()
|
contentLabel.selectAllTextPlus()
|
||||||
}
|
}
|
||||||
|
@ -97,6 +97,10 @@ Row {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
container: selectableLabelContainer
|
container: selectableLabelContainer
|
||||||
index: model.index
|
index: model.index
|
||||||
|
leftPadding: eventContent.spacing
|
||||||
|
rightPadding: leftPadding
|
||||||
|
bottomPadding: previewLinksRepeater.count > 0 ?
|
||||||
|
mainColumn.bottomPadding : 0
|
||||||
|
|
||||||
text: theme.chat.message.styleInclude +
|
text: theme.chat.message.styleInclude +
|
||||||
eventContent.eventText +
|
eventContent.eventText +
|
||||||
|
@ -115,12 +119,6 @@ Row {
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
textFormat: Text.RichText
|
textFormat: Text.RichText
|
||||||
|
|
||||||
leftPadding: theme.spacing
|
|
||||||
rightPadding: leftPadding
|
|
||||||
topPadding: nameLabel.visible ? 0 : bottomPadding
|
|
||||||
bottomPadding: theme.spacing / 2
|
|
||||||
|
|
||||||
|
|
||||||
function selectAllText() {
|
function selectAllText() {
|
||||||
// Select the message body without the date or name
|
// Select the message body without the date or name
|
||||||
container.clearSelection()
|
container.clearSelection()
|
||||||
|
@ -142,6 +140,22 @@ Row {
|
||||||
|
|
||||||
HoverHandler { id: contentHover }
|
HoverHandler { id: contentHover }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: previewLinksRepeater
|
||||||
|
model: previewLinks
|
||||||
|
|
||||||
|
HLoader {
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (modelData[0] == "image") {
|
||||||
|
setSource(
|
||||||
|
"EventImage.qml",
|
||||||
|
{ source: modelData[1] },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
import QtQuick 2.12
|
import QtQuick 2.12
|
||||||
import QtQuick.Layouts 1.12
|
import QtQuick.Layouts 1.12
|
||||||
import "../../Base"
|
import "../../Base"
|
||||||
|
import "../../utils.js" as Utils
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: eventDelegate
|
id: eventDelegate
|
||||||
|
width: eventList.width
|
||||||
|
|
||||||
|
topPadding:
|
||||||
|
model.event_type == "RoomCreateEvent" ? 0 :
|
||||||
|
dayBreak ? theme.spacing * 4 :
|
||||||
|
talkBreak ? theme.spacing * 6 :
|
||||||
|
combine ? theme.spacing / 2 :
|
||||||
|
theme.spacing * 2
|
||||||
|
|
||||||
|
|
||||||
// Remember timeline goes from newest message at index 0 to oldest
|
// Remember timeline goes from newest message at index 0 to oldest
|
||||||
property var previousItem: eventList.model.get(model.index + 1)
|
property var previousItem: eventList.model.get(model.index + 1)
|
||||||
|
@ -38,14 +48,9 @@ Column {
|
||||||
readonly property bool unselectableNameLine:
|
readonly property bool unselectableNameLine:
|
||||||
hideNameLine && ! (onRight && ! combine)
|
hideNameLine && ! (onRight && ! combine)
|
||||||
|
|
||||||
width: eventList.width
|
readonly property var previewLinks: model.preview_links
|
||||||
|
|
||||||
topPadding:
|
property string hoveredImage: ""
|
||||||
model.event_type == "RoomCreateEvent" ? 0 :
|
|
||||||
dayBreak ? theme.spacing * 4 :
|
|
||||||
talkBreak ? theme.spacing * 6 :
|
|
||||||
combine ? theme.spacing / 2 :
|
|
||||||
theme.spacing * 2
|
|
||||||
|
|
||||||
|
|
||||||
Daybreak {
|
Daybreak {
|
||||||
|
@ -70,15 +75,8 @@ Column {
|
||||||
TapHandler {
|
TapHandler {
|
||||||
acceptedButtons: Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
onTapped: {
|
onTapped: {
|
||||||
contextMenu.link = eventContent.hoveredLink
|
contextMenu.link = eventContent.hoveredLink
|
||||||
contextMenu.popup()
|
contextMenu.image = eventDelegate.hoveredImage
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TapHandler {
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
onLongPressed: {
|
|
||||||
contextMenu.link = eventContent.hoveredLink
|
|
||||||
contextMenu.popup()
|
contextMenu.popup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,8 +85,17 @@ Column {
|
||||||
id: contextMenu
|
id: contextMenu
|
||||||
|
|
||||||
property string link: ""
|
property string link: ""
|
||||||
|
property string image: ""
|
||||||
|
|
||||||
onClosed: link = ""
|
onClosed: { link = ""; image = "" }
|
||||||
|
|
||||||
|
HMenuItem {
|
||||||
|
id: copyImage
|
||||||
|
icon.name: "copy-link"
|
||||||
|
text: qsTr("Copy image address")
|
||||||
|
visible: Boolean(contextMenu.image)
|
||||||
|
onTriggered: Utils.copyToClipboard(contextMenu.image)
|
||||||
|
}
|
||||||
|
|
||||||
HMenuItem {
|
HMenuItem {
|
||||||
id: copyLink
|
id: copyLink
|
||||||
|
@ -101,12 +108,25 @@ Column {
|
||||||
HMenuItem {
|
HMenuItem {
|
||||||
icon.name: "copy-text"
|
icon.name: "copy-text"
|
||||||
text: qsTr("Copy text")
|
text: qsTr("Copy text")
|
||||||
visible: enabled || ! copyLink.visible
|
visible: enabled || (! copyLink.visible && ! copyImage.visible)
|
||||||
enabled: Boolean(selectableLabelContainer.joinedSelection)
|
enabled: Boolean(selectableLabelContainer.joinedSelection)
|
||||||
onTriggered:
|
onTriggered:
|
||||||
Utils.copyToClipboard(selectableLabelContainer.joinedSelection)
|
Utils.copyToClipboard(selectableLabelContainer.joinedSelection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HMenuItem {
|
||||||
|
icon.name: "settings"
|
||||||
|
text: qsTr("Print event item")
|
||||||
|
visible: debugMode
|
||||||
|
onTriggered: print(JSON.stringify(Utils.getItem(
|
||||||
|
modelSources[[
|
||||||
|
"Event", chatPage.userId, chatPage.roomId
|
||||||
|
]],
|
||||||
|
"client_id",
|
||||||
|
model.client_id
|
||||||
|
), null, 4))
|
||||||
|
}
|
||||||
|
|
||||||
HMenuItem {
|
HMenuItem {
|
||||||
icon.name: "settings"
|
icon.name: "settings"
|
||||||
text: qsTr("Set as debug console target")
|
text: qsTr("Set as debug console target")
|
||||||
|
@ -114,6 +134,6 @@ Column {
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
mainUI.debugConsole.target = [eventDelegate, eventContent]
|
mainUI.debugConsole.target = [eventDelegate, eventContent]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
31
src/qml/Chat/Timeline/EventImage.qml
Normal file
31
src/qml/Chat/Timeline/EventImage.qml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import QtQuick 2.12
|
||||||
|
import "../../Base"
|
||||||
|
|
||||||
|
HImage {
|
||||||
|
id: image
|
||||||
|
x: eventContent.spacing
|
||||||
|
sourceSize.width: maxDimension
|
||||||
|
sourceSize.height: maxDimension
|
||||||
|
width: Math.min(
|
||||||
|
mainColumn.width - eventContent.spacing * 2,
|
||||||
|
implicitWidth,
|
||||||
|
maxDimension,
|
||||||
|
)
|
||||||
|
|
||||||
|
property int maxDimension: window.settings.messageImageMaxThumbnailSize
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
onTapped: Qt.openUrlExternally(image.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: hover
|
||||||
|
onHoveredChanged: eventDelegate.hoveredImage = hovered ? image.source : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: image
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
}
|
||||||
|
}
|
|
@ -177,7 +177,7 @@ Rectangle {
|
||||||
}
|
}
|
||||||
|
|
||||||
HNoticePage {
|
HNoticePage {
|
||||||
text: qsTr("No messages visible yet.")
|
text: qsTr("No messages visible yet")
|
||||||
|
|
||||||
visible: eventList.model.count < 1
|
visible: eventList.model.count < 1
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
Loading…
Reference in New Issue
Block a user