1 // For license of this file, see <project-root-folder>/LICENSE.md.
2 
3 #include "core/messagesmodel.h"
4 
5 #include "core/messagesmodelcache.h"
6 #include "database/databasefactory.h"
7 #include "database/databasequeries.h"
8 #include "definitions/definitions.h"
9 #include "miscellaneous/application.h"
10 #include "miscellaneous/iconfactory.h"
11 #include "miscellaneous/skinfactory.h"
12 #include "miscellaneous/textfactory.h"
13 #include "services/abstract/recyclebin.h"
14 #include "services/abstract/serviceroot.h"
15 
16 #include <QPainter>
17 #include <QPainterPath>
18 #include <QSqlError>
19 #include <QSqlField>
20 
21 #include <cmath>
22 
MessagesModel(QObject * parent)23 MessagesModel::MessagesModel(QObject* parent)
24   : QSqlQueryModel(parent), m_cache(new MessagesModelCache(this)), m_messageHighlighter(MessageHighlighter::NoHighlighting),
25   m_customDateFormat(QString()), m_selectedItem(nullptr), m_itemHeight(-1), m_displayFeedIcons(false) {
26   setupFonts();
27   setupIcons();
28   setupHeaderData();
29   updateDateFormat();
30   updateFeedIconsDisplay();
31   loadMessages(nullptr);
32 }
33 
~MessagesModel()34 MessagesModel::~MessagesModel() {
35   qDebugNN << LOGSEC_MESSAGEMODEL << "Destroying MessagesModel instance.";
36 }
37 
setupIcons()38 void MessagesModel::setupIcons() {
39   m_favoriteIcon = qApp->icons()->fromTheme(QSL("mail-mark-important"));
40   m_readIcon = qApp->icons()->fromTheme(QSL("mail-mark-read"));
41   m_unreadIcon = qApp->icons()->fromTheme(QSL("mail-mark-unread"));
42   m_enclosuresIcon = qApp->icons()->fromTheme(QSL("mail-attachment"));
43 
44   for (int i = int(MSG_SCORE_MIN); i <= int(MSG_SCORE_MAX); i += 10) {
45     m_scoreIcons.append(generateIconForScore(double(i)));
46   }
47 }
48 
generateIconForScore(double score)49 QIcon MessagesModel::generateIconForScore(double score) {
50   QPixmap pix(64, 64);
51   QPainter paint(&pix);
52 
53   paint.setRenderHint(QPainter::RenderHint::Antialiasing);
54 
55   int level = std::min(MSG_SCORE_MAX, std::max(MSG_SCORE_MIN, std::floor(score / 10.0)));
56   QPainterPath path;
57 
58   path.addRoundedRect(QRectF(2, 2, 60, 60), 5, 5);
59 
60   QPen pen(Qt::GlobalColor::black, 2);
61 
62   paint.setPen(pen);
63   paint.fillPath(path, Qt::GlobalColor::white);
64   paint.drawPath(path);
65 
66 #if QT_VERSION >= 0x050D00 // Qt >= 5.13.0
67   path.clear();
68 #else
69   path = QPainterPath();
70 #endif
71 
72   paint.setPen(Qt::GlobalColor::transparent);
73 
74   int bar_height = 6 * level;
75 
76   path.addRoundedRect(QRectF(2, 64 - bar_height - 2, 60, bar_height), 5, 5);
77   paint.fillPath(path, QColor::fromHsv(int(score), 200, 230));
78 
79   return pix;
80 }
81 
cache() const82 MessagesModelCache* MessagesModel::cache() const {
83   return m_cache;
84 }
85 
repopulate()86 void MessagesModel::repopulate() {
87   m_cache->clear();
88   setQuery(selectStatement(), m_db);
89 
90   if (lastError().isValid()) {
91     qCriticalNN << LOGSEC_MESSAGEMODEL << "Error when setting new msg view query: '" << lastError().text() << "'.";
92     qCriticalNN << LOGSEC_MESSAGEMODEL << "Used SQL select statement: '" << selectStatement() << "'.";
93   }
94 
95   while (canFetchMore()) {
96     fetchMore();
97   }
98 
99   qDebugNN << LOGSEC_MESSAGEMODEL
100            << "Repopulated model, SQL statement is now:\n"
101            << QUOTE_W_SPACE_DOT(selectStatement());
102 }
103 
setData(const QModelIndex & index,const QVariant & value,int role)104 bool MessagesModel::setData(const QModelIndex& index, const QVariant& value, int role) {
105   Q_UNUSED(role)
106   m_cache->setData(index, value, record(index.row()));
107   return true;
108 }
109 
setupFonts()110 void MessagesModel::setupFonts() {
111   QFont fon;
112 
113   fon.fromString(qApp->settings()->value(GROUP(Messages), Messages::ListFont, Application::font("MessagesView").toString()).toString());
114 
115   m_normalFont = fon;
116   m_boldFont = m_normalFont;
117   m_boldFont.setBold(true);
118   m_normalStrikedFont = m_normalFont;
119   m_boldStrikedFont = m_boldFont;
120   m_normalStrikedFont.setStrikeOut(true);
121   m_boldStrikedFont.setStrikeOut(true);
122 
123   m_itemHeight = qApp->settings()->value(GROUP(GUI), SETTING(GUI::HeightRowMessages)).toInt();
124 
125   if (m_itemHeight > 0) {
126     m_boldFont.setPixelSize(int(m_itemHeight * 0.6));
127     m_normalFont.setPixelSize(int(m_itemHeight * 0.6));
128     m_boldStrikedFont.setPixelSize(int(m_itemHeight * 0.6));
129     m_normalStrikedFont.setPixelSize(int(m_itemHeight * 0.6));
130   }
131 }
132 
loadMessages(RootItem * item)133 void MessagesModel::loadMessages(RootItem* item) {
134   m_selectedItem = item;
135 
136   if (item == nullptr) {
137     setFilter(QSL(DEFAULT_SQL_MESSAGES_FILTER));
138   }
139   else {
140     if (!item->getParentServiceRoot()->loadMessagesForItem(item, this)) {
141       setFilter(QSL(DEFAULT_SQL_MESSAGES_FILTER));
142       qCriticalNN << LOGSEC_MESSAGEMODEL
143                   << "Loading of messages from item '"
144                   << item->title() << "' failed.";
145       qApp->showGuiMessage(Notification::Event::GeneralEvent,
146                            tr("Loading of articles from item '%1' failed.").arg(item->title()),
147                            tr("Loading of articles failed, maybe messages could not be downloaded."),
148                            QSystemTrayIcon::MessageIcon::Critical,
149                            true);
150     }
151   }
152 
153   repopulate();
154 }
155 
setMessageImportantById(int id,RootItem::Importance important)156 bool MessagesModel::setMessageImportantById(int id, RootItem::Importance important) {
157   for (int i = 0; i < rowCount(); i++) {
158     int found_id = data(i, MSG_DB_ID_INDEX, Qt::EditRole).toInt();
159 
160     if (found_id == id) {
161       bool set = setData(index(i, MSG_DB_IMPORTANT_INDEX), int(important));
162 
163       if (set) {
164         emit dataChanged(index(i, 0), index(i, MSG_DB_CUSTOM_HASH_INDEX));
165       }
166 
167       return set;
168     }
169   }
170 
171   return false;
172 }
173 
highlightMessages(MessagesModel::MessageHighlighter highlight)174 void MessagesModel::highlightMessages(MessagesModel::MessageHighlighter highlight) {
175   m_messageHighlighter = highlight;
176   emit layoutAboutToBeChanged();
177   emit layoutChanged();
178 }
179 
messageId(int row_index) const180 int MessagesModel::messageId(int row_index) const {
181   return data(row_index, MSG_DB_ID_INDEX, Qt::EditRole).toInt();
182 }
183 
messageImportance(int row_index) const184 RootItem::Importance MessagesModel::messageImportance(int row_index) const {
185   return RootItem::Importance(data(row_index, MSG_DB_IMPORTANT_INDEX, Qt::EditRole).toInt());
186 }
187 
loadedItem() const188 RootItem* MessagesModel::loadedItem() const {
189   return m_selectedItem;
190 }
191 
updateDateFormat()192 void MessagesModel::updateDateFormat() {
193   if (qApp->settings()->value(GROUP(Messages), SETTING(Messages::UseCustomDate)).toBool()) {
194     m_customDateFormat = qApp->settings()->value(GROUP(Messages), SETTING(Messages::CustomDateFormat)).toString();
195   }
196   else {
197     m_customDateFormat = QString();
198   }
199 }
200 
updateFeedIconsDisplay()201 void MessagesModel::updateFeedIconsDisplay() {
202   m_displayFeedIcons = qApp->settings()->value(GROUP(Messages), SETTING(Messages::DisplayFeedIconsInList)).toBool();
203 }
204 
reloadWholeLayout()205 void MessagesModel::reloadWholeLayout() {
206   emit layoutAboutToBeChanged();
207   emit layoutChanged();
208 }
209 
messageAt(int row_index) const210 Message MessagesModel::messageAt(int row_index) const {
211   return Message::fromSqlRecord(m_cache->containsData(row_index) ? m_cache->record(row_index) : record(row_index));
212 }
213 
setupHeaderData()214 void MessagesModel::setupHeaderData() {
215   m_headerData <<
216 
217     /*: Tooltip for ID of message.*/ tr("Id") <<
218 
219     /*: Tooltip for "read" column in msg list.*/ tr("Read") <<
220 
221     /*: Tooltip for "important" column in msg list.*/ tr("Important") <<
222 
223     /*: Tooltip for "deleted" column in msg list.*/ tr("Deleted") <<
224 
225     /*: Tooltip for "pdeleted" column in msg list.*/ tr("Permanently deleted") <<
226 
227     /*: Tooltip for custom ID of feed of message.*/ tr("Feed ID") <<
228 
229     /*: Tooltip for title of message.*/ tr("Title") <<
230 
231     /*: Tooltip for url of message.*/ tr("Url") <<
232 
233     /*: Tooltip for author of message.*/ tr("Author") <<
234 
235     /*: Tooltip for creation date of message.*/ tr("Date") <<
236 
237     /*: Tooltip for contents of message.*/ tr("Contents") <<
238 
239     /*: Tooltip for attachments of message.*/ tr("Attachments") <<
240 
241     /*: Tooltip for score of message.*/ tr("Score") <<
242 
243     /*: Tooltip for account ID of message.*/ tr("Account ID") <<
244 
245     /*: Tooltip for custom ID of message.*/ tr("Custom ID") <<
246 
247     /*: Tooltip for custom hash string of message.*/ tr("Custom hash") <<
248 
249     /*: Tooltip for name of feed for message.*/ tr("Feed") <<
250 
251     /*: Tooltip for indication of presence of enclosures.*/ tr("Has enclosures");
252 
253   m_tooltipData
254     << tr("ID of the article.")
255     << tr("Is article read?")
256     << tr("Is article important?")
257     << tr("Is article deleted?")
258     << tr("Is article permanently deleted from recycle bin?")
259     << tr("ID of feed which this article belongs to.")
260     << tr("Title of the article.")
261     << tr("Url of the article.")
262     << tr("Author of the article.")
263     << tr("Creation date of the article.")
264     << tr("Contents of the article.")
265     << tr("List of attachments.")
266     << tr("Score of the article.")
267     << tr("Account ID of the article.")
268     << tr("Custom ID of the article")
269     << tr("Custom hash of the article.")
270     << tr("Custom ID of feed of the article.")
271     << tr("Indication of enclosures presence within the article.");
272 }
273 
flags(const QModelIndex & index) const274 Qt::ItemFlags MessagesModel::flags(const QModelIndex& index) const {
275   Q_UNUSED(index)
276   return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemNeverHasChildren;
277 }
278 
messagesAt(const QList<int> & row_indices) const279 QList<Message> MessagesModel::messagesAt(const QList<int>& row_indices) const {
280   QList<Message> msgs; msgs.reserve(row_indices.size());
281 
282   for (int idx : row_indices) {
283     msgs << messageAt(idx);
284   }
285 
286   return msgs;
287 }
288 
data(int row,int column,int role) const289 QVariant MessagesModel::data(int row, int column, int role) const {
290   return data(index(row, column), role);
291 }
292 
data(const QModelIndex & idx,int role) const293 QVariant MessagesModel::data(const QModelIndex& idx, int role) const {
294   // This message is not in cache, return real data from live query.
295   switch (role) {
296     // Human readable data for viewing.
297     case Qt::ItemDataRole::DisplayRole: {
298       int index_column = idx.column();
299 
300       if (index_column == MSG_DB_DCREATED_INDEX) {
301         QDateTime dt = TextFactory::parseDateTime(QSqlQueryModel::data(idx, role).value<qint64>()).toLocalTime();
302 
303         if (m_customDateFormat.isEmpty()) {
304           return QLocale().toString(dt, QLocale::FormatType::ShortFormat);
305         }
306         else {
307           return dt.toString(m_customDateFormat);
308         }
309       }
310       else if (index_column == MSG_DB_CONTENTS_INDEX) {
311         // Do not display full contents here.
312         QString contents = data(idx, Qt::EditRole).toString().mid(0, 64).simplified() + QL1S("...");
313 
314         return contents;
315       }
316       else if (index_column == MSG_DB_AUTHOR_INDEX) {
317         const QString author_name = QSqlQueryModel::data(idx, role).toString();
318 
319         return author_name.isEmpty() ? QSL("-") : author_name;
320       }
321       else if (index_column != MSG_DB_IMPORTANT_INDEX &&
322                index_column != MSG_DB_READ_INDEX &&
323                index_column != MSG_DB_HAS_ENCLOSURES &&
324                index_column != MSG_DB_SCORE_INDEX) {
325         return QSqlQueryModel::data(idx, role);
326       }
327       else {
328         return QVariant();
329       }
330     }
331 
332     case LOWER_TITLE_ROLE:
333       return m_cache->containsData(idx.row())
334           ? m_cache->data(idx).toString().toLower()
335           : QSqlQueryModel::data(idx, Qt::ItemDataRole::EditRole).toString().toLower();
336 
337     case Qt::ItemDataRole::EditRole:
338       return m_cache->containsData(idx.row())
339           ? m_cache->data(idx)
340           : QSqlQueryModel::data(idx, role);
341 
342     case Qt::ItemDataRole::ToolTipRole: {
343       if (!qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::EnableTooltipsFeedsMessages)).toBool()) {
344         return QVariant();
345       }
346       else {
347         if (idx.column() == MSG_DB_SCORE_INDEX) {
348           return data(idx, Qt::ItemDataRole::EditRole);
349         }
350         else {
351           return data(idx, Qt::ItemDataRole::DisplayRole);
352         }
353       }
354     }
355 
356     case Qt::ItemDataRole::FontRole: {
357       QModelIndex idx_read = index(idx.row(), MSG_DB_READ_INDEX);
358       QVariant data_read = data(idx_read, Qt::ItemDataRole::EditRole);
359       const bool is_bin = qobject_cast<RecycleBin*>(loadedItem()) != nullptr;
360       bool is_deleted;
361 
362       if (is_bin) {
363         QModelIndex idx_del = index(idx.row(), MSG_DB_PDELETED_INDEX);
364 
365         is_deleted = data(idx_del, Qt::ItemDataRole::EditRole).toBool();
366       }
367       else {
368         QModelIndex idx_del = index(idx.row(), MSG_DB_DELETED_INDEX);
369 
370         is_deleted = data(idx_del, Qt::ItemDataRole::EditRole).toBool();
371       }
372 
373       const bool striked = is_deleted;
374 
375       if (data_read.toBool()) {
376         return striked ? m_normalStrikedFont : m_normalFont;
377       }
378       else {
379         return striked ? m_boldStrikedFont : m_boldFont;
380       }
381     }
382 
383     case Qt::ItemDataRole::ForegroundRole:
384       switch (m_messageHighlighter) {
385         case MessageHighlighter::HighlightImportant: {
386           QModelIndex idx_important = index(idx.row(), MSG_DB_IMPORTANT_INDEX);
387           QVariant dta = m_cache->containsData(idx_important.row()) ? m_cache->data(idx_important) : QSqlQueryModel::data(idx_important);
388 
389           return dta.toInt() == 1 ? qApp->skins()->currentSkin().m_colorPalette[Skin::PaletteColors::Highlight] : QVariant();
390         }
391 
392         case MessageHighlighter::HighlightUnread: {
393           QModelIndex idx_read = index(idx.row(), MSG_DB_READ_INDEX);
394           QVariant dta = m_cache->containsData(idx_read.row()) ? m_cache->data(idx_read) : QSqlQueryModel::data(idx_read);
395 
396           return dta.toInt() == 0 ? qApp->skins()->currentSkin().m_colorPalette[Skin::PaletteColors::Highlight] : QVariant();
397         }
398 
399         case MessageHighlighter::NoHighlighting:
400         default:
401           return QVariant();
402       }
403 
404     case Qt::ItemDataRole::DecorationRole: {
405       const int index_column = idx.column();
406 
407       if (index_column == MSG_DB_READ_INDEX) {
408         if (m_displayFeedIcons && m_selectedItem != nullptr) {
409           QModelIndex idx_feedid = index(idx.row(), MSG_DB_FEED_CUSTOM_ID_INDEX);
410           QVariant dta = m_cache->containsData(idx_feedid.row())
411                            ? m_cache->data(idx_feedid)
412                            : QSqlQueryModel::data(idx_feedid);
413           QString feed_custom_id = dta.toString();
414           auto acc = m_selectedItem->getParentServiceRoot()->feedIconForMessage(feed_custom_id);
415 
416           if (acc.isNull()) {
417             return qApp->icons()->fromTheme(QSL("application-rss+xml"));
418           }
419           else {
420             return acc;
421           }
422         }
423         else {
424           QModelIndex idx_read = index(idx.row(), MSG_DB_READ_INDEX);
425           QVariant dta = m_cache->containsData(idx_read.row()) ? m_cache->data(idx_read) : QSqlQueryModel::data(idx_read);
426 
427           return dta.toInt() == 1 ? m_readIcon : m_unreadIcon;
428         }
429       }
430       else if (index_column == MSG_DB_IMPORTANT_INDEX) {
431         QModelIndex idx_important = index(idx.row(), MSG_DB_IMPORTANT_INDEX);
432         QVariant dta = m_cache->containsData(idx_important.row()) ? m_cache->data(idx_important) : QSqlQueryModel::data(idx_important);
433 
434         return dta.toInt() == 1 ? m_favoriteIcon : QVariant();
435       }
436       else if (index_column == MSG_DB_HAS_ENCLOSURES) {
437         QModelIndex idx_important = index(idx.row(), MSG_DB_HAS_ENCLOSURES);
438         QVariant dta = QSqlQueryModel::data(idx_important);
439 
440         return dta.toBool() ? m_enclosuresIcon : QVariant();
441       }
442       else if (index_column == MSG_DB_SCORE_INDEX) {
443         QVariant dta = QSqlQueryModel::data(idx);
444         int level = std::min(MSG_SCORE_MAX, std::max(MSG_SCORE_MIN, std::floor(dta.toDouble() / 10.0)));
445 
446         return m_scoreIcons.at(level);
447       }
448       else {
449         return QVariant();
450       }
451     }
452 
453     default:
454       return QVariant();
455   }
456 }
457 
setMessageRead(int row_index,RootItem::ReadStatus read)458 bool MessagesModel::setMessageRead(int row_index, RootItem::ReadStatus read) {
459   if (data(row_index, MSG_DB_READ_INDEX, Qt::EditRole).toInt() == int(read)) {
460     // Read status is the same is the one currently set.
461     // In that case, no extra work is needed.
462     return true;
463   }
464 
465   Message message = messageAt(row_index);
466 
467   if (!m_selectedItem->getParentServiceRoot()->onBeforeSetMessagesRead(m_selectedItem, QList<Message>() << message, read)) {
468     // Cannot change read status of the item. Abort.
469     return false;
470   }
471 
472   // Rewrite "visible" data in the model.
473   bool working_change = setData(index(row_index, MSG_DB_READ_INDEX), int(read));
474 
475   if (!working_change) {
476     // If rewriting in the model failed, then cancel all actions.
477     qDebug("Setting of new data to the model failed for message read change.");
478     return false;
479   }
480 
481   if (DatabaseQueries::markMessagesReadUnread(m_db, QStringList() << QString::number(message.m_id), read)) {
482     return m_selectedItem->getParentServiceRoot()->onAfterSetMessagesRead(m_selectedItem, QList<Message>() << message, read);
483   }
484   else {
485     return false;
486   }
487 }
488 
setMessageReadById(int id,RootItem::ReadStatus read)489 bool MessagesModel::setMessageReadById(int id, RootItem::ReadStatus read) {
490   for (int i = 0; i < rowCount(); i++) {
491     int found_id = data(i, MSG_DB_ID_INDEX, Qt::EditRole).toInt();
492 
493     if (found_id == id) {
494       bool set = setData(index(i, MSG_DB_READ_INDEX), int(read));
495 
496       if (set) {
497         emit dataChanged(index(i, 0), index(i, MSG_DB_CUSTOM_HASH_INDEX));
498       }
499 
500       return set;
501     }
502   }
503 
504   return false;
505 }
506 
switchMessageImportance(int row_index)507 bool MessagesModel::switchMessageImportance(int row_index) {
508   const QModelIndex target_index = index(row_index, MSG_DB_IMPORTANT_INDEX);
509   const RootItem::Importance current_importance = (RootItem::Importance) data(target_index, Qt::EditRole).toInt();
510   const RootItem::Importance next_importance = current_importance == RootItem::Importance::Important
511                                                ? RootItem::Importance::NotImportant
512                                                : RootItem::Importance::Important;
513   const Message message = messageAt(row_index);
514   const QPair<Message, RootItem::Importance> pair(message, next_importance);
515 
516   if (!m_selectedItem->getParentServiceRoot()->onBeforeSwitchMessageImportance(m_selectedItem,
517                                                                                QList<QPair<Message, RootItem::Importance>>() << pair)) {
518     return false;
519   }
520 
521   // Rewrite "visible" data in the model.
522   const bool working_change = setData(target_index, int(next_importance));
523 
524   if (!working_change) {
525     // If rewriting in the model failed, then cancel all actions.
526     qDebugNN << LOGSEC_MESSAGEMODEL << "Setting of new data to the model failed for message importance change.";
527     return false;
528   }
529 
530   // Commit changes.
531   if (DatabaseQueries::markMessageImportant(m_db, message.m_id, next_importance)) {
532     emit dataChanged(index(row_index, 0), index(row_index, MSG_DB_FEED_CUSTOM_ID_INDEX), QVector<int>() << Qt::FontRole);
533 
534     return m_selectedItem->getParentServiceRoot()->onAfterSwitchMessageImportance(m_selectedItem,
535                                                                                   QList<QPair<Message, RootItem::Importance>>() << pair);
536   }
537   else {
538     return false;
539   }
540 }
541 
switchBatchMessageImportance(const QModelIndexList & messages)542 bool MessagesModel::switchBatchMessageImportance(const QModelIndexList& messages) {
543   QStringList message_ids; message_ids.reserve(messages.size());
544   QList<QPair<Message, RootItem::Importance>> message_states; message_states.reserve(messages.size());
545 
546   // Obtain IDs of all desired messages.
547   for (const QModelIndex& message : messages) {
548     const Message msg = messageAt(message.row());
549 
550     RootItem::Importance message_importance = messageImportance((message.row()));
551 
552     message_states.append(QPair<Message, RootItem::Importance>(msg, message_importance == RootItem::Importance::Important
553                                                                ? RootItem::Importance::NotImportant
554                                                                : RootItem::Importance::Important));
555     message_ids.append(QString::number(msg.m_id));
556     QModelIndex idx_msg_imp = index(message.row(), MSG_DB_IMPORTANT_INDEX);
557 
558     setData(idx_msg_imp, message_importance == RootItem::Importance::Important
559             ? int(RootItem::Importance::NotImportant)
560             : int(RootItem::Importance::Important));
561   }
562 
563   reloadWholeLayout();
564 
565   if (!m_selectedItem->getParentServiceRoot()->onBeforeSwitchMessageImportance(m_selectedItem, message_states)) {
566     return false;
567   }
568 
569   if (DatabaseQueries::switchMessagesImportance(m_db, message_ids)) {
570     return m_selectedItem->getParentServiceRoot()->onAfterSwitchMessageImportance(m_selectedItem, message_states);
571   }
572   else {
573     return false;
574   }
575 }
576 
setBatchMessagesDeleted(const QModelIndexList & messages)577 bool MessagesModel::setBatchMessagesDeleted(const QModelIndexList& messages) {
578   QStringList message_ids; message_ids.reserve(messages.size());
579   QList<Message> msgs; msgs.reserve(messages.size());
580 
581   // Obtain IDs of all desired messages.
582   for (const QModelIndex& message : messages) {
583     const Message msg = messageAt(message.row());
584 
585     msgs.append(msg);
586     message_ids.append(QString::number(msg.m_id));
587 
588     if (qobject_cast<RecycleBin*>(m_selectedItem) != nullptr) {
589       setData(index(message.row(), MSG_DB_PDELETED_INDEX), 1);
590     }
591     else {
592       setData(index(message.row(), MSG_DB_DELETED_INDEX), 1);
593     }
594   }
595 
596   reloadWholeLayout();
597 
598   if (!m_selectedItem->getParentServiceRoot()->onBeforeMessagesDelete(m_selectedItem, msgs)) {
599     return false;
600   }
601 
602   bool deleted;
603 
604   if (m_selectedItem->kind() != RootItem::Kind::Bin) {
605     deleted = DatabaseQueries::deleteOrRestoreMessagesToFromBin(m_db, message_ids, true);
606   }
607   else {
608     deleted = DatabaseQueries::permanentlyDeleteMessages(m_db, message_ids);
609   }
610 
611   if (deleted) {
612     return m_selectedItem->getParentServiceRoot()->onAfterMessagesDelete(m_selectedItem, msgs);
613   }
614   else {
615     return false;
616   }
617 }
618 
setBatchMessagesRead(const QModelIndexList & messages,RootItem::ReadStatus read)619 bool MessagesModel::setBatchMessagesRead(const QModelIndexList& messages, RootItem::ReadStatus read) {
620   QStringList message_ids; message_ids.reserve(messages.size());
621   QList<Message> msgs; msgs.reserve(messages.size());
622 
623   // Obtain IDs of all desired messages.
624   for (const QModelIndex& message : messages) {
625     Message msg = messageAt(message.row());
626 
627     msgs.append(msg);
628     message_ids.append(QString::number(msg.m_id));
629     setData(index(message.row(), MSG_DB_READ_INDEX), int(read));
630   }
631 
632   reloadWholeLayout();
633 
634   if (!m_selectedItem->getParentServiceRoot()->onBeforeSetMessagesRead(m_selectedItem, msgs, read)) {
635     return false;
636   }
637 
638   if (DatabaseQueries::markMessagesReadUnread(m_db, message_ids, read)) {
639     return m_selectedItem->getParentServiceRoot()->onAfterSetMessagesRead(m_selectedItem, msgs, read);
640   }
641   else {
642     return false;
643   }
644 }
645 
setBatchMessagesRestored(const QModelIndexList & messages)646 bool MessagesModel::setBatchMessagesRestored(const QModelIndexList& messages) {
647   QStringList message_ids; message_ids.reserve(messages.size());
648   QList<Message> msgs; msgs.reserve(messages.size());
649 
650   // Obtain IDs of all desired messages.
651   for (const QModelIndex& message : messages) {
652     const Message msg = messageAt(message.row());
653 
654     msgs.append(msg);
655     message_ids.append(QString::number(msg.m_id));
656     setData(index(message.row(), MSG_DB_PDELETED_INDEX), 0);
657     setData(index(message.row(), MSG_DB_DELETED_INDEX), 0);
658   }
659 
660   reloadWholeLayout();
661 
662   if (!m_selectedItem->getParentServiceRoot()->onBeforeMessagesRestoredFromBin(m_selectedItem, msgs)) {
663     return false;
664   }
665 
666   if (DatabaseQueries::deleteOrRestoreMessagesToFromBin(m_db, message_ids, false)) {
667     return m_selectedItem->getParentServiceRoot()->onAfterMessagesRestoredFromBin(m_selectedItem, msgs);
668   }
669   else {
670     return false;
671   }
672 }
673 
headerData(int section,Qt::Orientation orientation,int role) const674 QVariant MessagesModel::headerData(int section, Qt::Orientation orientation, int role) const {
675   Q_UNUSED(orientation)
676 
677   switch (role) {
678     case Qt::DisplayRole:
679 
680       // Display textual headers for all columns except "read" and
681       // "important" and "has enclosures" columns.
682       if (section != MSG_DB_READ_INDEX &&
683           section != MSG_DB_IMPORTANT_INDEX &&
684           section != MSG_DB_SCORE_INDEX &&
685           section != MSG_DB_HAS_ENCLOSURES) {
686         return m_headerData.at(section);
687       }
688       else {
689         return QVariant();
690       }
691 
692     case Qt::ToolTipRole:
693       return m_tooltipData.at(section);
694 
695     case Qt::EditRole:
696       return m_headerData.at(section);
697 
698     // Display icons for "read" and "important" columns.
699     case Qt::DecorationRole: {
700       switch (section) {
701         case MSG_DB_HAS_ENCLOSURES:
702           return m_enclosuresIcon;
703 
704         case MSG_DB_READ_INDEX:
705           return m_readIcon;
706 
707         case MSG_DB_IMPORTANT_INDEX:
708           return m_favoriteIcon;
709 
710         case MSG_DB_SCORE_INDEX:
711           return m_scoreIcons.at(5);
712 
713         default:
714           return QVariant();
715       }
716     }
717 
718     default:
719       return QVariant();
720   }
721 }
722