presentation/MailReaderState.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | #include "util/SpamHeaders.h" | ||
| 3 | #include "controller/MailDisplayPort.h" | ||
| 4 | #include <QVariantMap> | ||
| 5 | #include <QLocale> | ||
| 6 | #include <QPointer> | ||
| 7 | #include <QQuickTextDocument> | ||
| 8 | #include <QTimer> | ||
| 9 | #include <memory> | ||
| 10 | #include "service/NativeMailDocument.h" | ||
| 11 | |||
| 12 | class NativeDocumentService; | ||
| 13 | class NativeResourceDocument; | ||
| 14 | |||
| 15 | // A single current reader document, owned outside the scene tree. Logical tabs | ||
| 16 | // retain identities, not copies of this body. Native preparation is shared and | ||
| 17 | // lazy; native modes never construct WebEngine or a second parser/cache. | ||
| 18 | class MailReaderState final : public QObject, public MailDisplayPort { | ||
| 19 | 77 | Q_OBJECT | |
| 20 | Q_PROPERTY(QVariantMap identity READ identity NOTIFY changed) | ||
| 21 | Q_PROPERTY(QString generation READ generation NOTIFY changed) | ||
| 22 | Q_PROPERTY(QString subject READ subject NOTIFY changed) | ||
| 23 | Q_PROPERTY(QString sender READ sender NOTIFY changed) | ||
| 24 | Q_PROPERTY(QString recipient READ recipient NOTIFY changed) | ||
| 25 | // Sprint 97 S97.5.1: every Cc address (cache schema v24; "" before). | ||
| 26 | Q_PROPERTY(QString cc READ cc NOTIFY changed) | ||
| 27 | Q_PROPERTY(QString dateText READ dateText NOTIFY changed) | ||
| 28 | // Sprint 103 S103.6.3 (D-15): what a mail link stores (Message-ID without | ||
| 29 | // angle brackets, the day "YYYY-MM-DD"). | ||
| 30 | Q_PROPERTY(QString messageId READ messageId NOTIFY changed) | ||
| 31 | Q_PROPERTY(QString dateIso READ dateIso NOTIFY changed) | ||
| 32 | Q_PROPERTY(QString plainText READ plainText NOTIFY changed) | ||
| 33 | Q_PROPERTY(QString error READ error NOTIFY changed) | ||
| 34 | Q_PROPERTY(bool retryable READ retryable NOTIFY changed) | ||
| 35 | Q_PROPERTY(QString status READ status NOTIFY changed) | ||
| 36 | Q_PROPERTY(QString readingMode READ readingMode NOTIFY changed) | ||
| 37 | Q_PROPERTY(QString effectiveMode READ effectiveMode NOTIFY changed) | ||
| 38 | Q_PROPERTY(QString notice READ notice NOTIFY changed) | ||
| 39 | Q_PROPERTY(QVariantList attachments READ attachments NOTIFY changed) | ||
| 40 | // F100-02: hosts the HTML part loads images or styles from ("" mail has | ||
| 41 | // none). HTML-light and Text never load them; the reader offers to. | ||
| 42 | Q_PROPERTY(QStringList remoteHosts READ remoteHosts NOTIFY changed) | ||
| 43 | // F100-56: the server spam filter's verdict {known, spam, score, threshold, | ||
| 44 | // tool, tests, text}; known false when no filter wrote a header. | ||
| 45 | Q_PROPERTY(QVariantMap spam READ spam NOTIFY changed) | ||
| 46 | Q_PROPERTY(QObject *htmlRenderer READ htmlRenderer CONSTANT) | ||
| 47 | public: | ||
| 48 | explicit MailReaderState(QObject *parent = nullptr, NativeDocumentService *service = nullptr); | ||
| 49 | ~MailReaderState() override; | ||
| 50 | 794 | QObject &displayObject() override { return *this; } | |
| 51 | 794 | QMetaObject::Connection subscribeRetry(QObject *receiver, std::function<void()> retry) override { | |
| 52 | 794 | return connect(this, &MailReaderState::retryRequested, receiver, std::move(retry)); | |
| 53 | } | ||
| 54 | void presentMail(const MailId &, quint64, const MailHeader &, const MailBody &) override; | ||
| 55 | void presentLoading(const MailId &, quint64, const MailHeader &, const QString &, bool) override; | ||
| 56 | void presentError(const MailId &, quint64, const MailHeader &, const QString &, bool) override; | ||
| 57 | void presentBodyVariant(const MailId &identity, const MailBody &variant) override; | ||
| 58 | void clearDisplay() override; | ||
| 59 |
1/2✓ Branch 2 → 3 taken 8 times.
✗ Branch 2 → 4 not taken.
|
8 | Q_INVOKABLE void retry() { if (m_retryable) emit retryRequested(); } |
| 60 | QVariantMap identity() const; | ||
| 61 | 36 | QString generation() const { return QString::number(m_generation); } | |
| 62 | 4177 | QString subject() const { return m_header.subject; } | |
| 63 | 24561 | QString sender() const { return m_header.from; } | |
| 64 | 17978 | QString recipient() const { return m_header.to; } | |
| 65 | 7942 | QString cc() const { return m_header.cc; } | |
| 66 | 3976 | QString messageId() const { return m_header.messageId; } | |
| 67 |
5/9✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 7 not taken.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 5 → 6 taken 3 times.
✓ Branch 6 → 8 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✗ Branch 9 → 11 not taken.
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 16 not taken.
|
3 | QString dateIso() const { return m_header.date.isValid() ? m_header.date.toLocalTime().date().toString(Qt::ISODate) : QString(); } |
| 68 | 8124 | QString dateText() const { | |
| 69 |
3/3✓ Branch 2 → 3 taken 8124 times.
✓ Branch 3 → 4 taken 8124 times.
✓ Branch 4 → 5 taken 8124 times.
|
8124 | return QLocale().toString(m_header.date.toLocalTime(), QLocale::ShortFormat); |
| 70 | } | ||
| 71 | QString plainText() const; | ||
| 72 | 21 | QString error() const { return m_error; } | |
| 73 | 34 | bool retryable() const { return m_retryable; } | |
| 74 | 5 | const MailBody &body() const { return m_body; } | |
| 75 | 31218 | QString status() const { return m_status; } | |
| 76 | // The delayed "Loading message…" text is shown (P90.11 placeholder gate). | ||
| 77 | bool loadingPlaceholderActive() const { return m_loadingPlaceholderActive; } | ||
| 78 | 3 | QString readingMode() const { return m_mode; } | |
| 79 | 4017 | QString effectiveMode() const { return m_effectiveMode; } | |
| 80 | 4 | QString notice() const { return m_notice; } | |
| 81 | // Downloadable attachments of the displayed mail (metadata only, same | ||
| 82 | // visibility rule as the Widgets attachment bar). | ||
| 83 | QVariantList attachments() const; | ||
| 84 | QStringList remoteHosts() const; | ||
| 85 | QVariantMap spam() const; | ||
| 86 | void presentSpamVerdict(const MailId &identity, const MailHeader &header) override; | ||
| 87 | static QVariantMap spamOf(const QString &spamHeaderLines); | ||
| 88 | static QVariantMap spamOfVerdict(const SpamHeaders::Verdict &verdict); | ||
| 89 | // Hosts of http(s) and protocol-relative URLs in src, srcset, | ||
| 90 | // background, poster and CSS url(); lower case, at most 20. Pure. | ||
| 91 | static QStringList remoteHostsOf(const QString &html); | ||
| 92 | 767 | QObject *htmlRenderer() const { return m_htmlRenderer; } | |
| 93 | // The host binds this once before loading its QML scene, not on status changes. | ||
| 94 | 1590 | void setHtmlRenderer(QObject *renderer) { m_htmlRenderer = renderer; } | |
| 95 | 25765 | quint64 renderRevision() const { return m_renderGeneration; } | |
| 96 |
5/6✓ Branch 2 → 3 taken 784 times.
✓ Branch 2 → 7 taken 6871 times.
✓ Branch 3 → 4 taken 614 times.
✓ Branch 3 → 7 taken 170 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 614 times.
|
7655 | bool htmlRequested() const { return m_bodyReady && m_visible && m_effectiveMode == "html"; } |
| 97 | // P90.20: true while further plain-text slices still wait to be appended. | ||
| 98 | 729 | bool plainLayoutPending() const { return m_plainShown < m_plainLimit; } | |
| 99 | void finishHtml(quint64 revision, const QString &error); | ||
| 100 | void prepareHtml(quint64 revision); | ||
| 101 | public slots: | ||
| 102 | void setCachePath(const QString &path); | ||
| 103 | void setReadingMode(const QString &mode); | ||
| 104 | Q_INVOKABLE void attachDocument(QQuickTextDocument *target); | ||
| 105 | Q_INVOKABLE void setVisible(bool visible); | ||
| 106 | signals: | ||
| 107 | void changed(); | ||
| 108 | void retryRequested(); | ||
| 109 | // P90.8: the active reading mode needs a body variant that a mode | ||
| 110 | // projection did not load (emptiness is not authoritative). The host | ||
| 111 | // answers with a targeted variant fetch that merges into this body — | ||
| 112 | // no new selection generation. | ||
| 113 | void reprojectionRequested(bool needHtml); | ||
| 114 | private: | ||
| 115 | // Plain-text characters laid out at most (GUI-thread layout, ~0.4 µs per | ||
| 116 | // character measured: 256 Ki ≈ 110 ms instead of 11 s for 50 MiB). | ||
| 117 | static constexpr qsizetype MaxShownPlainChars = 256 * 1024; | ||
| 118 | // P90.20: plain text is laid out in slices of this size, one per event-loop | ||
| 119 | // turn, so no single GUI slice lays out more than ~16 Ki characters. | ||
| 120 | static constexpr qsizetype PlainSliceChars = 16 * 1024; | ||
| 121 | // Documents from this size on render only their visible part. | ||
| 122 | static constexpr qsizetype CulledDocumentChars = 16 * 1024; | ||
| 123 | void applyViewportCulling(qsizetype characters); | ||
| 124 | void appendPlainSlice(); | ||
| 125 | void cancelPreparation(); | ||
| 126 | void deferPreparationPlaceholder(); | ||
| 127 | void render(); | ||
| 128 | void replaceDocument(const QString &text, const NativeMailDocument *prepared = nullptr); | ||
| 129 | MailId m_identity; | ||
| 130 | quint64 m_generation = 0; | ||
| 131 | MailHeader m_header; | ||
| 132 | MailBody m_body; | ||
| 133 | QString m_error; | ||
| 134 | bool m_retryable = false; | ||
| 135 | QString m_notice, m_cachePath; | ||
| 136 | QString m_status = QStringLiteral("empty"); | ||
| 137 | QString m_mode = QStringLiteral("text"), m_effectiveMode = QStringLiteral("text"); | ||
| 138 | QPointer<NativeDocumentService> m_service; | ||
| 139 | QPointer<QQuickTextDocument> m_target; | ||
| 140 | std::unique_ptr<NativeResourceDocument> m_document; | ||
| 141 | // A neutral document has no mail content or image capabilities and can be | ||
| 142 | // kept across rapid selections while the display read is still pending. | ||
| 143 | bool m_documentIsNeutral = true; | ||
| 144 | quint64 m_renderGeneration = 0; | ||
| 145 | bool m_visible = true; | ||
| 146 | bool m_bodyReady = false; | ||
| 147 | bool m_needsPreparation = false; | ||
| 148 | // P90.11: true while the delayed loading placeholder is the visible body. | ||
| 149 | // Internal render/sanitize phases then keep the document stable instead | ||
| 150 | // of swapping in a second placeholder text for the same selection. | ||
| 151 | bool m_loadingPlaceholderActive = false; | ||
| 152 | QTimer m_preparePlaceholderTimer; | ||
| 153 | // Characters of m_body.textPlain in the document / to be shown in total. | ||
| 154 | qsizetype m_plainShown = 0, m_plainLimit = 0; | ||
| 155 | QTimer m_plainSliceTimer; | ||
| 156 | QPointer<QObject> m_htmlRenderer; | ||
| 157 | // remoteHosts() of the shown body, computed once per message and variant. | ||
| 158 | mutable QStringList m_remoteHosts; | ||
| 159 | mutable QString m_remoteHostsKey; | ||
| 160 | }; | ||
| 161 |