File indexing completed on 2023-05-30 09:17:31

0001 /**
0002  * SPDX-FileCopyrightText: 2018 Aleix Pol Gonzalez <aleixpol@kde.org>
0003  * SPDX-FileCopyrightText: 2018 Simon Redman <simon@ergotech.com>
0004  *
0005  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006  */
0007 
0008 #include "conversationmodel.h"
0009 
0010 #include <KLocalizedString>
0011 #include <QQmlApplicationEngine>
0012 #include <QQmlContext>
0013 
0014 #include "attachmentinfo.h"
0015 #include "interfaces/conversationmessage.h"
0016 #include "smshelper.h"
0017 
0018 #include "sms_conversation_debug.h"
0019 
0020 ConversationModel::ConversationModel(QObject *parent)
0021     : QStandardItemModel(parent)
0022     , m_conversationsInterface(nullptr)
0023 {
0024     auto roles = roleNames();
0025     roles.insert(FromMeRole, "fromMe");
0026     roles.insert(DateRole, "date");
0027     roles.insert(SenderRole, "sender");
0028     roles.insert(AvatarRole, "avatar");
0029     roles.insert(AttachmentsRole, "attachments");
0030     setItemRoleNames(roles);
0031 }
0032 
0033 ConversationModel::~ConversationModel()
0034 {
0035 }
0036 
0037 qint64 ConversationModel::threadId() const
0038 {
0039     return m_threadId;
0040 }
0041 
0042 void ConversationModel::setThreadId(const qint64 &threadId)
0043 {
0044     if (m_threadId == threadId)
0045         return;
0046 
0047     m_threadId = threadId;
0048     clear();
0049     knownMessageIDs.clear();
0050     if (m_threadId != INVALID_THREAD_ID && !m_deviceId.isEmpty()) {
0051         requestMoreMessages();
0052         m_thumbnailsProvider->clear();
0053     }
0054 }
0055 
0056 void ConversationModel::setDeviceId(const QString &deviceId)
0057 {
0058     if (deviceId == m_deviceId)
0059         return;
0060 
0061     qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "setDeviceId"
0062                                                << "of" << this;
0063     if (m_conversationsInterface) {
0064         disconnect(m_conversationsInterface, SIGNAL(conversationUpdated(QDBusVariant)), this, SLOT(handleConversationUpdate(QDBusVariant)));
0065         disconnect(m_conversationsInterface, SIGNAL(conversationLoaded(qint64, quint64)), this, SLOT(handleConversationLoaded(qint64, quint64)));
0066         disconnect(m_conversationsInterface, SIGNAL(conversationCreated(QDBusVariant)), this, SLOT(handleConversationCreated(QDBusVariant)));
0067         delete m_conversationsInterface;
0068     }
0069 
0070     m_deviceId = deviceId;
0071 
0072     m_conversationsInterface = new DeviceConversationsDbusInterface(deviceId, this);
0073     connect(m_conversationsInterface, SIGNAL(conversationUpdated(QDBusVariant)), this, SLOT(handleConversationUpdate(QDBusVariant)));
0074     connect(m_conversationsInterface, SIGNAL(conversationLoaded(qint64, quint64)), this, SLOT(handleConversationLoaded(qint64, quint64)));
0075     connect(m_conversationsInterface, SIGNAL(conversationCreated(QDBusVariant)), this, SLOT(handleConversationCreated(QDBusVariant)));
0076 
0077     connect(m_conversationsInterface, SIGNAL(attachmentReceived(QString, QString)), this, SIGNAL(filePathReceived(QString, QString)));
0078 
0079     QQmlApplicationEngine *engine = qobject_cast<QQmlApplicationEngine *>(QQmlEngine::contextForObject(this)->engine());
0080     m_thumbnailsProvider = dynamic_cast<ThumbnailsProvider *>(engine->imageProvider(QStringLiteral("thumbnailsProvider")));
0081 
0082     // Clear any previous data on device change
0083     m_thumbnailsProvider->clear();
0084 }
0085 
0086 void ConversationModel::setAddressList(const QList<ConversationAddress> &addressList)
0087 {
0088     m_addressList = addressList;
0089 }
0090 
0091 bool ConversationModel::sendReplyToConversation(const QString &textMessage, QList<QUrl> attachmentUrls)
0092 {
0093     QVariantList fileUrls;
0094     for (const auto &url : attachmentUrls) {
0095         fileUrls << QVariant::fromValue(url.toLocalFile());
0096     }
0097 
0098     m_conversationsInterface->replyToConversation(m_threadId, textMessage, fileUrls);
0099     return true;
0100 }
0101 
0102 bool ConversationModel::startNewConversation(const QString &textMessage, const QList<ConversationAddress> &addressList, QList<QUrl> attachmentUrls)
0103 {
0104     QVariantList addresses;
0105 
0106     for (const auto &address : addressList) {
0107         addresses << QVariant::fromValue(address);
0108     }
0109 
0110     QVariantList fileUrls;
0111     for (const auto &url : attachmentUrls) {
0112         fileUrls << QVariant::fromValue(url.toLocalFile());
0113     }
0114 
0115     m_conversationsInterface->sendWithoutConversation(addresses, textMessage, fileUrls);
0116     return true;
0117 }
0118 
0119 void ConversationModel::requestMoreMessages(const quint32 &howMany)
0120 {
0121     if (m_threadId == INVALID_THREAD_ID) {
0122         return;
0123     }
0124     const auto &numMessages = knownMessageIDs.size();
0125     m_conversationsInterface->requestConversation(m_threadId, numMessages, numMessages + howMany);
0126 }
0127 
0128 void ConversationModel::createRowFromMessage(const ConversationMessage &message, int pos)
0129 {
0130     if (message.threadID() != m_threadId) {
0131         // Because of the asynchronous nature of the current implementation of this model, if the
0132         // user clicks quickly between threads or for some other reason a message comes when we're
0133         // not expecting it, we should not display it in the wrong place
0134         qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "Got a message for a thread" << message.threadID() << "but we are currently viewing" << m_threadId
0135                                                    << "Discarding.";
0136         return;
0137     }
0138 
0139     if (knownMessageIDs.contains(message.uID())) {
0140         qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "Ignoring duplicate message with ID" << message.uID();
0141         return;
0142     }
0143 
0144     ConversationAddress sender;
0145     if (!message.addresses().isEmpty()) {
0146         sender = message.addresses().first();
0147     } else {
0148         qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "Conversation with ID " << message.threadID() << " did not have any addresses";
0149     }
0150 
0151     QString senderName = message.isIncoming() ? SmsHelper::getTitleForAddresses({sender}) : QString();
0152     QString displayBody = message.body();
0153 
0154     auto item = new QStandardItem;
0155     item->setText(displayBody);
0156     item->setData(message.isOutgoing(), FromMeRole);
0157     item->setData(message.date(), DateRole);
0158     item->setData(senderName, SenderRole);
0159 
0160     QList<QVariant> attachmentInfoList;
0161     const QList<Attachment> attachmentList = message.attachments();
0162 
0163     for (const Attachment &attachment : attachmentList) {
0164         AttachmentInfo attachmentInfo(attachment);
0165         attachmentInfoList.append(QVariant::fromValue(attachmentInfo));
0166 
0167         if (attachment.mimeType().startsWith(QLatin1String("image")) || attachment.mimeType().startsWith(QLatin1String("video"))) {
0168             // The message contains thumbnail as Base64 String, convert it back into image thumbnail
0169             const QByteArray byteArray = attachment.base64EncodedFile().toUtf8();
0170             QPixmap thumbnail;
0171             thumbnail.loadFromData(QByteArray::fromBase64(byteArray));
0172 
0173             m_thumbnailsProvider->addImage(attachment.uniqueIdentifier(), thumbnail.toImage());
0174         }
0175     }
0176 
0177     item->setData(attachmentInfoList, AttachmentsRole);
0178 
0179     insertRow(pos, item);
0180     knownMessageIDs.insert(message.uID());
0181 }
0182 
0183 void ConversationModel::handleConversationUpdate(const QDBusVariant &msg)
0184 {
0185     ConversationMessage message = ConversationMessage::fromDBus(msg);
0186 
0187     if (message.threadID() != m_threadId) {
0188         // If a conversation which we are not currently viewing was updated, discard the information
0189         qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "Saw update for thread" << message.threadID() << "but we are currently viewing" << m_threadId;
0190         return;
0191     }
0192     createRowFromMessage(message, 0);
0193 }
0194 
0195 void ConversationModel::handleConversationCreated(const QDBusVariant &msg)
0196 {
0197     ConversationMessage message = ConversationMessage::fromDBus(msg);
0198 
0199     if (m_threadId == INVALID_THREAD_ID && SmsHelper::isPhoneNumberMatch(m_addressList[0].address(), message.addresses().first().address())
0200         && !message.isMultitarget()) {
0201         m_threadId = message.threadID();
0202         createRowFromMessage(message, 0);
0203     }
0204 }
0205 
0206 void ConversationModel::handleConversationLoaded(qint64 threadID, quint64 numMessages)
0207 {
0208     Q_UNUSED(numMessages)
0209     if (threadID != m_threadId) {
0210         return;
0211     }
0212     // If we get this flag, it means that the phone will not be responding with any more messages
0213     // so we should not be showing a loading indicator
0214     Q_EMIT loadingFinished();
0215 }
0216 
0217 QString ConversationModel::getCharCountInfo(const QString &message) const
0218 {
0219     SmsCharCount count = SmsHelper::getCharCount(message);
0220 
0221     if (count.messages > 1) {
0222         // Show remaining char count and message count
0223         return QString::number(count.remaining) + QLatin1Char('/') + QString::number(count.messages);
0224     }
0225     if (count.messages == 1 && count.remaining < 10) {
0226         // Show only remaining char count
0227         return QString::number(count.remaining);
0228     } else {
0229         // Do not show anything
0230         return QString();
0231     }
0232 }
0233 
0234 void ConversationModel::requestAttachmentPath(const qint64 &partID, const QString &uniqueIdentifier)
0235 {
0236     m_conversationsInterface->requestAttachmentFile(partID, uniqueIdentifier);
0237 }