File indexing completed on 2024-04-28 08:49:14
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"