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 }