Warning, /plasma-mobile/spacebar/src/contents/ui/MessagesPage.qml is written in an unsupported language. File is not indexed.

0001 // SPDX-FileCopyrightText: 2020 Jonah Brüchert <jbb@kaidan.im>
0002 // SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
0003 // SPDX-FileCopyrightText: 2021 Michael Lang <criticaltemp@protonmail.com>
0004 //
0005 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 
0007 import QtCore
0008 import QtQuick
0009 import QtQuick.Layouts
0010 import QtQuick.Controls as Controls
0011 import Qt5Compat.GraphicalEffects
0012 import QtQuick.Dialogs
0013 import Qt.labs.platform
0014 
0015 import org.kde.kirigami as Kirigami
0016 import org.kde.kirigamiaddons.delegates as Delegates
0017 
0018 import org.kde.spacebar
0019 
0020 Kirigami.ScrollablePage {
0021     id: msgPage
0022 
0023     property MessageModel messageModel;
0024     property real pointSize: Kirigami.Theme.defaultFont.pointSize + SettingsManager.messageFontSize
0025     property var people: messageModel ? messageModel.people : []
0026     property string sendingNumber: messageModel ? messageModel.sendingNumber : ""
0027     property string attachmentsFolder: messageModel ? messageModel.attachmentsFolder : "";
0028     property ListModel files: ListModel {}
0029     property var tapbackKeys: ["♥️", "👍" , "👎", "😂", "‼️", "❓"]
0030     property real lastHeight: applicationWindow().height
0031 
0032     Connections {
0033         target: pageStack
0034         function onCurrentItemChanged () {
0035             if (!pageStack.currentItem.hasOwnProperty("messageModel")) {
0036                 messageModel.disableNotifications(Utils.phoneNumberList(""))
0037             }
0038         }
0039     }
0040 
0041     Component.onCompleted: {
0042         // 80 is the pixels taken up by other elements in the page header
0043         const width = Math.max(root.width - pageStack.currentItem.width, pageStack.currentItem.width) - 80
0044         const characters = Math.floor(width / 10, 0)
0045 
0046         if (people.length > 1) {
0047             for (let i = 0; i < people.length; i++) {
0048                 const name = people[i].name.split(" ")[0] || people[i].phoneNumber
0049                 if (i > 0) {
0050                     title += ", "
0051                     if (title.length + name.length + 5 > characters) {
0052                         title += i18n("and %1 more", people.length - i)
0053                         break
0054                     }
0055                 }
0056                 title += name
0057             }
0058         } else if (people.length === 1) {
0059             title = people[0].name || people[0].phoneNumber
0060         } else {
0061             title = i18n("New message")
0062         }
0063     }
0064 
0065     function getContrastYIQColor(hexcolor) {
0066         hexcolor = hexcolor.replace("#", "");
0067         const r = parseInt(hexcolor.substr(0, 2), 16);
0068         const g = parseInt(hexcolor.substr(2, 2), 16);
0069         const b = parseInt(hexcolor.substr(4, 2), 16);
0070         const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
0071 
0072         return (yiq >= 128) ? Qt.rgba(0, 0, 0, 0.9) : Qt.rgba(255, 255, 255, 0.9);
0073     }
0074 
0075     function filesTotalSize() {
0076         let size = 0
0077         for (let i = 0; i < files.count; i++) {
0078             size += (files.get(i).size || 0)
0079         }
0080 
0081         return size
0082     }
0083 
0084     function formatBytes(bytes, decimals = 1) {
0085         if (bytes === 0) return '';
0086 
0087         const k = 1024;
0088         const sizes = ['B', 'KiB', 'MiB', 'GiB'];
0089         const i = Math.floor(Math.log(bytes) / Math.log(k));
0090 
0091         return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
0092     }
0093 
0094     function filesToList() {
0095         const list = [];
0096         for (let i = 0; i < files.count; i++) {
0097             list.push(files.get(i).filePath || files.get(i).text);
0098         }
0099 
0100         return list;
0101     }
0102 
0103     function lookupSenderName(number) {
0104         if (people.length < 2) {
0105             return ""
0106         }
0107         const person = people.find(o => o.phoneNumber === number)
0108         return person.name || person.phoneNumber
0109     }
0110 
0111     onHeightChanged: {
0112         applicationWindow().controlsVisible = applicationWindow().contentItem.height > 200
0113         const toolbarHeight = applicationWindow().controlsVisible ? 0 : root.globalToolBar.preferredHeight
0114         if (listView.atYEnd) {
0115             listView.positionViewAtEnd()
0116         } else {
0117             listView.contentY = listView.contentY + lastHeight - applicationWindow().height - toolbarHeight
0118             lastHeight = applicationWindow().height + toolbarHeight
0119         }
0120     }
0121 
0122     actions: [
0123         Kirigami.Action {
0124             displayHint: Kirigami.DisplayHint.IconOnly
0125             visible: people.length === 1
0126             icon.name: "call-start"
0127             text: i18n("Call")
0128             onTriggered: Qt.openUrlExternally("tel:" + people[0].phoneNumber)
0129         },
0130         Kirigami.Action {
0131             displayHint: Kirigami.DisplayHint.IconOnly
0132             icon.name: "view-list-details"
0133             text: i18n("Details")
0134             onTriggered: pageStack.push("qrc:/ChatDetailPage.qml", { people: people })
0135         }
0136     ]
0137 
0138     header: ColumnLayout {
0139         Kirigami.InlineMessage {
0140             id: premiumWarning
0141             Layout.fillWidth: true
0142             Layout.margins: Kirigami.Units.largeSpacing
0143             Layout.bottomMargin: 0
0144             type: Kirigami.MessageType.Warning
0145             text: i18n("Texting this premium SMS number might cause you to be charged money")
0146             visible: messageModel && Utils.isPremiumNumber(messageModel.phoneNumberList)
0147         }
0148 
0149         Kirigami.InlineMessage {
0150             id: maxAttachmentsError
0151             Layout.fillWidth: true
0152             Layout.leftMargin: Kirigami.Units.smallSpacing
0153             Layout.rightMargin: Kirigami.Units.smallSpacing
0154             Layout.topMargin: Kirigami.Units.smallSpacing
0155             type: Kirigami.MessageType.Error
0156             text: i18n("Max attachment limit exceeded")
0157             visible: files.count > SettingsManager.maxAttachments
0158         }
0159 
0160         Kirigami.InlineMessage {
0161             id: mmscUrlMissingError
0162             Layout.fillWidth: true
0163             Layout.leftMargin: Kirigami.Units.smallSpacing
0164             Layout.rightMargin: Kirigami.Units.smallSpacing
0165             Layout.topMargin: Kirigami.Units.smallSpacing
0166             type: Kirigami.MessageType.Error
0167             text: i18n("No MMSC configured")
0168             visible: SettingsManager.mmsc.length === 0 && (files.count > 0 || people.length > 1)
0169         }
0170 
0171         Kirigami.InlineMessage {
0172             id: messageExpiredError
0173             Layout.fillWidth: true
0174             Layout.leftMargin: Kirigami.Units.smallSpacing
0175             Layout.rightMargin: Kirigami.Units.smallSpacing
0176             Layout.topMargin: Kirigami.Units.smallSpacing
0177             type: Kirigami.MessageType.Information
0178             text: i18n("Message has expired and will be deleted")
0179             visible: false
0180         }
0181 
0182         Kirigami.InlineMessage {
0183             id: messageGroupAsIndividual
0184             Layout.fillWidth: true
0185             Layout.leftMargin: Kirigami.Units.smallSpacing
0186             Layout.rightMargin: Kirigami.Units.smallSpacing
0187             Layout.topMargin: Kirigami.Units.smallSpacing
0188             type: Kirigami.MessageType.Information
0189             text: i18n("Message will be sent as individual messages")
0190             visible: people.length > 1 && !SettingsManager.groupConversation
0191         }
0192     }
0193 
0194     ListView {
0195         id: listView
0196         model: messageModel
0197         spacing: Kirigami.Units.largeSpacing
0198         currentIndex: -1
0199 
0200         // configure chat bubble colors
0201         Kirigami.Theme.inherit: false
0202         Kirigami.Theme.colorSet: Kirigami.Theme.Button
0203         property string incomingColor: SettingsManager.customMessageColors ? SettingsManager.incomingMessageColor : Kirigami.Theme.backgroundColor
0204         property string outgoingColor: SettingsManager.customMessageColors ? SettingsManager.outgoingMessageColor : Kirigami.Theme.highlightColor
0205 
0206         // adjust text for highlight color
0207         property string incomingTextColor: getContrastYIQColor(incomingColor)
0208         property string outgoingTextColor: getContrastYIQColor(outgoingColor)
0209 
0210         property bool async: true
0211         property int lastIndex: -1
0212         property int lastCount: 0
0213         property bool fetched: false
0214         property bool lastAtYEnd: true
0215         property int unreadCount: 0
0216         property int lastContentY
0217 
0218         function checkFetchMore() {
0219             forceActiveFocus()
0220             listView.lastAtYEnd = listView.atYEnd
0221 
0222             if (!fetched) {
0223                 let idx = indexAt(contentX, contentY)
0224                 if (idx === -1 && listView.atYBeginning) {
0225                     idx = 0
0226                 }
0227 
0228                 if (idx > -1 && idx < 15) {
0229                     if (idx === 0) {
0230                         // prevent moving to wrong position during fetch
0231                         listView.interactive = false
0232                     }
0233                     fetched = true
0234                     listView.lastCount = listView.count
0235                     listView.lastIndex = idx
0236                     messageModel.fetchAllMessages()
0237                 }
0238             }
0239         }
0240 
0241         function positionAtEnd() {
0242             listView.unreadCount = 0
0243             listView.async = false
0244             listView.positionViewAtEnd()
0245             listView.async = true
0246             listView.lastContentY = listView.contentY
0247         }
0248 
0249         Connections {
0250             target: messageModel
0251             function onMessagesFetched() {
0252                 listView.interactive = true
0253                 if (listView.lastIndex > -1) {
0254                     listView.positionViewAtIndex(listView.lastIndex + listView.count - listView.lastCount, ListView.Visible)
0255                     listView.lastIndex = -1
0256                 } else if (listView.lastAtYEnd) {
0257                     listView.positionAtEnd()
0258                 } else {
0259                     listView.unreadCount++
0260                 }
0261             }
0262         }
0263 
0264         WheelHandler {
0265             acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
0266             onWheel: event => listView.flick(0, event.angleDelta.y * 5)
0267         }
0268 
0269         onMovementStarted: checkFetchMore()
0270         onMovementEnded: checkFetchMore()
0271         onFlickStarted: checkFetchMore()
0272         onFlickEnded: checkFetchMore()
0273 
0274         add: Transition {
0275             NumberAnimation { properties: "x,y"; duration: Kirigami.Units.shortDuration }
0276         }
0277         remove: Transition {
0278             NumberAnimation { properties: "x,y"; duration: Kirigami.Units.shortDuration }
0279         }
0280         displaced: Transition {
0281             NumberAnimation { properties: "x,y"; duration: Kirigami.Units.shortDuration }
0282         }
0283 
0284         section.property: "date"
0285         section.delegate: Controls.Control {
0286             bottomPadding: Kirigami.Units.gridUnit * 1.5
0287             width: parent.width
0288             contentItem:
0289             RowLayout {
0290                 spacing: Kirigami.Units.largeSpacing
0291 
0292                 Rectangle {
0293                     color: Kirigami.Theme.backgroundColor
0294                     height: 1
0295                     Layout.fillWidth: true
0296                 }
0297 
0298                 Text {
0299                     text: Qt.formatDate(section, Qt.locale().dateFormat(Locale.LongFormat))
0300                     horizontalAlignment: Text.AlignHCenter
0301                     font: Kirigami.Theme.smallFont
0302                     color: Kirigami.Theme.disabledTextColor
0303                 }
0304 
0305                 Rectangle {
0306                     color: Kirigami.Theme.backgroundColor
0307                     height: 1
0308                     Layout.fillWidth: true
0309                 }
0310             }
0311         }
0312 
0313         delegate: Item {
0314             id: delegateParent
0315             width: listView.width
0316             height: rect.height + senderDisplay.implicitHeight
0317 
0318             Kirigami.ShadowedRectangle {
0319                 id: rect
0320                 anchors.margins: Kirigami.Units.largeSpacing
0321                 anchors.left: model.sentByMe ? undefined : parent.left
0322                 anchors.right: model.sentByMe ? parent.right : undefined
0323 
0324                 property int padding: Kirigami.Units.largeSpacing * 2
0325                 property var tapbacks: model.tapbacks ? JSON.parse(model.tapbacks) : ""
0326 
0327                 radius: Kirigami.Units.gridUnit
0328                 corners.bottomRightRadius: model.sentByMe ? 0 : -1
0329                 corners.topLeftRadius: model.sentByMe ? -1 : 0
0330                 shadow.size: Kirigami.Units.smallSpacing
0331                 shadow.color: !model.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
0332                 border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
0333                 border.width: 1
0334                 color: model.sentByMe ? listView.outgoingColor : listView.incomingColor
0335                 height: content.height + padding
0336                 width: content.width + padding * 1.5
0337 
0338                 MouseArea {
0339                     id: rectMouse
0340                     anchors.fill: parent
0341                     onClicked: if (content.attachments.length > 1 || modelText.truncated) {
0342                         pageStack.layers.push("qrc:/FullscreenPage.qml", {
0343                             recipients: msgPage.title,
0344                             text: model.text,
0345                             attachments: content.attachments,
0346                             folder: attachmentsFolder
0347                         } )
0348                     }
0349                     onPressAndHold: {
0350                         menu.index = model.index
0351                         menu.id = model.id
0352                         menu.text = model.text
0353                         menu.attachments = content.attachments
0354                         menu.smil = model.smil
0355                         menu.resend = model.sentByMe && model.deliveryState == MessageModel.Failed
0356                         menu.tapbacks = rect.tapbacks
0357                         menu.open()
0358                     }
0359                 }
0360 
0361                 property string sender: {
0362                     if (!model.sentByMe && model.fromNumber) {
0363                         return lookupSenderName(model.fromNumber)
0364                     }
0365                     return ""
0366                 }
0367 
0368                 Text {
0369                     id: senderDisplay
0370                     visible: rect.sender
0371                     anchors.left: parent.left
0372                     anchors.bottom: parent.top
0373                     leftPadding: Kirigami.Units.smallSpacing
0374                     text: rect.sender
0375                     font: Kirigami.Theme.smallFont
0376                     color: Kirigami.Theme.disabledTextColor
0377                 }
0378 
0379                 ColumnLayout {
0380                     id: content
0381                     spacing: Kirigami.Units.largeSpacing
0382                     anchors.centerIn: parent
0383 
0384                     property color textColor: model.sentByMe ? listView.outgoingTextColor : listView.incomingTextColor
0385                     property var attachments: model.attachments ? JSON.parse(model.attachments) : []
0386                     property bool clipped: false
0387 
0388                     Component.onCompleted: {
0389                         if (attachments.length === 1 && attachments[0].mimeType.indexOf("image/") >= 0) {
0390                             rect.color = "transparent"
0391                             rect.border.color = "transparent"
0392                             rect.padding = 0
0393                             if (listView.async) {
0394                                 // reserve space for async loaded image to ensure smooth scrolling
0395                                 content.height = msgPage.height / 2
0396                             }
0397                         }
0398                     }
0399 
0400                     // message contents
0401                     Controls.Label {
0402                         id: modelText
0403                         visible: !!model.text
0404                         Layout.alignment: model.text && model.text.length > 1 ? Qt.AlignTop : Qt.AlignHCenter
0405                         Layout.minimumWidth: Kirigami.Units.gridUnit / 5
0406                         Layout.maximumWidth: Math.round(delegateParent.width * 0.7)
0407                         maximumLineCount: 12
0408                         text: Utils.textToHtml(model.text)
0409                         wrapMode: Text.Wrap
0410                         textFormat: Text.StyledText
0411                         linkColor: model.sentByMe ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.linkColor
0412                         color: content.textColor
0413                         font.pointSize: pointSize
0414                         font.family: "Noto Sans, Noto Color Emoji"
0415                         onLinkActivated: Qt.openUrlExternally(link)
0416                     }
0417 
0418                     // download message contents
0419                     RowLayout {
0420                         visible: model.pendingDownload
0421                         Layout.maximumWidth: Math.round(delegateParent.width * 0.7)
0422 
0423                         MouseArea {
0424                             enabled: parent.visible && model.deliveryState != MessageModel.Pending
0425                             width: parent.width
0426                             height: parent.height
0427                             onClicked: {
0428                                 model.deliveryState = MessageModel.Pending
0429 
0430                                 // check if expired
0431                                 if (new Date(model.expires) < new Date()) {
0432                                     messageExpiredError.visible = true
0433                                     expiredNotify.start()
0434                                 } else {
0435                                     messageModel.downloadMessage(model.id, model.contentLocation, model.expires)
0436                                 }
0437                             }
0438                         }
0439 
0440                         Timer {
0441                             id: expiredNotify
0442                             interval: 5000
0443                             onTriggered: {
0444                                 listView.currentIndex = index
0445                                 messageModel.deleteMessage(model.id, index,[])
0446                                 messageExpiredError.visible = false
0447                             }
0448                         }
0449 
0450                         Controls.BusyIndicator {
0451                             scale: pointSize / Kirigami.Theme.defaultFont.pointSize
0452                             running: model.deliveryState == MessageModel.Pending
0453 
0454                             Kirigami.Icon {
0455                                 visible: !parent.running
0456                                 anchors.fill: parent
0457                                 source: model.deliveryState === MessageModel.Failed ? "state-error" : "folder-download-symbolic"
0458                                 color: content.textColor
0459                             }
0460                         }
0461 
0462                         Column {
0463                             Kirigami.Heading {
0464                                 text: model.subject || i18n("MMS message")
0465                                 wrapMode: Text.Wrap
0466                                 color: content.textColor
0467                                 font.pointSize: pointSize
0468                                 level: 3
0469                                 type: Kirigami.Heading.Type.Primary
0470                             }
0471                             Controls.Label {
0472                                 text: model.size ? i18n("Message size: %1", formatBytes(model.size)) : ""
0473                                 wrapMode: Text.Wrap
0474                                 color: content.textColor
0475                                 font.pointSize: pointSize
0476                             }
0477                             Controls.Label {
0478                                 text: i18n("Expires: %1", model.expiresDateTime)
0479                                 wrapMode: Text.Wrap
0480                                 color: content.textColor
0481                                 font.pointSize: pointSize
0482                             }
0483                         }
0484                     }
0485 
0486                     // message attachments
0487                     Repeater {
0488                         model: content.attachments
0489 
0490                         Column {
0491                             Layout.alignment: Qt.AlignHCenter
0492                             spacing: Kirigami.Units.smallSpacing
0493                             Layout.minimumWidth: Kirigami.Units.largeSpacing * 2
0494                             Layout.minimumHeight: Kirigami.Units.largeSpacing * 2
0495 
0496                             readonly property bool isImage: modelData.mimeType.indexOf("image/") >= 0
0497                             readonly property string filePath: "file://" + attachmentsFolder + "/" + modelData.fileName
0498 
0499                             RowLayout {
0500                                 visible: !isImage && !modelData.text
0501                                 Kirigami.Icon {
0502                                     scale: pointSize / Kirigami.Theme.defaultFont.pointSize
0503                                     source: modelData.iconName
0504                                 }
0505                                 Text {
0506                                     text: modelData.name
0507                                     color: content.textColor
0508                                     font.pointSize: pointSize
0509                                 }
0510                                 MouseArea {
0511                                     width: parent.width
0512                                     height: parent.height
0513                                     onDoubleClicked: Qt.openUrlExternally(filePath)
0514                                 }
0515                             }
0516 
0517                             Image {
0518                                 id: image
0519                                 source: isImage ? filePath : ""
0520                                 fillMode: Image.PreserveAspectFit
0521                                 sourceSize.width: Math.round(delegateParent.width * 0.7)
0522                                 height: Math.min(Math.max(msgPage.width / 2, msgPage.height) * 0.5, image.implicitHeight)
0523                                 asynchronous: listView.async
0524                                 onStatusChanged: if (status == Image.Ready) content.height = undefined
0525                                 cache: false
0526                                 MouseArea {
0527                                     anchors.fill: parent
0528                                     onClicked: {
0529                                         pageStack.layers.push("qrc:/PreviewPage.qml", {
0530                                             filePath: filePath,
0531                                             type: modelData.mimeType
0532                                         } )
0533                                     }
0534                                     onPressAndHold: (x, y) => rectMouse.pressAndHold(x, y)
0535                                 }
0536 
0537                                 // rounded corners on image
0538                                 layer.enabled: true
0539                                 layer.effect: OpacityMask {
0540                                     maskSource: Item {
0541                                         width: image.width
0542                                         height: image.height
0543                                         Rectangle {
0544                                             anchors.fill: parent
0545                                             radius: Kirigami.Units.gridUnit / 2
0546                                         }
0547                                     }
0548                                 }
0549 
0550                                 AnimatedImage {
0551                                     source: parent.source && modelData.mimeType === "image/gif" ? parent.source : ""
0552                                     anchors.fill: parent
0553                                     cache: false
0554                                 }
0555                             }
0556 
0557                             // text contents
0558                             Controls.Label {
0559                                 visible: !!modelData.text
0560                                 width: Math.min(delegateParent.width * 0.7, implicitWidth)
0561                                 text: Utils.textToHtml(modelData.text)
0562                                 maximumLineCount: 12
0563                                 wrapMode: Text.Wrap
0564                                 textFormat: Text.StyledText
0565                                 linkColor: modelData.sentByMe ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.linkColor
0566                                 color: content.textColor
0567                                 font.pointSize: pointSize
0568                                 font.family: "Noto Sans, Noto Color Emoji"
0569                                 Component.onCompleted: content.clipped = truncated
0570                                 onLinkActivated: Qt.openUrlExternally(link)
0571                             }
0572                         }
0573                     }
0574 
0575                     ColumnLayout {
0576                         visible: modelText.truncated || content.clipped
0577                         Rectangle {
0578                             width: content.implicitWidth
0579                             height: 1
0580                             color: Kirigami.Theme.disabledTextColor
0581                         }
0582 
0583                         RowLayout {
0584                             width: content.width
0585 
0586                             Kirigami.Icon {
0587                                 Layout.maximumWidth: pointSize * 2
0588                                 Layout.maximumHeight: pointSize * 2
0589                                 source: "view-fullscreen"
0590                                 color: content.textColor
0591                             }
0592 
0593                             Text {
0594                                 text: i18n("View all")
0595                                 color: content.textColor
0596                                 font.pointSize: pointSize
0597                                 font.capitalization: Font.AllUppercase
0598                             }
0599 
0600                             Item {
0601                                 Layout.fillWidth: true
0602                             }
0603 
0604                             Kirigami.Icon {
0605                                 Layout.maximumWidth: pointSize * 2
0606                                 Layout.maximumHeight: pointSize * 2
0607                                 source: "arrow-right"
0608                                 color: content.textColor
0609                             }
0610                         }
0611                     }
0612                 }
0613 
0614                 Controls.Label {
0615                     anchors.left: model.sentByMe ? undefined : parent.right
0616                     anchors.right: model.sentByMe ? parent.left : undefined
0617                     anchors.bottom: parent.bottom
0618                     padding: Kirigami.Units.smallSpacing
0619                     bottomPadding: 0
0620                     text: Qt.formatTime(model.time, Qt.DefaultLocaleShortDate)
0621                     font: Kirigami.Theme.smallFont
0622                     color: Kirigami.Theme.disabledTextColor
0623 
0624                     Kirigami.Icon {
0625                         anchors.right: parent.left
0626                         anchors.bottom: parent.bottom
0627                         implicitHeight: Math.round(Kirigami.Units.gridUnit * 0.7)
0628                         implicitWidth: implicitHeight
0629                         source: {
0630                             if (visible) {
0631                                 switch (model.deliveryState) {
0632                                     case MessageModel.Unknown:
0633                                         return "dontknow";
0634                                     case MessageModel.Pending:
0635                                         return "content-loading-symbolic";
0636                                     case MessageModel.Sent:
0637                                         return "answer-correct";
0638                                     case MessageModel.Failed:
0639                                         return "error"
0640                                 }
0641                             }
0642 
0643                             return undefined
0644                         }
0645 
0646                         visible: !!(model.sentByMe && (model.deliveryState != MessageModel.Sent || model.index == (listView.count - 1)))
0647                         color: model.deliveryState == MessageModel.Failed ? Kirigami.Theme.negativeTextColor : Kirigami.Theme.disabledTextColor
0648                     }
0649                 }
0650 
0651                 Rectangle {
0652                     visible: rect.tapbacks
0653                     anchors.left: model.sentByMe ? undefined : parent.left
0654                     anchors.right: model.sentByMe ? parent.right : undefined
0655                     anchors.bottom: parent.bottom
0656                     anchors.leftMargin: model.sentByMe ? undefined : parent.width - Kirigami.Units.largeSpacing * 2
0657                     anchors.rightMargin: model.sentByMe ? parent.width - Kirigami.Units.largeSpacing * 2 : undefined
0658                     anchors.bottomMargin: parent.height - Kirigami.Units.largeSpacing * 1.5
0659                     color: model.sentByMe ?  listView.incomingColor : listView.outgoingColor
0660                     height: tapback.height
0661                     width: tapback.width
0662                     radius: height / 2
0663                     border.width: 2
0664                     border.color: Kirigami.Theme.backgroundColor
0665 
0666                     Rectangle {
0667                         anchors.left: model.sentByMe ? undefined : parent.left
0668                         anchors.right: model.sentByMe ? parent.right : undefined
0669                         anchors.top: parent.top
0670                         anchors.leftMargin: model.sentByMe ? undefined : parent.width - height
0671                         anchors.rightMargin: model.sentByMe ? parent.width - height : undefined
0672                         anchors.topMargin: parent.height - height * 1.2
0673                         color: parent.color
0674                         width: Kirigami.Units.smallSpacing * 3
0675                         height: Kirigami.Units.smallSpacing * 3
0676                         radius: height / 2
0677 
0678                         Rectangle {
0679                             anchors.left: model.sentByMe ? undefined : parent.right
0680                             anchors.right: model.sentByMe ? parent.left : undefined
0681                             anchors.top: parent.top
0682                             anchors.leftMargin: model.sentByMe ? undefined : parent.width - height * 2
0683                             anchors.rightMargin: model.sentByMe ? parent.width - height * 2 : undefined
0684                             anchors.topMargin: parent.height - height / 2
0685                             color: parent.color
0686                             width: Kirigami.Units.smallSpacing * 1.5
0687                             height: Kirigami.Units.smallSpacing * 1.5
0688                             radius: height / 2
0689                         }
0690                     }
0691 
0692                     Row {
0693                         id: tapback
0694                         property color textColor: model.sentByMe ?  listView.incomingTextColor : listView.outgoingTextColor
0695                         padding: Kirigami.Units.largeSpacing
0696 
0697                         Repeater {
0698                             model: tapbackKeys.filter(key => rect.tapbacks && rect.tapbacks[key] && rect.tapbacks[key].length > 0)
0699 
0700                             Text {
0701                                 text: modelData
0702                                 font.family: "Noto Sans, Noto Color Emoji"
0703                                 fontSizeMode: Text.Fit
0704                                 minimumPixelSize: 10
0705                                 font.pixelSize: 72
0706                                 height: pointSize * 2
0707                                 width: height
0708                                 horizontalAlignment: Text.AlignHCenter
0709                                 verticalAlignment: Text.AlignVCenter
0710                                 anchors.verticalCenter: parent.verticalCenter
0711                             }
0712                         }
0713                     }
0714                 }
0715             }
0716         }
0717 
0718         footerPositioning: ListView.OverlayFooter
0719         footer: Controls.RoundButton {
0720             z: 3
0721             visible: listView.unreadCount > 0 || listView.contentY < listView.lastContentY - listView.height * 2
0722             anchors.horizontalCenter: parent.horizontalCenter
0723             contentItem: Row {
0724                 spacing: Kirigami.Units.smallSpacing
0725                 Kirigami.Icon {
0726                     anchors.verticalCenter: parent.verticalCenter
0727                     source: "go-down-symbolic"
0728                     height: Kirigami.Units.iconSizes.small
0729                     width: height
0730                     color: listView.incomingTextColor
0731                 }
0732                 Text {
0733                     visible: listView.unreadCount > 0
0734                     text: i18np("%1 new message", "%1 new messages", listView.unreadCount)
0735                     color: listView.incomingTextColor
0736                     font.bold: true
0737                 }
0738             }
0739             flat: true
0740             background: Rectangle {
0741                 color: Kirigami.ColorUtils.tintWithAlpha(listView.incomingColor, Kirigami.Theme.textColor, 0.15)
0742                 radius: parent.radius
0743             }
0744             onClicked: listView.positionAtEnd()
0745         }
0746     }
0747 
0748     Kirigami.OverlayDrawer {
0749         id: menu
0750 
0751         property int index
0752         property string id
0753         property string text
0754         property var attachments: []
0755         property string smil
0756         property bool resend: false
0757         property var tapbacks
0758 
0759         edge: Qt.BottomEdge
0760 
0761         contentItem: ColumnLayout {
0762             RowLayout {
0763                 Repeater {
0764                     model: tapbackKeys
0765                     ColumnLayout {
0766                         property int count: menu.tapbacks &&  menu.tapbacks[modelData] ? menu.tapbacks[modelData].length : 0
0767 
0768                         Controls.RoundButton {
0769                             padding: Kirigami.Units.largeSpacing * 2
0770                             flat: !highlighted
0771                             highlighted: count > 0 && menu.tapbacks[modelData].indexOf(sendingNumber) >= 0
0772                             onPressed: {
0773                                 if (!menu.tapbacks) {
0774                                     menu.tapbacks = {}
0775                                 }
0776 
0777                                 if (!menu.tapbacks[modelData]) {
0778                                     menu.tapbacks[modelData] = []
0779                                 }
0780 
0781                                 const isRemoved = menu.tapbacks[modelData].indexOf(sendingNumber) >= 0
0782                                 messageModel.sendTapback(menu.id, modelData, isRemoved)
0783                                 menu.close()
0784                             }
0785 
0786                             Text {
0787                                 anchors.centerIn: parent
0788                                 text: modelData
0789                                 font.family: "Noto Color Emoji"
0790                                 fontSizeMode: Text.Fit
0791                                 minimumPixelSize: 10
0792                                 font.pixelSize: 72
0793                                 height: pointSize * 3
0794                                 width: height
0795                                 horizontalAlignment: Text.AlignHCenter
0796                                 verticalAlignment: Text.AlignVCenter
0797                                 anchors.verticalCenter: parent.verticalCenter
0798                             }
0799                         }
0800 
0801                         Text {
0802                             Layout.alignment: Qt.AlignHCenter
0803                             text: count > 0 ? count : ""
0804                             color: Kirigami.Theme.disabledTextColor
0805                             font.pointSize: pointSize - 2
0806                         }
0807                     }
0808                 }
0809             }
0810             Delegates.RoundedItemDelegate {
0811                 visible: menu.text.match(/[0-9]{6}/)
0812                 Layout.fillWidth: true
0813                 text: i18n("Copy code")
0814                 icon.name: "edit-copy"
0815                 onClicked: {
0816                     Utils.copyTextToClipboard(menu.text.match(/[0-9]{6}/))
0817                     menu.close()
0818                 }
0819             }
0820             Delegates.RoundedItemDelegate {
0821                 visible: menu.text.indexOf('href="') >= 0
0822                 Layout.fillWidth: true
0823                 text: i18n("Copy link")
0824                 icon.name: "edit-copy"
0825                 onClicked: {
0826                     const start = menu.text.indexOf('href="')
0827                     const finish = menu.text.indexOf('"', start + 6)
0828                     let link = menu.text.substring(start + 6, finish)
0829                     Utils.copyTextToClipboard(link)
0830                     menu.close()
0831                 }
0832             }
0833             Delegates.RoundedItemDelegate {
0834                 visible: menu.text || menu.attachments.reduce((a,c) => a += (c.text || ""), "")
0835                 Layout.fillWidth: true
0836                 text: i18n("Copy text")
0837                 icon.name: "edit-copy"
0838                 onClicked: {
0839                     Utils.copyTextToClipboard(menu.text || menu.attachments.reduce((a,c) => a += (c.text || ""), ""))
0840                     menu.close()
0841                 }
0842             }
0843             Delegates.RoundedItemDelegate {
0844                 visible: menu.attachments.length > 0
0845                 Layout.fillWidth: true
0846                 text: i18n("Save attachment")
0847                 icon.name: "mail-attachment-symbolic"
0848                 onClicked: {
0849                     attachmentList.selected = []
0850                     attachmentList.items = menu.attachments.filter(o => o.fileName)
0851                     attachmentList.open()
0852                     menu.close()
0853                 }
0854             }
0855             Delegates.RoundedItemDelegate {
0856                 text: i18n("Delete message")
0857                 Layout.fillWidth: true
0858                 icon.name: "edit-delete"
0859                 onClicked: {
0860                     listView.currentIndex = menu.index
0861                     messageModel.deleteMessage(menu.id, menu.index, menu.attachments.map(o => o.fileName))
0862                     menu.close()
0863                 }
0864             }
0865             Delegates.RoundedItemDelegate {
0866                 visible: menu.resend
0867                 Layout.fillWidth: true
0868                 text: i18nc("Retry sending message", "Resend")
0869                 icon.name: "edit-redo"
0870                 onClicked: {
0871                     messageModel.sendMessage(menu.text, menu.attachments.map(o => "file://" + attachmentsFolder + "/" + o.fileName), menu.attachments.reduce((a,c) => a += (c.size || 0), 0))
0872                     listView.currentIndex = menu.index
0873                     messageModel.deleteMessage(menu.id, menu.index, menu.attachments.map(o => o.fileName))
0874                     menu.close()
0875                 }
0876             }
0877         }
0878     }
0879 
0880     Kirigami.Dialog {
0881         property var items: []
0882         property var selected: []
0883 
0884         id: attachmentList
0885         title: i18n("Save attachment")
0886 
0887         ListView {
0888             id: listItems
0889             model: attachmentList.items
0890             implicitWidth: Kirigami.Units.gridUnit * 30
0891             implicitHeight: (Kirigami.Units.iconSizes.medium + Kirigami.Units.largeSpacing * 2) * attachmentList.items.length
0892             onCountChanged: currentIndex = -1
0893 
0894             delegate: Delegates.RoundedItemDelegate {
0895                 id: delegateItem
0896                 width: listItems.width
0897                 implicitHeight: Kirigami.Units.iconSizes.medium + Kirigami.Units.largeSpacing * 2
0898                 verticalPadding: 0
0899                 contentItem: RowLayout {
0900                     spacing: Kirigami.Units.largeSpacing
0901 
0902                     RowLayout {
0903                         Controls.CheckBox {
0904                             Layout.fillHeight: true
0905                             Layout.preferredWidth: height
0906                             checked: attachmentList.selected.indexOf(modelData.fileName) >= 0
0907                             checkable: true
0908                             onToggled: delegateItem.clicked()
0909                         }
0910                     }
0911 
0912                     Controls.Label {
0913                         Layout.fillWidth: true
0914                         text: (modelData.name || modelData.fileName)
0915                         elide: Text.ElideRight
0916                         color: Kirigami.Theme.textColor
0917                     }
0918 
0919                 }
0920                 onClicked: {
0921                     const index = attachmentList.selected.indexOf(modelData.fileName)
0922                     if (index >=0) {
0923                         attachmentList.selected.splice(index, 1)
0924                     } else {
0925                         attachmentList.selected.push(modelData.fileName)
0926                     }
0927                     attachmentList.selected = attachmentList.selected
0928                 }
0929             }
0930         }
0931 
0932         footer: Row {
0933             spacing: Kirigami.Units.gridUnit
0934             layoutDirection: Qt.RightToLeft
0935 
0936             Controls.Button {
0937                 enabled: attachmentList.selected.length > 0
0938                 text: i18n("Save")
0939                 onClicked: {
0940                     messageModel.saveAttachments(attachmentList.selected)
0941                     attachmentList.close()
0942                 }
0943             }
0944             Controls.Button {
0945                 text: i18n("Cancel")
0946                 onClicked: attachmentList.close()
0947             }
0948         }
0949     }
0950 
0951     footer: ColumnLayout {
0952         spacing: 0
0953         width: parent.width
0954 
0955         Controls.ScrollView {
0956             id: scrollView
0957             Layout.minimumWidth: parent.width
0958             bottomPadding: 0
0959             implicitWidth: flow.implicitWidth
0960             implicitHeight: files.count > 0 ? Math.min(msgPage.availableHeight - composeArea.height - Kirigami.Units.largeSpacing, flow.implicitHeight) : 1
0961             contentWidth: availableWidth
0962             contentHeight: flow.implicitHeight
0963             clip: true
0964             background: Rectangle {
0965                 color: Kirigami.Theme.alternateBackgroundColor
0966             }
0967 
0968             Flow {
0969                 id: flow
0970                 anchors.fill: parent
0971                 padding: Kirigami.Units.smallSpacing
0972                 spacing: 0
0973 
0974                 Repeater {
0975                     model: files
0976                     Item {
0977                         width: fileItem.width + Kirigami.Units.largeSpacing
0978                         height: fileItem.height + Kirigami.Units.largeSpacing
0979 
0980                         property bool isImage: mimeType.indexOf("image/") >= 0
0981 
0982                         Rectangle {
0983                             id: fileItem
0984                             anchors.centerIn: parent
0985                             implicitWidth: (isImage ? attachImg.width : layout.implicitWidth) + 2
0986                             implicitHeight: (isImage ? attachImg.implicitHeight : layout.implicitHeight) + 2
0987                             border.width: 1
0988                             border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
0989                             color: Kirigami.Theme.backgroundColor
0990                             radius: Kirigami.Units.largeSpacing
0991 
0992                             RowLayout {
0993                                 id: layout
0994                                 visible: !isImage
0995                                 Kirigami.Icon {
0996                                     source: iconName
0997                                 }
0998                                 Text {
0999                                     text: name
1000                                     color: Kirigami.Theme.textColor
1001                                 }
1002                                 Item {
1003                                     width: Kirigami.Units.largeSpacing
1004                                 }
1005                                 MouseArea {
1006                                     width: parent.width
1007                                     height: parent.height
1008                                     onDoubleClicked: Qt.openUrlExternally(filePath)
1009                                 }
1010                             }
1011 
1012                             Image {
1013                                 id: attachImg
1014                                 anchors.centerIn: parent
1015                                 source: isImage ? filePath : ""
1016                                 sourceSize.height: Kirigami.Units.gridUnit * 8
1017                                 width: Math.min(flow.width - Kirigami.Units.largeSpacing * 2, implicitWidth)
1018                                 cache: false
1019                                 fillMode: Image.PreserveAspectCrop
1020 
1021                                 MouseArea {
1022                                     anchors.fill: parent
1023                                     onClicked: pageStack.layers.push("qrc:/PreviewPage.qml", {
1024                                         filePath: filePath,
1025                                         type: mimeType
1026                                     } )
1027                                 }
1028 
1029                                 // rounded corners on image
1030                                 layer.enabled: true
1031                                 layer.effect: OpacityMask {
1032                                     maskSource: Item {
1033                                         width: attachImg.width
1034                                         height: attachImg.height
1035                                         Rectangle {
1036                                             anchors.fill: parent
1037                                             radius: Kirigami.Units.largeSpacing
1038                                         }
1039                                     }
1040                                 }
1041 
1042                                 AnimatedImage {
1043                                     source: parent.source && mimeType === "image/gif" ? parent.source : ""
1044                                     anchors.fill: parent
1045                                 }
1046                             }
1047                         }
1048 
1049                         Controls.RoundButton {
1050                             anchors.right: parent.right
1051                             anchors.top: parent.top
1052                             icon.name: "remove"
1053                             icon.color: Kirigami.Theme.negativeTextColor
1054                             icon.width: Kirigami.Units.gridUnit * 1.5
1055                             icon.height: Kirigami.Units.gridUnit * 1.5
1056                             padding: 0
1057                             width: Kirigami.Units.gridUnit * 1.5
1058                             height: Kirigami.Units.gridUnit * 1.5
1059                             onPressed: files.remove(index)
1060                             Controls.ToolTip.delay: 1000
1061                             Controls.ToolTip.timeout: 5000
1062                             Controls.ToolTip.visible: hovered
1063                             Controls.ToolTip.text: i18nc("Remove item from list", "Remove")
1064                         }
1065                     }
1066                 }
1067             }
1068 
1069             Text {
1070                 visible: files.count > 0
1071                 anchors.right: parent.right
1072                 anchors.bottom: parent.bottom
1073                 bottomPadding: textarea.lineCount < 3 && textarea.length > 0 ? Kirigami.Units.gridUnit : 0
1074                 text: files.count > 0 ? formatBytes(filesTotalSize()) : ""
1075                 color: Kirigami.Theme.disabledTextColor
1076             }
1077         }
1078 
1079         RowLayout {
1080             id: composeArea
1081 
1082             Controls.Button {
1083                 id: attachAction
1084                 Layout.alignment: Qt.AlignLeft | Qt.AlignBottom
1085                 Layout.margins: Kirigami.Units.smallSpacing
1086                 icon.name: "mail-attachment-symbolic"
1087                 icon.width: Kirigami.Units.iconSizes.smallMedium
1088                 icon.height: Kirigami.Units.iconSizes.smallMedium
1089                 flat: true
1090                 hoverEnabled: false
1091                 onPressed: fileDialog.open()
1092             }
1093 
1094             Controls.TextArea {
1095                 id: textarea
1096                 Layout.fillWidth: true
1097                 Layout.minimumHeight: sendButton.height + Kirigami.Units.smallSpacing * 2
1098                 verticalAlignment: TextEdit.AlignVCenter
1099                 placeholderText: {
1100                     if (!sendingNumber) {
1101                         return i18n("Write Message...")
1102                     } else {
1103                         return i18nc("%1 is a phone number", "Send Message from %1...", sendingNumber)
1104                     }
1105                 }
1106                 font.pointSize: pointSize
1107                 font.family: "Noto Sans, Noto Color Emoji"
1108                 textFormat: Text.PlainText
1109                 wrapMode: Text.Wrap
1110                 background: Rectangle {
1111                     color: Kirigami.Theme.backgroundColor
1112                 }
1113                 inputMethodHints: Qt.ImhNoPredictiveText
1114             }
1115 
1116             Controls.Action {
1117                 id: sendAction
1118                 onTriggered: {
1119                     messageModel.sendMessage(textarea.text, filesToList(), filesTotalSize())
1120                     files.clear()
1121                     textarea.text = ""
1122                     listView.positionAtEnd()
1123                 }
1124             }
1125 
1126             Shortcut{
1127                 sequences: ["Ctrl+Enter", "Ctrl+Return"]
1128                 onActivated: sendAction.trigger()
1129             }
1130 
1131             Controls.Button {
1132                 id: sendButton
1133                 Layout.alignment: Qt.AlignRight | Qt.AlignBottom
1134                 Layout.margins: Kirigami.Units.smallSpacing
1135                 icon.name: "document-send"
1136                 icon.width: Kirigami.Units.iconSizes.smallMedium
1137                 icon.height: Kirigami.Units.iconSizes.smallMedium
1138                 hoverEnabled: false
1139                 enabled: people.length > 0 && (textarea.length > 0 || files.count > 0) && !maxAttachmentsError.visible
1140                 onPressed: sendAction.trigger()
1141 
1142                 Controls.Label {
1143                     text: textarea.length
1144                     font: Kirigami.Theme.smallFont
1145                     color: Kirigami.Theme.disabledTextColor
1146                     visible: textarea.length > 0
1147                     anchors.left: parent.left
1148                     anchors.bottom: parent.top
1149                     anchors.margins: Kirigami.Units.smallSpacing * 1.5
1150                 }
1151             }
1152         }
1153 
1154         FileDialog {
1155             id: fileDialog
1156             title: i18n("Choose a file")
1157             folder: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
1158             fileMode: FileDialog.OpenFiles
1159             onAccepted: {
1160                 for (let i = 0; i < fileDialog.files.length; i++) {
1161                     msgPage.files.append(messageModel.fileInfo(fileDialog.files[i]))
1162                 }
1163             }
1164         }
1165     }
1166 }