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 }