fix(LinksMessageView): Refactor LinksMessageView to remove business logic from qml

LinksMessageView component will receive the urls from nim as string and it will only forward the string to getLinkPreviewData slot implemented in nim together with some settings (supported img extensions and unfurling preferences)
On nim side the urls will be parsed and validated using the settings received from qml.
Images are now validated before sending them to the UI using the HEAD request.
This commit is contained in:
Alex Jbanca 2023-02-03 15:05:32 +02:00 committed by Alex Jbanca
parent a93320f684
commit f9f860a215
12 changed files with 304 additions and 373 deletions

View File

@ -1,5 +1,6 @@
import chronicles
import io_interface
import json
import ../../../../../../app/global/global_singleton
import ../../../../../../app_service/service/contacts/service as contact_service
@ -184,7 +185,7 @@ proc init*(self: Controller) =
self.events.on(SIGNAL_MESSAGE_LINK_PREVIEW_DATA_LOADED) do(e: Args):
let args = LinkPreviewDataArgs(e)
self.delegate.onPreviewDataLoaded(args.response)
self.delegate.onPreviewDataLoaded($(args.response), args.uuid)
self.events.on(SIGNAL_MAKE_SECTION_CHAT_ACTIVE) do(e: Args):
let args = ActiveSectionChatArgs(e)
@ -270,8 +271,8 @@ proc deleteMessage*(self: Controller, messageId: string) =
proc editMessage*(self: Controller, messageId: string, contentType: int, updatedMsg: string) =
self.messageService.editMessage(messageId, contentType, updatedMsg)
proc getLinkPreviewData*(self: Controller, link: string, uuid: string): string =
self.messageService.asyncGetLinkPreviewData(link, uuid)
proc getLinkPreviewData*(self: Controller, link: string, uuid: string, whiteListedSites: string, whiteListedImgExtensions: string, unfurlImages: bool): string =
self.messageService.asyncGetLinkPreviewData(link, uuid, whiteListedSites, whiteListedImgExtensions, unfurlImages)
proc getSearchedMessageId*(self: Controller): string =
return self.searchedMessageId

View File

