Replace link previews with new unfurled data from the message (#11603)

This commit is contained in:
Igor Sirotin 2023-07-22 02:08:44 +03:00 committed by GitHub
parent b75d8630ca
commit cc5f057b3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 116 additions and 229 deletions

View File

@ -117,6 +117,7 @@ proc createMessageItemFromDto(self: Module, message: MessageDto, communityId: st
message.sticker.url,
message.sticker.pack,
message.links,
message.linkPreviews,
newTransactionParametersItem("","","","","","",-1,""),
message.mentionedUsersPks,
contactDetails.dto.trustStatus,

View File

@ -2,7 +2,7 @@ import NimQml
import ./io_interface
import ./gif_column_model
import ./preserved_properties
import ./link_preview_model as link_preview_model
import ../../../../../../app/modules/shared_models/link_preview_model as link_preview_model
import ../../../../../../app_service/service/gif/dto
QtObject:

View File

@ -116,6 +116,7 @@ proc createFetchMoreMessagesItem(self: Module): Item =
sticker = "",
stickerPack = -1,
links = @[],
linkPreviews = @[],
transactionParameters = newTransactionParametersItem("","","","","","",-1,""),
mentionedUsersPks = @[],
senderTrustStatus = TrustStatus.Unknown,
@ -175,6 +176,7 @@ proc createChatIdentifierItem(self: Module): Item =
sticker = "",
stickerPack = -1,
links = @[],
linkPreviews = @[],
transactionParameters = newTransactionParametersItem("","","","","","",-1,""),
mentionedUsersPks = @[],
senderTrustStatus = TrustStatus.Unknown,
@ -291,6 +293,7 @@ method newMessagesLoaded*(self: Module, messages: seq[MessageDto], reactions: se
sticker = message.sticker.url,
message.sticker.pack,
message.links,
message.linkPreviews,
newTransactionParametersItem(message.transactionParameters.id,
message.transactionParameters.fromAddress,
message.transactionParameters.address,
@ -425,6 +428,7 @@ method messagesAdded*(self: Module, messages: seq[MessageDto]) =
sticker = message.sticker.url,
message.sticker.pack,
message.links,
message.linkPreviews,
newTransactionParametersItem(message.transactionParameters.id,
message.transactionParameters.fromAddress,
message.transactionParameters.address,

View File

@ -198,6 +198,7 @@ proc buildPinnedMessageItem(self: Module, message: MessageDto, actionInitiatedBy
message.sticker.url,
message.sticker.pack,
message.links,
message.linkPreviews,
newTransactionParametersItem(message.transactionParameters.id,
message.transactionParameters.fromAddress,
message.transactionParameters.address,

View File

@ -458,7 +458,7 @@ method getModuleAsVariant*(self: Module): QVariant =
method getChatContentModule*(self: Module, chatId: string): QVariant =
if(not self.chatContentModules.contains(chatId)):
error "unexisting chat key: ", chatId, methodName="getChatContentModule"
error "unexisting chat key", chatId, methodName="getChatContentModule"
return
return self.chatContentModules[chatId].getModuleAsVariant()

View File

@ -1,5 +1,5 @@
import strformat
import ../../../../../../app_service/service/message/dto/link_preview
import ../../../app_service/service/message/dto/link_preview
type
Item* = ref object

View File

@ -1,6 +1,6 @@
import NimQml, strformat, tables
import ./link_preview_item
import ../../../../../../app_service/service/message/dto/link_preview
import ../../../app_service/service/message/dto/link_preview
type
ModelRole {.pure.} = enum
@ -14,6 +14,19 @@ type
ThumbnailUrl
ThumbnailDataUri
#
# TODO:
#
# Consider adding a `LinkType` property, which would probably follow the way of unfurling:
# - basic (maybe divide OpenGraph/oEmbed/...)
# - image (contantType == "image/.*")
# - status link (hostname == "status.app")
#
# In future, we can also unfurl other types, like PDF/audio/etc !
#
# This is needed on receiver side to know how to render the preview
# and what to do on click.
QtObject:
type
Model* = ref object of QAbstractListModel
@ -28,9 +41,14 @@ QtObject:
proc setup(self: Model) =
self.QAbstractListModel.setup
proc newLinkPreviewModel*(): Model =
proc newLinkPreviewModel*(linkPreviews: seq[LinkPreview] = @[]): Model =
new(result, delete)
result.setup
for linkPreview in linkPreviews:
var item = Item()
item.unfurled = true
item.linkPreview = linkPreview
result.items.add(item)
proc newModel*(): Model =
new(result, delete)

View File

@ -2,6 +2,8 @@ import json, strformat, strutils
import ../../../app_service/common/types
import ../../../app_service/service/contacts/dto/contact_details
import ../../../app_service/service/message/dto/message
import ../../../app_service/service/message/dto/link_preview
import ./link_preview_model as link_preview_model
export types.ContentType
import message_reaction_model, message_reaction_item, message_transaction_parameters_item
@ -41,6 +43,7 @@ type
editMode: bool
isEdited: bool
links: seq[string]
linkPreviewModel: link_preview_model.Model
transactionParameters: TransactionParametersItem
mentionedUsersPks: seq[string]
senderTrustStatus: TrustStatus
@ -87,6 +90,7 @@ proc initItem*(
sticker: string,
stickerPack: int,
links: seq[string],
linkPreviews: seq[LinkPreview],
transactionParameters: TransactionParametersItem,
mentionedUsersPks: seq[string],
senderTrustStatus: TrustStatus,
@ -136,6 +140,7 @@ proc initItem*(
result.editMode = false
result.isEdited = false
result.links = links
result.linkPreviewModel = newLinkPreviewModel(linkPreviews)
result.transactionParameters = transactionParameters
result.mentionedUsersPks = mentionedUsersPks
result.gapFrom = 0
@ -212,6 +217,7 @@ proc initNewMessagesMarkerItem*(clock, timestamp: int64): Item =
sticker = "",
stickerPack = -1,
links = @[],
linkPreviews = @[],
transactionParameters = newTransactionParametersItem("","","","","","",-1,""),
mentionedUsersPks = @[],
senderTrustStatus = TrustStatus.Unknown,
@ -443,6 +449,9 @@ proc links*(self: Item): seq[string] {.inline.} =
proc `links=`*(self: Item, links: seq[string]) {.inline.} =
self.links = links
proc linkPreviewModel*(self: Item): link_preview_model.Model {.inline.} =
return self.linkPreviewModel
proc mentionedUsersPks*(self: Item): seq[string] {.inline.} =
self.mentionedUsersPks

View File

@ -2,7 +2,7 @@ import NimQml, Tables, json, sets, algorithm, sequtils, strutils, strformat, sug
import message_item, message_reaction_item, message_transaction_parameters_item
import ../../../app_service/service/message/dto/message# as message_dto
import ../../../app_service/service/message/dto/message
import ../../../app_service/service/contacts/dto/contact_details
type
@ -43,6 +43,7 @@ type
EditMode
IsEdited
Links
LinkPreviewModel
TransactionParameters
MentionedUsersPks
SenderTrustStatus
@ -147,6 +148,7 @@ QtObject:
ModelRole.EditMode.int: "editMode",
ModelRole.IsEdited.int: "isEdited",
ModelRole.Links.int: "links",
ModelRole.LinkPreviewModel.int: "linkPreviewModel",
ModelRole.TransactionParameters.int: "transactionParameters",
ModelRole.MentionedUsersPks.int: "mentionedUsersPks",
ModelRole.SenderTrustStatus.int: "senderTrustStatus",
@ -292,6 +294,8 @@ QtObject:
result = newQVariant(item.isEdited)
of ModelRole.Links:
result = newQVariant(item.links.join(" "))
of ModelRole.LinkPreviewModel:
result = newQVariant(item.linkPreviewModel)
of ModelRole.TransactionParameters:
result = newQVariant($(%*{
"id": item.transactionParameters.id,

View File

@ -2,6 +2,7 @@
import json, strutils
import ../../../common/types
import link_preview
include ../../../common/json_utils
@ -112,6 +113,7 @@ type MessageDto* = object
messageType*: int
contactRequestState*: int
links*: seq[string]
linkPreviews*: seq[LinkPreview]
editedAt*: int
deleted*: bool
deletedForMe*: bool
@ -258,6 +260,11 @@ proc toMessageDto*(jsonObj: JsonNode): MessageDto =
for link in linksArr:
result.links.add(link.getStr)
var linkPreviewsArr: JsonNode
if jsonObj.getProp("linkPreviews", linkPreviewsArr):
for element in linkPreviewsArr.getElems():
result.linkPreviews.add(element.toLinkPreview())
var parsedTextArr: JsonNode
if(jsonObj.getProp("parsedText", parsedTextArr) and parsedTextArr.kind == JArray):
for pTextObj in parsedTextArr:

View File

@ -770,7 +770,11 @@ QtObject:
info "expected response is not a json object", methodName="onAsyncGetLinkPreviewData"
return
self.events.emit(SIGNAL_MESSAGE_LINK_PREVIEW_DATA_LOADED, LinkPreviewDataArgs(response: responseObj["previewData"], uuid: responseObj["uuid"].getStr()))
let args = LinkPreviewDataArgs(
response: responseObj["previewData"],
uuid: responseObj["uuid"].getStr()
)
self.events.emit(SIGNAL_MESSAGE_LINK_PREVIEW_DATA_LOADED, args)
proc asyncGetLinkPreviewData*(self: Service, links: string, uuid: string, whiteListedSites: string, whiteListedImgExtensions: string, unfurlImages: bool): string =
let arg = AsyncGetLinkPreviewDataTaskArg(

View File

@ -35,6 +35,7 @@ proc createTestMessageItem(id: string, clock: int64): Item =
sticker = "",
stickerPack = -1,
links = @[],
linkPreviews = @[],
transactionParameters = newTransactionParametersItem("","","","","","",-1,""),
mentionedUsersPks = @[],
senderTrustStatus = TrustStatus.Unknown,

View File

@ -53,7 +53,6 @@ Control {
property string resendError: ""
property double timestamp: 0
property var reactionsModel: []
property bool hasLinks
property bool showHeader: true
property bool isActiveMessage: false
@ -351,7 +350,8 @@ Control {
}
Loader {
id: linksLoader
active: !root.editMode && root.hasLinks
Layout.fillWidth: true
active: !root.editMode && root.linkPreviewModel.count > 0
visible: active
}
Loader {

View File

@ -297,6 +297,7 @@ Item {
onEditModeOnChanged: root.editModeChanged(editModeOn)
isEdited: model.isEdited
linkUrls: model.links
linkPreviewModel: model.linkPreviewModel
messageAttachments: model.messageAttachments
transactionParams: model.transactionParameters
hasMention: model.mentioned

View File

@ -170,8 +170,11 @@ Item {
}
function onOpenLink(link: string) {
console.warn("opening external url without asking user")
// Qt sometimes inserts random HTML tags; and this will break on invalid URL inside QDesktopServices::openUrl(link)
link = appMain.rootStore.plainText(link)
if (appMain.rootStore.showBrowserSelector) {
popups.openChooseBrowserPopup(link)
} else {

View File

@ -13,122 +13,79 @@ import shared.panels 1.0
import shared.stores 1.0
import shared.controls.chat 1.0
Column {
ColumnLayout {
id: root
property var store
property var messageStore
property var linkPreviewModel
//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, var mouse, var imageSource)
signal linksLoaded()
spacing: 4
Repeater {
id: linksRepeater
model: linksModel
model: root.linkPreviewModel
delegate: Loader {
id: linkMessageLoader
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
// properties from the model
required property bool unfurled
required property string title
required property string description
required property string hostname
required property int thumbnailWidth
required property int thumbnailHeight
required property string thumbnailUrl
required property string thumbnailDataUri
asynchronous: true
sourceComponent: unfurledLinkComponent
StateGroup {
//Using StateGroup as a warkardound for https://bugreports.qt.io/browse/QTBUG-47796
id: linkPreviewLoaderState
states:[
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
when: !linkMessageLoader.isImage && !linkMessageLoader.isStatusDeepLink
PropertyChanges { target: linkMessageLoader; sourceComponent: unfurledLinkComponent }
},
State {
name: "statusInvitation"
when: unfurl && isStatusDeepLink
PropertyChanges { target: linkMessageLoader; sourceComponent: invitationBubble }
}
// NOTE: New unfurling not yet suppport images and status links.
// Uncomment code below when implemented:
// - https://github.com/status-im/status-go/issues/3761
// - https://github.com/status-im/status-go/issues/3762
// State {
// name: "loadImage"
// when: linkMessageLoader.isImage
// PropertyChanges { target: linkMessageLoader; sourceComponent: unfurledImageComponent }
// },
// State {
// name: "statusInvitation"
// when: linkMessageLoader.isStatusDeepLink
// PropertyChanges { target: linkMessageLoader; sourceComponent: invitationBubble }
// }
]
}
}
}
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 }
}
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) }
}
}
ListModel {
id: linksModel
property var rawData
onRawDataChanged: {
linksModel.clear()
rawData.links.forEach((link) => {
linksModel.append(link)
})
root.linksLoaded()
}
}
Component {
id: unfurledImageComponent
MessageBorder {
width: linkImage.width
height: linkImage.height
implicitWidth: linkImage.width
implicitHeight: linkImage.height
isCurrentUser: root.isCurrentUser
StatusChatImageLoader {
id: linkImage
readonly property bool globalAnimationEnabled: root.messageStore.playAnimation
property bool localAnimationEnabled: true
objectName: "LinksMessageView_unfurledImageComponent_linkImage"
anchors.centerIn: parent
source: result.thumbnailUrl
@ -176,22 +133,24 @@ Column {
onTriggered: { linkImage.localAnimationEnabled = false }
}
}
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: invitationData ? invitationData.communityId : ""
anchors.left: parent.left
visible: !!invitationData
loading: invitationData.fetching
onInvitationDataChanged: {
if (!invitationData)
linksModel.remove(index)
}
Connections {
enabled: !!invitationData && invitationData.fetching
@ -206,10 +165,11 @@ Column {
Component {
id: unfurledLinkComponent
MessageBorder {
id: unfurledLink
width: linkImage.visible ? linkImage.width + 2 : 300
height: {
implicitWidth: linkImage.visible ? linkImage.width + 2 : 300
implicitHeight: {
if (linkImage.visible) {
return linkImage.height + (Style.current.smallPadding * 2) + (linkTitle.height + 2 + linkSite.height)
}
@ -220,26 +180,22 @@ Column {
StatusChatImageLoader {
id: linkImage
objectName: "LinksMessageView_unfurledLinkComponent_linkImage"
source: result.thumbnailUrl
visible: result.thumbnailUrl.length
readonly property int previewWidth: parseInt(result.width)
imageWidth: Math.min(300, previewWidth > 0 ? previewWidth : 300)
source: thumbnailUrl
visible: thumbnailUrl.length
imageWidth: Math.min(300, thumbnailWidth > 0 ? thumbnailWidth : 300)
isCurrentUser: root.isCurrentUser
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
isOnline: root.store.mainModuleInst.isOnline
asynchronous: true
onClicked: {
if (!!result.callback) {
return result.callback()
}
Global.openLink(result.address)
}
}
StatusBaseText {
id: linkTitle
text: result.title
text: title
font.pixelSize: 13
font.weight: Font.Medium
wrapMode: Text.Wrap
@ -253,7 +209,7 @@ Column {
StatusBaseText {
id: linkSite
text: result.site
text: hostname
font.pixelSize: 12
font.weight: Font.Thin
color: Theme.palette.baseColor1
@ -270,125 +226,9 @@ Column {
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--
}
}
Component {
id: enableLinkComponent
Rectangle {
id: enableLinkRoot
width: 300
height: childrenRect.height + Style.current.smallPadding
radius: 16
border.width: 1
border.color: Style.current.border
color: Style.current.background
StatusFlatRoundButton {
anchors.top: parent.top
anchors.topMargin: Style.current.smallPadding
anchors.right: parent.right
anchors.rightMargin: Style.current.smallPadding
icon.width: 20
icon.height: 20
icon.name: "close-circle"
onClicked: linksModel.remove(index)
}
Image {
id: unfurlingImage
source: Style.png("unfurling-image")
width: 132
height: 94
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Style.current.smallPadding
}
StatusBaseText {
id: enableText
text: isImage ? qsTr("Enable automatic image unfurling") :
qsTr("Enable link previews in chat?")
horizontalAlignment: Text.AlignHCenter
width: parent.width
wrapMode: Text.WordWrap
anchors.top: unfurlingImage.bottom
anchors.topMargin: Style.current.halfPadding
color: Theme.palette.directColor1
}
StatusBaseText {
id: infoText
text: qsTr("Once enabled, links posted in the chat may share your metadata with their owners")
horizontalAlignment: Text.AlignHCenter
width: parent.width
wrapMode: Text.WordWrap
anchors.top: enableText.bottom
font.pixelSize: 13
color: Theme.palette.baseColor1
}
Separator {
id: sep1
anchors.top: infoText.bottom
anchors.topMargin: Style.current.smallPadding
}
StatusFlatButton {
id: enableBtn
objectName: "LinksMessageView_enableBtn"
text: qsTr("Enable in Settings")
onClicked: {
Global.changeAppSectionBySectionType(Constants.appSection.profile, Constants.settingsSubsection.messaging);
}
width: parent.width
anchors.top: sep1.bottom
Component.onCompleted: {
background.radius = 0;
}
}
Separator {
id: sep2
anchors.top: enableBtn.bottom
anchors.topMargin: 0
}
Item {
width: parent.width
height: 44
anchors.top: sep2.bottom
clip: true
StatusFlatButton {
id: dontAskBtn
width: parent.width
height: (parent.height+Style.current.padding)
anchors.top: parent.top
anchors.topMargin: -Style.current.padding
contentItem: Item {
StatusBaseText {
anchors.centerIn: parent
anchors.verticalCenterOffset: Style.current.halfPadding
font: dontAskBtn.font
color: dontAskBtn.enabled ? dontAskBtn.textColor : dontAskBtn.disabledTextColor
text: qsTr("Don't ask me again")
}
}
onClicked: RootStore.setNeverAskAboutUnfurlingAgain(true)
Component.onCompleted: {
background.radius = Style.current.padding;
}
}
}
}
}
}

View File

@ -61,6 +61,7 @@ Loader {
property string messagePinnedBy: ""
property var reactionsModel: []
property string linkUrls: ""
property var linkPreviewModel
property string messageAttachments: ""
property var transactionParams
@ -741,23 +742,16 @@ Loader {
}
}
hasLinks: !!root.linkUrls
linksComponent: Component {
LinksMessageView {
id: linksMessageView
links: root.linkUrls
linkPreviewModel: root.linkPreviewModel
messageStore: root.messageStore
store: root.rootStore
isCurrentUser: root.amISender
onImageClicked: (image, mouse, imageSource) => {
d.onImageClicked(image, mouse, imageSource)
}
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
&& `<p>${root.linkUrls}</p>` === root.messageText
}
}
}