presentation/MailSearchSession.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include "MailPresentationModel.h" | ||
| 4 | #include "SearchMenuModel.h" | ||
| 5 | #include "SearchTermModel.h" | ||
| 6 | #include "data/MailCache.h" | ||
| 7 | #include "data/MailRequest.h" | ||
| 8 | #include "data/MailSearchSummary.h" | ||
| 9 | #include "ui/MailListModel.h" | ||
| 10 | #include <QElapsedTimer> | ||
| 11 | #include <QObject> | ||
| 12 | #include <QSet> | ||
| 13 | #include <QTimer> | ||
| 14 | #include <functional> | ||
| 15 | |||
| 16 | class MailSearchReadService; | ||
| 17 | class ServerSearchSession; | ||
| 18 | |||
| 19 | // Sprint 101 S101.1.2 (D-2, D-3, D-6, D-11, D-12, D-29): one search of one | ||
| 20 | // search tab. It owns the tab's canonical query and its history, its own | ||
| 21 | // result rows (the mail-row projection of the folder list, never the folder | ||
| 22 | // list itself — SF-02), infinite paging through the shared search reader, | ||
| 23 | // the summary of every hit (counts, days, facets) and the merge of the | ||
| 24 | // server search when the tab is the server search's owner. | ||
| 25 | // | ||
| 26 | // Everything a view shows — chips, title, field text, the timeline's range | ||
| 27 | // — is derived from query() (rule 8). Results are identities (rule 9). | ||
| 28 | // | ||
| 29 | // Owner: MailSearchSessions (one per search tab). GUI thread only. | ||
| 30 | class MailSearchSession final : public QObject { | ||
| 31 | 7 | Q_OBJECT | |
| 32 | Q_PROPERTY(QString tabId READ tabId CONSTANT) | ||
| 33 | Q_PROPERTY(QString accountId READ accountId CONSTANT) | ||
| 34 | Q_PROPERTY(QString query READ query NOTIFY queryChanged) | ||
| 35 | Q_PROPERTY(QString fieldText READ fieldText NOTIFY queryChanged) | ||
| 36 | Q_PROPERTY(QVariantList terms READ terms NOTIFY queryChanged) | ||
| 37 | Q_PROPERTY(QString title READ title NOTIFY queryChanged) | ||
| 38 | Q_PROPERTY(QString dateFrom READ dateFromIso NOTIFY queryChanged) | ||
| 39 | Q_PROPERTY(QString dateTo READ dateToIso NOTIFY queryChanged) | ||
| 40 | Q_PROPERTY(QString state READ state NOTIFY stateChanged) | ||
| 41 | Q_PROPERTY(QString stateText READ stateText NOTIFY stateChanged) | ||
| 42 | Q_PROPERTY(QAbstractItemModel *results READ results CONSTANT) | ||
| 43 | Q_PROPERTY(int loadedCount READ loadedCount NOTIFY rowsChanged) | ||
| 44 | Q_PROPERTY(bool hasMore READ hasMore NOTIFY rowsChanged) | ||
| 45 | Q_PROPERTY(bool pagePending READ pagePending NOTIFY rowsChanged) | ||
| 46 | Q_PROPERTY(qint64 total READ total NOTIFY summaryChanged) | ||
| 47 | Q_PROPERTY(QString summaryState READ summaryState NOTIFY summaryChanged) | ||
| 48 | Q_PROPERTY(bool serverRunning READ serverRunning NOTIFY serverChanged) | ||
| 49 | Q_PROPERTY(int serverDone READ serverDone NOTIFY serverChanged) | ||
| 50 | Q_PROPERTY(int serverTotal READ serverTotal NOTIFY serverChanged) | ||
| 51 | Q_PROPERTY(int serverAdded READ serverAdded NOTIFY serverChanged) | ||
| 52 | // S106-024: folders whose server search the server refused. | ||
| 53 | Q_PROPERTY(int serverFailed READ serverFailed NOTIFY serverChanged) | ||
| 54 | Q_PROPERTY(bool canGoBack READ canGoBack NOTIFY historyChanged) | ||
| 55 | Q_PROPERTY(bool canGoForward READ canGoForward NOTIFY historyChanged) | ||
| 56 | Q_PROPERTY(bool live READ live NOTIFY rowsChanged) | ||
| 57 | // S101.6.1 (D-13): results marked for a bulk action (Ctrl/Shift click, Ctrl+A). | ||
| 58 | Q_PROPERTY(int markedCount READ markedCount NOTIFY markedChanged) | ||
| 59 | |||
| 60 | public: | ||
| 61 | struct Deps { | ||
| 62 | MailCache *cache = nullptr; | ||
| 63 | MailSearchReadService *reader = nullptr; | ||
| 64 | ServerSearchSession *server = nullptr; | ||
| 65 | std::function<bool()> indexReady; | ||
| 66 | std::function<quint64()> indexGeneration; | ||
| 67 | // A cached result of this account in a folder the account lists. | ||
| 68 | std::function<bool(const MailId &, const QString &folderPath)> acceptResult; | ||
| 69 | // The folder paths the account lists: the summary counts only these (G4). | ||
| 70 | std::function<QSet<QString>()> allowedFolders; | ||
| 71 | std::function<bool()> serverEnabled; | ||
| 72 | std::function<SearchTermModel::Lookups()> lookups; | ||
| 73 | // D-32: the folder tree in display order (the sidebar's folder facet). | ||
| 74 | std::function<QList<SearchMenuModel::FolderRow>()> folderTree; | ||
| 75 | }; | ||
| 76 | // The states of D-27 and §6.2.4. "empty": no query yet (D-25). | ||
| 77 | enum class State { Empty, Searching, Ready, Limited, Failed, Indexing, Busy }; | ||
| 78 | static constexpr int PageSize = 200; | ||
| 79 | static constexpr int MaximumHistory = 50; | ||
| 80 | // D-6: the server search starts after this much quiet typing or on Enter. | ||
| 81 | static constexpr int ServerDelayMs = 800; | ||
| 82 | |||
| 83 | MailSearchSession(QString tabId, QString accountId, Deps deps, QObject *parent = nullptr); | ||
| 84 | ~MailSearchSession() override; | ||
| 85 | |||
| 86 | 403 | QString tabId() const { return m_tabId; } | |
| 87 | 102 | QString accountId() const { return m_accountId; } | |
| 88 | 3517 | QString query() const { return m_query; } | |
| 89 | QString fieldText() const; | ||
| 90 | QVariantList terms() const; | ||
| 91 | QString title() const; | ||
| 92 | QString dateFromIso() const; | ||
| 93 | QString dateToIso() const; | ||
| 94 | QString state() const; | ||
| 95 | 5 | QString stateText() const { return m_stateText; } | |
| 96 | 89 | State stateValue() const { return m_state; } | |
| 97 | 2923 | QAbstractItemModel *results() { return &m_results; } | |
| 98 | 89 | MailPresentationModel *resultModel() { return &m_results; } | |
| 99 |
1/1✓ Branch 3 → 4 taken 5309 times.
|
5309 | int loadedCount() const { return m_results.rowCount(); } |
| 100 | 296 | bool hasMore() const { return m_hasMore; } | |
| 101 | 199 | bool pagePending() const { return m_pagePending; } | |
| 102 | // The summary total plus server-only hits; -1 while unknown. | ||
| 103 | qint64 total() const; | ||
| 104 | QString summaryState() const; | ||
| 105 | 462 | const MailSearchSummary &summary() const { return m_summary; } | |
| 106 | 481 | bool serverRunning() const { return m_serverRunning; } | |
| 107 | 1 | int serverDone() const { return m_serverDone; } | |
| 108 | 161 | int serverTotal() const { return m_serverTotal; } | |
| 109 | 3 | int serverAdded() const { return m_serverAdded; } | |
| 110 | 320 | int serverFailed() const { return m_serverFailed; } | |
| 111 | 45 | bool canGoBack() const { return m_historyIndex > 0; } | |
| 112 | 19 | bool canGoForward() const { return m_historyIndex + 1 < m_history.size(); } | |
| 113 | // D-3: a released session keeps only its query and anchor. | ||
| 114 | 10 | bool live() const { return m_live; } | |
| 115 | |||
| 116 | // A new search or refinement: one history step (D-2). `coalesce` replaces | ||
| 117 | // the current step instead (typing in the field, D-6). | ||
| 118 | Q_INVOKABLE void setQuery(const QString &query, bool coalesce = false); | ||
| 119 | // D-6/D-28: the title field. Edits re-run locally after the adaptive | ||
| 120 | // debounce and start the server after ServerDelayMs; submit runs now. | ||
| 121 | Q_INVOKABLE void editFieldText(const QString &text); | ||
| 122 | Q_INVOKABLE void submitFieldText(const QString &text); | ||
| 123 | Q_INVOKABLE void revertFieldText(); | ||
| 124 | // The field text `text` stands for after its typed constraint tokens left | ||
| 125 | // the field (D-28); equal to fieldText() when the field is in step. | ||
| 126 | Q_INVOKABLE QString normalizedFieldText(const QString &text) const; | ||
| 127 | // Chip edits (SearchTermModel), each one history step. | ||
| 128 | Q_INVOKABLE void removeTerm(int index); | ||
| 129 | Q_INVOKABLE void replaceTerm(int index, const QString &value); | ||
| 130 | Q_INVOKABLE void negateTerm(int index); | ||
| 131 | Q_INVOKABLE void addTerm(const QString &kind, const QString &value, bool negated = false); | ||
| 132 | Q_INVOKABLE void setState(const QString &kind, int value); | ||
| 133 | Q_INVOKABLE int termState(const QString &kind) const; | ||
| 134 | // CB-05: "Ctrl+Shift+I" / `filter off` in a search tab — the three status | ||
| 135 | // chips (unread, important, attachments) off in one history step. | ||
| 136 | Q_INVOKABLE void clearStatusFilters(); | ||
| 137 | // D-15a: "filter unread important" in a search tab — exactly these status | ||
| 138 | // chips on, the others off (one history step); a negated chip that is | ||
| 139 | // not named ("is:read" without "unread") stays. | ||
| 140 | Q_INVOKABLE void setStatusFilters(bool unread, bool flagged, bool attachment); | ||
| 141 | // D-8: ISO dates; empty strings remove the date constraint. | ||
| 142 | Q_INVOKABLE void setDateRange(const QString &from, const QString &to); | ||
| 143 | Q_INVOKABLE bool back(); | ||
| 144 | Q_INVOKABLE bool forward(); | ||
| 145 | Q_INVOKABLE void fetchMore(); | ||
| 146 | Q_INVOKABLE void retry(); | ||
| 147 | Q_INVOKABLE void startServerSearch(); | ||
| 148 | // §6.2.5/SD-12: after an empty result, the relaxations that bring | ||
| 149 | // results back: {label, query, count}, at most four. | ||
| 150 | Q_INVOKABLE QVariantList relaxations() const; | ||
| 151 | // S101.4.3 (SearchMenuModel): "Filter ▾" and chip menus with the counts | ||
| 152 | // of the hits; applyChoice runs a picked entry as one history step and is | ||
| 153 | // false for ids the view handles (editors, view actions) or that change | ||
| 154 | // nothing. | ||
| 155 | Q_INVOKABLE QVariantList filterMenu() const; | ||
| 156 | Q_INVOKABLE QVariantList chipMenu(int termIndex) const; | ||
| 157 | Q_INVOKABLE bool applyChoice(const QString &id); | ||
| 158 | // SD-09/PT-01: the people among the hits matching the field's last word; | ||
| 159 | // picking one turns that word into a From/To chip (one history step). | ||
| 160 | Q_INVOKABLE QVariantList fieldCompletions(const QString &text) const; | ||
| 161 | Q_INVOKABLE bool pickFieldCompletion(const QString &text, const QString &id); | ||
| 162 | // D-32: the sidebar's facets — the folders with hits in tree order and | ||
| 163 | // the top values of "from" / "label" (SearchMenuModel entries). | ||
| 164 | Q_INVOKABLE QVariantList folderFacet() const; | ||
| 165 | Q_INVOKABLE QVariantList valueFacet(const QString &kind, int limit) const; | ||
| 166 | Q_INVOKABLE int rowForIdentity(const QVariantMap &identity) const; | ||
| 167 | Q_INVOKABLE QVariantMap mailAt(int row) const; | ||
| 168 | |||
| 169 | // The registry's lifecycle calls. | ||
| 170 | void activate(); // the tab became the active one | ||
| 171 | void deactivate(); // another tab became active (D-12: the server pauses) | ||
| 172 | void release(); // D-3: drop the rows, keep the query | ||
| 173 | void refreshAfterIndexReady(); | ||
| 174 | void setSelected(const MailId &mail); | ||
| 175 | MailId selected() const { return m_selected; } | ||
| 176 | // The selected result's date (the timeline's caret, D-9); invalid when | ||
| 177 | // nothing or a mail outside the loaded rows is selected. | ||
| 178 | QDateTime selectedDateTime() const; | ||
| 179 | // S101.6.1 (D-13): Ctrl toggles a row, Shift marks the range from the | ||
| 180 | // anchor, a plain click clears; the first mark takes the selected result | ||
| 181 | // along. Only loaded rows can be marked; a new query clears the marks. | ||
| 182 | 538 | int markedCount() const { return int(m_marked.size()); } | |
| 183 | // S101.5.9: the result rows' section unit ("year", "month", "week", | ||
| 184 | // "day"; "" = no sections), the timeline's current unit. | ||
| 185 | void setPeriodUnit(const QString &unit); | ||
| 186 | void mark(int row, Qt::KeyboardModifiers modifiers); | ||
| 187 | void markAllLoaded(); | ||
| 188 | void clearMarks(); | ||
| 189 | QList<MailId> markedIds() const; // in view order | ||
| 190 | // §6.2.4 hour level: the first loaded result (view row) of that local | ||
| 191 | // hour; -1 when none is loaded. | ||
| 192 | Q_INVOKABLE int rowForHour(int hour) const; | ||
| 193 | // The date of a loaded result (view row); invalid outside the loaded rows. | ||
| 194 | QDateTime dateTimeAt(int row) const; | ||
| 195 | bool contains(const MailId &mail) const; | ||
| 196 | // Actions on results (moves, flags) keep the rows in step. | ||
| 197 | void removeRows(qint64 folderId, const QList<qint64> &uids); | ||
| 198 | void updateFlags(qint64 folderId, qint64 uid, quint32 flags); | ||
| 199 | // The visual Seen/Flagged commits of the controller (any folder). | ||
| 200 | void setFlag(qint64 folderId, qint64 uid, quint32 flag, bool on); | ||
| 201 | // The view row of a result and the result of a view row (rule 9: rows | ||
| 202 | // stay inside the session; callers pass identities). | ||
| 203 | int rowOf(const MailId &mail) const; | ||
| 204 | MailId mailIdAt(int row) const; | ||
| 205 | QDateTime dateOf(const MailId &mail) const; // invalid when the mail is not loaded | ||
| 206 | // Called by the registry for the server search it owns. | ||
| 207 | void serverHits(const QList<qint64> &uids, qint64 folderId, const QString &folderPath); | ||
| 208 | void serverProgress(int done, int total); | ||
| 209 | void serverFinished(); | ||
| 210 | void serverFolderFailed(); | ||
| 211 | |||
| 212 | // D-29: a server hit's header must satisfy the header constraints the | ||
| 213 | // server cannot check (attachment, labels, exclusions, …). Text is not | ||
| 214 | // checked: the server found it, possibly in the body. | ||
| 215 | static bool matchesHeader(const MailHeader &header, const QString &folderPath, | ||
| 216 | const MailCache::SearchFilter &filter); | ||
| 217 | |||
| 218 | signals: | ||
| 219 | void queryChanged(); | ||
| 220 | void stateChanged(); | ||
| 221 | void rowsChanged(); | ||
| 222 | void summaryChanged(); | ||
| 223 | void serverChanged(); | ||
| 224 | void historyChanged(); | ||
| 225 | void selectionChanged(); | ||
| 226 | void markedChanged(); | ||
| 227 | // D-29/D-31: the rows were replaced; `selected` survived (valid) or the | ||
| 228 | // view should select the first row (invalid) — the registry decides. | ||
| 229 | void rowsReplaced(const MailId &selected); | ||
| 230 | // The session wants to own the shared server search (registry decides). | ||
| 231 | void serverSearchRequested(); | ||
| 232 | |||
| 233 | private: | ||
| 234 | void run(const QString &canonical, bool coalesce, bool keepAnchor); | ||
| 235 | // F-27 (SPRINT-104): the shared body of back()/forward(). | ||
| 236 | bool navigateHistory(int step); | ||
| 237 | void pushHistory(const QString &canonical, bool coalesce); | ||
| 238 | void startLocal(bool keepAnchor); | ||
| 239 | void requestPage(bool replace); | ||
| 240 | void pageArrived(MailSearchPage page, bool replace, quint64 generation); | ||
| 241 | QList<MailHeader> admit(const QList<MailSearchHit> &hits); | ||
| 242 | void requestSummary(); | ||
| 243 | void summaryArrived(MailSearchSummary summary, quint64 generation); | ||
| 244 | void requestRelaxations(); | ||
| 245 | void relaxationStep(quint64 generation, int position); | ||
| 246 | void setStateValue(State state, const QString &text = {}); | ||
| 247 | void countServerHit(const MailHeader &header, const QString &folderPath); | ||
| 248 | // S106-245: one mail entering (+1) or leaving (-1) the result moves every | ||
| 249 | // count and facet the summary shows, the same way in both directions. | ||
| 250 | void adjustSummary(const MailHeader &header, int delta); | ||
| 251 | void cancelReads(); | ||
| 252 | SearchMenuModel::Context menuContext() const; | ||
| 253 | QString identityKey(const MailHeader &header) const; | ||
| 254 | |||
| 255 | QString m_tabId; | ||
| 256 | QString m_accountId; | ||
| 257 | Deps m_deps; | ||
| 258 | QString m_query; | ||
| 259 | QString m_text; // parsed free text (SQL) | ||
| 260 | MailCache::SearchFilter m_filter; // parsed constraints | ||
| 261 | QStringList m_history; | ||
| 262 | int m_historyIndex = -1; | ||
| 263 | bool m_lastStepTyped = false; | ||
| 264 | MailListModel m_rows; | ||
| 265 | MailPresentationModel m_results; | ||
| 266 | QSet<QString> m_identities; // D-11: canonical Message-ID or row key | ||
| 267 | int m_offset = 0; | ||
| 268 | bool m_hasMore = false; | ||
| 269 | bool m_pagePending = false; | ||
| 270 | bool m_live = true; | ||
| 271 | quint64 m_generation = 0; | ||
| 272 | quint64 m_indexGeneration = 0; | ||
| 273 | QObject m_pageOwner; | ||
| 274 | QObject m_summaryOwner; | ||
| 275 | QObject m_relaxationOwner; | ||
| 276 | MailSearchSummary m_summary; | ||
| 277 | bool m_summaryPending = false; | ||
| 278 | QList<SearchTermModel::Relaxation> m_relaxationCandidates; | ||
| 279 | QHash<int, qint64> m_relaxationCounts; // candidate index → count | ||
| 280 | QHash<qint64, QString> m_folderPaths; // folder id → path of the shown rows (facets) | ||
| 281 | State m_state = State::Empty; | ||
| 282 | QString m_stateText; | ||
| 283 | MailId m_selected; | ||
| 284 | QSet<QPair<qint64, qint64>> m_marked; // (folderId, uid) | ||
| 285 | QString m_periodUnit; | ||
| 286 | MailId m_markAnchor; | ||
| 287 | bool m_active = false; | ||
| 288 | bool m_serverWanted = false; | ||
| 289 | bool m_serverRunning = false; | ||
| 290 | int m_serverDone = 0; | ||
| 291 | int m_serverTotal = 0; | ||
| 292 | int m_serverAdded = 0; | ||
| 293 | int m_serverFailed = 0; | ||
| 294 | QTimer m_localDebounce; | ||
| 295 | QTimer m_serverDelay; | ||
| 296 | QString m_pendingFieldText; | ||
| 297 | QElapsedTimer m_editClock; | ||
| 298 | qint64 m_lastEdit = -1; | ||
| 299 | QElapsedTimer m_runClock; | ||
| 300 | }; | ||
| 301 |