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)
}
}