1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-08-08 16:55:39 +02:00
jami-client-qt/src/app/commoncomponents/DataTransferMessageDelegate.qml
Andreas Traczyk 99d415b1fe chat view: don't attempt to redefine a Loader's final property
We can't define a property called `status` for a Loader as it exists already. At best, the app will crash as it should be unable to create the chat view. At worst, this will introduce undefined behavior by confounding transfer/loader status assignments and comparisons.

Gitlab: #1537 (crash)
Change-Id: I66fb6da25cae695f7f1f520200f6eed8a2c93d03
2024-01-24 07:53:08 -05:00

449 lines
20 KiB
QML

/*
* Copyright (C) 2021-2024 Savoir-faire Linux Inc.
* Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import net.jami.Models 1.1
import net.jami.Constants 1.1
import net.jami.Adapters 1.1
Loader {
id: root
property var mediaInfo
property bool showTime
property bool showDay
property int timestamp: Timestamp
property string formattedTime: MessagesAdapter.getFormattedTime(root.timestamp)
property string formattedDay: MessagesAdapter.getFormattedDay(root.timestamp)
property int seq: MsgSeq.single
property string author: Author
property string body: Body
property var transferStatus: Status
width: ListView.view ? ListView.view.width : 0
sourceComponent: {
if (root.transferStatus === Interaction.Status.TRANSFER_FINISHED) {
mediaInfo = MessagesAdapter.getMediaInfo(root.body)
if (Object.keys(mediaInfo).length !== 0 && WITH_WEBENGINE)
return localMediaMsgComp
}
return dataTransferMsgComp
}
opacity: 0
Behavior on opacity { NumberAnimation { duration: 100 } }
onLoaded: opacity = 1
Component {
id: dataTransferMsgComp
SBSMessageBase {
id: dataTransferItem
transferId: Id
property var transferStats: MessagesAdapter.getTransferStats(transferId, root.transferStatus)
property bool canOpen: root.transferStatus === Interaction.Status.TRANSFER_FINISHED || isOutgoing
property real maxMsgWidth: root.width - senderMargin -
2 * hPadding - avatarBlockWidth
- buttonsLoader.width - 24 - 6 - 24
isOutgoing: Author === CurrentAccount.uri
showTime: root.showTime
seq: root.seq
author: Author
location: Body
transferName: TransferName
readers: Readers
timestamp: root.timestamp
formattedTime: root.formattedTime
formattedDay: root.formattedTime
extraHeight: progressBar.visible ? 18 : 0
innerContent.children: [
RowLayout {
id: transferItem
spacing: 6
anchors.right: isOutgoing ? parent.right : undefined
HoverHandler {
target: parent
enabled: canOpen
onHoveredChanged: {
if (enabled && hovered) {
dataTransferItem.hoveredLink = UtilsAdapter.urlFromLocalPath(location)
} else {
dataTransferItem.hoveredLink = ""
}
}
cursorShape: enabled ?
Qt.PointingHandCursor :
Qt.ArrowCursor
}
Loader {
id: buttonsLoader
objectName: "buttonsLoader"
property string iconSource
Layout.margins: 8
sourceComponent: {
switch (root.transferStatus) {
case Interaction.Status.TRANSFER_CREATED:
case Interaction.Status.TRANSFER_FINISHED:
iconSource = JamiResources.link_black_24dp_svg
return terminatedComp
case Interaction.Status.TRANSFER_CANCELED:
case Interaction.Status.TRANSFER_ERROR:
case Interaction.Status.TRANSFER_UNJOINABLE_PEER:
case Interaction.Status.TRANSFER_TIMEOUT_EXPIRED:
case Interaction.Status.TRANSFER_AWAITING_HOST:
iconSource = JamiResources.download_black_24dp_svg
return optionsComp
case Interaction.Status.TRANSFER_ONGOING:
iconSource = JamiResources.close_black_24dp_svg
return optionsComp
default:
iconSource = JamiResources.error_outline_black_24dp_svg
return terminatedComp
}
}
Component {
id: terminatedComp
Control {
width: 50
height: 50
padding: 13
background: Rectangle {
color: JamiTheme.blackColor
opacity: 0.15
radius: msgRadius
}
contentItem: ResponsiveImage {
source: buttonsLoader.iconSource
color: UtilsAdapter.luma(bubble.color) ? JamiTheme.fileIconLightColor : JamiTheme.fileIconDarkColor
}
}
}
Component {
id: optionsComp
PushButton {
source: buttonsLoader.iconSource
normalColor: JamiTheme.chatviewBgColor
imageColor: JamiTheme.chatviewButtonColor
onClicked: {
if (root.transferStatus === Interaction.Status.TRANSFER_ONGOING) {
return MessagesAdapter.cancelFile(transferId)
} else {
return MessagesAdapter.acceptFile(transferId)
}
}
}
}
}
Column {
Layout.rightMargin: 24
spacing: 4
TextEdit {
width: Math.min(implicitWidth, maxMsgWidth)
topPadding: 10
text: CurrentConversation.isSwarm ?
transferName :
location
wrapMode: Label.WrapAtWordBoundaryOrAnywhere
font.pointSize: 11
renderType: Text.NativeRendering
readOnly: true
color: UtilsAdapter.luma(bubble.color)
? JamiTheme.chatviewTextColorLight
: JamiTheme.chatviewTextColorDark
MouseArea {
anchors.fill: parent
cursorShape: canOpen ?
Qt.PointingHandCursor :
Qt.ArrowCursor
onClicked: function (mouse) {
if (canOpen) {
dataTransferItem.hoveredLink = UtilsAdapter.urlFromLocalPath(location)
Qt.openUrlExternally(new Url(dataTransferItem.hoveredLink))
} else {
dataTransferItem.hoveredLink = ""
}
}
}
}
Label {
id: transferInfo
width: Math.min(implicitWidth, maxMsgWidth)
bottomPadding: 10
text: {
var res = ""
if (transferStats.totalSize !== undefined) {
if (transferStats.progress !== 0 &&
transferStats.progress !== transferStats.totalSize) {
res += UtilsAdapter.humanFileSize(transferStats.progress) + " / "
}
var totalSize = transferStats.totalSize !== 0 ? transferStats.totalSize : TotalSize
res += UtilsAdapter.humanFileSize(totalSize)
}
return res
}
wrapMode: Label.WrapAtWordBoundaryOrAnywhere
font.pointSize: 10
renderType: Text.NativeRendering
color: UtilsAdapter.luma(bubble.color)
? JamiTheme.chatviewTextColorLight
: JamiTheme.chatviewTextColorDark
}
}
}
,ProgressBar {
id: progressBar
visible: root.transferStatus === Interaction.Status.TRANSFER_ONGOING
height: visible * implicitHeight
value: transferStats.progress / transferStats.totalSize
width: transferItem.width
anchors.right: isOutgoing ? parent.right : undefined
}
]
}
}
Component {
id: localMediaMsgComp
SBSMessageBase {
id: localMediaMsgItem
isOutgoing: Author === CurrentAccount.uri
transferId: Id
property var transferStats: MessagesAdapter.getTransferStats(transferId, root.transferStatus)
showTime: root.showTime
seq: root.seq
author: Author
location: Body
transferName: TransferName
readers: Readers
formattedTime: MessagesAdapter.getFormattedTime(root.timestamp)
formattedDay: MessagesAdapter.getFormattedDay(root.timestamp)
property real contentWidth
Component.onCompleted: {
if (transferStats.totalSize !== undefined) {
var totalSize = transferStats.totalSize !== 0 ? transferStats.totalSize : TotalSize
var txt = UtilsAdapter.humanFileSize(totalSize)
}
bubble.timestampItem.timeLabel.text += " - " + txt
bubble.color = "transparent"
if (mediaInfo.isImage)
bubble.z = 1
else
timeUnderBubble = true
}
onContentWidthChanged: {
if (bubble.timestampItem.timeLabel.width > contentWidth)
timeUnderBubble = true
else {
bubble.timestampItem.timeColor = JamiTheme.whiteColor
bubble.timestampItem.timeLabel.opacity = 1
}
}
innerContent.children: [
Loader {
id: localMediaCompLoader
anchors.right: isOutgoing ? parent.right : undefined
asynchronous: true
width: sourceComponent.width
height: sourceComponent.height
sourceComponent: {
if (mediaInfo.isImage)
return imageComp
if (mediaInfo.isAnimatedImage)
return animatedImageComp
return avComp
}
Component {
id: avComp
Loader {
Component.onCompleted: {
var qml = WITH_WEBENGINE ?
"qrc:/webengine/MediaPreviewBase.qml" :
"qrc:/nowebengine/MediaPreviewBase.qml"
setSource( qml, { isVideo: mediaInfo.isVideo, html:mediaInfo.html } )
}
}
}
Component {
id: animatedImageComp
AnimatedImage {
id: animatedImg
anchors.right: isOutgoing ? parent.right : undefined
property real minSize: 192
property real maxSize: 256
cache: false
fillMode: Image.PreserveAspectCrop
mipmap: true
antialiasing: true
autoTransform: true
asynchronous: true
source: UtilsAdapter.urlFromLocalPath(Body)
property real aspectRatio: implicitWidth / implicitHeight
property real adjustedWidth: Math.min(maxSize,
Math.max(minSize,
innerContent.width - senderMargin))
width: adjustedWidth
height: Math.ceil(adjustedWidth / aspectRatio)
Rectangle {
color: JamiTheme.previewImageBackgroundColor
z: -1
anchors.fill: parent
}
layer.enabled: true
layer.effect: OpacityMask {
maskSource: MessageBubble {
out: isOutgoing
type: seq
width: animatedImg.width
height: animatedImg.height
radius: msgRadius
}
}
onWidthChanged: {
localMediaMsgItem.contentWidth = width
}
Component.onCompleted: localMediaMsgItem.bubble.imgSource = source
LinearGradient {
id: gradient
anchors.fill: parent
start: Qt.point(0, height / 3)
gradient: Gradient {
GradientStop {
position: 0.0
color: JamiTheme.transparentColor
}
GradientStop {
position: 1.0
color: JamiTheme.darkGreyColorOpacityFade
}
}
}
}
}
Component {
id: imageComp
Image {
id: img
anchors.right: isOutgoing ? parent.right : undefined
cache: true
fillMode: Image.PreserveAspectFit
mipmap: true
antialiasing: true
autoTransform: true
asynchronous: true
source: Body !== undefined ? UtilsAdapter.urlFromLocalPath(Body) : ''
Component.onCompleted: localMediaMsgItem.bubble.imgSource = source
// The sourceSize represents the maximum source dimensions.
// This should not be a dynamic binding, as property changes
// (resizing the chat view) here will trigger a reload of the image.
sourceSize: Qt.size(256, 256)
// Now we setup bindings for the destination image component size.
// This based on the width available (width of the chat view), and
// a restriction on the height.
readonly property real aspectRatio: paintedWidth / paintedHeight
readonly property real idealWidth: innerContent.width - senderMargin
onStatusChanged: {
if (img.status == Image.Ready && aspectRatio) {
height = Qt.binding(() => JamiQmlUtils.clamp(idealWidth / aspectRatio, 64, 256))
width = Qt.binding(() => height * aspectRatio)
}
}
onWidthChanged: {
localMediaMsgItem.contentWidth = width
}
Rectangle {
color: JamiTheme.previewImageBackgroundColor
z: -1
anchors.fill: parent
}
layer.enabled: true
layer.effect: OpacityMask {
maskSource: MessageBubble {
out: isOutgoing
type: seq
width: img.width
height: img.height
radius: msgRadius
}
}
LinearGradient {
id: gradient
anchors.fill: parent
start: Qt.point(0, height / 3)
gradient: Gradient {
GradientStop {
position: 0.0
color: JamiTheme.transparentColor
}
GradientStop {
position: 1.0
color: JamiTheme.darkGreyColorOpacityFade
}
}
}
}
}
}
]
}
}
}