File indexing completed on 2024-04-21 04:56:56

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, &DeviceConversationsDbusInterface::conversationUpdated, this, &ConversationModel::handleConversationUpdate);
0065         disconnect(m_conversationsInterface, &DeviceConversationsDbusInterface::conversationLoaded, this, &ConversationModel::handleConversationLoaded);
0066         disconnect(m_conversationsInterface, &DeviceConversationsDbusInterface::conversationCreated, this, &ConversationModel::handleConversationCreated);
0067         delete m_conversationsInterface;
0068     }
0069 
0070     m_deviceId = deviceId;
0071 
0072     m_conversationsInterface = new DeviceConversationsDbusInterface(deviceId, this);
0073     connect(m_conversationsInterface, &DeviceConversationsDbusInterface::conversationUpdated, this, &ConversationModel::handleConversationUpdate);
0074     connect(m_conversationsInterface, &DeviceConversationsDbusInterface::conversationLoaded, this, &ConversationModel::handleConversationLoaded);
0075     connect(m_conversationsInterface, &DeviceConversationsDbusInterface::conversationCreated, this, &ConversationModel::handleConversationCreated);
0076 
0077     connect(m_conversationsInterface, &DeviceConversationsDbusInterface::attachmentReceived, this, &ConversationModel::filePathReceived);
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     Q_EMIT deviceIdChanged(deviceId);
0086 }
0087 
0088 void ConversationModel::setAddressList(const QList<ConversationAddress> &addressList)
0089 {
0090     m_addressList = addressList;
0091 }
0092 
0093 bool ConversationModel::sendReplyToConversation(const QString &textMessage, QList<QUrl> attachmentUrls)
0094 {
0095     QVariantList fileUrls;
0096     for (const auto &url : attachmentUrls) {
0097         fileUrls << QVariant::fromValue(url.toLocalFile());
0098     }
0099 
0100     m_conversationsInterface->replyToConversation(m_threadId, textMessage, fileUrls);
0101     return true;
0102 }
0103 
0104 bool ConversationModel::startNewConversation(const QString &textMessage, const QList<ConversationAddress> &addressList, QList<QUrl> attachmentUrls)
0105 {
0106     QVariantList addresses;
0107 
0108     for (const auto &address : addressList) {
0109         addresses << QVariant::fromValue(address);
0110     }
0111 
0112     QVariantList fileUrls;
0113     for (const auto &url : attachmentUrls) {
0114         fileUrls << QVariant::fromValue(url.toLocalFile());
0115     }
0116 
0117     m_conversationsInterface->sendWithoutConversation(addresses, textMessage, fileUrls);
0118     return true;
0119 }
0120 
0121 void ConversationModel::requestMoreMessages(const quint32 &howMany)
0122 {
0123     if (m_threadId == INVALID_THREAD_ID) {
0124         return;
0125     }
0126     const auto &numMessages = knownMessageIDs.size();
0127     m_conversationsInterface->requestConversation(m_threadId, numMessages, numMessages + howMany);
0128 }
0129 
0130 void ConversationModel::createRowFromMessage(const ConversationMessage &message, int pos)
0131 {
0132     if (message.threadID() != m_threadId) {
0133         // Because of the asynchronous nature of the current implementation of this model, if the
0134         // user clicks quickly between threads or for some other reason a message comes when we're
0135         // not expecting it, we should not display it in the wrong place
0136         qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "Got a message for a thread" << message.threadID() << "but we are currently viewing" << m_threadId
0137                                                    << "Discarding.";
0138         return;
0139     }
0140 
0141     if (knownMessageIDs.contains(message.uID())) {
0142         qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "Ignoring duplicate message with ID" << message.uID();
0143         return;
0144     }
0145 
0146     ConversationAddress sender;
0147     if (!message.addresses().isEmpty()) {
0148         sender = message.addresses().first();
0149     } else {
0150         qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "Conversation with ID " << message.threadID() << " did not have any addresses";
0151     }
0152 
0153     QString senderName = message.isIncoming() ? SmsHelper::getTitleForAddresses({sender}) : QString();
0154     QString displayBody = message.body();
0155 
0156     auto item = new QStandardItem;
0157     item->setText(displayBody);
0158     item->setData(message.isOutgoing(), FromMeRole);
0159     item->setData(message.date(), DateRole);
0160     item->setData(senderName, SenderRole);
0161 
0162     QList<QVariant> attachmentInfoList;
0163     const QList<Attachment> attachmentList = message.attachments();
0164 
0165     for (const Attachment &attachment : attachmentList) {
0166         AttachmentInfo attachmentInfo(attachment);
0167         attachmentInfoList.append(QVariant::fromValue(attachmentInfo));
0168 
0169         if (attachment.mimeType().startsWith(QLatin1String("image")) || attachment.mimeType().startsWith(QLatin1String("video"))) {
0170             // The message contains thumbnail as Base64 String, convert it back into image thumbnail
0171             const QByteArray byteArray = attachment.base64EncodedFile().toUtf8();
0172             QPixmap thumbnail;
0173             thumbnail.loadFromData(QByteArray::fromBase64(byteArray));
0174 
0175             m_thumbnailsProvider->addImage(attachment.uniqueIdentifier(), thumbnail.toImage());
0176         }
0177     }
0178 
0179     item->setData(attachmentInfoList, AttachmentsRole);
0180 
0181     insertRow(pos, item);
0182     knownMessageIDs.insert(message.uID());
0183 }
0184 
0185 void ConversationModel::handleConversationUpdate(const QDBusVariant &msg)
0186 {
0187     ConversationMessage message = ConversationMessage::fromDBus(msg);
0188 
0189     if (message.threadID() != m_threadId) {
0190         // If a conversation which we are not currently viewing was updated, discard the information
0191         qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "Saw update for thread" << message.threadID() << "but we are currently viewing" << m_threadId;
0192         return;
0193     }
0194     createRowFromMessage(message, 0);
0195 }
0196 
0197 void ConversationModel::handleConversationCreated(const QDBusVariant &msg)
0198 {
0199     ConversationMessage message = ConversationMessage::fromDBus(msg);
0200 
0201     if (m_threadId == INVALID_THREAD_ID && SmsHelper::isPhoneNumberMatch(m_addressList[0].address(), message.addresses().first().address())
0202         && !message.isMultitarget()) {
0203         m_threadId = message.threadID();
0204         createRowFromMessage(message, 0);
0205     }
0206 }
0207 
0208 void ConversationModel::handleConversationLoaded(qint64 threadID)
0209 {
0210     if (threadID != m_threadId) {
0211         return;
0212     }
0213     // If we get this flag, it means that the phone will not be responding with any more messages
0214     // so we should not be showing a loading indicator
0215     Q_EMIT loadingFinished();
0216 }
0217 
0218 QString ConversationModel::getCharCountInfo(const QString &message) const
0219 {
0220     SmsCharCount count = SmsHelper::getCharCount(message);
0221 
0222     if (count.messages > 1) {
0223         // Show remaining char count and message count
0224         return QString::number(count.remaining) + QLatin1Char('/') + QString::number(count.messages);
0225     }
0226     if (count.messages == 1 && count.remaining < 10) {
0227         // Show only remaining char count
0228         return QString::number(count.remaining);
0229     } else {
0230         // Do not show anything
0231         return QString();
0232     }
0233 }
0234 
0235 void ConversationModel::requestAttachmentPath(const qint64 &partID, const QString &uniqueIdentifier)
0236 {
0237     m_conversationsInterface->requestAttachmentFile(partID, uniqueIdentifier);
0238 }
0239 
0240 #include "moc_conversationmodel.cpp"