1 // For license of this file, see <project-root-folder>/LICENSE.md.
2
3 #include "services/abstract/serviceroot.h"
4
5 #include "3rd-party/boolinq/boolinq.h"
6 #include "core/feedsmodel.h"
7 #include "core/messagesmodel.h"
8 #include "database/databasequeries.h"
9 #include "exceptions/applicationexception.h"
10 #include "miscellaneous/application.h"
11 #include "miscellaneous/iconfactory.h"
12 #include "miscellaneous/textfactory.h"
13 #include "services/abstract/cacheforserviceroot.h"
14 #include "services/abstract/category.h"
15 #include "services/abstract/feed.h"
16 #include "services/abstract/importantnode.h"
17 #include "services/abstract/labelsnode.h"
18 #include "services/abstract/recyclebin.h"
19 #include "services/abstract/unreadnode.h"
20
21 #include <QThread>
22
ServiceRoot(RootItem * parent)23 ServiceRoot::ServiceRoot(RootItem* parent)
24 : RootItem(parent), m_recycleBin(new RecycleBin(this)), m_importantNode(new ImportantNode(this)),
25 m_labelsNode(new LabelsNode(this)), m_unreadNode(new UnreadNode(this)),
26 m_accountId(NO_PARENT_CATEGORY), m_networkProxy(QNetworkProxy()) {
27 setKind(RootItem::Kind::ServiceRoot);
28 appendCommonNodes();
29 }
30
31 ServiceRoot::~ServiceRoot() = default;
32
deleteViaGui()33 bool ServiceRoot::deleteViaGui() {
34 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
35
36 if (DatabaseQueries::deleteAccount(database, accountId())) {
37 stop();
38 requestItemRemoval(this);
39 return true;
40 }
41 else {
42 return false;
43 }
44 }
45
markAsReadUnread(RootItem::ReadStatus status)46 bool ServiceRoot::markAsReadUnread(RootItem::ReadStatus status) {
47 auto* cache = dynamic_cast<CacheForServiceRoot*>(this);
48
49 if (cache != nullptr) {
50 cache->addMessageStatesToCache(customIDSOfMessagesForItem(this), status);
51 }
52
53 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
54
55 if (DatabaseQueries::markAccountReadUnread(database, accountId(), status)) {
56 updateCounts(false);
57 itemChanged(getSubTree());
58 requestReloadMessageList(status == RootItem::ReadStatus::Read);
59 return true;
60 }
61 else {
62 return false;
63 }
64 }
65
addItemMenu()66 QList<QAction*> ServiceRoot::addItemMenu() {
67 return QList<QAction*>();
68 }
69
recycleBin() const70 RecycleBin* ServiceRoot::recycleBin() const {
71 return m_recycleBin;
72 }
73
downloadAttachmentOnMyOwn(const QUrl & url) const74 bool ServiceRoot::downloadAttachmentOnMyOwn(const QUrl& url) const {
75 Q_UNUSED(url)
76 return false;
77 }
78
contextMenuFeedsList()79 QList<QAction*> ServiceRoot::contextMenuFeedsList() {
80 return serviceMenu();
81 }
82
contextMenuMessagesList(const QList<Message> & messages)83 QList<QAction*> ServiceRoot::contextMenuMessagesList(const QList<Message>& messages) {
84 Q_UNUSED(messages)
85 return {};
86 }
87
serviceMenu()88 QList<QAction*> ServiceRoot::serviceMenu() {
89 if (m_serviceMenu.isEmpty()) {
90 if (isSyncable()) {
91 auto* act_sync_tree = new QAction(qApp->icons()->fromTheme(QSL("view-refresh")), tr("Synchronize folders && other items"), this);
92
93 connect(act_sync_tree, &QAction::triggered, this, &ServiceRoot::syncIn);
94 m_serviceMenu.append(act_sync_tree);
95
96 auto* cache = toCache();
97
98 if (cache != nullptr) {
99 auto* act_sync_cache = new QAction(qApp->icons()->fromTheme(QSL("view-refresh")), tr("Synchronize article cache"), this);
100
101 connect(act_sync_cache, &QAction::triggered, this, [cache]() {
102 cache->saveAllCachedData(false);
103 });
104
105 m_serviceMenu.append(act_sync_cache);
106 }
107 }
108 }
109
110 return m_serviceMenu;
111 }
112
isSyncable() const113 bool ServiceRoot::isSyncable() const {
114 return false;
115 }
116
start(bool freshly_activated)117 void ServiceRoot::start(bool freshly_activated) {
118 Q_UNUSED(freshly_activated)
119 }
120
stop()121 void ServiceRoot::stop() {}
122
updateCounts(bool including_total_count)123 void ServiceRoot::updateCounts(bool including_total_count) {
124 QList<Feed*> feeds;
125 auto str = getSubTree();
126
127 for (RootItem* child : qAsConst(str)) {
128 if (child->kind() == RootItem::Kind::Feed) {
129 feeds.append(child->toFeed());
130 }
131 else if (child->kind() != RootItem::Kind::Labels &&
132 child->kind() != RootItem::Kind::Category &&
133 child->kind() != RootItem::Kind::ServiceRoot) {
134 child->updateCounts(including_total_count);
135 }
136 }
137
138 if (feeds.isEmpty()) {
139 return;
140 }
141
142 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
143 bool ok;
144 QMap<QString, QPair<int, int>> counts = DatabaseQueries::getMessageCountsForAccount(database, accountId(), including_total_count, &ok);
145
146 if (ok) {
147 for (Feed* feed : feeds) {
148 if (counts.contains(feed->customId())) {
149 feed->setCountOfUnreadMessages(counts.value(feed->customId()).first);
150
151 if (including_total_count) {
152 feed->setCountOfAllMessages(counts.value(feed->customId()).second);
153 }
154 }
155 else {
156 feed->setCountOfUnreadMessages(0);
157
158 if (including_total_count) {
159 feed->setCountOfAllMessages(0);
160 }
161 }
162 }
163 }
164 }
165
canBeDeleted() const166 bool ServiceRoot::canBeDeleted() const {
167 return true;
168 }
169
completelyRemoveAllData()170 void ServiceRoot::completelyRemoveAllData() {
171 // Purge old data from SQL and clean all model items.
172 cleanAllItemsFromModel(true);
173 removeOldAccountFromDatabase(true, true);
174 updateCounts(true);
175 itemChanged({ this });
176 requestReloadMessageList(true);
177 }
178
feedIconForMessage(const QString & feed_custom_id) const179 QIcon ServiceRoot::feedIconForMessage(const QString& feed_custom_id) const {
180 QString low_id = feed_custom_id.toLower();
181 RootItem* found_item = getItemFromSubTree([low_id](const RootItem* it) {
182 return it->kind() == RootItem::Kind::Feed && it->customId().toLower() == low_id;
183 });
184
185 if (found_item != nullptr) {
186 return found_item->icon();
187 }
188 else {
189 return QIcon();
190 }
191 }
192
removeOldAccountFromDatabase(bool delete_messages_too,bool delete_labels_too)193 void ServiceRoot::removeOldAccountFromDatabase(bool delete_messages_too, bool delete_labels_too) {
194 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
195
196 DatabaseQueries::deleteAccountData(database,
197 accountId(),
198 delete_messages_too,
199 delete_labels_too);
200 }
201
cleanAllItemsFromModel(bool clean_labels_too)202 void ServiceRoot::cleanAllItemsFromModel(bool clean_labels_too) {
203 auto chi = childItems();
204
205 for (RootItem* top_level_item : qAsConst(chi)) {
206 if (top_level_item->kind() != RootItem::Kind::Bin &&
207 top_level_item->kind() != RootItem::Kind::Important &&
208 top_level_item->kind() != RootItem::Kind::Unread &&
209 top_level_item->kind() != RootItem::Kind::Labels) {
210 requestItemRemoval(top_level_item);
211 }
212 }
213
214 if (labelsNode() != nullptr && clean_labels_too) {
215 auto lbl_chi = labelsNode()->childItems();
216
217 for (RootItem* lbl : qAsConst(lbl_chi)) {
218 requestItemRemoval(lbl);
219 }
220 }
221 }
222
appendCommonNodes()223 void ServiceRoot::appendCommonNodes() {
224 if (recycleBin() != nullptr && !childItems().contains(recycleBin())) {
225 appendChild(recycleBin());
226 }
227
228 if (importantNode() != nullptr && !childItems().contains(importantNode())) {
229 appendChild(importantNode());
230 }
231
232 if (unreadNode() != nullptr && !childItems().contains(unreadNode())) {
233 appendChild(unreadNode());
234 }
235
236 if (labelsNode() != nullptr && !childItems().contains(labelsNode())) {
237 appendChild(labelsNode());
238 }
239 }
240
cleanFeeds(const QList<Feed * > & items,bool clean_read_only)241 bool ServiceRoot::cleanFeeds(const QList<Feed*>& items, bool clean_read_only) {
242 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
243
244 if (DatabaseQueries::cleanFeeds(database, textualFeedIds(items), clean_read_only, accountId())) {
245 getParentServiceRoot()->updateCounts(true);
246 getParentServiceRoot()->itemChanged(getParentServiceRoot()->getSubTree());
247 getParentServiceRoot()->requestReloadMessageList(true);
248 return true;
249 }
250 else {
251 return false;
252 }
253 }
254
storeNewFeedTree(RootItem * root)255 void ServiceRoot::storeNewFeedTree(RootItem* root) {
256 try {
257 DatabaseQueries::storeAccountTree(qApp->database()->driver()->connection(metaObject()->className()), root, accountId());
258 }
259 catch (const ApplicationException& ex) {
260 qFatal("Cannot store account tree: '%s'.", qPrintable(ex.message()));
261 }
262 }
263
removeLeftOverMessages()264 void ServiceRoot::removeLeftOverMessages() {
265 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
266
267 DatabaseQueries::purgeLeftoverMessages(database, accountId());
268 }
269
removeLeftOverMessageFilterAssignments()270 void ServiceRoot::removeLeftOverMessageFilterAssignments() {
271 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
272
273 DatabaseQueries::purgeLeftoverMessageFilterAssignments(database, accountId());
274 }
275
removeLeftOverMessageLabelAssignments()276 void ServiceRoot::removeLeftOverMessageLabelAssignments() {
277 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
278
279 DatabaseQueries::purgeLeftoverLabelAssignments(database, accountId());
280 }
281
undeletedMessages() const282 QList<Message> ServiceRoot::undeletedMessages() const {
283 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
284
285 return DatabaseQueries::getUndeletedMessagesForAccount(database, accountId());
286 }
287
supportsFeedAdding() const288 bool ServiceRoot::supportsFeedAdding() const {
289 return false;
290 }
291
supportsCategoryAdding() const292 bool ServiceRoot::supportsCategoryAdding() const {
293 return false;
294 }
295
supportedLabelOperations() const296 ServiceRoot::LabelOperation ServiceRoot::supportedLabelOperations() const {
297 return LabelOperation::Adding | LabelOperation::Editing | LabelOperation::Deleting;
298 }
299
saveAccountDataToDatabase()300 void ServiceRoot::saveAccountDataToDatabase() {
301 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
302
303 try {
304 DatabaseQueries::createOverwriteAccount(database, this);
305 }
306 catch (const ApplicationException& ex) {
307 qFatal("Account was not saved into database: '%s'.", qPrintable(ex.message()));
308 }
309 }
310
customDatabaseData() const311 QVariantHash ServiceRoot::customDatabaseData() const {
312 return {};
313 }
314
setCustomDatabaseData(const QVariantHash & data)315 void ServiceRoot::setCustomDatabaseData(const QVariantHash& data) {
316 Q_UNUSED(data)
317 }
318
wantsBaggedIdsOfExistingMessages() const319 bool ServiceRoot::wantsBaggedIdsOfExistingMessages() const {
320 return false;
321 }
322
aboutToBeginFeedFetching(const QList<Feed * > & feeds,const QHash<QString,QHash<BagOfMessages,QStringList>> & stated_messages,const QHash<QString,QStringList> & tagged_messages)323 void ServiceRoot::aboutToBeginFeedFetching(const QList<Feed*>& feeds,
324 const QHash<QString, QHash<BagOfMessages, QStringList>>& stated_messages,
325 const QHash<QString, QStringList>& tagged_messages) {
326 Q_UNUSED(feeds)
327 Q_UNUSED(stated_messages)
328 Q_UNUSED(tagged_messages)
329 }
330
itemChanged(const QList<RootItem * > & items)331 void ServiceRoot::itemChanged(const QList<RootItem*>& items) {
332 emit dataChanged(items);
333 }
334
requestReloadMessageList(bool mark_selected_messages_read)335 void ServiceRoot::requestReloadMessageList(bool mark_selected_messages_read) {
336 emit reloadMessageListRequested(mark_selected_messages_read);
337 }
338
requestItemExpand(const QList<RootItem * > & items,bool expand)339 void ServiceRoot::requestItemExpand(const QList<RootItem*>& items, bool expand) {
340 emit itemExpandRequested(items, expand);
341 }
342
requestItemExpandStateSave(RootItem * subtree_root)343 void ServiceRoot::requestItemExpandStateSave(RootItem* subtree_root) {
344 emit itemExpandStateSaveRequested(subtree_root);
345 }
346
requestItemReassignment(RootItem * item,RootItem * new_parent)347 void ServiceRoot::requestItemReassignment(RootItem* item, RootItem* new_parent) {
348 emit itemReassignmentRequested(item, new_parent);
349 }
350
requestItemRemoval(RootItem * item)351 void ServiceRoot::requestItemRemoval(RootItem* item) {
352 emit itemRemovalRequested(item);
353 }
354
addNewFeed(RootItem * selected_item,const QString & url)355 void ServiceRoot::addNewFeed(RootItem* selected_item, const QString& url) {
356 Q_UNUSED(selected_item)
357 Q_UNUSED(url)
358 }
359
addNewCategory(RootItem * selected_item)360 void ServiceRoot::addNewCategory(RootItem* selected_item) {
361 Q_UNUSED(selected_item)
362 }
363
storeCustomFeedsData()364 QMap<QString, QVariantMap> ServiceRoot::storeCustomFeedsData() {
365 QMap<QString, QVariantMap> custom_data;
366 auto str = getSubTreeFeeds();
367
368 for (const Feed* feed : qAsConst(str)) {
369 QVariantMap feed_custom_data;
370
371 // TODO: This could potentially call Feed::customDatabaseData() and append it
372 // to this map and also subsequently restore.
373 feed_custom_data.insert(QSL("auto_update_interval"), feed->autoUpdateInitialInterval());
374 feed_custom_data.insert(QSL("auto_update_type"), int(feed->autoUpdateType()));
375 feed_custom_data.insert(QSL("msg_filters"), QVariant::fromValue(feed->messageFilters()));
376 custom_data.insert(feed->customId(), feed_custom_data);
377 }
378
379 return custom_data;
380 }
381
restoreCustomFeedsData(const QMap<QString,QVariantMap> & data,const QHash<QString,Feed * > & feeds)382 void ServiceRoot::restoreCustomFeedsData(const QMap<QString, QVariantMap>& data, const QHash<QString, Feed*>& feeds) {
383 QMapIterator<QString, QVariantMap> i(data);
384
385 while (i.hasNext()) {
386 i.next();
387 const QString custom_id = i.key();
388
389 if (feeds.contains(custom_id)) {
390 Feed* feed = feeds.value(custom_id);
391 QVariantMap feed_custom_data = i.value();
392
393 feed->setAutoUpdateInitialInterval(feed_custom_data.value(QSL("auto_update_interval")).toInt());
394 feed->setAutoUpdateType(static_cast<Feed::AutoUpdateType>(feed_custom_data.value(QSL("auto_update_type")).toInt()));
395 feed->setMessageFilters(feed_custom_data.value(QSL("msg_filters")).value<QList<QPointer<MessageFilter>>>());
396 }
397 }
398 }
399
networkProxy() const400 QNetworkProxy ServiceRoot::networkProxy() const {
401 return m_networkProxy;
402 }
403
setNetworkProxy(const QNetworkProxy & network_proxy)404 void ServiceRoot::setNetworkProxy(const QNetworkProxy& network_proxy) {
405 m_networkProxy = network_proxy;
406
407 emit proxyChanged(network_proxy);
408 }
409
importantNode() const410 ImportantNode* ServiceRoot::importantNode() const {
411 return m_importantNode;
412 }
413
labelsNode() const414 LabelsNode* ServiceRoot::labelsNode() const {
415 return m_labelsNode;
416 }
417
unreadNode() const418 UnreadNode* ServiceRoot::unreadNode() const {
419 return m_unreadNode;
420 }
421
syncIn()422 void ServiceRoot::syncIn() {
423 QIcon original_icon = icon();
424
425 setIcon(qApp->icons()->fromTheme(QSL("view-refresh")));
426 itemChanged(QList<RootItem*>() << this);
427 RootItem* new_tree = obtainNewTreeForSyncIn();
428
429 if (new_tree != nullptr) {
430 auto feed_custom_data = storeCustomFeedsData();
431
432 // Remove from feeds model, then from SQL but leave messages intact.
433 bool uses_remote_labels = (supportedLabelOperations() & LabelOperation::Synchronised) == LabelOperation::Synchronised;
434
435 // Remove stuff.
436 cleanAllItemsFromModel(uses_remote_labels);
437 removeOldAccountFromDatabase(false, uses_remote_labels);
438
439 // Restore some local settings to feeds etc.
440 restoreCustomFeedsData(feed_custom_data, new_tree->getHashedSubTreeFeeds());
441
442 // Model is clean, now store new tree into DB and
443 // set primary IDs of the items.
444 storeNewFeedTree(new_tree);
445
446 // We have new feed, some feeds were maybe removed,
447 // so remove left over messages and filter assignments.
448 removeLeftOverMessages();
449 removeLeftOverMessageFilterAssignments();
450 removeLeftOverMessageLabelAssignments();
451
452 auto chi = new_tree->childItems();
453
454 for (RootItem* top_level_item : qAsConst(chi)) {
455 if (top_level_item->kind() != Kind::Labels) {
456 top_level_item->setParent(nullptr);
457 requestItemReassignment(top_level_item, this);
458 }
459 else {
460 // It seems that some labels got synced-in.
461 if (labelsNode() != nullptr) {
462 auto lbl_chi = top_level_item->childItems();
463
464 for (RootItem* new_lbl : qAsConst(lbl_chi)) {
465 new_lbl->setParent(nullptr);
466 requestItemReassignment(new_lbl, labelsNode());
467 }
468 }
469 }
470 }
471
472 new_tree->clearChildren();
473 new_tree->deleteLater();
474
475 updateCounts(true);
476 requestReloadMessageList(true);
477 }
478
479 setIcon(original_icon);
480 itemChanged(getSubTree());
481 requestItemExpand(getSubTree(), true);
482 }
483
performInitialAssembly(const Assignment & categories,const Assignment & feeds,const QList<Label * > & labels)484 void ServiceRoot::performInitialAssembly(const Assignment& categories,
485 const Assignment& feeds,
486 const QList<Label*>& labels) {
487 assembleCategories(categories);
488 assembleFeeds(feeds);
489 labelsNode()->loadLabels(labels);
490 updateCounts(true);
491 }
492
obtainNewTreeForSyncIn() const493 RootItem* ServiceRoot::obtainNewTreeForSyncIn() const {
494 return nullptr;
495 }
496
customIDSOfMessagesForItem(RootItem * item)497 QStringList ServiceRoot::customIDSOfMessagesForItem(RootItem* item) {
498 if (item->getParentServiceRoot() != this) {
499 // Not item from this account.
500 return QStringList();
501 }
502 else {
503 QStringList list;
504
505 switch (item->kind()) {
506 case RootItem::Kind::Labels:
507 case RootItem::Kind::Category: {
508 auto chi = item->childItems();
509
510 for (RootItem* child : qAsConst(chi)) {
511 list.append(customIDSOfMessagesForItem(child));
512 }
513
514 return list;
515 }
516
517 case RootItem::Kind::Label: {
518 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
519
520 list = DatabaseQueries::customIdsOfMessagesFromLabel(database, item->toLabel());
521 break;
522 }
523
524 case RootItem::Kind::ServiceRoot: {
525 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
526
527 list = DatabaseQueries::customIdsOfMessagesFromAccount(database, accountId());
528 break;
529 }
530
531 case RootItem::Kind::Bin: {
532 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
533
534 list = DatabaseQueries::customIdsOfMessagesFromBin(database, accountId());
535 break;
536 }
537
538 case RootItem::Kind::Feed: {
539 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
540
541 list = DatabaseQueries::customIdsOfMessagesFromFeed(database, item->customId(), accountId());
542 break;
543 }
544
545 case RootItem::Kind::Important: {
546 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
547
548 list = DatabaseQueries::customIdsOfImportantMessages(database, accountId());
549 break;
550 }
551
552 case RootItem::Kind::Unread: {
553 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
554
555 list = DatabaseQueries::customIdsOfUnreadMessages(database, accountId());
556 break;
557 }
558
559 default:
560 break;
561 }
562
563 qDebug() << "Custom IDs of messages for some operation are:" << list;
564 return list;
565 }
566 }
567
markFeedsReadUnread(const QList<Feed * > & items,RootItem::ReadStatus read)568 bool ServiceRoot::markFeedsReadUnread(const QList<Feed*>& items, RootItem::ReadStatus read) {
569 QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
570
571 if (DatabaseQueries::markFeedsReadUnread(database, textualFeedIds(items), accountId(), read)) {
572 getParentServiceRoot()->updateCounts(false);
573 getParentServiceRoot()->itemChanged(getParentServiceRoot()->getSubTree());
574 getParentServiceRoot()->requestReloadMessageList(read == RootItem::ReadStatus::Read);
575 return true;
576 }
577 else {
578 return false;
579 }
580 }
581
textualFeedUrls(const QList<Feed * > & feeds) const582 QStringList ServiceRoot::textualFeedUrls(const QList<Feed*>& feeds) const {
583 QStringList stringy_urls;
584
585 stringy_urls.reserve(feeds.size());
586
587 for (const Feed* feed : feeds) {
588 stringy_urls.append(!feed->source().isEmpty() ? feed->source() : QSL("no-url"));
589 }
590
591 return stringy_urls;
592 }
593
textualFeedIds(const QList<Feed * > & feeds) const594 QStringList ServiceRoot::textualFeedIds(const QList<Feed*>& feeds) const {
595 QStringList stringy_ids; stringy_ids.reserve(feeds.size());
596
597 for (const Feed* feed : feeds) {
598 stringy_ids.append(QSL("'%1'").arg(feed->customId()));
599 }
600
601 return stringy_ids;
602 }
603
customIDsOfMessages(const QList<ImportanceChange> & changes)604 QStringList ServiceRoot::customIDsOfMessages(const QList<ImportanceChange>& changes) {
605 QStringList list; list.reserve(changes.size());
606
607 for (const auto& change : changes) {
608 list.append(change.first.m_customId);
609 }
610
611 return list;
612 }
613
customIDsOfMessages(const QList<Message> & messages)614 QStringList ServiceRoot::customIDsOfMessages(const QList<Message>& messages) {
615 QStringList list; list.reserve(messages.size());
616
617 for (const Message& message : messages) {
618 list.append(message.m_customId);
619 }
620
621 return list;
622 }
623
accountId() const624 int ServiceRoot::accountId() const {
625 return m_accountId;
626 }
627
setAccountId(int account_id)628 void ServiceRoot::setAccountId(int account_id) {
629 m_accountId = account_id;
630
631 auto* cache = dynamic_cast<CacheForServiceRoot*>(this);
632
633 if (cache != nullptr) {
634 cache->setUniqueId(account_id);
635 }
636 }
637
loadMessagesForItem(RootItem * item,MessagesModel * model)638 bool ServiceRoot::loadMessagesForItem(RootItem* item, MessagesModel* model) {
639 if (item->kind() == RootItem::Kind::Bin) {
640 model->setFilter(QSL("Messages.is_deleted = 1 AND Messages.is_pdeleted = 0 AND Messages.account_id = %1")
641 .arg(QString::number(accountId())));
642 }
643 else if (item->kind() == RootItem::Kind::Important) {
644 model->setFilter(QSL("Messages.is_important = 1 AND Messages.is_deleted = 0 AND Messages.is_pdeleted = 0 AND Messages.account_id = %1")
645 .arg(QString::number(accountId())));
646 }
647 else if (item->kind() == RootItem::Kind::Unread) {
648 model->setFilter(QSL("Messages.is_read = 0 AND Messages.is_deleted = 0 AND Messages.is_pdeleted = 0 AND Messages.account_id = %1")
649 .arg(QString::number(accountId())));
650 }
651 else if (item->kind() == RootItem::Kind::Label) {
652 // Show messages with particular label.
653 model->setFilter(QSL("Messages.is_deleted = 0 AND Messages.is_pdeleted = 0 AND Messages.account_id = %1 AND "
654 "(SELECT COUNT(*) FROM LabelsInMessages WHERE account_id = %1 AND message = Messages.custom_id AND label = '%2') > 0")
655 .arg(QString::number(accountId()), item->customId()));
656 }
657 else if (item->kind() == RootItem::Kind::Labels) {
658 // Show messages with any label.
659 model->setFilter(QSL("Messages.is_deleted = 0 AND Messages.is_pdeleted = 0 AND Messages.account_id = %1 AND "
660 "(SELECT COUNT(*) FROM LabelsInMessages WHERE account_id = %1 AND message = Messages.custom_id) > 0")
661 .arg(QString::number(accountId())));
662 }
663 else if (item->kind() == RootItem::Kind::ServiceRoot) {
664 model->setFilter(
665 QSL("Messages.is_deleted = 0 AND Messages.is_pdeleted = 0 AND Messages.account_id = %1").arg(
666 QString::number(accountId())));
667
668 qDebugNN << "Displaying messages from account:" << QUOTE_W_SPACE_DOT(accountId());
669 }
670 else {
671 QList<Feed*> children = item->getSubTreeFeeds();
672 QString filter_clause = textualFeedIds(children).join(QSL(", "));
673
674 if (filter_clause.isEmpty()) {
675 filter_clause = QSL("null");
676 }
677
678 model->setFilter(
679 QSL("Feeds.custom_id IN (%1) AND Messages.is_deleted = 0 AND Messages.is_pdeleted = 0 AND Messages.account_id = %2").arg(
680 filter_clause,
681 QString::
682 number(accountId())));
683
684 QString urls = textualFeedUrls(children).join(QSL(", "));
685
686 qDebugNN << "Displaying messages from feeds IDs:" << QUOTE_W_SPACE(filter_clause)
687 << "and URLs:" << QUOTE_W_SPACE_DOT(urls);
688 }
689
690 return true;
691 }
692
onBeforeSetMessagesRead(RootItem * selected_item,const QList<Message> & messages,RootItem::ReadStatus read)693 bool ServiceRoot::onBeforeSetMessagesRead(RootItem* selected_item, const QList<Message>& messages, RootItem::ReadStatus read) {
694 Q_UNUSED(selected_item)
695
696 auto cache = dynamic_cast<CacheForServiceRoot*>(this);
697
698 if (cache != nullptr) {
699 cache->addMessageStatesToCache(customIDsOfMessages(messages), read);
700 }
701
702 return true;
703 }
704
onAfterSetMessagesRead(RootItem * selected_item,const QList<Message> & messages,RootItem::ReadStatus read)705 bool ServiceRoot::onAfterSetMessagesRead(RootItem* selected_item, const QList<Message>& messages, RootItem::ReadStatus read) {
706 Q_UNUSED(selected_item)
707 Q_UNUSED(messages)
708 Q_UNUSED(read)
709
710 updateCounts(true);
711 itemChanged(getSubTree());
712 return true;
713 }
714
onBeforeSwitchMessageImportance(RootItem * selected_item,const QList<ImportanceChange> & changes)715 bool ServiceRoot::onBeforeSwitchMessageImportance(RootItem* selected_item, const QList<ImportanceChange>& changes) {
716 Q_UNUSED(selected_item)
717
718 auto cache = dynamic_cast<CacheForServiceRoot*>(this);
719
720 if (cache != nullptr) {
721 // Now, we need to separate the changes because of Nextcloud API limitations.
722 QList<Message> mark_starred_msgs;
723 QList<Message> mark_unstarred_msgs;
724
725 for (const ImportanceChange& pair : changes) {
726 if (pair.second == RootItem::Importance::Important) {
727 mark_starred_msgs.append(pair.first);
728 }
729 else {
730 mark_unstarred_msgs.append(pair.first);
731 }
732 }
733
734 if (!mark_starred_msgs.isEmpty()) {
735 cache->addMessageStatesToCache(mark_starred_msgs, RootItem::Importance::Important);
736 }
737
738 if (!mark_unstarred_msgs.isEmpty()) {
739 cache->addMessageStatesToCache(mark_unstarred_msgs, RootItem::Importance::NotImportant);
740 }
741 }
742
743 return true;
744 }
745
onAfterSwitchMessageImportance(RootItem * selected_item,const QList<ImportanceChange> & changes)746 bool ServiceRoot::onAfterSwitchMessageImportance(RootItem* selected_item, const QList<ImportanceChange>& changes) {
747 Q_UNUSED(selected_item)
748 Q_UNUSED(changes)
749
750 updateCounts(true);
751 itemChanged(getSubTree());
752 return true;
753 }
754
onBeforeMessagesDelete(RootItem * selected_item,const QList<Message> & messages)755 bool ServiceRoot::onBeforeMessagesDelete(RootItem* selected_item, const QList<Message>& messages) {
756 Q_UNUSED(selected_item)
757 Q_UNUSED(messages)
758 return true;
759 }
760
onAfterMessagesDelete(RootItem * selected_item,const QList<Message> & messages)761 bool ServiceRoot::onAfterMessagesDelete(RootItem* selected_item, const QList<Message>& messages) {
762 Q_UNUSED(selected_item)
763 Q_UNUSED(messages)
764
765 updateCounts(true);
766 itemChanged(getSubTree());
767 return true;
768 }
769
onBeforeLabelMessageAssignmentChanged(const QList<Label * > & labels,const QList<Message> & messages,bool assign)770 bool ServiceRoot::onBeforeLabelMessageAssignmentChanged(const QList<Label*>& labels,
771 const QList<Message>& messages,
772 bool assign) {
773 auto cache = dynamic_cast<CacheForServiceRoot*>(this);
774
775 if (cache != nullptr) {
776 boolinq::from(labels).for_each([cache, messages, assign](Label* lbl) {
777 cache->addLabelsAssignmentsToCache(messages, lbl, assign);
778 });
779 }
780
781 return true;
782 }
783
onAfterLabelMessageAssignmentChanged(const QList<Label * > & labels,const QList<Message> & messages,bool assign)784 bool ServiceRoot::onAfterLabelMessageAssignmentChanged(const QList<Label*>& labels,
785 const QList<Message>& messages,
786 bool assign) {
787 Q_UNUSED(messages)
788 Q_UNUSED(assign)
789
790 boolinq::from(labels).for_each([](Label* lbl) {
791 lbl->updateCounts(true);
792 });
793
794 auto list = boolinq::from(labels).select([](Label* lbl) {
795 return static_cast<RootItem*>(lbl);
796 }).toStdList();
797
798 getParentServiceRoot()->itemChanged(FROM_STD_LIST(QList<RootItem*>, list));
799 return true;
800 }
801
onBeforeMessagesRestoredFromBin(RootItem * selected_item,const QList<Message> & messages)802 bool ServiceRoot::onBeforeMessagesRestoredFromBin(RootItem* selected_item, const QList<Message>& messages) {
803 Q_UNUSED(selected_item)
804 Q_UNUSED(messages)
805
806 return true;
807 }
808
onAfterMessagesRestoredFromBin(RootItem * selected_item,const QList<Message> & messages)809 bool ServiceRoot::onAfterMessagesRestoredFromBin(RootItem* selected_item, const QList<Message>& messages) {
810 Q_UNUSED(selected_item)
811 Q_UNUSED(messages)
812
813 updateCounts(true);
814 itemChanged(getSubTree());
815 return true;
816 }
817
toCache() const818 CacheForServiceRoot* ServiceRoot::toCache() const {
819 return dynamic_cast<CacheForServiceRoot*>(const_cast<ServiceRoot*>(this));
820 }
821
assembleFeeds(const Assignment & feeds)822 void ServiceRoot::assembleFeeds(const Assignment& feeds) {
823 QHash<int, Category*> categories = getHashedSubTreeCategories();
824
825 for (const AssignmentItem& feed : feeds) {
826 if (feed.first == NO_PARENT_CATEGORY) {
827 // This is top-level feed, add it to the root item.
828 appendChild(feed.second);
829 }
830 else if (categories.contains(feed.first)) {
831 // This feed belongs to this category.
832 categories.value(feed.first)->appendChild(feed.second);
833 }
834 else {
835 qWarningNN << LOGSEC_CORE << "Feed" << QUOTE_W_SPACE(feed.second->title()) << "is loose, skipping it.";
836 }
837 }
838 }
839
assembleCategories(const Assignment & categories)840 void ServiceRoot::assembleCategories(const Assignment& categories) {
841 Assignment editable_categories = categories;
842 QHash<int, RootItem*> assignments;
843
844 assignments.insert(NO_PARENT_CATEGORY, this);
845
846 // Add top-level categories.
847 while (!editable_categories.isEmpty()) {
848 for (int i = 0; i < editable_categories.size(); i++) {
849 if (assignments.contains(editable_categories.at(i).first)) {
850 // Parent category of this category is already added.
851 assignments.value(editable_categories.at(i).first)->appendChild(editable_categories.at(i).second);
852
853 // Now, added category can be parent for another categories, add it.
854 assignments.insert(editable_categories.at(i).second->id(), editable_categories.at(i).second);
855
856 // Remove the category from the list, because it was
857 // added to the final collection.
858 editable_categories.removeAt(i);
859 i--;
860 }
861 }
862 }
863 }
864
operator |(ServiceRoot::LabelOperation lhs,ServiceRoot::LabelOperation rhs)865 ServiceRoot::LabelOperation operator|(ServiceRoot::LabelOperation lhs, ServiceRoot::LabelOperation rhs) {
866 return static_cast<ServiceRoot::LabelOperation>(static_cast<char>(lhs) | static_cast<char>(rhs));
867 }
868
operator &(ServiceRoot::LabelOperation lhs,ServiceRoot::LabelOperation rhs)869 ServiceRoot::LabelOperation operator&(ServiceRoot::LabelOperation lhs, ServiceRoot::LabelOperation rhs) {
870 return static_cast<ServiceRoot::LabelOperation>(static_cast<char>(lhs) & static_cast<char>(rhs));
871 }
872
updateMessages(QList<Message> & messages,Feed * feed,bool force_update)873 QPair<int, int> ServiceRoot::updateMessages(QList<Message>& messages, Feed* feed, bool force_update) {
874 QPair<int, int> updated_messages = { 0, 0 };
875
876 if (messages.isEmpty()) {
877 qDebugNN << "No messages to be updated/added in DB for feed"
878 << QUOTE_W_SPACE_DOT(feed->customId());
879 return updated_messages;
880 }
881
882 QList<RootItem*> items_to_update;
883 bool is_main_thread = QThread::currentThread() == qApp->thread();
884
885 qDebugNN << LOGSEC_CORE
886 << "Updating messages in DB. Main thread:"
887 << QUOTE_W_SPACE_DOT(is_main_thread);
888
889 bool ok = false;
890 QSqlDatabase database = is_main_thread ?
891 qApp->database()->driver()->connection(metaObject()->className()) :
892 qApp->database()->driver()->connection(QSL("feed_upd"));
893
894 updated_messages = DatabaseQueries::updateMessages(database, messages, feed, force_update, &ok);
895
896 if (updated_messages.first > 0 || updated_messages.second > 0) {
897 // Something was added or updated in the DB, update numbers.
898 feed->updateCounts(true);
899
900 if (recycleBin() != nullptr) {
901 recycleBin()->updateCounts(true);
902 items_to_update.append(recycleBin());
903 }
904
905 if (importantNode() != nullptr) {
906 importantNode()->updateCounts(true);
907 items_to_update.append(importantNode());
908 }
909
910 if (unreadNode() != nullptr) {
911 unreadNode()->updateCounts(true);
912 items_to_update.append(unreadNode());
913 }
914
915 if (labelsNode() != nullptr) {
916 labelsNode()->updateCounts(true);
917 items_to_update.append(labelsNode());
918 }
919 }
920
921 // Some messages were really added to DB, reload feed in model.
922 items_to_update.append(feed);
923 getParentServiceRoot()->itemChanged(items_to_update);
924
925 return updated_messages;
926 }
927