presentation/QuickMailBackend.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | #include "FolderIconCatalog.h" | ||
| 3 | #include "StatusMessageState.h" | ||
| 4 | #include "FolderSubscriptionEditor.h" | ||
| 5 | #include "MessageCopyQueue.h" | ||
| 6 | #include "DavCollectionDiscovery.h" | ||
| 7 | #include "AccountConnectionTest.h" | ||
| 8 | #include "QuickPreferences.h" | ||
| 9 | #include "app/core/SettingsSyncSession.h" | ||
| 10 | |||
| 11 | #include "ComposeDraftStore.h" | ||
| 12 | #include "FolderPresentationModel.h" | ||
| 13 | #include "MailPresentationModel.h" | ||
| 14 | #include "QuickMailSelection.h" | ||
| 15 | #include "QuickPimBackend.h" | ||
| 16 | #include "QuickSourceViewer.h" | ||
| 17 | #include "QuickFolderActions.h" | ||
| 18 | #include "QuickMailMenu.h" | ||
| 19 | #include "QuickAccountSettings.h" | ||
| 20 | #include "QuickComposeBackend.h" | ||
| 21 | #include "QuickInvitation.h" | ||
| 22 | #include "QuickCalendarIcs.h" | ||
| 23 | #include "QuickContactVcf.h" | ||
| 24 | #include "QuickMailEventLink.h" | ||
| 25 | #include "EventQuickInput.h" | ||
| 26 | #include "QuickThreadListModel.h" | ||
| 27 | #include "MailSelectionSummary.h" | ||
| 28 | #include "data/MailSort.h" | ||
| 29 | #include "QuickRulesBackend.h" | ||
| 30 | #include "MailSearchSessions.h" | ||
| 31 | #include "data/MailRequest.h" | ||
| 32 | #include "service/FolderPredictionService.h" | ||
| 33 | #include <QObject> | ||
| 34 | #include <QHash> | ||
| 35 | #include <QPointer> | ||
| 36 | #include <QSet> | ||
| 37 | #include <QThreadPool> | ||
| 38 | #include <QTimer> | ||
| 39 | #include <QUrl> | ||
| 40 | #include <QCache> | ||
| 41 | |||
| 42 | #include "ConflictCenter.h" | ||
| 43 | #include "QuickConflictResolver.h" | ||
| 44 | #include <atomic> | ||
| 45 | #include <functional> | ||
| 46 | #include <memory> | ||
| 47 | #include <optional> | ||
| 48 | |||
| 49 | class MailSessionCore; | ||
| 50 | class WorkspaceState; | ||
| 51 | class SearchCoordinator; | ||
| 52 | class MailSearchReadService; | ||
| 53 | class ExternalWhitelistModel; | ||
| 54 | class MailPayloadPreview; | ||
| 55 | struct FolderInfo; | ||
| 56 | |||
| 57 | // Frontend adapter over the shared product session. It owns no transport/cache. | ||
| 58 | class QuickMailBackend final : public QObject { | ||
| 59 | 2316 | Q_OBJECT | |
| 60 | Q_PROPERTY(QuickPreferences *preferences READ preferences CONSTANT) | ||
| 61 | Q_PROPERTY(QAbstractItemModel *folders READ folders CONSTANT) | ||
| 62 | // S91.1.1: calendar, tasks and contacts (QML `backend.pim.*`). | ||
| 63 | Q_PROPERTY(QuickPimBackend *pim READ pim CONSTANT) | ||
| 64 | // S91.1.2: raw source, inline PDF and attachment export (QML `backend.source.*`). | ||
| 65 | Q_PROPERTY(QuickSourceViewer *source READ source CONSTANT) | ||
| 66 | // S91.3.1: send, drafts, sessions, attachments (QML `backend.compose.*`). | ||
| 67 | Q_PROPERTY(QuickComposeBackend *compose READ compose CONSTANT) | ||
| 68 | // Sprint 93 S93.5.2: invitation card of the displayed mail | ||
| 69 | // (QML `backend.invitation.*`); the host reports the displayed mail. | ||
| 70 | Q_PROPERTY(QuickInvitation *invitation READ invitation CONSTANT) | ||
| 71 | // Sprint 93 S93.5.3: calendar file import/export (QML `backend.ics.*`). | ||
| 72 | Q_PROPERTY(QuickCalendarIcs *ics READ ics CONSTANT) | ||
| 73 | // Sprint 97 S97.5.8: vCard import and export (QML `backend.vcf.*`). | ||
| 74 | Q_PROPERTY(QuickContactVcf *vcf READ vcf CONSTANT) | ||
| 75 | // Sprint 93 S93.5.6: mail <-> appointment links (QML `backend.mailEvent.*`). | ||
| 76 | Q_PROPERTY(QuickMailEventLink *mailEvent READ mailEvent CONSTANT) | ||
| 77 | // Sprint 93 S93.4.5: ":event <text>" grammar (QML `backend.eventInput`). | ||
| 78 | Q_PROPERTY(QuickEventInput *eventInput READ eventInput CONSTANT) | ||
| 79 | Q_PROPERTY(QAbstractItemModel *messageCopies READ messageCopies CONSTANT) | ||
| 80 | // S91.2.2: accounts, DAV accounts, image policy, reading mode, settings sync | ||
| 81 | // (QML `backend.settings.*`). | ||
| 82 | Q_PROPERTY(QuickAccountSettings *settings READ settings CONSTANT) | ||
| 83 | Q_PROPERTY(QuickRulesBackend *rules READ rules CONSTANT) | ||
| 84 | // Sprint 107: the conflict resolver (`backend.resolver`) and the open | ||
| 85 | // conflicts of every area (`backend.conflicts`). | ||
| 86 | Q_PROPERTY(QuickConflictResolver *resolver READ resolver CONSTANT) | ||
| 87 | Q_PROPERTY(ConflictCenter *conflicts READ conflicts CONSTANT) | ||
| 88 | // Sprint 101: the search tabs (QML `backend.searchTabs.active`). | ||
| 89 | Q_PROPERTY(MailSearchSessions *searchTabs READ searchTabs CONSTANT) | ||
| 90 | Q_PROPERTY(QuickMailMenu *mailMenu READ mailMenu CONSTANT) | ||
| 91 | // F95-10: the header's "☰" menu (own instance: its picks are no mail commands). | ||
| 92 | Q_PROPERTY(QuickMailMenu *appMenu READ appMenu CONSTANT) | ||
| 93 | // F100-03/F100-09: native menus of the views (PNativeMenu, routed by name). | ||
| 94 | Q_PROPERTY(QuickMailMenu *contextMenu READ contextMenu CONSTANT) | ||
| 95 | Q_PROPERTY(QuickMailMenu *searchMenu READ searchMenu CONSTANT) | ||
| 96 | Q_PROPERTY(QString accountEmail READ accountEmail NOTIFY accountChanged) | ||
| 97 | Q_PROPERTY(QString accountName READ accountName NOTIFY accountChanged) | ||
| 98 | Q_PROPERTY(QString activeAccountId READ activeAccountId NOTIFY accountChanged) | ||
| 99 | Q_PROPERTY(QVariantList accountChoices READ accountChoices NOTIFY accountChanged) | ||
| 100 | Q_PROPERTY(QAbstractItemModel *folderSubscriptions READ folderSubscriptions CONSTANT) | ||
| 101 | // S91.2.1: folder operations/properties/cache maintenance (QML `backend.folderActions.*`). | ||
| 102 | Q_PROPERTY(QuickFolderActions *folderActions READ folderActions CONSTANT) | ||
| 103 | Q_PROPERTY(QString folderPath READ folderPath NOTIFY folderChanged) | ||
| 104 | // Sprint 108 D-21: the shown folder is the account's drafts folder (Continue editing). | ||
| 105 | Q_PROPERTY(bool inDraftsFolder READ inDraftsFolder NOTIFY folderChanged) | ||
| 106 | Q_PROPERTY(bool threaded READ threaded WRITE setThreaded NOTIFY threadedChanged) | ||
| 107 | Q_PROPERTY(bool threadProjectionPending READ threadProjectionPending NOTIFY threadedChanged) | ||
| 108 | // True only while a thread rebuild runs AND it exceeds the feedback | ||
| 109 | // threshold (default 300 ms): fast cached rebuilds never flash a | ||
| 110 | // placeholder, slow ones announce themselves once they are really slow. | ||
| 111 | Q_PROPERTY(bool threadProjectionSlow READ threadProjectionSlow NOTIFY threadProjectionSlowChanged) | ||
| 112 | // Diagnostic (mailjd-bench fan-out gate): folder-prediction batches sent | ||
| 113 | // to the prediction worker since start. No NOTIFY; not used by QML. | ||
| 114 | Q_PROPERTY(qulonglong predictionRequestCount READ predictionRequestCount) | ||
| 115 | Q_PROPERTY(bool folderSwitchInProgress READ folderSwitchInProgress NOTIFY folderSwitchInProgressChanged) | ||
| 116 | Q_PROPERTY(QAbstractItemModel *mailList READ mailList NOTIFY threadedChanged) | ||
| 117 | Q_PROPERTY(QString status READ status NOTIFY statusChanged) | ||
| 118 | Q_PROPERTY(bool configured READ configured NOTIFY accountChanged) | ||
| 119 | Q_PROPERTY(bool navigationPending READ navigationPending NOTIFY navigationChanged) | ||
| 120 | Q_PROPERTY(qint64 selectionCount READ selectionCount NOTIFY selectionChanged) | ||
| 121 | // F100-77: the reader's summary of a multiple selection. | ||
| 122 | Q_PROPERTY(MailSelectionSummary *selectionSummary READ selectionSummary CONSTANT) | ||
| 123 | Q_PROPERTY(QStringList selectedLabels READ selectedLabels NOTIFY selectedLabelsChanged) | ||
| 124 | Q_PROPERTY(bool selectionPending READ selectionPending NOTIFY selectionChanged) | ||
| 125 | Q_PROPERTY(bool actionPending READ actionPending NOTIFY actionChanged) | ||
| 126 | Q_PROPERTY(QVariantList searchSuggestions READ searchSuggestions NOTIFY searchSuggestionsChanged) | ||
| 127 | Q_PROPERTY(QStringList expandedFolders READ expandedFolders NOTIFY folderExpansionChanged) | ||
| 128 | Q_PROPERTY(QVariantMap suggestion READ suggestion NOTIFY suggestionsChanged) | ||
| 129 | // F105 feedback: the reader header shows whether the shown mail is | ||
| 130 | // important and its labels — also in a mail tab, where no list row does | ||
| 131 | // ({important, labels: chips as in the list}; empty without a mail). | ||
| 132 | Q_PROPERTY(QVariantMap readerMarks READ readerMarks NOTIFY readerMarksChanged) | ||
| 133 | Q_PROPERTY(bool suggestionsVisible READ suggestionsVisible NOTIFY suggestionsChanged) | ||
| 134 | // The folder-suggestion column belongs to the inbox only (user decision | ||
| 135 | // 2026-10-06); suggestionsVisible stays the persistent switch. | ||
| 136 | Q_PROPERTY(bool suggestionColumnAvailable READ suggestionColumnAvailable NOTIFY folderChanged) | ||
| 137 | // F100-66: the folder list's order, flat and threaded, read from the cache | ||
| 138 | // in that order (persisted as view/mailSort). Search tabs sort their own | ||
| 139 | // results (MailPresentationModel::sortByColumn). | ||
| 140 | Q_PROPERTY(QString sortColumn READ sortColumn NOTIFY mailSortChanged) | ||
| 141 | Q_PROPERTY(bool sortAscending READ sortAscending NOTIFY mailSortChanged) | ||
| 142 | Q_PROPERTY(QStringList sortableColumns READ sortableColumns NOTIFY threadedChanged) | ||
| 143 | public: | ||
| 144 | // Status line text from outside the backend (S93.5.1 reminders without | ||
| 145 | // a notification service). | ||
| 146 | 9 | void showStatus(const QString &status) { setStatus(status); } | |
| 147 | 4498 | QuickPreferences *preferences() { return &m_preferences; } | |
| 148 | 769 | MailSelectionSummary *selectionSummary() { return &m_selectionSummary; } | |
| 149 | QuickMailBackend(MailSessionCore &session, WorkspaceState &workspace, | ||
| 150 | QString profileDirectory, QObject *parent = nullptr, | ||
| 151 | MailSearchReadService *searchReader = nullptr); | ||
| 152 | ~QuickMailBackend() override; | ||
| 153 | 1233 | MailPresentationModel *mailModel() { return &m_mail; } | |
| 154 | int loadedRowForIdentity(const MailId &id) const; | ||
| 155 | 84337 | QuickPimBackend *pim() { return &m_pim; } | |
| 156 | 864 | QAbstractItemModel *folders() { return &m_folders; } | |
| 157 | 5775 | QuickAccountSettings *settings() { return &m_settings; } | |
| 158 | 12116 | QuickRulesBackend *rules() { return &m_rules; } | |
| 159 | 2062 | QuickConflictResolver *resolver() { return &m_resolver; } | |
| 160 | 7359 | ConflictCenter *conflicts() { return &m_conflicts; } | |
| 161 | 22236 | MailSearchSessions *searchTabs() { return m_searchTabs.get(); } | |
| 162 | 1563 | QuickMailMenu *mailMenu() { return &m_mailMenu; } | |
| 163 | 1567 | QuickMailMenu *appMenu() { return &m_appMenu; } | |
| 164 | 5539 | QuickMailMenu *contextMenu() { return &m_contextMenu; } | |
| 165 | 2331 | QuickMailMenu *searchMenu() { return &m_searchMenu; } | |
| 166 | QString accountName() const; | ||
| 167 | QString accountEmail() const; | ||
| 168 | QString activeAccountId() const; | ||
| 169 | QVariantList accountChoices() const; | ||
| 170 | Q_INVOKABLE bool selectAccount(const QString &id); | ||
| 171 | 766 | QAbstractItemModel *messageCopies() { return &m_messageCopies; } | |
| 172 | 766 | QAbstractItemModel *folderSubscriptions() { return &m_folderSubscriptions; } | |
| 173 | 4062 | QuickFolderActions *folderActions() { return &m_folderActions; } | |
| 174 | Q_INVOKABLE void beginFolderSubscriptions(); | ||
| 175 | 7990 | QString folderPath() const { return m_folderPath; } | |
| 176 |
4/4✓ Branch 3 → 4 taken 118 times.
✓ Branch 3 → 7 taken 4156 times.
✓ Branch 5 → 6 taken 21 times.
✓ Branch 5 → 7 taken 97 times.
|
4274 | bool inDraftsFolder() const { return !m_draftsFolder.isEmpty() && m_folderPath == m_draftsFolder; } |
| 177 | 1755 | QString status() const { return m_statusState.text(); } | |
| 178 | // Test/diagnostic access to the status line (the website showcase clears | ||
| 179 | // the feedback of one step before it captures the next). | ||
| 180 | 4 | StatusMessageState *statusState() { return &m_statusState; } | |
| 181 | // Unconfirmed server actions of the active account for the status details. | ||
| 182 | Q_INVOKABLE QVariantList pendingActionDetails() const; | ||
| 183 | 2452 | bool threaded() const { return m_threaded; } | |
| 184 | 85 | bool threadProjectionPending() const { return m_threadProjectionPending; } | |
| 185 | 2590 | bool folderSwitchInProgress() const { return m_folderSwitchInProgress; } | |
| 186 | 780 | bool threadProjectionSlow() const { return m_threadProjectionSlow; } | |
| 187 | ✗ | qulonglong predictionRequestCount() const { return m_predictionRequestCount; } | |
| 188 | void setThreadProjectionSlowThresholdMs(int ms); | ||
| 189 | void setThreaded(bool on); | ||
| 190 | // The visible main list: the threaded projection while thread view is on | ||
| 191 | // and no search runs (search results always present flat). | ||
| 192 | QAbstractItemModel *mailList(); | ||
| 193 | QThreadPool *workerPool() { return &m_threadPool; } // tests (S106-291) | ||
| 194 | bool configured() const; | ||
| 195 |
3/4✓ Branch 2 → 3 taken 3839 times.
✓ Branch 2 → 5 taken 9 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 3839 times.
|
3848 | bool navigationPending() const { return m_navigationActive || !m_navigation.isEmpty(); } |
| 196 | qint64 selectionCount() const; | ||
| 197 | // Labels on the currently selected mail (raw IMAP keywords, e.g. "$label1"). | ||
| 198 | QStringList selectedLabels() const; | ||
| 199 | 8740 | bool selectionPending() const { return m_selection.pending(); } | |
| 200 | 3101 | bool actionPending() const { return m_actionPending; } | |
| 201 | 300 | QVariantList searchSuggestions() const { return m_searchSuggestions; } | |
| 202 | QStringList expandedFolders() const; | ||
| 203 | QVariantMap suggestion() const; | ||
| 204 | QVariantMap readerMarks() const; | ||
| 205 | // Sprint 105 S105.3.8: why a row's folder suggestion was made and whether | ||
| 206 | // it is confident ({reason, strong}); empty for rows without one. | ||
| 207 | Q_INVOKABLE QVariantMap suggestionDetails(const QVariantMap &identity) const; | ||
| 208 | static QString suggestionReason(const FolderSuggestion &suggestion); | ||
| 209 | 7161 | bool suggestionsVisible() const { return m_suggestionsVisible; } | |
| 210 | 799 | QString sortColumn() const { return MailSortOrder::idOf(m_mailSort.column); } | |
| 211 | 800 | bool sortAscending() const { return m_mailSort.ascending; } | |
| 212 | // Threads sort by their newest mail or their first mail's sender/subject. | ||
| 213 | QStringList sortableColumns() const; | ||
| 214 | // A click on a column title: the same column again flips the direction, | ||
| 215 | // another column starts with its natural direction. | ||
| 216 | Q_INVOKABLE void sortMailList(const QString &columnId); | ||
| 217 | const MailSortOrder &mailSort() const { return m_mailSort; } | ||
| 218 | // IMAP's INBOX name is case-insensitive (RFC 3501 section 5.1). | ||
| 219 | 2898 | bool suggestionColumnAvailable() const { return m_folderPath.compare(QLatin1String("INBOX"), Qt::CaseInsensitive) == 0; } | |
| 220 | // List rows get predictions only while the column is on and available. | ||
| 221 |
4/4✓ Branch 2 → 3 taken 1867 times.
✓ Branch 2 → 6 taken 2590 times.
✓ Branch 4 → 5 taken 1762 times.
✓ Branch 4 → 6 taken 105 times.
|
4457 | bool suggestionColumnShown() const { return m_suggestionsVisible && suggestionColumnAvailable(); } |
| 222 | Q_INVOKABLE void setSuggestionsVisible(bool on); // F100-55: also from the column chooser | ||
| 223 | bool acceptsIdentity(const MailId &identity) const; | ||
| 224 | void restoreWorkspaceSelection(); | ||
| 225 | Q_INVOKABLE bool selectFolder(const QString &path); | ||
| 226 | // Sprint 105 D-14 (g i/s/d/a/j): the path of "inbox", "sent", "drafts", | ||
| 227 | // "archive" or "junk" of the active account ("" when it has none). | ||
| 228 | Q_INVOKABLE QString specialFolder(const QString &role) const; | ||
| 229 | Q_INVOKABLE bool markFolderRead(const QString &path); | ||
| 230 | Q_INVOKABLE QStringList matchingFolders(const QString &prefix) const; | ||
| 231 | Q_INVOKABLE bool executeCommand(const QString &id); | ||
| 232 | // Sprint 98: `line` is a command-bar line ("folder rename", "mark read"); | ||
| 233 | // its first word is the command, the second word its value. | ||
| 234 | Q_INVOKABLE QString namedCommandUnavailableReason(const QString &line) const; | ||
| 235 | Q_INVOKABLE bool canExecuteNamedCommand(const QString &line) const; | ||
| 236 | Q_INVOKABLE bool executeNamedCommand(const QString &name, const QString &argument); | ||
| 237 | Q_INVOKABLE bool moveSelectedMail(const QString &path); | ||
| 238 | Q_INVOKABLE void beginMailRowSelection(int row, int modifiers = 0); | ||
| 239 | Q_INVOKABLE void selectMailRow(int row, int modifiers = 0); | ||
| 240 | Q_INVOKABLE void selectAllMails(); | ||
| 241 | Q_INVOKABLE void watchSelectionRows(int first, int last); | ||
| 242 | // Lazy header paging for the flat list (Widgets parity: the QTreeView drove | ||
| 243 | // MailListModel::fetchMore as the viewport approached the loaded window end). | ||
| 244 | Q_INVOKABLE bool canFetchMoreMails() const; | ||
| 245 | Q_INVOKABLE void fetchMoreMails(); | ||
| 246 | Q_INVOKABLE bool canFetchPreviousMails() const; | ||
| 247 | Q_INVOKABLE void fetchPreviousMails(); | ||
| 248 | Q_INVOKABLE void previewSearch(const QString &query); | ||
| 249 | Q_INVOKABLE bool submitSearch(const QString &query, int suggestion = -1); | ||
| 250 | // Sprint 101 D-1/D-2: a full search opens (or reuses) a search tab; | ||
| 251 | // `newTab` asks for a second one (Alt+Enter, "In neuem Tab öffnen"). | ||
| 252 | Q_INVOKABLE bool openSearch(const QString &query, bool newTab = false); | ||
| 253 | Q_INVOKABLE void cancelSearchPreview(); | ||
| 254 | Q_INVOKABLE bool setFolderExpanded(const QString &path, bool expanded); | ||
| 255 | 6365 | QuickSourceViewer *source() { return &m_source; } | |
| 256 | 1 | int pageRows() const { return m_pageRows; } | |
| 257 | // The selectable folder paths known to the backend (updated by LIST). | ||
| 258 | ✗ | Q_INVOKABLE QSet<QString> availableFolders() const { return m_availableFolders; } | |
| 259 | 17345 | QuickComposeBackend *compose() { return &m_compose; } | |
| 260 | 8418 | QuickInvitation *invitation() { return &m_invitation; } | |
| 261 | 768 | QuickCalendarIcs *ics() { return &m_ics; } | |
| 262 | 1691 | QuickContactVcf *vcf() { return &m_vcf; } | |
| 263 | 39 | QuickMailEventLink *mailEvent() { return &m_mailEvent; } | |
| 264 | 10 | QuickEventInput *eventInput() { return &m_eventInput; } | |
| 265 | MailId displayedMailId() const; | ||
| 266 | signals: | ||
| 267 | // One viewport commit from the list (measured: <= 1 per rendered frame). | ||
| 268 | void viewportWatched(int first, int last); | ||
| 269 | void suggestionsChanged(); | ||
| 270 | void readerMarksChanged(); | ||
| 271 | // Persistence only (F95-15); QML keeps reading suggestionsChanged. | ||
| 272 | void suggestionsVisibleChanged(); | ||
| 273 | void folderExpansionChanged(); | ||
| 274 | void threadedChanged(); | ||
| 275 | void mailSortChanged(); | ||
| 276 | void threadProjectionSlowChanged(); | ||
| 277 | void folderSwitchInProgressChanged(); | ||
| 278 | void accountChanged(); | ||
| 279 | void folderChanged(); | ||
| 280 | void statusChanged(); | ||
| 281 | void navigationChanged(); | ||
| 282 | void selectionChanged(); | ||
| 283 | void selectedLabelsChanged(); | ||
| 284 | void actionChanged(); | ||
| 285 | void searchSuggestionsChanged(); | ||
| 286 | // F4.2(b): emitted when an armed quick-move retry gave up, so the shell | ||
| 287 | // opens the manual move console exactly like the old direct fallback. | ||
| 288 | void manualMoveRequested(); | ||
| 289 | private: | ||
| 290 | QuickPreferences m_preferences; | ||
| 291 | // Constructor wiring in its original order (S91.0.4). Qt calls slots in | ||
| 292 | // connection order, so the constructor must call these exactly in this | ||
| 293 | // sequence; each block moves as a whole when its unit is extracted. | ||
| 294 | void wireProjectionTimers(); | ||
| 295 | void wireThreadInvalidation(); | ||
| 296 | void wireIndexedProviders(); | ||
| 297 | void wireHeaderProjection(); | ||
| 298 | void wireProjectionSignals(); | ||
| 299 | void wireComposeSource(); | ||
| 300 | void wireFolderSubscriptionsAndFilters(); | ||
| 301 | void wireFolderOperations(); | ||
| 302 | void wirePredictions(); | ||
| 303 | void wireRemovalFollowUp(); | ||
| 304 | void wireFolderChangeResets(); | ||
| 305 | void wireSearch(MailSearchReadService *searchReader); | ||
| 306 | void wireSearchSuggestions(); | ||
| 307 | void wireSelection(); | ||
| 308 | void wireAccountReload(); | ||
| 309 | void wireSelectionTrace(); | ||
| 310 | void wireNavigation(); | ||
| 311 | void wireControllerStatus(); | ||
| 312 | void wireConnectionStatus(); | ||
| 313 | void refreshConnectionStatus(); | ||
| 314 | // Sprint 100 S100.1.5: mail connection and rule state for the Accounts page. | ||
| 315 | AccountOverview::Active activeAccountState(); | ||
| 316 | QString m_lastImapError; | ||
| 317 | // executeCommand() handlers in dispatch order (S91.2.7); each returns | ||
| 318 | // nullopt when the command is not its own. | ||
| 319 | std::optional<bool> handleSuggestionCommand(const QString &id); | ||
| 320 | std::optional<bool> handleSurfaceClose(const QString &id); | ||
| 321 | std::optional<bool> handleIndexedNavigation(const QString &id); | ||
| 322 | std::optional<bool> handleTabNavigation(const QString &id); | ||
| 323 | std::optional<bool> handleSearchTabNavigation(const QString &id); | ||
| 324 | std::optional<bool> handleMainListNavigation(const QString &id); | ||
| 325 | std::optional<bool> handleViewCommand(const QString &id); | ||
| 326 | std::optional<bool> handleUndoCommand(const QString &id); | ||
| 327 | std::optional<bool> handleMailMoveCommand(const QString &id, const MailId &selected); | ||
| 328 | std::optional<bool> handleMailViewCommand(const QString &id, const MailId &selected); | ||
| 329 | std::optional<bool> handleLabelCommand(const QString &id, const MailId &selected); | ||
| 330 | std::optional<bool> handleFlagCommand(const QString &id, const MailId &selected); | ||
| 331 | // MAIL-SELECTION-ACTIONS: the selection mail actions act on (more than one | ||
| 332 | // message in the main folder view), else null; and one state for all of it. | ||
| 333 | std::shared_ptr<const MailSelectionSnapshot> actionSelection() const; | ||
| 334 | QMap<qint64, QList<qint64>> searchBulkGroups() const; | ||
| 335 | bool bulkSelection() const; | ||
| 336 | bool actOnSelection(MutationKind kind, const QString &label, bool desired, | ||
| 337 | std::function<void(bool)> done = {}); | ||
| 338 | void invalidatePredictions(); | ||
| 339 | void requestPredictions(); | ||
| 340 | void nextPredictionBatch(quint64 generation); | ||
| 341 | // F4.2(b): a quick-move press that outruns the new selection's prediction | ||
| 342 | // (debounce or worker batch still in flight) arms a bounded retry instead | ||
| 343 | // of falling back to the manual move console. | ||
| 344 | bool canArmQuickMoveRetry(const MailId &selected) const; | ||
| 345 | void armQuickMoveRetry(const MailId &selected); | ||
| 346 | void disarmQuickMove(); | ||
| 347 | void tryArmedQuickMove(); | ||
| 348 | // Groups identities per folder and queues bounded service batches; the | ||
| 349 | // selected mail's batch is always queued first (see requestPredictions). | ||
| 350 | void appendPredictionRequests(const QList<MailId> &identities); | ||
| 351 | // Drops cached predictions outside the selected mail and the watched | ||
| 352 | // viewport ("clears obsolete results"); the retained in-scope entries make | ||
| 353 | // a revisited mail answer instantly. | ||
| 354 | void prunePredictionsToScope(); | ||
| 355 | FolderSuggestion::Candidate visiblePrediction(const MailId &id) const; | ||
| 356 | QString predictionText(const MailId &id) const; | ||
| 357 | // Populates the session thread model from the presented folder's complete | ||
| 358 | // cached projection while thread view is on; clears it when off, so the | ||
| 359 | // full-folder tree is never retained behind a flat list. | ||
| 360 | void syncThreadProjection(); | ||
| 361 | // F3.12: large folder projections build off the GUI thread; this cancels a | ||
| 362 | // pending build and bumps the generation. | ||
| 363 | void cancelThreadProjection(); | ||
| 364 | // True while the threaded model is the one actually presented (preference on, | ||
| 365 | // no search; pending builds keep this projection selected). | ||
| 366 | bool threadedPresentation() const; | ||
| 367 | // Row-oriented main-list dispatch: search and flat mode answer from the | ||
| 368 | // paged flat model, threaded mode from the flattened thread projection. | ||
| 369 | int activeRowCount() const; | ||
| 370 | QVariantMap activeMailAt(int row) const; | ||
| 371 | int activeRowForIdentity(const QVariantMap &identity) const; | ||
| 372 | MailSelectionKey activeKeyAt(int row) const; | ||
| 373 | // F-41: presented-order identities between two selection keys. | ||
| 374 | QList<qint64> visibleRangeBetween(const MailSelectionKey &anchor, | ||
| 375 | const MailSelectionKey &key) const; | ||
| 376 | void loadLocalFolders(); | ||
| 377 | void applyFolderList(const QList<FolderInfo> &folders, bool authoritative); | ||
| 378 | // F-09 (SPRINT-104, D-4): permanent deletion in the open folder. | ||
| 379 | void ensureDeleteForever(); | ||
| 380 | QList<qint64> deleteForeverUids(const MailId &selected) const; | ||
| 381 | // P90.16: reserve the visible successor before an removing action (S/X, | ||
| 382 | // archive, delete, move). Identity has priority over row numbers: when a | ||
| 383 | // later projection refresh invalidates the selection, reconcileSelection() | ||
| 384 | // consumes the token instead of jumping to row 0. The token is cleared by | ||
| 385 | // folder/account switches, superseded by a newer action and consumed by | ||
| 386 | // the next reconcileSelection that observes a valid selection. | ||
| 387 | void reserveActionContinuity(int lastRemovedVisualRow); | ||
| 388 | void clearActionContinuity(); | ||
| 389 | void reconcileSelection(); | ||
| 390 | void presentCachedFolder(const QString &path); | ||
| 391 | void publishSelection(); | ||
| 392 | void requestSelectedMail(const MailId &id); | ||
| 393 | void setStatus(const QString &status); | ||
| 394 | void cancelNavigation(); | ||
| 395 | void startNavigation(); | ||
| 396 | void refreshSelectionScope(); | ||
| 397 | MailSessionCore &m_session; | ||
| 398 | WorkspaceState &m_workspace; | ||
| 399 | QString m_profileDirectory; | ||
| 400 | MailPresentationModel m_mail; | ||
| 401 | QuickThreadListModel m_threadList; | ||
| 402 | QuickPimBackend m_pim; | ||
| 403 | FolderPresentationModel m_folders; | ||
| 404 | QuickAccountSettings m_settings; | ||
| 405 | QuickMailSelection m_selection; | ||
| 406 | MailSelectionSummary m_selectionSummary{{}, this}; // header lookup set in the constructor | ||
| 407 | MailId m_mainCurrent; | ||
| 408 | // P90.16 action-continuity token: the mail being removed, its visual rank | ||
| 409 | // and the reserved surviving successor identity. | ||
| 410 | MailId m_actionAnchor; | ||
| 411 | MailId m_actionSuccessor; | ||
| 412 | int m_actionVisualRow = -1; | ||
| 413 | QString m_folderPath; | ||
| 414 | QString m_previousFolder; | ||
| 415 | StatusMessageState m_statusState; | ||
| 416 | FolderSubscriptionEditor m_folderSubscriptions; | ||
| 417 | MessageCopyQueue m_messageCopies; | ||
| 418 | QSet<QString> m_availableFolders; | ||
| 419 | QSet<QString> m_openTaskMessageIds; // S94.5.5, canonical Message-IDs | ||
| 420 | void refreshTaskLinks(); | ||
| 421 | // P90.17: in-memory folder identity admission cache (path ↔ id) so the | ||
| 422 | // click/selection paths never pay SQLite round trips in acceptsIdentity(). | ||
| 423 | QHash<QString, qint64> m_folderIdByPath; | ||
| 424 | QHash<qint64, QString> m_folderPathById; | ||
| 425 | QHash<qint64, quint32> m_folderEpochById; | ||
| 426 | void rebuildFolderIdentityCache(); | ||
| 427 | qint64 folderIdForPath(const QString &path) const; | ||
| 428 | bool m_reconciling = false; | ||
| 429 | bool m_waitingCachedPage = false; | ||
| 430 | QList<int> m_navigation; | ||
| 431 | bool m_navigationActive = false; | ||
| 432 | bool m_applyingNavigation = false; | ||
| 433 | bool m_applyingSelection = false; | ||
| 434 | bool m_actionPending = false; | ||
| 435 | std::unique_ptr<SearchCoordinator> m_search; | ||
| 436 | std::unique_ptr<MailSearchSessions> m_searchTabs; | ||
| 437 | void wireSearchTabs(MailSearchReadService *searchReader); | ||
| 438 | QTimer m_searchDebounce; | ||
| 439 | QString m_searchPreviewQuery; | ||
| 440 | QVariantList m_searchSuggestions; | ||
| 441 | QTimer m_predictionDebounce; | ||
| 442 | // Armed quick-move retry state (F4.2(b)); m_quickMoveArmed.isValid() | ||
| 443 | // means a pressed s is waiting for the selected mail's prediction. | ||
| 444 | QTimer m_quickMoveFallback; | ||
| 445 | MailId m_quickMoveArmed; | ||
| 446 | QHash<MailId, FolderSuggestion> m_predictions; | ||
| 447 | QHash<qint64, quint32> m_predictionEpochs; | ||
| 448 | QHash<qint64, QString> m_predictionFolders; | ||
| 449 | QSet<MailId> m_alternatePredictions; | ||
| 450 | QList<FolderPredictionRequest> m_predictionRequests; | ||
| 451 | // Batches handed to the worker and not yet answered: taken off the queue, | ||
| 452 | // yet still "in flight" for a pressed s (canArmQuickMoveRetry). | ||
| 453 | int m_predictionBatchesRunning = 0; | ||
| 454 | quint64 m_predictionGeneration = 0; | ||
| 455 | qulonglong m_predictionRequestCount = 0; | ||
| 456 | // Viewport batches are small so the suggestion column fills progressively | ||
| 457 | // while later batches are still classifying on the single worker. | ||
| 458 | static constexpr int ViewportPredictionChunk = 32; | ||
| 459 | // Warm per-mail predictions are kept across selection changes (epoch-fenced | ||
| 460 | // at use time); the cap bounds memory when many folders were visited. | ||
| 461 | static constexpr int MaximumCachedPredictions = 512; | ||
| 462 | // The shown mail's flags and labels as read from the cache, with the | ||
| 463 | // visual-first changes of this session on top (the journal writes the | ||
| 464 | // cache later; a rollback sends the restored value). | ||
| 465 | struct ReaderMarks { | ||
| 466 | MailId id; | ||
| 467 | bool loaded = false; | ||
| 468 | bool important = false; | ||
| 469 | QStringList labels; | ||
| 470 | std::optional<bool> importantOverride; | ||
| 471 | QHash<QString, bool> labelOverrides; | ||
| 472 | }; | ||
| 473 | mutable ReaderMarks m_readerMarks; | ||
| 474 | void readerMarkChanged(qint64 folderId, qint64 uid, const std::function<void(ReaderMarks &)> &apply); | ||
| 475 | // S105.3.8 (ux.md §12): a suggestion at or above this calibrated | ||
| 476 | // confidence is shown in the normal text colour, below it muted. | ||
| 477 | static constexpr double StrongSuggestion = 0.9; | ||
| 478 | int m_predictionFirst = -1, m_predictionLast = -1; | ||
| 479 | bool m_suggestionsVisible = false; | ||
| 480 | QString m_junkFolder; | ||
| 481 | QuickSourceViewer m_source; | ||
| 482 | // Visible rows reported by the list viewport; one page of mail navigation. | ||
| 483 | int m_pageRows = 10; | ||
| 484 | QuickComposeBackend m_compose; | ||
| 485 | QuickInvitation m_invitation{ | ||
| 486 | m_session, | ||
| 487 | 29 | {[this] { return m_pim.calendarChoices(); }, [this] { return m_pim.lastCalendar(); }, | |
| 488 | 1 | [this](const QStringList &recipients, const QByteArray &message, const QString &doneText) { | |
| 489 |
1/1✓ Branch 3 → 4 taken 1 time.
|
1 | return m_compose.enqueuePrepared(recipients, message, doneText); |
| 490 | }, | ||
| 491 | 1 | [this](const QString &status) { setStatus(status); }}, | |
| 492 | this}; | ||
| 493 | QuickCalendarIcs m_ics{ | ||
| 494 | m_session, | ||
| 495 | 5072 | {[this] { return m_pim.calendarChoices(); }, [this] { return m_pim.lastCalendar(); }, | |
| 496 | 3 | [this] { return displayedMailId(); }, [this](const QString &status) { setStatus(status); }, | |
| 497 | m_profileDirectory + QStringLiteral("/cache/ics-export"), QString(), | ||
| 498 | 1 | [this] { return m_pim.taskActions()->lastList(); }}, | |
| 499 | this}; | ||
| 500 | QuickContactVcf m_vcf{ | ||
| 501 | m_session, | ||
| 502 | 5 | {[this] { return displayedMailId(); }, [this](const QString &status) { setStatus(status); }, QString()}, | |
| 503 | this}; | ||
| 504 | 2 | QuickMailEventLink m_mailEvent{m_session, m_pim, [this] { return displayedMailId(); }, this}; | |
| 505 | 3 | QuickEventInput m_eventInput{[this] { return m_pim.calendarChoices(); }, this}; | |
| 506 | QList<FolderInfo> m_folderInfos; | ||
| 507 | QString m_archiveFolder; | ||
| 508 | QString m_trashFolder; | ||
| 509 | class MailPermanentDelete *m_deleteForever = nullptr; // F-09 | ||
| 510 | QString m_draftsFolder; | ||
| 511 | QString m_sentFolder; | ||
| 512 | bool m_threaded = false; | ||
| 513 | MailSortOrder m_mailSort; | ||
| 514 | void applyMailSort(); | ||
| 515 | // One worker shared with QuickComposeBackend (send and server-draft | ||
| 516 | // builds). Queued jobs must run: a send job is the only path that ends | ||
| 517 | // m_sendPending (S106-291), so nothing clears this pool. | ||
| 518 | QThreadPool m_threadPool; | ||
| 519 | struct ThreadSnapshot { | ||
| 520 | MailThreadModel::Prepared data; | ||
| 521 | qint64 externalVersion = -1; | ||
| 522 | quint32 uidValidity = 0; | ||
| 523 | }; | ||
| 524 | // Additional inactive-folder retention: conservative byte charges from the | ||
| 525 | // prepared forest, not a number-of-folders cap. Oversized entries are refused. | ||
| 526 | QCache<qint64, ThreadSnapshot> m_threadSnapshots{1024}; // KiB | ||
| 527 | quint64 m_threadDataGeneration = 0; | ||
| 528 | bool m_threadRefreshQueued = false; | ||
| 529 | QSet<qint64> m_moveExclusionRefreshQueued; | ||
| 530 | bool m_folderSwitchInProgress = false; // list view drops its delegate buffer meanwhile | ||
| 531 | bool hasPendingMove(const MailId &mail) const; | ||
| 532 | QuickFolderActions m_folderActions; | ||
| 533 | QuickMailMenu m_mailMenu{this}; | ||
| 534 | QuickMailMenu m_appMenu{this}; | ||
| 535 | QuickMailMenu m_contextMenu{this}; | ||
| 536 | QuickMailMenu m_searchMenu{this}; | ||
| 537 | QHash<qint64, QSet<qint64>> m_visualMoveUids; // folder -> moves awaiting the durable writer | ||
| 538 | void queueIndexedMoveExclusionRefresh(qint64 folderId); | ||
| 539 | QTimer m_headerProjectionTimer; | ||
| 540 | QString m_headerProjectionFolder; | ||
| 541 | void retainThreadSnapshot(qint64 folderId, qint64 externalVersion); | ||
| 542 | bool restoreThreadSnapshot(qint64 folderId); | ||
| 543 | quint64 m_threadProjectionGeneration = 0; | ||
| 544 | bool m_threadProjectionPending = false; | ||
| 545 | bool m_threadProjectionFailed = false; | ||
| 546 | bool m_threadProjectionSlow = false; | ||
| 547 | int m_threadProjectionSlowThresholdMs = 300; | ||
| 548 | quint64 m_folderHoldGeneration = 0; // folder switch that owns the held rows | ||
| 549 | QTimer m_threadProjectionSlowTimer; | ||
| 550 | // P90 cold build: plain cached rows while the first indexed page builds. | ||
| 551 | QTimer m_provisionalPageTimer; | ||
| 552 | QObject m_provisionalPageOwner; // own read lane: never cancels other reads | ||
| 553 | static constexpr int ProvisionalPageDelayMs = 100; | ||
| 554 | void requestProvisionalPage(); | ||
| 555 | void armThreadProjectionSlowFlag(); | ||
| 556 | void clearThreadProjectionSlowFlag(); | ||
| 557 | // Moves the currently selected mail into targetFolder through the shared | ||
| 558 | // journaled move path; the removal follow-up advances the selection. | ||
| 559 | bool moveCurrentMailToFolder(const QString &targetFolder); | ||
| 560 | // Sprint 95: server rules of the active account (`backend.rules`). | ||
| 561 | QuickRulesBackend::Deps rulesDeps(); | ||
| 562 | void reportManualMove(const QList<MailId> &mails, const QString &targetPath); | ||
| 563 | QuickRulesBackend m_rules{rulesDeps(), this}; | ||
| 564 | // Sprint 107 (QuickMailBackendConflicts.cpp). | ||
| 565 | QuickConflictResolver::Deps resolverDeps(); | ||
| 566 | ConflictCenter::Deps conflictDeps(); | ||
| 567 | QuickConflictResolver m_resolver{resolverDeps(), this}; | ||
| 568 | ConflictCenter m_conflicts{conflictDeps(), this}; | ||
| 569 | }; | ||
| 570 |