1 // For license of this file, see <project-root-folder>/LICENSE.md.
2 
3 #include "core/messagesproxymodel.h"
4 
5 #include "core/messagesmodel.h"
6 #include "core/messagesmodelcache.h"
7 #include "miscellaneous/application.h"
8 #include "miscellaneous/regexfactory.h"
9 #include "miscellaneous/settings.h"
10 
11 #include <QTimer>
12 
MessagesProxyModel(MessagesModel * source_model,QObject * parent)13 MessagesProxyModel::MessagesProxyModel(MessagesModel* source_model, QObject* parent)
14   : QSortFilterProxyModel(parent), m_sourceModel(source_model), m_showUnreadOnly(false) {
15   setObjectName(QSL("MessagesProxyModel"));
16 
17   setSortRole(Qt::ItemDataRole::EditRole);
18   setSortCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive);
19 
20   setFilterKeyColumn(-1);
21   setFilterRole(LOWER_TITLE_ROLE);
22 
23   setDynamicSortFilter(false);
24   setSourceModel(m_sourceModel);
25 }
26 
~MessagesProxyModel()27 MessagesProxyModel::~MessagesProxyModel() {
28   qDebugNN << LOGSEC_MESSAGEMODEL << "Destroying MessagesProxyModel instance.";
29 }
30 
getNextPreviousUnreadItemIndex(int default_row)31 QModelIndex MessagesProxyModel::getNextPreviousUnreadItemIndex(int default_row) {
32   const bool started_from_zero = default_row == 0;
33   QModelIndex next_index = getNextUnreadItemIndex(default_row, rowCount() - 1);
34 
35   // There is no next message, check previous.
36   if (!next_index.isValid() && !started_from_zero) {
37     next_index = getNextUnreadItemIndex(0, default_row - 1);
38   }
39 
40   return next_index;
41 }
42 
getNextUnreadItemIndex(int default_row,int max_row) const43 QModelIndex MessagesProxyModel::getNextUnreadItemIndex(int default_row, int max_row) const {
44   while (default_row <= max_row) {
45     // Get info if the message is read or not.
46     const QModelIndex proxy_index = index(default_row, MSG_DB_READ_INDEX);
47     const bool is_read = m_sourceModel->data(mapToSource(proxy_index).row(),
48                                              MSG_DB_READ_INDEX, Qt::EditRole).toInt() == 1;
49 
50     if (!is_read) {
51       // We found unread message, mark it.
52       return proxy_index;
53     }
54     else {
55       default_row++;
56     }
57   }
58 
59   return QModelIndex();
60 }
61 
lessThan(const QModelIndex & left,const QModelIndex & right) const62 bool MessagesProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const {
63   Q_UNUSED(left)
64   Q_UNUSED(right)
65 
66   // NOTE: Comparisons are done by SQL servers itself, not client-side.
67   return false;
68 }
69 
filterAcceptsRow(int source_row,const QModelIndex & source_parent) const70 bool MessagesProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const {
71   // We want to show only regexped messages when "all" should be visible
72   // and we want to show only regexped AND unread messages when unread should be visible.
73   //
74   // But also, we want to see messages which have their dirty states cached, because
75   // otherwise they would just disappeaar from the list for example when batch marked as read
76   // which is distracting.
77   return
78     QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent) &&
79     (m_sourceModel->cache()->containsData(source_row) ||
80      (!m_showUnreadOnly || !m_sourceModel->messageAt(source_row).m_isRead));
81 }
82 
showUnreadOnly() const83 bool MessagesProxyModel::showUnreadOnly() const {
84   return m_showUnreadOnly;
85 }
86 
setShowUnreadOnly(bool show_unread_only)87 void MessagesProxyModel::setShowUnreadOnly(bool show_unread_only) {
88   m_showUnreadOnly = show_unread_only;
89   qApp->settings()->setValue(GROUP(Messages), Messages::ShowOnlyUnreadMessages, show_unread_only);
90 }
91 
mapListFromSource(const QModelIndexList & indexes,bool deep) const92 QModelIndexList MessagesProxyModel::mapListFromSource(const QModelIndexList& indexes, bool deep) const {
93   QModelIndexList mapped_indexes;
94 
95   for (const QModelIndex& index : indexes) {
96     if (deep) {
97       // Construct new source index.
98       mapped_indexes << mapFromSource(m_sourceModel->index(index.row(), index.column()));
99     }
100     else {
101       mapped_indexes << mapFromSource(index);
102     }
103   }
104 
105   return mapped_indexes;
106 }
107 
match(const QModelIndex & start,int role,const QVariant & entered_value,int hits,Qt::MatchFlags flags) const108 QModelIndexList MessagesProxyModel::match(const QModelIndex& start, int role,
109                                           const QVariant& entered_value, int hits, Qt::MatchFlags flags) const {
110   QModelIndexList result;
111   const int match_type = flags & 0x0F;
112   const Qt::CaseSensitivity case_sensitivity = Qt::CaseSensitivity::CaseInsensitive;
113   const bool wrap = (flags& Qt::MatchFlag::MatchWrap) > 0;
114   const bool all_hits = (hits == -1);
115   QString entered_text;
116   int from = start.row();
117   int to = rowCount();
118 
119   for (int i = 0; (wrap && i < 2) || (!wrap && i < 1); i++) {
120     for (int r = from; (r < to) && (all_hits || result.count() < hits); r++) {
121       QModelIndex idx = index(r, start.column());
122 
123       if (!idx.isValid()) {
124         continue;
125       }
126 
127       QVariant item_value = m_sourceModel->data(mapToSource(idx).row(), MSG_DB_TITLE_INDEX, role);
128 
129       // QVariant based matching.
130       if (match_type == Qt::MatchExactly) {
131         if (entered_value == item_value) {
132           result.append(idx);
133         }
134       }
135 
136       // QString based matching.
137       else {
138         if (entered_text.isEmpty()) {
139           entered_text = entered_value.toString();
140         }
141 
142         QString item_text = item_value.toString();
143 
144         switch (match_type) {
145 #if QT_VERSION >= 0x050F00 // Qt >= 5.15.0
146           case Qt::MatchFlag::MatchRegularExpression:
147 #else
148           case Qt::MatchFlag::MatchRegExp:
149 #endif
150             if (QRegularExpression(entered_text,
151                                    QRegularExpression::PatternOption::CaseInsensitiveOption |
152                                    QRegularExpression::PatternOption::UseUnicodePropertiesOption).match(item_text).hasMatch()) {
153               result.append(idx);
154             }
155 
156             break;
157 
158           case Qt::MatchWildcard:
159             if (QRegularExpression(RegexFactory::wildcardToRegularExpression(entered_text),
160                                    QRegularExpression::PatternOption::CaseInsensitiveOption |
161                                    QRegularExpression::PatternOption::UseUnicodePropertiesOption).match(item_text).hasMatch()) {
162               result.append(idx);
163             }
164 
165             break;
166 
167           case Qt::MatchStartsWith:
168             if (item_text.startsWith(entered_text, case_sensitivity)) {
169               result.append(idx);
170             }
171 
172             break;
173 
174           case Qt::MatchEndsWith:
175             if (item_text.endsWith(entered_text, case_sensitivity)) {
176               result.append(idx);
177             }
178 
179             break;
180 
181           case Qt::MatchFixedString:
182             if (item_text.compare(entered_text, case_sensitivity) == 0) {
183               result.append(idx);
184             }
185 
186             break;
187 
188           case Qt::MatchContains:
189           default:
190             if (item_text.contains(entered_text, case_sensitivity)) {
191               result.append(idx);
192             }
193 
194             break;
195         }
196       }
197     }
198 
199     // Prepare for the next iteration.
200     from = 0;
201     to = start.row();
202   }
203 
204   return result;
205 }
206 
sort(int column,Qt::SortOrder order)207 void MessagesProxyModel::sort(int column, Qt::SortOrder order) {
208   // NOTE: Ignore here, sort is done elsewhere (server-side).
209   Q_UNUSED(column)
210   Q_UNUSED(order)
211 }
212 
mapListToSource(const QModelIndexList & indexes) const213 QModelIndexList MessagesProxyModel::mapListToSource(const QModelIndexList& indexes) const {
214   QModelIndexList source_indexes; source_indexes.reserve(indexes.size());
215 
216   for (const QModelIndex& index : indexes) {
217     source_indexes << mapToSource(index);
218   }
219 
220   return source_indexes;
221 }
222