@ -123,10 +123,10 @@ method editMessage*(self: AccessInterface, messageId: string, contentType: int,
method onHistoryCleared*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method getLinkPreviewData*(self: AccessInterface, link: string, uuid: string): string {.base.} =
method getLinkPreviewData*(self: AccessInterface, link: string, uuid: string, whiteListedSites: string, whiteListedImgExtensions: string, unfurlImages: bool): string {.base.} =
raise newException(ValueError, "No implementation available")
method onPreviewDataLoaded*(self: AccessInterface, previewData: string) {.base.} =
method onPreviewDataLoaded*(self: AccessInterface, previewData: string, uuid: string) {.base.} =
raise newException(ValueError, "No implementation available")
method requestMoreMessages*(self: AccessInterface) {.base.} =

View File

@ -585,11 +585,11 @@ method updateChatFetchMoreMessages*(self: Module) =
if (self.controller.getChatDetails().hasMoreMessagesToRequest()):
self.view.model().insertItemBasedOnClock(self.createFetchMoreMessagesItem())
method getLinkPreviewData*(self: Module, link: string, uuid: string): string =
return self.controller.getLinkPreviewData(link, uuid)
method getLinkPreviewData*(self: Module, link: string, uuid: string, whiteListedSites: string, whiteListedImgExtensions: string, unfurlImages: bool): string =
return self.controller.getLinkPreviewData(link, uuid, whiteListedSites, whiteListedImgExtensions, unfurlImages)
method onPreviewDataLoaded*(self: Module, previewData: string) =
self.view.onPreviewDataLoaded(previewData)
method onPreviewDataLoaded*(self: Module, previewData: string, uuid: string) =
self.view.onPreviewDataLoaded(previewData, uuid)
method switchToMessage*(self: Module, messageId: string) =
let index = self.view.model().findIndexForMessageId(messageId)

View File

@ -118,13 +118,13 @@ QtObject:
proc editMessage*(self: View, messageId: string, contentType: int, updatedMsg: string) {.slot.} =
self.delegate.editMessage(messageId, contentType, updatedMsg)
proc getLinkPreviewData*(self: View, link: string, uuid: string): string {.slot.} =
return self.delegate.getLinkPreviewData(link, uuid)
proc getLinkPreviewData*(self: View, link: string, uuid: string, whiteListedSites: string, whiteListedImgExtensions: string, unfurlImages: bool): string {.slot.} =
return self.delegate.getLinkPreviewData(link, uuid, whiteListedSites, whiteListedImgExtensions, unfurlImages)
proc linkPreviewDataWasReceived*(self: View, previewData: string) {.signal.}
proc linkPreviewDataWasReceived*(self: View, previewData: string, uuid: string) {.signal.}
proc onPreviewDataLoaded*(self: View, previewData: string) {.slot.} =
self.linkPreviewDataWasReceived(previewData)
proc onPreviewDataLoaded*(self: View, previewData: string, uuid: string) {.slot.} =
self.linkPreviewDataWasReceived(previewData, uuid)
proc switchToMessage(self: View, messageIndex: int) {.signal.}
proc emitSwitchToMessageSignal*(self: View, messageIndex: int) =

View File

@ -1,3 +1,4 @@
import std/uri, std/httpclient
include ../../common/json_utils
include ../../../app/core/tasks/common
@ -152,25 +153,87 @@ const asyncMarkCertainMessagesReadTask: Task = proc(argEncoded: string) {.gcsafe
type
AsyncGetLinkPreviewDataTaskArg = ref object of QObjectTaskArg
link: string
links: string
uuid: string
whiteListedUrls: string
whiteListedImgExtensions: string
unfurlImages: bool
const asyncGetLinkPreviewDataTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncGetLinkPreviewDataTaskArg](argEncoded)
var previewData = %* {
"links": %*[]
}
if arg.links == "":
arg.finish(previewData)
return
var success = true
var result: JsonNode = %* {}
try:
let response = status_go_chat.getLinkPreviewData(arg.link)
result = response.result
except:
success = false
let parsedWhiteListUrls = parseJson(arg.whiteListedUrls)
let parsedWhiteListImgExtensions = arg.whiteListedImgExtensions.split(",")
let httpClient = newHttpClient()
for link in arg.links.split(" "):
if link == "":
continue
let responseJson = %*{
"link": arg.link,
"uuid": arg.uuid,
"success": success,
"result": result,
let uri = parseUri(link)
let path = uri.path
let domain = uri.hostname.toLower()
let isSupportedImage = any(parsedWhiteListImgExtensions, proc (extenstion: string): bool = path.endsWith(extenstion))
let isWhitelistedUrl = parsedWhiteListUrls.hasKey(domain)
let processUrl = isWhitelistedUrl or isSupportedImage
if domain == "" or processUrl == false:
continue
let canUnfurl = parsedWhiteListUrls{domain}.getBool() or (isSupportedImage and arg.unfurlImages)
let responseJson = %*{
"link": link,
"success": true,
"unfurl": canUnfurl,
"isStatusDeepLink": false,
"result": %*{}
}
arg.finish(responseJson)
if canUnfurl == false:
previewData["links"].add(responseJson)
continue
#1. if it's an image, we use httpclient to validate the url
if isSupportedImage:
let headResponse = httpclient.head(link)
#validate image url
responseJson["success"] = %(headResponse.code() == Http200 and headResponse.contentType().startsWith("image/"))
responseJson["result"] = %*{
"site": domain,
"thumbnailUrl": link,
"contentType": headResponse.contentType()
}
previewData["links"].add(responseJson)
continue
#2. Process whitelisted url
#status deep links are handled internally
if domain == "status-im" or domain == "join.status.im":
responseJson["success"] = %true
responseJson["isStatusDeepLink"] = %true
responseJson["result"] = %*{
"site": domain,
"contentType": "text/html"
}
previewData["links"].add(responseJson)
continue
#other links are handled by status-go
try:
let response = status_go_chat.getLinkPreviewData(link)
responseJson["result"] = response.result
responseJson["success"] = %true
except:
responseJson["success"] = %false
previewData["links"].add(responseJson)
let tpl: tuple[previewData: JsonNode, uuid: string] = (previewData, arg.uuid)
arg.finish(tpl)

View File

@ -1,4 +1,4 @@
import NimQml, tables, json, re, sequtils, strformat, strutils, chronicles, times
import NimQml, tables, json, re, sequtils, strformat, strutils, chronicles, times, oids
import ../../../app/core/tasks/[qt, threadpool]
import ../../../app/core/signals/types
@ -111,7 +111,8 @@ type
message*: MessageDto
LinkPreviewDataArgs* = ref object of Args
response*: string
response*: JsonNode
uuid*: string
ReloadMessagesArgs* = ref object of Args
communityId*: string
@ -671,17 +672,27 @@ QtObject:
errDesription = e.msg
proc onAsyncGetLinkPreviewData*(self: Service, response: string) {.slot.} =
self.events.emit(SIGNAL_MESSAGE_LINK_PREVIEW_DATA_LOADED, LinkPreviewDataArgs(response: response))
let responseObj = response.parseJson
if (responseObj.kind != JObject):
info "expected response is not a json object", methodName="onAsyncGetLinkPreviewData"
return
proc asyncGetLinkPreviewData*(self: Service, link: string, uuid: string) =
self.events.emit(SIGNAL_MESSAGE_LINK_PREVIEW_DATA_LOADED, LinkPreviewDataArgs(response: responseObj["previewData"], uuid: responseObj["uuid"].getStr()))
proc asyncGetLinkPreviewData*(self: Service, links: string, uuid: string, whiteListedSites: string, whiteListedImgExtensions: string, unfurlImages: bool): string =
let arg = AsyncGetLinkPreviewDataTaskArg(
tptr: cast[ByteAddress](asyncGetLinkPreviewDataTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onAsyncGetLinkPreviewData",
link: link,
links: links,
whiteListedUrls: whiteListedSites,
whiteListedImgExtensions: whiteListedImgExtensions,
unfurlImages: unfurlImages,
uuid: uuid
)
self.threadpool.start(arg)
return $genOid()
# See render-inline in status-mobile/src/status_im/ui/screens/chat/message/message.cljs
proc renderInline(self: Service, parsedText: ParsedText, communityChats: seq[ChatDto]): string =

View File

@ -391,7 +391,9 @@ QtObject {
function getLinkTitleAndCb(link) {
const result = {
title: "Status",
callback: null
callback: null,
fetching: true,
communityId: ""
}
// User profile
@ -407,37 +409,31 @@ QtObject {
}*/
// Community
const communityId = Utils.getCommunityIdFromShareLink(link)
if (communityId !== "") {
const communityName = getSectionNameById(communityId)
result.communityId = Utils.getCommunityIdFromShareLink(link)
if(!result.communityId) return result
if (!communityName) {
// Unknown community, fetch the info if possible
root.requestCommunityInfo(communityId)
result.communityId = communityId
result.fetching = true
return result
}
result.title = qsTr("Join the %1 community").arg(communityName)
result.communityId = communityId
result.callback = function () {
const isUserMemberOfCommunity = isUserMemberOfCommunity(communityId)
if (isUserMemberOfCommunity) {
setActiveCommunity(communityId)
return
}
const userCanJoin = userCanJoin(communityId)
// TODO find what to do when you can't join
if (userCanJoin) {
requestToJoinCommunity(communityId, userProfileInst.preferredName)
}
}
const communityName = getSectionNameById(result.communityId)
if (!communityName) {
// Unknown community, fetch the info if possible
root.requestCommunityInfo(communityId)
return result
}
result.title = qsTr("Join the %1 community").arg(communityName)
result.fetching = false
result.callback = function () {
const isUserMemberOfCommunity = isUserMemberOfCommunity(result.communityId)
if (isUserMemberOfCommunity) {
setActiveCommunity(result.communityId)
return
}
const userCanJoin = userCanJoin(result.communityId)
// TODO find what to do when you can't join
if (userCanJoin) {
requestToJoinCommunity(result.communityId, userProfileInst.preferredName)
}
}
return result
}

View File

@ -154,18 +154,7 @@ Item {
anchors.right: parent.right
spacing: 0
verticalLayoutDirection: ListView.BottomToTop
function checkHeaderHeight() {
if (!chatLogView.headerItem) {
return
}
if (chatLogView.contentItem.height - chatLogView.headerItem.height < chatLogView.height) {
chatLogView.headerItem.height = chatLogView.height - (chatLogView.contentItem.height - chatLogView.headerItem.height) - 36
} else {
chatLogView.headerItem.height = 0
}
}
cacheBuffer: height * 2 // cache 2 screens worth of items
model: messageStore.messagesModel
@ -182,18 +171,6 @@ Item {
visible: chatLogView.visibleArea.heightRatio < 1
}
// This header and Connections is to create an invisible padding so that the chat identifier is at the top
// The Connections is necessary, because doing the check inside the header created a binding loop (the contentHeight includes the header height
// If the content height is smaller than the full height, we "show" the padding so that the chat identifier is at the top, otherwise we disable the Connections
header: Item {
height: 0
width: chatLogView.width
}
Timer {
id: timer
}
Button {
id: scrollDownButton

View File

@ -7,7 +7,6 @@ import shared 1.0
Item {
id: root
default property alias inner: contents.children
property bool isCurrentUser: false
readonly property int smallCorner: Style.current.radius / 2
readonly property int bigCorner: Style.current.radius * 2
@ -85,11 +84,5 @@ Item {
}
}
}
Item {
id: contents
width: root.width
height: root.height
}
}
}

View File

@ -20,6 +20,7 @@ Item {
property bool allCornersRounded: false
property bool isOnline: true // TODO: mark as required when migrating to 5.15 or above
property bool imageLoaded: (imageMessage.status === Image.Ready)
property alias asynchronous: imageMessage.asynchronous
signal clicked(var image, var mouse)

View File

@ -1,4 +1,4 @@
import QtQuick 2.13
import QtQuick 2.15
import QtGraphicalEffects 1.13
import QtQuick.Layouts 1.13
@ -20,197 +20,101 @@ Column {
property var messageStore
property var container
property alias linksModel: linksRepeater.model
//receiving space separated url list
property string links: ""
readonly property alias unfurledLinksCount: d.unfurledLinksCount
readonly property alias unfurledImagesCount: d.unfurledImagesCount
property bool isCurrentUser: false
signal imageClicked(var image)
signal linksLoaded()
spacing: 4
QtObject {
id: d
property bool isImageLink: false
property int unfurledLinksCount: 0
}
Repeater {
id: linksRepeater
model: linksModel
delegate: Loader {
id: linkMessageLoader
property bool fetched: false
property var linkData
readonly property string uuid: Utils.uuid()
property bool loadingFailed: false
active: true
Connections {
target: localAccountSensitiveSettings
function onWhitelistedUnfurlingSitesChanged() {
fetched = false
linkMessageLoader.sourceComponent = undefined
linkMessageLoader.sourceComponent = linkMessageLoader.getSourceComponent()
}
function onNeverAskAboutUnfurlingAgainChanged() {
linkMessageLoader.sourceComponent = undefined
linkMessageLoader.sourceComponent = linkMessageLoader.getSourceComponent()
}
function onDisplayChatImagesChanged() {
linkMessageLoader.sourceComponent = undefined
linkMessageLoader.sourceComponent = linkMessageLoader.getSourceComponent()
}
required property var result
required property string link
required property int index
required property bool unfurl
required property bool success
required property bool isStatusDeepLink
readonly property bool isImage: result.contentType ? result.contentType.startsWith("image/") : false
readonly property bool neverAskAboutUnfurlingAgain: RootStore.neverAskAboutUnfurlingAgain
active: success
asynchronous: true
StateGroup {
//Using StateGroup as a warkardound for https://bugreports.qt.io/browse/QTBUG-47796
id: linkPreviewLoaderState
states:[
State {
name: "neverAskAboutUnfurling"
when: !unfurl && neverAskAboutUnfurlingAgain
PropertyChanges { target: linkMessageLoader; sourceComponent: undefined; }
StateChangeScript { name: "removeFromModel"; script: linksModel.remove(index)}
},
State {
name: "askToEnableUnfurling"
when: !unfurl && !neverAskAboutUnfurlingAgain
PropertyChanges { target: linkMessageLoader; sourceComponent: enableLinkComponent }
},
State {
name: "loadImage"
when: unfurl && isImage
PropertyChanges { target: linkMessageLoader; sourceComponent: unfurledImageComponent }
},
State {
name: "loadLinkPreview"
when: unfurl && !isImage && !isStatusDeepLink
PropertyChanges { target: linkMessageLoader; sourceComponent: unfurledLinkComponent }
},
State {
name: "statusInvitation"
when: unfurl && isStatusDeepLink
PropertyChanges { target: linkMessageLoader; sourceComponent: invitationBubble }
}
]
}
}
}
Connections {
id: linkFetchConnections
enabled: false
target: root.messageStore.messageModule
function onLinkPreviewDataWasReceived(previewData: string) {
let response = {}
try {
response = JSON.parse(previewData)
} catch (e) {
console.error(previewData, e)
linkMessageLoader.loadingFailed = true
return
}
if (response.uuid !== linkMessageLoader.uuid) return
linkFetchConnections.enabled = false
QtObject {
id: d
property bool hasImageLink: false
property int unfurledLinksCount: 0
property int unfurledImagesCount: 0
readonly property string uuid: Utils.uuid()
readonly property string whiteListedImgExtensions: Constants.acceptedImageExtensions.toString()
readonly property string whiteListedUrls: JSON.stringify(localAccountSensitiveSettings.whitelistedUnfurlingSites)
readonly property string getLinkPreviewDataId: messageStore.messageModule.getLinkPreviewData(root.links, d.uuid, whiteListedUrls, whiteListedImgExtensions, localAccountSensitiveSettings.displayChatImages)
onGetLinkPreviewDataIdChanged: { linkFetchConnections.enabled = true }
}
if (!response.success) {
console.error("could not get preview data")
linkMessageLoader.loadingFailed = true
return
}
Connections {
id: linkFetchConnections
enabled: false
target: root.messageStore.messageModule
function onLinkPreviewDataWasReceived(previewData, uuid) {
if(d.uuid != uuid) return
linkFetchConnections.enabled = false
try { linksModel.rawData = JSON.parse(previewData) }
catch(e) { console.warn("error parsing link preview data", previewData) }
}
}
linkData = response.result
linkMessageLoader.loadingFailed = false
linkMessageLoader.height = undefined // Reset height so it's not 0
if (linkData.contentType.startsWith("image/")) {
return linkMessageLoader.sourceComponent = unfurledImageComponent
}
if (linkData.site && linkData.title) {
linkData.address = link
return linkMessageLoader.sourceComponent = unfurledLinkComponent
}
}
}
Connections {
id: linkCommunityFetchConnections
enabled: false
target: root.store.communitiesModuleInst
function onCommunityAdded(communityId: string) {
if (communityId !== linkData.communityId) {
return
}
linkCommunityFetchConnections.enabled = false
const data = root.store.getLinkDataForStatusLinks(link)
if (data) {
linkData = data
if (!data.fetching && data.communityId) {
return linkMessageLoader.sourceComponent = invitationBubble
}
// do not show unfurledLinkComponent
return
}
}
}
Connections {
target: root.store.mainModuleInst
enabled: linkMessageLoader.loadingFailed
function onIsOnlineChanged() {
if (!root.store.mainModuleInst.isOnline)
return
linkMessageLoader.fetched = false
linkMessageLoader.sourceComponent = undefined
linkMessageLoader.sourceComponent = linkMessageLoader.getSourceComponent()
}
}
function getSourceComponent() {
// Reset the height in case we set it to 0 below. See note below
// for more information
this.height = undefined
const linkHostname = Utils.getHostname(link)
if (!localAccountSensitiveSettings.whitelistedUnfurlingSites) {
localAccountSensitiveSettings.whitelistedUnfurlingSites = {}
}
const whitelistHosts = Object.keys(localAccountSensitiveSettings.whitelistedUnfurlingSites)
const linkExists = whitelistHosts.some(hostname => linkHostname.endsWith(hostname))
const linkWhiteListed = linkExists && whitelistHosts.some(hostname =>
linkHostname.endsWith(hostname) && localAccountSensitiveSettings.whitelistedUnfurlingSites[hostname] === true)
if (!linkWhiteListed && linkExists && !RootStore.neverAskAboutUnfurlingAgain && !model.isImage) {
return enableLinkComponent
}
if (linkWhiteListed) {
if (fetched) {
if (linkData.communityId) {
return invitationBubble
}
return unfurledLinkComponent
}
fetched = true
const data = root.store.getLinkDataForStatusLinks(link)
if (data) {
linkData = data
if (data.fetching && data.communityId) {
linkCommunityFetchConnections.enabled = true
return
}
if (data.communityId) {
return invitationBubble
}
// do not show unfurledLinkComponent
return
}
linkFetchConnections.enabled = true
root.messageStore.getLinkPreviewData(link, linkMessageLoader.uuid)
}
if (model.isImage) {
if (RootStore.displayChatImages) {
linkData = {
thumbnailUrl: link
}
return unfurledImageComponent
}
else if (!(RootStore.neverAskAboutUnfurlingAgain || (d.isImageLink && index > 0))) {
d.isImageLink = true
return enableLinkComponent
}
}
// setting the height to 0 allows the "enable link" dialog to
// disappear correctly when RootStore.neverAskAboutUnfurlingAgain
// is true. The height is reset at the top of this method.
this.height = 0
return undefined
}
Component.onCompleted: {
// putting this is onCompleted prevents automatic binding, where
// QML warns of a binding loop detected
this.sourceComponent = linkMessageLoader.getSourceComponent()
}
ListModel {
id: linksModel
property var rawData
onRawDataChanged: {
linksModel.clear()
rawData.links.forEach((link) => {
linksModel.append(link)
})
root.linksLoaded()
}
}
@ -224,28 +128,73 @@ Column {
StatusChatImageLoader {
id: linkImage
readonly property bool globalAnimationEnabled: root.messageStore.playAnimation
property bool localAnimationEnabled: true
objectName: "LinksMessageView_unfurledImageComponent_linkImage"
anchors.centerIn: parent
container: root.container
source: linkData.thumbnailUrl
source: result.thumbnailUrl
imageWidth: 300
isCurrentUser: root.isCurrentUser
onClicked: imageClicked(linkImage.imageAlias)
playing: root.messageStore.playAnimation
playing: globalAnimationEnabled && localAnimationEnabled
isOnline: root.store.mainModuleInst.isOnline
asynchronous: true
isAnimated: result.contentType ? result.contentType.toLowerCase().endsWith("gif") : false
onClicked: isAnimated && !playing ? localAnimationEnabled = true : root.imageClicked(linkImage.imageAlias)
Loader {
width: 45
height: 38
anchors.left: parent.left
anchors.leftMargin: 12
anchors.bottom: parent.bottom
anchors.bottomMargin: 12
active: linkImage.isAnimated && !linkImage.playing
sourceComponent: Item {
anchors.fill: parent
Rectangle {
anchors.fill: parent
color: "black"
radius: Style.current.radius
opacity: .4
}
StatusBaseText {
anchors.centerIn: parent
text: "GIF"
font.pixelSize: 13
}
}
}
Timer {
id: animationPlayingTimer
interval: 10000
running: linkImage.isAnimated && linkImage.playing
onTriggered: { linkImage.localAnimationEnabled = false }
}
}
Component.onCompleted: d.unfurledLinksCount++
Component.onDestruction: d.unfurledLinksCount--
Component.onCompleted: d.unfurledImagesCount++
Component.onDestruction: d.unfurledImagesCount--
}
}
Component {
id: invitationBubble
InvitationBubbleView {
property var invitationData: root.store.getLinkDataForStatusLinks(link)
onInvitationDataChanged: { if(!invitationData) linksModel.remove(index) }
store: root.store
communityId: linkData.communityId
communityId: invitationData ? invitationData.communityId : ""
anchors.left: parent.left
visible: invitationData && !invitationData.fetching
Connections {
enabled: invitationData && invitationData.fetching
target: root.store.communitiesModuleInst
function onCommunityAdded(communityId: string) {
if (communityId !== invitationData.communityId) return
invitationData = root.store.getLinkDataForStatusLinks(link)
}
}
}
}
@ -253,15 +202,12 @@ Column {
id: unfurledLinkComponent
MessageBorder {
id: unfurledLink
readonly property bool isGIFImage: linkData.thumbnailUrl.endsWith(".gif")
width: linkImage.visible ? linkImage.width + 2 : 300
height: {
if (linkImage.visible) {
return linkImage.height + (!unfurledLink.isGIFImage ?
((Style.current.smallPadding * 2) + (linkTitle.height + 2 + linkSite.height)) : 0)
return linkImage.height + (Style.current.smallPadding * 2) + (linkTitle.height + 2 + linkSite.height)
}
return (Style.current.smallPadding * 2) + (!unfurledLink.isGIFImage ?
(linkTitle.height + 2 + linkSite.height) : 0)
return (Style.current.smallPadding * 2) + linkTitle.height + 2 + linkSite.height
}
isCurrentUser: root.isCurrentUser
@ -269,81 +215,29 @@ Column {
id: linkImage
objectName: "LinksMessageView_unfurledLinkComponent_linkImage"
container: root.container
source: linkData.thumbnailUrl
visible: linkData.thumbnailUrl.length
readonly property int previewWidth: parseInt(linkData.width)
source: result.thumbnailUrl
visible: result.thumbnailUrl.length
readonly property int previewWidth: parseInt(result.width)
imageWidth: Math.min(300, previewWidth > 0 ? previewWidth : 300)
isCurrentUser: root.isCurrentUser
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
isOnline: root.store.mainModuleInst.isOnline
asynchronous: true
onClicked: {
if (unfurledLink.isGIFImage) {
if (playing) {
imageClicked(linkImage.imageAlias);
} else {
if (root.messageStore.playAnimation) {
linkImage.playing = true;
animationPlayingTimer.restart();
}
}
} else {
if (!!linkData.callback) {
return linkData.callback()
}
Global.openLink(linkData.address)
}
}
onImageLoadedChanged: {
if (imageLoaded && root.messageStore.playAnimation) {
playing = true;
animationPlayingTimer.start();
}
}
Timer {
id: animationPlayingTimer
interval: 10000
running: false
onTriggered: {
if (root.messageStore.playAnimation) {
linkImage.playing = false;
}
}
}
}
Loader {
width: 45
height: 38
anchors.left: parent.left
anchors.leftMargin: 12
anchors.bottom: parent.bottom
anchors.bottomMargin: 12
active: !linkImage.playing
sourceComponent: Item {
anchors.fill: parent
Rectangle {
anchors.fill: parent
color: "black"
radius: Style.current.radius
opacity: .4
}
StatusBaseText {
anchors.centerIn: parent
text: "GIF"
font.pixelSize: 13
if (!!result.callback) {
return result.callback()
}
Global.openLink(result.address)
}
}
StatusBaseText {
id: linkTitle
text: linkData.title
text: result.title
font.pixelSize: 13
font.weight: Font.Medium
wrapMode: Text.Wrap
visible: !unfurledLink.isGIFImage
anchors.top: linkImage.visible ? linkImage.bottom : parent.top
anchors.topMargin: Style.current.smallPadding
anchors.left: parent.left
@ -354,8 +248,7 @@ Column {
StatusBaseText {
id: linkSite
visible: !unfurledLink.isGIFImage
text: linkData.site
text: result.site
font.pixelSize: 12
font.weight: Font.Thin
color: Theme.palette.baseColor1
@ -365,6 +258,20 @@ Column {
anchors.bottomMargin: Style.current.halfPadding
}
MouseArea {
anchors.top: linkImage.visible ? linkImage.top : linkTitle.top
anchors.left: linkImage.visible ? linkImage.left : linkTitle.left
anchors.right: linkImage.visible ? linkImage.right : linkTitle.right
anchors.bottom: linkSite.bottom
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!!result.callback) {
return result.callback()
}
Global.openLink(link)
}
}
Component.onCompleted: d.unfurledLinksCount++
Component.onDestruction: d.unfurledLinksCount--
}
@ -389,10 +296,7 @@ Column {
icon.width: 20
icon.height: 20
icon.name: "close-circle"
onClicked: {
enableLinkRoot.height = 0
enableLinkRoot.visible = false
}
onClicked: linksModel.remove(index)
}
Image {
@ -407,8 +311,8 @@ Column {
StatusBaseText {
id: enableText
text: d.isImageLink ? qsTr("Enable automatic image unfurling") :
qsTr("Enable link previews in chat?")
text: isImage ? qsTr("Enable automatic image unfurling") :
qsTr("Enable link previews in chat?")
horizontalAlignment: Text.AlignHCenter
width: parent.width
wrapMode: Text.WordWrap
@ -474,9 +378,7 @@ Column {
text: qsTr("Don't ask me again")
}
}
onClicked: {
RootStore.setNeverAskAboutUnfurlingAgain(true);
}
onClicked: RootStore.setNeverAskAboutUnfurlingAgain(true)
Component.onCompleted: {
background.radius = Style.current.padding;
}

View File

@ -207,11 +207,7 @@ Loader {
id: d
readonly property int chatButtonSize: 32
readonly property bool isSingleImage: linkUrlsModel.count === 1 && linkUrlsModel.get(0).isImage
&& `<p>${linkUrlsModel.get(0).link}</p>` === root.messageText
property int unfurledLinksCount: 0
property bool hideMessage: false
property string activeMessage
readonly property bool isMessageActive: d.activeMessage === root.messageId
@ -243,19 +239,7 @@ Loader {
}
}
onLinkUrlsChanged: {
linkUrlsModel.clear()
if (!root.linkUrls) {
return
}
root.linkUrls.split(" ").forEach(link => {
linkUrlsModel.append({link, isImage: Utils.hasImageExtension(link)})
})
}
ListModel {
id: linkUrlsModel
}
Connections {
enabled: d.isMessageActive
@ -475,7 +459,7 @@ Loader {
root.editModeOn ||
!root.rootStore.mainModuleInst.activeSection.joined
hideMessage: d.isSingleImage && d.unfurledLinksCount === 1
hideMessage: d.hideMessage
overrideBackground: root.placeholderMessage
profileClickable: !root.isDiscordMessage
@ -694,10 +678,11 @@ Loader {
}
}
hasLinks: linkUrlsModel.count
hasLinks: !!root.linkUrls
linksComponent: Component {
LinksMessageView {
linksModel: linkUrlsModel
id: linksMessageView
links: root.linkUrls
container: root
messageStore: root.messageStore
store: root.rootStore
@ -705,9 +690,11 @@ Loader {
onImageClicked: {
root.imageClicked(image);
}
Component.onCompleted: d.unfurledLinksCount = Qt.binding(() => unfurledLinksCount)
Component.onDestruction: d.unfurledLinksCount = 0
onLinksLoaded: {
// If there is only one image and no links, hide the message
// Handled in linksLoaded signal to evaulate it only once
d.hideMessage = linksMessageView.unfurledImagesCount === 1 && linksMessageView.unfurledLinksCount === 0
}
}
}