diff --git a/src/qml/ChatPage.qml b/src/qml/ChatPage.qml index 81dbe5c..acff4c8 100644 --- a/src/qml/ChatPage.qml +++ b/src/qml/ChatPage.qml @@ -1,403 +1,403 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ import QtQuick 2.6 import QtQuick.Controls 2.0 as Controls import QtQuick.Layouts 1.3 import org.kde.kirigami 2.2 as Kirigami import QtGraphicalEffects 1.0 import im.kaidan.kaidan 1.0 import EmojiModel 0.1 import "elements" Kirigami.ScrollablePage { property string chatName property bool isWritingSpoiler property string messageToCorrect title: chatName keyboardNavigationEnabled: true actions.contextualActions: [ Kirigami.Action { visible: !isWritingSpoiler icon.name: "password-show-off" text: qsTr("Send a spoiler message") onTriggered: isWritingSpoiler = true }, Kirigami.Action { visible: true icon.name: { kaidan.notificationsMuted(kaidan.messageModel.chatPartner) ? "player-volume" : "audio-volume-muted-symbolic" } text: { kaidan.notificationsMuted(kaidan.messageModel.chatPartner) ? qsTr("Unmute notifications") : qsTr("Mute notifications") } onTriggered: { kaidan.setNotificationsMuted( kaidan.messageModel.chatPartner, !kaidan.notificationsMuted(kaidan.messageModel.chatPartner) ) } function handleNotificationsMuted(jid) { text = kaidan.notificationsMuted(kaidan.messageModel.chatPartner) ? qsTr("Unmute notifications") : qsTr("Mute notifications") icon.name = kaidan.notificationsMuted(kaidan.messageModel.chatPartner) ? "player-volume" : "audio-volume-muted-symbolic" } Component.onCompleted: { kaidan.notificationsMutedChanged.connect(handleNotificationsMuted) } Component.onDestruction: { kaidan.notificationsMutedChanged.disconnect(handleNotificationsMuted) } }, Kirigami.Action { visible: true icon.name: "user-identity" text: qsTr("View profile") onTriggered: pageStack.push(userProfilePage, {jid: kaidan.messageModel.chatPartner, name: chatName}) } ] SendMediaSheet { id: sendMediaSheet } FileChooser { id: fileChooser title: qsTr("Select a file") onAccepted: { sendMediaSheet.jid = kaidan.messageModel.chatPartner sendMediaSheet.fileUrl = fileUrl sendMediaSheet.open() } } function openFileDialog(filterName, filter) { fileChooser.filterName = filterName fileChooser.filter = filter fileChooser.open() mediaDrawer.close() } Kirigami.OverlayDrawer { id: mediaDrawer edge: Qt.BottomEdge height: Kirigami.Units.gridUnit * 8 contentItem: RowLayout { id: content Layout.alignment: Qt.AlignHCenter Layout.fillHeight: true IconButton { buttonText: qsTr("Image") iconSource: "image-jpeg" onClicked: openFileDialog("Images", "*.jpg *.jpeg *.png *.gif") Layout.alignment: Qt.AlignHCenter } IconButton { buttonText: qsTr("Video") iconSource: "video-mp4" onClicked: openFileDialog("Videos", "*.mp4 *.mkv *.avi *.webm") Layout.alignment: Qt.AlignHCenter } IconButton { buttonText: qsTr("Audio") iconSource: "audio-mp3" onClicked: openFileDialog("Audio files", "*.mp3 *.wav *.flac *.ogg *.m4a *.mka") Layout.alignment: Qt.AlignHCenter } IconButton { buttonText: qsTr("Document") iconSource: "x-office-document" onClicked: openFileDialog("Documents", "*.doc *.docx *.odt") Layout.alignment: Qt.AlignHCenter } IconButton { buttonText: qsTr("Other file") iconSource: "text-x-plain" onClicked: openFileDialog("All files", "*") Layout.alignment: Qt.AlignHCenter } } } background: Image { id: bgimage source: kaidan.utils.getResourcePath("images/chat.png") anchors.fill: parent fillMode: Image.Tile horizontalAlignment: Image.AlignLeft verticalAlignment: Image.AlignTop } // Chat ListView { verticalLayoutDirection: ListView.BottomToTop spacing: Kirigami.Units.smallSpacing * 2 // connect the database model: kaidan.messageModel delegate: ChatMessage { msgId: model.id sender: model.sender sentByMe: model.sentByMe messageBody: model.body dateTime: new Date(model.timestamp) - isRead: model.isDelivered + isDelivered: model.isDelivered name: chatName mediaType: model.mediaType mediaGetUrl: model.mediaUrl mediaLocation: model.mediaLocation edited: model.isEdited isSpoiler: model.isSpoiler isShowingSpoiler: false spoilerHint: model.spoilerHint onMessageEditRequested: { messageToCorrect = id messageField.text = body messageField.state = "edit" } } } // Message Writing footer: Controls.Pane { id: sendingArea layer.enabled: sendingArea.enabled layer.effect: DropShadow { verticalOffset: 1 color: Kirigami.Theme.disabledTextColor samples: 20 spread: 0.3 cached: true // element is static } padding: 0 wheelEnabled: true background: Rectangle { color: Kirigami.Theme.backgroundColor } RowLayout { anchors.fill: parent Layout.preferredHeight: Kirigami.Units.gridUnit * 3 Controls.ToolButton { id: attachButton visible: kaidan.uploadServiceFound Layout.preferredWidth: Kirigami.Units.gridUnit * 3 Layout.preferredHeight: Kirigami.Units.gridUnit * 3 padding: 0 Kirigami.Icon { source: "document-send-symbolic" isMask: true smooth: true anchors.centerIn: parent width: Kirigami.Units.gridUnit * 2 height: width } onClicked: { if (Kirigami.Settings.isMobile) mediaDrawer.open() else openFileDialog("All files", "(*)") } } ColumnLayout { Layout.minimumHeight: messageField.height + Kirigami.Units.smallSpacing * 2 Layout.fillWidth: true spacing: 0 RowLayout { visible: isWritingSpoiler Controls.TextArea { id: spoilerHintField Layout.fillWidth: true placeholderText: qsTr("Spoiler hint") wrapMode: Controls.TextArea.Wrap selectByMouse: true background: Item {} } Controls.ToolButton { Layout.preferredWidth: Kirigami.Units.gridUnit * 1.5 Layout.preferredHeight: Kirigami.Units.gridUnit * 1.5 padding: 0 Kirigami.Icon { source: "tab-close" smooth: true anchors.centerIn: parent width: Kirigami.Units.gridUnit * 1.5 height: width } onClicked: { isWritingSpoiler = false spoilerHintField.text = "" } } } Kirigami.Separator { visible: isWritingSpoiler Layout.fillWidth: true } Controls.TextArea { id: messageField Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter placeholderText: qsTr("Compose message") wrapMode: Controls.TextArea.Wrap selectByMouse: true background: Item {} state: "compose" states: [ State { name: "compose" }, State { name: "edit" } ] Keys.onReturnPressed: { if (event.key === Qt.Key_Return) { if (event.modifiers & Qt.ControlModifier) { messageField.append("") } else { sendButton.onClicked() event.accepted = true } } } } } EmojiPicker { x: -width + parent.width y: -height - 16 width: Kirigami.Units.gridUnit * 20 height: Kirigami.Units.gridUnit * 15 id: emojiPicker model: EmojiProxyModel { group: Emoji.Group.People sourceModel: EmojiModel {} } textArea: messageField } Controls.ToolButton { id: emojiPickerButton Layout.preferredWidth: Kirigami.Units.gridUnit * 3 Layout.preferredHeight: Kirigami.Units.gridUnit * 3 padding: 0 Kirigami.Icon { source: "face-smile" enabled: sendButton.enabled isMask: false smooth: true anchors.centerIn: parent width: Kirigami.Units.gridUnit * 2 height: width } onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.open() } Controls.ToolButton { id: sendButton Layout.preferredWidth: Kirigami.Units.gridUnit * 3 Layout.preferredHeight: Kirigami.Units.gridUnit * 3 padding: 0 Kirigami.Icon { source: { if (messageField.state == "compose") return "document-send" else if (messageField.state == "edit") return "edit-symbolic" } enabled: sendButton.enabled isMask: true smooth: true anchors.centerIn: parent width: Kirigami.Units.gridUnit * 2 height: width } onClicked: { // don't send empty messages if (!messageField.text.length) { return } // disable the button to prevent sending // the same message several times sendButton.enabled = false // send the message if (messageField.state == "compose") { kaidan.sendMessage( kaidan.messageModel.chatPartner, messageField.text, isWritingSpoiler, spoilerHintField.text ) } else if (messageField.state == "edit") { kaidan.correctMessage( kaidan.messageModel.chatPartner, messageToCorrect, messageField.text ) } // clean up the text fields messageField.text = "" messageField.state = "compose" spoilerHintField.text = "" isWritingSpoiler = false messageToCorrect = '' // reenable the button sendButton.enabled = true } } } } } diff --git a/src/qml/elements/ChatMessage.qml b/src/qml/elements/ChatMessage.qml index 3da6d73..b3869eb 100644 --- a/src/qml/elements/ChatMessage.qml +++ b/src/qml/elements/ChatMessage.qml @@ -1,302 +1,302 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan 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. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan 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 Kaidan. If not, see . */ import QtQuick 2.6 import QtGraphicalEffects 1.0 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.2 as Controls import org.kde.kirigami 2.0 as Kirigami import im.kaidan.kaidan 1.0 RowLayout { id: root property string msgId property string sender property bool sentByMe: true property string messageBody property date dateTime - property bool isRead: false + property bool isDelivered: false property int mediaType property string mediaGetUrl property string mediaLocation property bool edited property bool isLoading: kaidan.transferCache.hasUpload(msgId) property string name property TransferJob upload: { if (mediaType !== Enums.MessageType.MessageText && isLoading) { return kaidan.transferCache.jobByMessageId(model.id) } return null } property bool isSpoiler property string spoilerHint property bool isShowingSpoiler: false property string avatarUrl: kaidan.avatarStorage.getAvatarUrl(sender) signal messageEditRequested(string id, string body) // own messages are on the right, others on the left layoutDirection: sentByMe ? Qt.RightToLeft : Qt.LeftToRight spacing: 8 width: ListView.view.width // placeholder Item { Layout.preferredWidth: root.layoutDirection === Qt.LeftToRight ? 5 : 10 } Avatar { id: avatar visible: !sentByMe avatarUrl: root.avatarUrl Layout.alignment: Qt.AlignHCenter | Qt.AlignTop name: root.name Layout.preferredHeight: Kirigami.Units.gridUnit * 2.2 Layout.preferredWidth: Kirigami.Units.gridUnit * 2.2 } // message bubble/box Item { Layout.preferredWidth: content.width + 13 Layout.preferredHeight: content.height + 8 Rectangle { id: box anchors.fill: parent color: sentByMe ? Kirigami.Theme.complementaryTextColor : Kirigami.Theme.highlightColor radius: Kirigami.Units.smallSpacing * 2 layer.enabled: box.visible layer.effect: DropShadow { verticalOffset: Kirigami.Units.gridUnit * 0.08 horizontalOffset: Kirigami.Units.gridUnit * 0.08 color: Kirigami.Theme.disabledTextColor samples: 10 spread: 0.1 } MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: { if (mouse.button === Qt.RightButton) contextMenu.popup() } onPressAndHold: { contextMenu.popup() } } Controls.Menu { id: contextMenu Controls.MenuItem { text: qsTr("Copy Message") enabled: bodyLabel.visible onTriggered: { if (!isSpoiler || isShowingSpoiler) kaidan.utils.copyToClipboard(messageBody); else kaidan.utils.copyToClipboard(spoilerHint); } } Controls.MenuItem { text: qsTr("Edit Message") enabled: kaidan.messageModel.canCorrectMessage(msgId) onTriggered: root.messageEditRequested(msgId, messageBody) } Controls.MenuItem { text: qsTr("Copy download URL") enabled: mediaGetUrl onTriggered: kaidan.utils.copyToClipboard(mediaGetUrl) } } } ColumnLayout { id: content spacing: 0 anchors.centerIn: parent anchors.margins: 4 RowLayout { id: spoilerHintRow visible: isSpoiler MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: { if (mouse.button === Qt.LeftButton) { isShowingSpoiler = !isShowingSpoiler } } } Controls.Label { id: dateLabeltest text: spoilerHint == "" ? qsTr("Spoiler") : spoilerHint color: sentByMe ? Kirigami.Theme.buttonTextColor : Kirigami.Theme.complementaryTextColor font.pixelSize: Kirigami.Units.gridUnit * 0.8 } Item { Layout.fillWidth: true height: 1 } Kirigami.Icon { height: 28 width: 28 source: isShowingSpoiler ? "password-show-off" : "password-show-on" color: sentByMe ? Kirigami.Theme.buttonTextColor : Kirigami.Theme.complementaryTextColor } } Kirigami.Separator { visible: isSpoiler Layout.fillWidth: true color: { var bgColor = sentByMe ? Kirigami.Theme.backgroundColor : Kirigami.Theme.highlightColor var textColor = sentByMe ? Kirigami.Theme.textColor : Kirigami.Theme.buttonTextColor return Qt.tint(textColor, Qt.rgba(bgColor.r, bgColor.g, bgColor.b, 0.7)) } } ColumnLayout { visible: isSpoiler && isShowingSpoiler || !isSpoiler Controls.ToolButton { visible: { mediaType !== Enums.MessageText && !isLoading && mediaLocation === "" && mediaGetUrl !== "" } text: qsTr("Download") onClicked: { print("Downloading " + mediaGetUrl + "...") kaidan.downloadMedia(msgId, mediaGetUrl) } } // media loader Loader { id: media source: { if (mediaType === Enums.MessageImage && mediaLocation !== "") "ChatMessageImage.qml" else "" } property string sourceUrl: "file://" + mediaLocation Layout.maximumWidth: root.width - Kirigami.Units.gridUnit * 6 Layout.preferredHeight: loaded ? item.paintedHeight : 0 } // message body Controls.Label { id: bodyLabel visible: messageBody !== "" && messageBody !== mediaGetUrl text: kaidan.utils.formatMessage(messageBody) textFormat: Text.StyledText wrapMode: Text.Wrap color: sentByMe ? Kirigami.Theme.buttonTextColor : Kirigami.Theme.complementaryTextColor onLinkActivated: Qt.openUrlExternally(link) Layout.maximumWidth: mediaType === Enums.MessageImage && media.width !== 0 ? media.width : root.width - Kirigami.Units.gridUnit * 6 } Kirigami.Separator { visible: isSpoiler && isShowingSpoiler Layout.fillWidth: true color: { var bgColor = sentByMe ? Kirigami.Theme.backgroundColor : Kirigami.Theme.highlightColor var textColor = sentByMe ? Kirigami.Theme.textColor : Kirigami.Theme.buttonTextColor return Qt.tint(textColor, Qt.rgba(bgColor.r, bgColor.g, bgColor.b, 0.7)) } } } - // message meta: date, isRead + // message meta: date, isDelivered RowLayout { // progress bar for upload/download status Controls.ProgressBar { visible: isLoading value: upload ? upload.progress : 0 } Controls.Label { id: dateLabel text: Qt.formatDateTime(dateTime, "dd. MMM yyyy, hh:mm") color: sentByMe ? Kirigami.Theme.disabledTextColor : Qt.darker(Kirigami.Theme.disabledTextColor, 1.3) font.pixelSize: Kirigami.Units.gridUnit * 0.8 } Image { id: checkmark - visible: (sentByMe && isRead) + visible: (sentByMe && isDelivered) source: kaidan.utils.getResourcePath("images/message_checkmark.svg") Layout.preferredHeight: Kirigami.Units.gridUnit * 0.65 Layout.preferredWidth: Kirigami.Units.gridUnit * 0.65 sourceSize.height: Kirigami.Units.gridUnit * 0.65 sourceSize.width: Kirigami.Units.gridUnit * 0.65 } Kirigami.Icon { source: "edit-symbolic" visible: edited Layout.preferredHeight: Kirigami.Units.gridUnit * 0.65 Layout.preferredWidth: Kirigami.Units.gridUnit * 0.65 } } } } // placeholder Item { Layout.fillWidth: true } function updateIsLoading() { isLoading = kaidan.transferCache.hasUpload(msgId) } Component.onCompleted: { kaidan.transferCache.jobsChanged.connect(updateIsLoading) } Component.onDestruction: { kaidan.transferCache.jobsChanged.disconnect(updateIsLoading) } }