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 "conversationlistmodel.h" 0009 0010 #include <QPainter> 0011 #include <QString> 0012 0013 #include <KLocalizedString> 0014 0015 #include "interfaces/conversationmessage.h" 0016 #include "interfaces/dbusinterfaces.h" 0017 #include "sms_conversations_list_debug.h" 0018 #include "smshelper.h" 0019 0020 #define INVALID_THREAD_ID -1 0021 #define INVALID_DATE -1 0022 0023 ConversationListModel::ConversationListModel(QObject *parent) 0024 : QStandardItemModel(parent) 0025 , m_conversationsInterface(nullptr) 0026 { 0027 // qCDebug(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "Constructing" << this; 0028 auto roles = roleNames(); 0029 roles.insert(FromMeRole, "fromMe"); 0030 roles.insert(SenderRole, "sender"); 0031 roles.insert(DateRole, "date"); 0032 roles.insert(AddressesRole, "addresses"); 0033 roles.insert(ConversationIdRole, "conversationId"); 0034 roles.insert(MultitargetRole, "isMultitarget"); 0035 roles.insert(AttachmentPreview, "attachmentPreview"); 0036 setItemRoleNames(roles); 0037 0038 ConversationMessage::registerDbusType(); 0039 } 0040 0041 ConversationListModel::~ConversationListModel() 0042 { 0043 } 0044 0045 void ConversationListModel::setDeviceId(const QString &deviceId) 0046 { 0047 if (deviceId == m_deviceId) { 0048 return; 0049 } 0050 0051 if (deviceId.isEmpty()) { 0052 return; 0053 } 0054 0055 qCDebug(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "setDeviceId" << deviceId << "of" << this; 0056 0057 if (m_conversationsInterface) { 0058 disconnect(m_conversationsInterface, SIGNAL(conversationCreated(QDBusVariant)), this, SLOT(handleCreatedConversation(QDBusVariant))); 0059 disconnect(m_conversationsInterface, SIGNAL(conversationUpdated(QDBusVariant)), this, SLOT(handleConversationUpdated(QDBusVariant))); 0060 delete m_conversationsInterface; 0061 m_conversationsInterface = nullptr; 0062 } 0063 0064 // This method still gets called *with a valid deviceID* when the device is not connected while the component is setting up 0065 // Detect that case and don't do anything. 0066 DeviceDbusInterface device(deviceId); 0067 if (!(device.isValid() && device.isReachable())) { 0068 return; 0069 } 0070 0071 m_deviceId = deviceId; 0072 Q_EMIT deviceIdChanged(); 0073 0074 m_conversationsInterface = new DeviceConversationsDbusInterface(deviceId, this); 0075 connect(m_conversationsInterface, SIGNAL(conversationCreated(QDBusVariant)), this, SLOT(handleCreatedConversation(QDBusVariant))); 0076 connect(m_conversationsInterface, SIGNAL(conversationUpdated(QDBusVariant)), this, SLOT(handleConversationUpdated(QDBusVariant))); 0077 0078 refresh(); 0079 } 0080 0081 void ConversationListModel::refresh() 0082 { 0083 if (m_deviceId.isEmpty()) { 0084 qWarning() << "refreshing null device"; 0085 return; 0086 } 0087 0088 prepareConversationsList(); 0089 m_conversationsInterface->requestAllConversationThreads(); 0090 } 0091 0092 void ConversationListModel::prepareConversationsList() 0093 { 0094 if (!m_conversationsInterface->isValid()) { 0095 qCWarning(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "Tried to prepareConversationsList with an invalid interface!"; 0096 return; 0097 } 0098 const QDBusPendingReply<QVariantList> validThreadIDsReply = m_conversationsInterface->activeConversations(); 0099 0100 setWhenAvailable( 0101 validThreadIDsReply, 0102 [this](const QVariantList &convs) { 0103 clear(); // If we clear before we receive the reply, there might be a (several second) visual gap! 0104 for (const QVariant &headMessage : convs) { 0105 createRowFromMessage(qdbus_cast<ConversationMessage>(headMessage)); 0106 } 0107 displayContacts(); 0108 }, 0109 this); 0110 } 0111 0112 void ConversationListModel::handleCreatedConversation(const QDBusVariant &msg) 0113 { 0114 const ConversationMessage message = ConversationMessage::fromDBus(msg); 0115 createRowFromMessage(message); 0116 } 0117 0118 void ConversationListModel::handleConversationUpdated(const QDBusVariant &msg) 0119 { 0120 const ConversationMessage message = ConversationMessage::fromDBus(msg); 0121 createRowFromMessage(message); 0122 } 0123 0124 void ConversationListModel::printDBusError(const QDBusError &error) 0125 { 0126 qCWarning(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << error; 0127 } 0128 0129 QStandardItem *ConversationListModel::conversationForThreadId(qint32 threadId) 0130 { 0131 for (int i = 0, c = rowCount(); i < c; ++i) { 0132 auto it = item(i, 0); 0133 if (it->data(ConversationIdRole) == threadId) 0134 return it; 0135 } 0136 return nullptr; 0137 } 0138 0139 QStandardItem *ConversationListModel::getConversationForAddress(const QString &address) 0140 { 0141 for (int i = 0; i < rowCount(); ++i) { 0142 const auto &it = item(i, 0); 0143 if (!it->data(MultitargetRole).toBool()) { 0144 if (SmsHelper::isPhoneNumberMatch(it->data(SenderRole).toString(), address)) { 0145 return it; 0146 } 0147 } 0148 } 0149 return nullptr; 0150 } 0151 0152 void ConversationListModel::createRowFromMessage(const ConversationMessage &message) 0153 { 0154 if (message.type() == -1) { 0155 // The Android side currently hacks in -1 if something weird comes up 0156 // TODO: Remove this hack when MMS support is implemented 0157 return; 0158 } 0159 0160 /** The address of everyone involved in this conversation, which we should not display (check if they are known contacts first) */ 0161 const QList<ConversationAddress> rawAddresses = message.addresses(); 0162 if (rawAddresses.isEmpty()) { 0163 qWarning() << "no addresses!" << message.body(); 0164 return; 0165 } 0166 0167 bool toadd = false; 0168 QStandardItem *item = conversationForThreadId(message.threadID()); 0169 // Check if we have a contact with which to associate this message, needed if there is no conversation with the contact and we received a message from them 0170 if (!item && !message.isMultitarget()) { 0171 item = getConversationForAddress(rawAddresses[0].address()); 0172 if (item) { 0173 item->setData(message.threadID(), ConversationIdRole); 0174 } 0175 } 0176 0177 if (!item) { 0178 toadd = true; 0179 item = new QStandardItem(); 0180 0181 const QString displayNames = SmsHelper::getTitleForAddresses(rawAddresses); 0182 const QIcon displayIcon = SmsHelper::getIconForAddresses(rawAddresses); 0183 0184 item->setText(displayNames); 0185 item->setIcon(displayIcon); 0186 item->setData(message.threadID(), ConversationIdRole); 0187 item->setData(rawAddresses[0].address(), SenderRole); 0188 } 0189 0190 // Get the body that we should display 0191 QString displayBody; 0192 if (message.containsTextBody()) { 0193 displayBody = message.body(); 0194 } else if (message.containsAttachment()) { 0195 const QString mimeType = message.attachments().last().mimeType(); 0196 if (mimeType.startsWith(QStringLiteral("image"))) { 0197 displayBody = i18nc("Used as a text placeholder when the most-recent message is an image", "Picture"); 0198 } else if (mimeType.startsWith(QStringLiteral("video"))) { 0199 displayBody = i18nc("Used as a text placeholder when the most-recent message is a video", "Video"); 0200 } else { 0201 // Craft a somewhat-descriptive string, like "pdf file" 0202 displayBody = i18nc("Used as a text placeholder when the most-recent message is an arbitrary attachment, resulting in something like \"pdf file\"", 0203 "%1 file", 0204 mimeType.right(mimeType.indexOf(QStringLiteral("/")))); 0205 } 0206 } 0207 0208 // Get the preview from the attachment, if it exists 0209 QIcon attachmentPreview; 0210 if (message.containsAttachment()) { 0211 attachmentPreview = SmsHelper::getThumbnailForAttachment(message.attachments().last()); 0212 } 0213 0214 // For displaying single line subtitle out of the multiline messages to keep the ListItems consistent 0215 displayBody = displayBody.mid(0, displayBody.indexOf(QStringLiteral("\n"))); 0216 0217 // Prepend the sender's name 0218 if (message.isOutgoing()) { 0219 displayBody = i18n("You: %1", displayBody); 0220 } else { 0221 // If the message is incoming, the sender is the first Address 0222 const QString senderAddress = item->data(SenderRole).toString(); 0223 const auto sender = SmsHelper::lookupPersonByAddress(senderAddress); 0224 const QString senderName = sender == nullptr ? senderAddress : SmsHelper::lookupPersonByAddress(senderAddress)->name(); 0225 displayBody = i18n("%1: %2", senderName, displayBody); 0226 } 0227 0228 // Update the message if the data is newer 0229 // This will be true if a conversation receives a new message, but false when the user 0230 // does something to trigger past conversation history loading 0231 bool oldDateExists; 0232 const qint64 oldDate = item->data(DateRole).toLongLong(&oldDateExists); 0233 if (!oldDateExists || message.date() >= oldDate) { 0234 // If there was no old data or incoming data is newer, update the record 0235 item->setData(QVariant::fromValue(message.addresses()), AddressesRole); 0236 item->setData(message.isOutgoing(), FromMeRole); 0237 item->setData(displayBody, Qt::ToolTipRole); 0238 item->setData(message.date(), DateRole); 0239 item->setData(message.isMultitarget(), MultitargetRole); 0240 if (!attachmentPreview.isNull()) { 0241 item->setData(attachmentPreview, AttachmentPreview); 0242 } 0243 } 0244 0245 if (toadd) 0246 appendRow(item); 0247 } 0248 0249 void ConversationListModel::displayContacts() 0250 { 0251 const QList<QSharedPointer<KPeople::PersonData>> personDataList = SmsHelper::getAllPersons(); 0252 0253 for (const auto &person : personDataList) { 0254 const QVariantList allPhoneNumbers = person->contactCustomProperty(QStringLiteral("all-phoneNumber")).toList(); 0255 0256 for (const QVariant &rawPhoneNumber : allPhoneNumbers) { 0257 // check for any duplicate phoneNumber and eliminate it 0258 if (!getConversationForAddress(rawPhoneNumber.toString())) { 0259 QStandardItem *item = new QStandardItem(); 0260 item->setText(person->name()); 0261 item->setIcon(person->photo()); 0262 0263 QList<ConversationAddress> addresses; 0264 addresses.append(ConversationAddress(rawPhoneNumber.toString())); 0265 item->setData(QVariant::fromValue(addresses), AddressesRole); 0266 0267 const QString displayBody = i18n("%1", rawPhoneNumber.toString()); 0268 item->setData(displayBody, Qt::ToolTipRole); 0269 item->setData(false, MultitargetRole); 0270 item->setData(qint64(INVALID_THREAD_ID), ConversationIdRole); 0271 item->setData(qint64(INVALID_DATE), DateRole); 0272 item->setData(rawPhoneNumber.toString(), SenderRole); 0273 appendRow(item); 0274 } 0275 } 0276 } 0277 } 0278 0279 void ConversationListModel::createConversationForAddress(const QString &address) 0280 { 0281 QStandardItem *item = new QStandardItem(); 0282 const QString canonicalizedAddress = SmsHelper::canonicalizePhoneNumber(address); 0283 item->setText(canonicalizedAddress); 0284 0285 QList<ConversationAddress> addresses; 0286 addresses.append(ConversationAddress(canonicalizedAddress)); 0287 item->setData(QVariant::fromValue(addresses), AddressesRole); 0288 0289 QString displayBody = i18n("%1", canonicalizedAddress); 0290 item->setData(displayBody, Qt::ToolTipRole); 0291 item->setData(false, MultitargetRole); 0292 item->setData(qint64(INVALID_THREAD_ID), ConversationIdRole); 0293 item->setData(qint64(INVALID_DATE), DateRole); 0294 item->setData(address, SenderRole); 0295 appendRow(item); 0296 }