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