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:
parent
a93320f684
commit
f9f860a215
|
@ -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
|
||||
|
|
|
@ -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.} =
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) =
|
||||
|
|
|
@ -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)
|
|
@ -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 =
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue