Commit 0b3d6bee authored by Aniket Kumar's avatar Aniket Kumar 🤵

Implementing Attachment class and adding support to display the thumbnails of attachments in QML.

parent 90a3ded0
Pipeline #30201 passed with stage
in 18 minutes and 31 seconds
......@@ -33,7 +33,6 @@ ConversationMessage::ConversationMessage(const QVariantMap& args)
m_threadID(args[QStringLiteral("thread_id")].toLongLong()),
m_uID(args[QStringLiteral("_id")].toInt())
{
QString test = QLatin1String(args[QStringLiteral("addresses")].typeName());
QVariantList jsonAddresses = args[QStringLiteral("addresses")].toList();
for (const QVariant& addressField : jsonAddresses) {
const auto& rawAddress = addressField.toMap();
......@@ -41,6 +40,18 @@ ConversationMessage::ConversationMessage(const QVariantMap& args)
}
QVariantMap::const_iterator subID_it = args.find(QStringLiteral("sub_id"));
m_subID = subID_it == args.end() ? -1 : subID_it->toLongLong();
if (args.contains(QStringLiteral("attachments"))) {
QVariant attachment = args.value(QStringLiteral("attachments"));
const QVariantList jsonAttachments = attachment.toList();
for (const QVariant& attachmentField : jsonAttachments) {
const auto& rawAttachment = attachmentField.toMap();
m_attachments.append(Attachment(rawAttachment[QStringLiteral("part_id")].value<qint64>(),
rawAttachment[QStringLiteral("mime_type")].value<QString>(),
rawAttachment[QStringLiteral("encoded_thumbnail")].value<QString>(),
rawAttachment[QStringLiteral("unique_identifier")].value<QString>()));
}
}
}
ConversationMessage::ConversationMessage (const qint32& eventField, const QString& body,
......@@ -48,7 +59,8 @@ ConversationMessage::ConversationMessage (const qint32& eventField, const QStrin
const qint32& type, const qint32& read,
const qint64& threadID,
const qint32& uID,
const qint64& subID)
const qint64& subID,
const QList<Attachment>& attachments)
: m_eventField(eventField)
, m_body(body)
, m_addresses(addresses)
......@@ -58,6 +70,7 @@ ConversationMessage::ConversationMessage (const qint32& eventField, const QStrin
, m_threadID(threadID)
, m_uID(uID)
, m_subID(subID)
, m_attachments(attachments)
{
}
......@@ -73,6 +86,13 @@ ConversationAddress::ConversationAddress(QString address)
: m_address(address)
{}
Attachment::Attachment(qint64 partID, QString mimeType, QString base64EncodedFile, QString uniqueIdentifier)
: m_partID(partID)
, m_mimeType(mimeType)
, m_base64EncodedFile(base64EncodedFile)
, m_uniqueIdentifier(uniqueIdentifier)
{}
void ConversationMessage::registerDbusType()
{
qDBusRegisterMetaType<ConversationMessage>();
......@@ -81,4 +101,6 @@ void ConversationMessage::registerDbusType()
qRegisterMetaType<ConversationAddress>();
qDBusRegisterMetaType<QList<ConversationAddress>>();
qRegisterMetaType<QList<ConversationAddress>>();
qDBusRegisterMetaType<Attachment>();
qRegisterMetaType<Attachment>();
}
......@@ -26,6 +26,7 @@
#include "kdeconnectinterfaces_export.h"
class ConversationAddress;
class Attachment;
class KDECONNECTINTERFACES_EXPORT ConversationMessage
{
......@@ -61,7 +62,8 @@ public:
ConversationMessage(const qint32& eventField, const QString& body, const QList<ConversationAddress>& addresses,
const qint64& date, const qint32& type, const qint32& read,
const qint64& threadID, const qint32& uID, const qint64& subID);
const qint64& threadID, const qint32& uID, const qint64& subID,
const QList<Attachment>& attachments);
static ConversationMessage fromDBus(const QDBusVariant&);
static void registerDbusType();
......@@ -75,12 +77,14 @@ public:
qint64 threadID() const { return m_threadID; }
qint32 uID() const { return m_uID; }
qint64 subID() const { return m_subID; }
QList<Attachment> attachments() const { return m_attachments; }
bool containsTextBody() const { return (eventField() & ConversationMessage::EventTextMessage); }
bool isMultitarget() const { return (eventField() & ConversationMessage::EventMultiTarget); }
bool isIncoming() const { return type() == MessageTypeInbox; }
bool isOutgoing() const { return type() == MessageTypeSent; }
bool containsAttachment() const { return !attachments().isEmpty(); }
/**
* Return the address of the other party of a single-target conversation
......@@ -135,6 +139,11 @@ protected:
* Value which determines SIM id (optional)
*/
qint64 m_subID;
/**
* Contains attachment related info of a MMS message (optional)
*/
QList<Attachment> m_attachments;
};
class KDECONNECTINTERFACES_EXPORT ConversationAddress
......@@ -148,6 +157,24 @@ private:
QString m_address;
};
class KDECONNECTINTERFACES_EXPORT Attachment
{
public:
Attachment() {}
Attachment(qint64 prtID, QString mimeType, QString base64EncodedFile, QString uniqueIdentifier);
qint64 partID() const { return m_partID; }
QString mimeType() const { return m_mimeType; }
QString base64EncodedFile() const { return m_base64EncodedFile; }
QString uniqueIdentifier() const { return m_uniqueIdentifier; }
private:
qint64 m_partID; // Part ID of the attachment of the message
QString m_mimeType; // Type of attachment (image, video, audio etc.)
QString m_base64EncodedFile; // Base64 encoded string of a file
QString m_uniqueIdentifier; // unique name of the attachment
};
inline QDBusArgument &operator<<(QDBusArgument &argument, const ConversationMessage &message)
{
argument.beginStructure();
......@@ -159,7 +186,8 @@ inline QDBusArgument &operator<<(QDBusArgument &argument, const ConversationMess
<< message.read()
<< message.threadID()
<< message.uID()
<< message.subID();
<< message.subID()
<< message.attachments();
argument.endStructure();
return argument;
}
......@@ -175,6 +203,7 @@ inline const QDBusArgument &operator>>(const QDBusArgument &argument, Conversati
qint64 threadID;
qint32 uID;
qint64 m_subID;
QList<Attachment> attachments;
argument.beginStructure();
argument >> event;
......@@ -186,9 +215,10 @@ inline const QDBusArgument &operator>>(const QDBusArgument &argument, Conversati
argument >> threadID;
argument >> uID;
argument >> m_subID;
argument >> attachments;
argument.endStructure();
message = ConversationMessage(event, body, addresses, date, type, read, threadID, uID, m_subID);
message = ConversationMessage(event, body, addresses, date, type, read, threadID, uID, m_subID, attachments);
return argument;
}
......@@ -214,7 +244,38 @@ inline const QDBusArgument& operator>>(const QDBusArgument& argument, Conversati
return argument;
}
inline QDBusArgument& operator<<(QDBusArgument& argument, const Attachment& attachment)
{
argument.beginStructure();
argument << attachment.partID()
<< attachment.mimeType()
<< attachment.base64EncodedFile()
<< attachment.uniqueIdentifier();
argument.endStructure();
return argument;
}
inline const QDBusArgument& operator>>(const QDBusArgument& argument, Attachment& attachment)
{
qint64 partID;
QString mimeType;
QString encodedFile;
QString uniqueIdentifier;
argument.beginStructure();
argument >> partID;
argument >> mimeType;
argument >> encodedFile;
argument >> uniqueIdentifier;
argument.endStructure();
attachment = Attachment(partID, mimeType, encodedFile, uniqueIdentifier);
return argument;
}
Q_DECLARE_METATYPE(ConversationMessage)
Q_DECLARE_METATYPE(ConversationAddress)
Q_DECLARE_METATYPE(Attachment)
#endif /* PLUGINS_TELEPHONY_CONVERSATIONMESSAGE_H_ */
......@@ -69,6 +69,16 @@
* // If this value is not defined or if it does not match a valid subscriber_id known by
* // Android, we will use whatever subscriber ID Android gives us as the default
*
* "attachments": <List<Attachment>> // List of Attachment objects, one for each attached file in the message.
*
* An Attachment object looks like:
* {
* "part_id": <long> // part_id of the attachment used to read the file from MMS database
* "mime_type": <int> // contains the mime type of the file (image, video, audio, etc.)
* "encoded_thumbnail": <String> // Optional base64-encoded thumbnail preview of the content for types which support it
* "unique_identifier": <String> // Unique name of te file
* }
*
* An Address object looks like:
* {
* "address": <String> // Address (phone number, email address, etc.) of this object
......
......@@ -41,6 +41,8 @@ add_executable(kdeconnect-sms
conversationmodel.cpp
conversationssortfilterproxymodel.cpp
resources.qrc
thumbnailsprovider.cpp
attachmentinfo.cpp
${sms_debug_files_SRCS})
target_link_libraries(kdeconnect-sms
......
/**
* Copyright (C) 2020 Aniket Kumar <anikketkumar786@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "attachmentinfo.h"
AttachmentInfo::AttachmentInfo()
{}
AttachmentInfo::AttachmentInfo(const Attachment& attachment)
: m_partID(attachment.partID()),
m_mimeType(attachment.mimeType()),
m_uniqueIdentifier(attachment.uniqueIdentifier())
{}
/**
* Copyright (C) 2020 Aniket Kumar <anikketkumar786@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef ATTACHMENTINFO_H
#define ATTACHMENTINFO_H
#include "conversationmessage.h"
/**
* This class is a compressed version of Attachment class
* as it will be exposed to the QML in a list object
*/
class AttachmentInfo
{
Q_GADGET
Q_PROPERTY(qint64 partID READ partID)
Q_PROPERTY(QString mimeType READ mimeType)
Q_PROPERTY(QString uniqueIdentifier READ uniqueIdentifier)
public:
AttachmentInfo();
AttachmentInfo(const Attachment& attachment);
qint64 partID() const { return m_partID; }
QString mimeType() const { return m_mimeType; }
QString uniqueIdentifier() const { return m_uniqueIdentifier; }
private:
qint64 m_partID;
QString m_mimeType;
QString m_uniqueIdentifier;
};
#endif // ATTACHMENTINFO_H
......@@ -200,7 +200,14 @@ void ConversationListModel::createRowFromMessage(const ConversationMessage& mess
// TODO: Upgrade to support other kinds of media
// Get the body that we should display
QString displayBody = message.containsTextBody() ? message.body() : i18n("(Unsupported Message Type)");
QString displayBody;
if (message.containsTextBody()) {
displayBody = message.body();
} else if (message.containsAttachment()) {
const QString mimeType = message.attachments().last().mimeType();
const QString type = QStringLiteral("\"") + mimeType.left(mimeType.indexOf(QStringLiteral("/"))) + QStringLiteral(" file\"");
displayBody = type;
}
// For displaying single line subtitle out of the multiline messages to keep the ListItems consistent
displayBody = displayBody.mid(0, displayBody.indexOf(QStringLiteral("\n")));
......
......@@ -22,9 +22,12 @@
#include "conversationmodel.h"
#include <KLocalizedString>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "interfaces/conversationmessage.h"
#include "smshelper.h"
#include "attachmentinfo.h"
#include "sms_conversation_debug.h"
......@@ -37,6 +40,7 @@ ConversationModel::ConversationModel(QObject* parent)
roles.insert(DateRole, "date");
roles.insert(SenderRole, "sender");
roles.insert(AvatarRole, "avatar");
roles.insert(AttachmentsRole, "attachments");
setItemRoleNames(roles);
}
......@@ -59,6 +63,7 @@ void ConversationModel::setThreadId(const qint64& threadId)
knownMessageIDs.clear();
if (m_threadId != INVALID_THREAD_ID && !m_deviceId.isEmpty()) {
requestMoreMessages();
m_thumbnailsProvider->clear();
}
}
......@@ -81,6 +86,12 @@ void ConversationModel::setDeviceId(const QString& deviceId)
connect(m_conversationsInterface, SIGNAL(conversationUpdated(QDBusVariant)), this, SLOT(handleConversationUpdate(QDBusVariant)));
connect(m_conversationsInterface, SIGNAL(conversationLoaded(qint64, quint64)), this, SLOT(handleConversationLoaded(qint64, quint64)));
connect(m_conversationsInterface, SIGNAL(conversationCreated(QDBusVariant)), this, SLOT(handleConversationCreated(QDBusVariant)));
QQmlApplicationEngine* engine = qobject_cast<QQmlApplicationEngine*>(QQmlEngine::contextForObject(this)->engine());
m_thumbnailsProvider = dynamic_cast<ThumbnailsProvider*>(engine->imageProvider(QStringLiteral("thumbnailsProvider")));
// Clear any previous data on device change
m_thumbnailsProvider->clear();
}
void ConversationModel::setAddressList(const QList<ConversationAddress>& addressList) {
......@@ -109,7 +120,7 @@ void ConversationModel::requestMoreMessages(const quint32& howMany)
if (m_threadId == INVALID_THREAD_ID) {
return;
}
const auto& numMessages = rowCount();
const auto& numMessages = knownMessageIDs.size();
m_conversationsInterface->requestConversation(m_threadId, numMessages, numMessages + howMany);
}
......@@ -132,18 +143,35 @@ void ConversationModel::createRowFromMessage(const ConversationMessage& message,
return;
}
// TODO: Upgrade to support other kinds of media
// Get the body that we should display
QString displayBody = message.containsTextBody() ? message.body() : i18n("(Unsupported Message Type)");
ConversationAddress sender = message.addresses().first();
QString senderName = message.isIncoming() ? SmsHelper::getTitleForAddresses({sender}) : QString();
QString displayBody = message.body();
auto item = new QStandardItem;
item->setText(displayBody);
item->setData(message.isOutgoing(), FromMeRole);
item->setData(message.date(), DateRole);
item->setData(senderName, SenderRole);
QList<QVariant> attachmentInfoList;
const QList<Attachment> attachmentList = message.attachments();
for (const Attachment& attachment : attachmentList) {
AttachmentInfo attachmentInfo(attachment);
attachmentInfoList.append(QVariant::fromValue(attachmentInfo));
if (attachment.mimeType().startsWith(QLatin1String("image")) || attachment.mimeType().startsWith(QLatin1String("video"))) {
// The message contains thumbnail as Base64 String, convert it back into image thumbnail
const QByteArray byteArray = attachment.base64EncodedFile().toUtf8();
QPixmap thumbnail;
thumbnail.loadFromData(QByteArray::fromBase64(byteArray));
m_thumbnailsProvider->addImage(attachment.uniqueIdentifier(), thumbnail.toImage());
}
}
item->setData(attachmentInfoList, AttachmentsRole);
insertRow(pos, item);
knownMessageIDs.insert(message.uID());
}
......
......@@ -27,6 +27,7 @@
#include "interfaces/conversationmessage.h"
#include "interfaces/dbusinterfaces.h"
#include "thumbnailsprovider.h"
#define INVALID_THREAD_ID -1
......@@ -47,6 +48,7 @@ public:
SenderRole, // The sender of the message. Undefined if this is an outgoing message
DateRole,
AvatarRole, // URI to the avatar of the sender of the message. Undefined if outgoing.
AttachmentsRole, // The list of attachments. Undefined if there is no attachment in a message
};
Q_ENUM(Roles)
......@@ -78,6 +80,7 @@ private:
void createRowFromMessage(const ConversationMessage &message, int pos);
DeviceConversationsDbusInterface* m_conversationsInterface;
ThumbnailsProvider* m_thumbnailsProvider;
QString m_deviceId;
qint64 m_threadId = INVALID_THREAD_ID;
QList<ConversationAddress> m_addressList;
......
......@@ -22,6 +22,7 @@
#include "conversationmodel.h"
#include "conversationlistmodel.h"
#include "conversationssortfilterproxymodel.h"
#include "thumbnailsprovider.h"
#include "kdeconnect-version.h"
#include <QApplication>
......@@ -74,6 +75,7 @@ int main(int argc, char *argv[])
QQmlApplicationEngine engine;
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
engine.addImageProvider(QStringLiteral("thumbnailsProvider"), new ThumbnailsProvider);
engine.rootContext()->setContextProperties({
{ QStringLiteral("initialMessage"), initialMessage },
{ QStringLiteral("initialDevice"), deviceid },
......
......@@ -35,6 +35,7 @@ Item {
property date dateTime
property string name
property bool multiTarget
property var attachmentList
signal messageCopyRequested(string message)
......@@ -60,6 +61,7 @@ Item {
height: messageColumn.height
Rectangle {
id: messageBox
Layout.maximumWidth: applicationWindow().wideScreen ? Math.min(messageColumn.contentWidth, root.width * 0.6) : messageColumn.contentWidth
Layout.fillWidth: true
Layout.alignment: root.sentByMe ? Qt.AlignRight : Qt.AlignLeft
......@@ -103,7 +105,7 @@ Item {
width: parent.width
height: childrenRect.height
property int contentWidth: Math.max(messageLabel.implicitWidth, dateLabel.implicitWidth)
property int contentWidth: Math.max(Math.max(messageLabel.implicitWidth, attachmentGrid.implicitWidth), dateLabel.implicitWidth)
Label {
id: authorLabel
width: parent.width
......@@ -115,8 +117,26 @@ Item {
horizontalAlignment: messageLabel.horizontalAlignment
}
Grid {
id: attachmentGrid
columns: 2
padding: attachmentList.length > 0 ? Kirigami.Units.largeSpacing : 0
layoutDirection: root.sentByMe ? Qt.RightToLeft : Qt.LeftToRight
Repeater {
model: attachmentList
delegate: MessageAttachments {
mimeType: modelData.mimeType
partID: modelData.partID
uniqueIdentifier: modelData.uniqueIdentifier
}
}
}
TextEdit {
id: messageLabel
visible: messageBody != ""
selectByMouse: true
readOnly: true
leftPadding: Kirigami.Units.largeSpacing
......@@ -141,8 +161,6 @@ Item {
}
}
Menu {
id: contextMenu
exit: Transition {PropertyAction { target: messageLabel; property: "persistentSelection"; value: false }}
......
......@@ -124,6 +124,7 @@ Kirigami.ScrollablePage
sentByMe: model.fromMe
dateTime: new Date(model.date)
multiTarget: isMultitarget
attachmentList: model.attachments
width: viewport.width
......
/**
* Copyright (C) 2020 Aniket Kumar <anikketkumar786@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import QtGraphicalEffects 1.12
import org.kde.kirigami 2.13 as Kirigami
import QtMultimedia 5.12
Item {
id: root
property int partID
property string mimeType
property string uniqueIdentifier
property string sourcePath
readonly property int elementWidth: 100
readonly property int elementHeight: 100
width: thumbnailElement.visible ? thumbnailElement.width : elementWidth
height: thumbnailElement.visible ? thumbnailElement.height : elementHeight
Image {
id: thumbnailElement
visible: mimeType.match("image") || mimeType.match("video")
source: visible ? "image://thumbnailsProvider/" + root.uniqueIdentifier : ""
property bool rounded: true
property bool adapt: true
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
layer.enabled: rounded
layer.effect: OpacityMask {
maskSource: Item {
width: thumbnailElement.width
height: thumbnailElement.height
Rectangle {
anchors.centerIn: parent
width: thumbnailElement.adapt ? thumbnailElement.width : Math.min(thumbnailElement.width, thumbnailElement.height)
height: thumbnailElement.adapt ? thumbnailElement.height : width
radius: messageBox.radius
}
}
}
Button {
icon.name: "media-playback-start"
visible: root.mimeType.match("video")
anchors.horizontalCenter: thumbnailElement.horizontalCenter
anchors.verticalCenter: thumbnailElement.verticalCenter
}
}
Rectangle {
id: audioElement
visible: root.mimeType.match("audio")
anchors.fill: parent
radius: messageBox.radius
color: "lightgrey"
ColumnLayout {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
spacing: Kirigami.Units.largeSpacing
Button {
id : audioPlayButton
icon.name: "media-playback-start"
Layout.alignment: Qt.AlignCenter
}
Label {
text: i18nd("kdeconnect-sms", "Audio clip")