File indexing completed on 2024-05-19 05:14:36
0001 /* 0002 This file is part of KAddressBook. 0003 0004 SPDX-FileCopyrightText: 2020 Konrad Czapla <kondzio89dev@gmail.com> 0005 0006 SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 #include "contactinfoproxymodel.h" 0010 0011 #include "kaddressbook_debug.h" 0012 0013 #include <Akonadi/Item> 0014 #include <Akonadi/ItemFetchJob> 0015 #include <Akonadi/ItemFetchScope> 0016 #include <Akonadi/Monitor> 0017 #include <KContacts/Addressee> 0018 #include <KLocalizedString> 0019 0020 #include <KJob> 0021 0022 ContactInfoProxyModel::ContactInfoProxyModel(QObject *parent) 0023 : QIdentityProxyModel(parent) 0024 , mMonitor(new Akonadi::Monitor(this)) 0025 { 0026 mMonitor->setTypeMonitored(Akonadi::Monitor::Items); 0027 mMonitor->itemFetchScope().fetchFullPayload(true); 0028 connect(mMonitor, &Akonadi::Monitor::itemChanged, this, &ContactInfoProxyModel::slotItemChanged); 0029 connect(this, &ContactInfoProxyModel::rowsAboutToBeRemoved, this, &ContactInfoProxyModel::slotRowsAboutToBeRemoved); 0030 } 0031 0032 QVariant ContactInfoProxyModel::data(const QModelIndex &index, int role) const 0033 { 0034 if (!index.isValid()) { 0035 return {}; 0036 qCWarning(KADDRESSBOOK_LOG) << "invalid index!"; 0037 } 0038 if (role >= Roles::PictureRole && role <= Roles::DescriptionRole) { 0039 const auto item = index.data(Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>(); 0040 Q_ASSERT(item.isValid()); 0041 if (item.hasPayload<KContacts::Addressee>()) { 0042 const auto contact = item.payload<KContacts::Addressee>(); 0043 switch (role) { 0044 case Roles::PictureRole: 0045 return contact.photo().data(); 0046 case Roles::InitialsRole: 0047 return getInitials(contact); 0048 case Roles::DescriptionRole: 0049 return getDescription(contact); 0050 } 0051 } else if (item.hasPayload<KContacts::ContactGroup>()) { 0052 const auto groupContacts = item.payload<KContacts::ContactGroup>(); 0053 if (!mPendingGroupItems.contains(item.id())) { 0054 if (!mGroupsCache.contains(item.id())) { 0055 mMonitor->setItemMonitored(item); 0056 mGroupsCache[item.id()] = ContactCacheData::List(); 0057 } 0058 0059 if (groupContacts.contactReferenceCount() > 0 && isCacheItemToFetch(item.id(), groupContacts)) { 0060 resolveGroup(item.id(), groupContacts); 0061 } 0062 } 0063 switch (role) { 0064 case Roles::PictureRole: 0065 return {}; 0066 case Roles::InitialsRole: 0067 return mPendingGroupItems.contains(item.id()) ? i18n("...") : getInitials(item.id(), groupContacts); 0068 case Roles::DescriptionRole: 0069 return mPendingGroupItems.contains(item.id()) ? i18n("Loading contacts details ...") : getDescription(item.id(), groupContacts); 0070 } 0071 } 0072 } 0073 return QIdentityProxyModel::data(index, role); 0074 } 0075 0076 QString ContactInfoProxyModel::getInitials(const KContacts::Addressee &contact) const 0077 { 0078 QString initials; 0079 if (!contact.givenName().isEmpty()) { 0080 initials.append(contact.givenName().front()); 0081 } 0082 if (!contact.familyName().isEmpty()) { 0083 initials.append(contact.familyName().front()); 0084 } 0085 0086 if (initials.isEmpty() && !contact.realName().isEmpty()) { 0087 initials.append(contact.realName().front()); 0088 } 0089 0090 return initials.toUpper(); 0091 } 0092 0093 QString ContactInfoProxyModel::getInitials(const Akonadi::Item::Id groupItemId, const KContacts::ContactGroup &groupContacts) const 0094 { 0095 QString initials; 0096 0097 for (int idx = 0; idx < groupContacts.dataCount(); idx++) { 0098 const QString name = groupContacts.data(idx).name().trimmed(); 0099 if (!name.isEmpty()) { 0100 initials.append(name.front()); 0101 } 0102 } 0103 0104 const auto groupCacheId = mGroupsCache[groupItemId]; 0105 for (const ContactCacheData &cacheContact : groupCacheId) { 0106 const QString name = cacheContact.name().trimmed(); 0107 if (!name.isEmpty()) { 0108 initials.append(name.front()); 0109 } 0110 } 0111 0112 return initials.toUpper(); 0113 } 0114 0115 QString ContactInfoProxyModel::getDescription(const KContacts::Addressee &contact) const 0116 { 0117 QString dataSeparator; 0118 QString emailAddress; 0119 QString phone; 0120 0121 if (!contact.preferredEmail().isEmpty()) { 0122 emailAddress = i18n("Email: %1", contact.preferredEmail()); 0123 } 0124 const QList<KContacts::PhoneNumber> phoneList = contact.phoneNumbers().toList(); 0125 QList<KContacts::PhoneNumber>::const_reverse_iterator itPhone = 0126 std::find_if(phoneList.rbegin(), phoneList.rend(), [&phoneList](const KContacts::PhoneNumber &phone) { 0127 return phone.isPreferred() || phoneList.at(0) == phone; 0128 }); 0129 if (itPhone != phoneList.rend()) { 0130 phone = i18n("Phone: %1", (*itPhone).number()); 0131 } 0132 if (!emailAddress.isEmpty() && !phone.isEmpty()) { 0133 dataSeparator = QStringLiteral(","); 0134 } 0135 0136 return i18n("%1%2 %3", emailAddress, dataSeparator, phone).trimmed(); 0137 } 0138 0139 QString ContactInfoProxyModel::getDescription(const Akonadi::Item::Id groupItemId, const KContacts::ContactGroup &groupContacts) const 0140 { 0141 QStringList groupDescription; 0142 QString contactDescription; 0143 0144 for (int idx = 0; idx < groupContacts.dataCount(); idx++) { 0145 QString dataSeparator; 0146 if (!groupContacts.data(idx).name().isEmpty() && !groupContacts.data(idx).email().isEmpty()) { 0147 dataSeparator = QStringLiteral("-"); 0148 } 0149 contactDescription = i18n("%1 %2 %3", groupContacts.data(idx).name(), dataSeparator, groupContacts.data(idx).email()); 0150 groupDescription << contactDescription.trimmed(); 0151 contactDescription.clear(); 0152 } 0153 for (int idx = 0; idx < groupContacts.contactReferenceCount(); idx++) { 0154 const KContacts::ContactGroup::ContactReference contactRef = groupContacts.contactReference(idx); 0155 0156 ContactCacheData::ConstListIterator it = findCacheItem(groupItemId, contactRef); 0157 if (it != mGroupsCache[groupItemId].end()) { 0158 QString cacheSeparator; 0159 QString email; 0160 email = contactRef.preferredEmail().isEmpty() ? it->email() : contactRef.preferredEmail(); 0161 if (it->name().isEmpty() && email.isEmpty()) { 0162 continue; 0163 } else if (!it->name().isEmpty() && !email.isEmpty()) { 0164 cacheSeparator = QStringLiteral("-"); 0165 } 0166 contactDescription = i18n("%1 %2 %3", it->name(), cacheSeparator, email); 0167 groupDescription << contactDescription.trimmed(); 0168 contactDescription.clear(); 0169 } 0170 } 0171 return groupDescription.join(QStringLiteral(", ")); 0172 } 0173 0174 QStringList ContactInfoProxyModel::getIdsContactGroup(const KContacts::ContactGroup &group) const 0175 { 0176 QStringList groupRefIds; 0177 groupRefIds.reserve(group.contactReferenceCount()); 0178 for (int idx = 0; idx < group.contactReferenceCount(); idx++) { 0179 const KContacts::ContactGroup::ContactReference &reference = group.contactReference(idx); 0180 0181 groupRefIds += reference.gid().isEmpty() ? reference.uid() : reference.gid(); 0182 } 0183 return groupRefIds; 0184 } 0185 0186 QStringList ContactInfoProxyModel::getIdsCacheContactGroup(const Akonadi::Item::Id groupItemId) const 0187 { 0188 QStringList groupCacheRefIds; 0189 groupCacheRefIds.reserve(mGroupsCache[groupItemId].size()); 0190 for (const auto &cacheContact : mGroupsCache[groupItemId]) { 0191 groupCacheRefIds += cacheContact.gid().isEmpty() ? cacheContact.uid() : cacheContact.gid(); 0192 } 0193 return groupCacheRefIds; 0194 } 0195 0196 bool ContactInfoProxyModel::isCacheItemToFetch(const Akonadi::Item::Id groupItemId, const KContacts::ContactGroup &group) const 0197 { 0198 QStringList groupRefIds = getIdsContactGroup(group); 0199 QStringList groupCacheRefIds = getIdsCacheContactGroup(groupItemId); 0200 0201 auto sortFunc = [](const QString &lhs, const QString &rhs) -> bool { 0202 return lhs.toLongLong() < rhs.toLongLong(); 0203 }; 0204 0205 std::sort(groupRefIds.begin(), groupRefIds.end(), sortFunc); 0206 groupRefIds.erase(std::unique(groupRefIds.begin(), groupRefIds.end()), groupRefIds.end()); 0207 0208 std::sort(groupCacheRefIds.begin(), groupCacheRefIds.end(), sortFunc); 0209 0210 return !std::equal(groupRefIds.begin(), groupRefIds.end(), groupCacheRefIds.begin(), groupCacheRefIds.end()); 0211 } 0212 0213 ContactInfoProxyModel::ContactCacheData::ListIterator ContactInfoProxyModel::findCacheItem(const Akonadi::Item::Id groupItemId, 0214 const ContactInfoProxyModel::ContactCacheData &cacheContact) 0215 { 0216 ContactCacheData::ListIterator it = 0217 std::find_if(mGroupsCache[groupItemId].begin(), mGroupsCache[groupItemId].end(), [&cacheContact](const ContactCacheData &contact) -> bool { 0218 return contact == cacheContact; 0219 }); 0220 return it; 0221 } 0222 0223 ContactInfoProxyModel::ContactCacheData::ConstListIterator 0224 ContactInfoProxyModel::findCacheItem(const Akonadi::Item::Id groupItemId, const ContactInfoProxyModel::ContactCacheData &cacheContact) const 0225 { 0226 ContactCacheData::ConstListIterator it = 0227 std::find_if(mGroupsCache[groupItemId].cbegin(), mGroupsCache[groupItemId].cend(), [&cacheContact](const ContactCacheData &contact) -> bool { 0228 return contact == cacheContact; 0229 }); 0230 return it; 0231 } 0232 0233 QMap<const char *, QVariant> ContactInfoProxyModel::buildFetchProperties(const Akonadi::Item::Id groupItemId) const 0234 { 0235 return QMap<const char *, QVariant>{ 0236 {"groupItemId", QVariant::fromValue((groupItemId))}, 0237 }; 0238 } 0239 0240 void ContactInfoProxyModel::resolveGroup(const Akonadi::Item::Id groupItemId, const KContacts::ContactGroup &groupContacts) const 0241 { 0242 Akonadi::Item::List groupItemsList; 0243 0244 for (int idx = 0; idx < groupContacts.contactReferenceCount(); idx++) { 0245 const KContacts::ContactGroup::ContactReference contactRef = groupContacts.contactReference(idx); 0246 0247 if (findCacheItem(groupItemId, contactRef) == mGroupsCache[groupItemId].cend()) { 0248 mGroupsCache[groupItemId].push_back(contactRef); 0249 Akonadi::Item newItem; 0250 0251 if (contactRef.gid().isEmpty()) { 0252 newItem.setId(contactRef.uid().toLongLong()); 0253 } else { 0254 newItem.setGid(contactRef.gid()); 0255 } 0256 groupItemsList << newItem; 0257 } 0258 } 0259 if (!groupItemsList.isEmpty()) { 0260 mPendingGroupItems << groupItemId; 0261 fetchItems(groupItemsList, buildFetchProperties(groupItemId)); 0262 } 0263 } 0264 0265 void ContactInfoProxyModel::fetchItems(const Akonadi::Item::List &items, const QMap<const char *, QVariant> &properties) const 0266 { 0267 auto job = new Akonadi::ItemFetchJob(items); 0268 job->fetchScope().fetchFullPayload(); 0269 job->fetchScope().setIgnoreRetrievalErrors(true); 0270 0271 for (const auto &property : properties.toStdMap()) { 0272 job->setProperty(property.first, property.second); 0273 } 0274 0275 connect(job, &Akonadi::ItemFetchJob::result, this, &ContactInfoProxyModel::slotFetchJobFinished); 0276 } 0277 0278 void ContactInfoProxyModel::slotFetchJobFinished(KJob *job) 0279 { 0280 if (job->error()) { 0281 qCWarning(KADDRESSBOOK_LOG) << " error during fetching items" << job->errorString(); 0282 return; 0283 } 0284 auto fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job); 0285 0286 const auto groupItemId = job->property("groupItemId").value<Akonadi::Item::Id>(); 0287 0288 const auto items = fetchJob->items(); 0289 for (const Akonadi::Item &item : items) { 0290 ContactCacheData::List::iterator it_contact = findCacheItem(groupItemId, item); 0291 if (it_contact != mGroupsCache[groupItemId].end()) { 0292 if (it_contact->setData(item)) { 0293 mMonitor->setItemMonitored(item); 0294 } else { 0295 qCWarning(KADDRESSBOOK_LOG) << QStringLiteral("item with id %1 cannot be saved into cache").arg(item.id()); 0296 } 0297 } 0298 } 0299 0300 if (mPendingGroupItems.contains(groupItemId)) { 0301 mPendingGroupItems.removeOne(groupItemId); 0302 } 0303 const QModelIndex index = Akonadi::EntityTreeModel::modelIndexesForItem(this, Akonadi::Item(groupItemId)).constFirst(); 0304 Q_EMIT dataChanged(index, index, mKrole); 0305 } 0306 0307 void ContactInfoProxyModel::slotItemChanged(const Akonadi::Item &item, const QSet<QByteArray> &partIdentifiers) 0308 { 0309 Q_UNUSED(partIdentifiers) 0310 Q_ASSERT(item.isValid()); 0311 0312 if (item.hasPayload<KContacts::Addressee>()) { 0313 QMapIterator<Akonadi::Item::Id, ContactCacheData::List> it_group(mGroupsCache); 0314 while (it_group.hasNext()) { 0315 it_group.next(); 0316 ContactCacheData::ListIterator it_contact = findCacheItem(it_group.key(), item); 0317 if (it_contact != mGroupsCache[it_group.key()].end()) { 0318 if (it_contact->setData(item)) { 0319 const QModelIndex index = Akonadi::EntityTreeModel::modelIndexesForItem(this, Akonadi::Item(it_group.key())).constFirst(); 0320 Q_EMIT dataChanged(index, index, mKrole); 0321 } else { 0322 qCWarning(KADDRESSBOOK_LOG) << QStringLiteral("changed item with id %1 cannot be saved into cache").arg(item.id()); 0323 } 0324 } 0325 } 0326 } else if (item.hasPayload<KContacts::ContactGroup>()) { 0327 if (mGroupsCache.contains(item.id())) { 0328 const auto groupContacts = item.payload<KContacts::ContactGroup>(); 0329 mGroupsCache[item.id()].clear(); 0330 if (groupContacts.contactReferenceCount() > 0 && isCacheItemToFetch(item.id(), groupContacts)) { 0331 resolveGroup(item.id(), groupContacts); 0332 } 0333 const QModelIndex index = Akonadi::EntityTreeModel::modelIndexesForItem(this, Akonadi::Item(item.id())).constFirst(); 0334 Q_EMIT dataChanged(index, index, mKrole); 0335 } 0336 } 0337 } 0338 0339 void ContactInfoProxyModel::slotRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last) 0340 { 0341 for (int idx = first; idx <= last; idx++) { 0342 const auto item = this->index(idx, 0, parent).data(Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>(); 0343 Q_ASSERT(item.isValid()); 0344 if (item.hasPayload<KContacts::Addressee>()) { 0345 QMapIterator<Akonadi::Item::Id, ContactCacheData::List> it_group(mGroupsCache); 0346 while (it_group.hasNext()) { 0347 it_group.next(); 0348 ContactCacheData::List::iterator it_contact = findCacheItem(it_group.key(), item); 0349 if (it_contact != mGroupsCache[it_group.key()].end()) { 0350 mGroupsCache[it_group.key()].erase(it_contact); 0351 } 0352 } 0353 } else if (item.hasPayload<KContacts::ContactGroup>()) { 0354 if (mGroupsCache.contains(item.id())) { 0355 mGroupsCache.remove(item.id()); 0356 } 0357 } 0358 } 0359 } 0360 0361 bool ContactInfoProxyModel::ContactCacheData::setData(const Akonadi::Item &item) 0362 { 0363 bool result(false); 0364 if (validateItem(item)) { 0365 const auto contact = item.payload<KContacts::Addressee>(); 0366 mName = contact.realName(); 0367 mEmail = contact.preferredEmail(); 0368 result = true; 0369 } 0370 return result; 0371 } 0372 0373 bool ContactInfoProxyModel::ContactCacheData::validateItem(const Akonadi::Item &item) const 0374 { 0375 return item.isValid() && mUid == QString::number(item.id()) && mGid == item.gid() && item.hasPayload<KContacts::Addressee>(); 0376 } 0377 0378 bool operator==(const ContactInfoProxyModel::ContactCacheData &lhs, const ContactInfoProxyModel::ContactCacheData &rhs) 0379 { 0380 return !lhs.gid().isEmpty() ? lhs.gid() == rhs.gid() : !lhs.uid().isEmpty() ? lhs.uid() == rhs.uid() : false; 0381 } 0382 0383 #include "moc_contactinfoproxymodel.cpp"