diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/keymap.cpp | 5 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 6 | ||||
| -rw-r--r-- | src/messageview.cpp | 115 | ||||
| -rw-r--r-- | src/messageview.h | 11 |
4 files changed, 132 insertions, 5 deletions
diff --git a/src/keymap.cpp b/src/keymap.cpp index d8aacc5..b18f5d5 100644 --- a/src/keymap.cpp +++ b/src/keymap.cpp @@ -36,6 +36,7 @@ QStringList KeyMap::knownActions() QStringLiteral("complete_query"), QStringLiteral("toggle_html"), QStringLiteral("load_remote"), + QStringLiteral("message_details"), QStringLiteral("zoom_in"), QStringLiteral("zoom_out"), QStringLiteral("zoom_reset"), @@ -71,6 +72,10 @@ QList<QPair<QString, QString>> KeyMap::defaultBindings() { QStringLiteral("Ctrl+Space"), QStringLiteral("complete_query") }, { QStringLiteral("Ctrl+H"), QStringLiteral("toggle_html") }, { QStringLiteral("Ctrl+M"), QStringLiteral("load_remote") }, + // Shifted because Ctrl+D is delete. Both are "D for details/delete" + // words, and the destructive one keeps the unshifted key it already + // had rather than being moved to make room. + { QStringLiteral("Ctrl+Shift+D"), QStringLiteral("message_details") }, // Ctrl++ is what the '+' key really delivers on a layout where '+' is // unshifted, an Italian one among them, confirmed against the actual // keyboard. QTest::keyClick() cannot reproduce it, so a synthetic-input diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 0f169d9..a0ae7c4 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -458,6 +458,11 @@ void MainWindow::registerActions() tr("Load remote images for the current thread"), [this]() { m_messageView->loadRemoteContent(); }); + addAction(QStringLiteral("message_details"), tr("Message &details"), + tr("Show the full headers of every message in the thread"), + [this]() { + m_messageView->showDetailsDialog(); + }); addAction(QStringLiteral("zoom_in"), tr("Zoom &in"), tr("Enlarge the message text"), [this]() { m_messageView->zoomIn(); @@ -539,6 +544,7 @@ void MainWindow::buildMenus() viewMenu->addSeparator(); viewMenu->addAction(m_actions.value(QStringLiteral("toggle_html"))); viewMenu->addAction(m_actions.value(QStringLiteral("load_remote"))); + viewMenu->addAction(m_actions.value(QStringLiteral("message_details"))); viewMenu->addSeparator(); viewMenu->addAction(m_actions.value(QStringLiteral("zoom_in"))); viewMenu->addAction(m_actions.value(QStringLiteral("zoom_out"))); diff --git a/src/messageview.cpp b/src/messageview.cpp index 4423791..d7167bf 100644 --- a/src/messageview.cpp +++ b/src/messageview.cpp @@ -22,6 +22,8 @@ #include <QDesktopServices> #include <QDialog> #include <QDialogButtonBox> +#include <QFontDatabase> +#include <QPlainTextEdit> #include <QDir> #include <QFileDialog> #include <QHBoxLayout> @@ -124,6 +126,22 @@ MessageView::MessageView(QWidget *parent) m_headerLabel->setWordWrap(true); m_headerLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + // To the right of the header, per the user's decision: the summary answers + // "who is this from", this answers "what actually happened to it". A button + // and not only a shortcut, since "everything needs a memorized key" is the + // complaint this whole backlog started from. + m_detailsButton = new QPushButton(tr("Details..."), this); + m_detailsButton->setObjectName(QStringLiteral("messageDetails")); + m_detailsButton->setToolTip(tr("Show the full headers of every message")); + connect(m_detailsButton, &QPushButton::clicked, + this, &MessageView::showDetailsDialog); + m_detailsButton->hide(); + + auto *headerRow = new QHBoxLayout; + headerRow->addWidget(m_headerLabel, 1); + // Top-aligned so it stays put as the header grows to four rows. + headerRow->addWidget(m_detailsButton, 0, Qt::AlignTop); + m_blockedLabel = new QLabel(tr("Remote content blocked"), this); m_loadRemoteButton = new QPushButton(tr("Load remote content"), this); connect(m_loadRemoteButton, &QPushButton::clicked, @@ -135,6 +153,7 @@ MessageView::MessageView(QWidget *parent) blockedRow->addStretch(); m_attachmentBar = new QWidget(this); + m_attachmentBar->setObjectName(QStringLiteral("attachmentBar")); new QHBoxLayout(m_attachmentBar); // Tags live under the message rather than in the thread list, where @@ -143,7 +162,7 @@ MessageView::MessageView(QWidget *parent) m_tagStrip->hide(); auto *layout = new QVBoxLayout(this); - layout->addWidget(m_headerLabel); + layout->addLayout(headerRow); layout->addLayout(blockedRow); layout->addWidget(m_view, 1); layout->addWidget(m_attachmentBar); @@ -260,17 +279,103 @@ void MessageView::updateHeader() { if (m_items.isEmpty()) { m_headerLabel->clear(); + m_detailsButton->hide(); return; } + m_detailsButton->show(); + // The thread's subject comes from its first message; later replies carry // Re: prefixes that add nothing. const QString subject = m_items.first().message.subject; - m_headerLabel->setText( - QStringLiteral("<b>%1</b><br><small>%2</small>") - .arg(subject.toHtmlEscaped(), - tr("%n message(s) in thread", "", m_items.size()))); + QString text = QStringLiteral("<b>%1</b>").arg(subject.toHtmlEscaped()); + + // The header adapts to what it can say honestly. From, To and Cc are + // per-message, and the pane shows a whole thread, so they are only + // unambiguous when the thread holds exactly one message. For a real thread + // the recipient differs message to message (once the user replies, one is + // addressed to them and the next to the other party), and neither the union + // nor the intersection is "the" recipient. Rather than pick one or compute + // a participants list, the thread case says only the subject and the count, + // and the per-message detail belongs to the dialog. + if (m_items.size() == 1) { + const ParsedMessage &message = m_items.first().message; + + // Every value here is attacker-controlled and the label is RichText, so + // escaping is not cosmetic: an unescaped From injects markup into the + // application's own chrome rather than into the sandboxed page. + auto row = [&text](const QString &label, const QString &value) { + if (value.isEmpty()) + return; // An empty row reads as a rendering fault. + text += QStringLiteral("<br><small>%1 %2</small>") + .arg(label.toHtmlEscaped(), value.toHtmlEscaped()); + }; + + row(tr("From:"), message.from); + row(tr("To:"), message.to); + row(tr("Cc:"), message.cc); + } else { + text += QStringLiteral("<br><small>%1</small>") + .arg(tr("%n message(s) in thread", "", m_items.size())); + } + + m_headerLabel->setText(text); +} + +void MessageView::showDetailsDialog() +{ + if (m_items.isEmpty()) + return; + + QDialog dialog(this); + dialog.setWindowTitle(tr("Message details")); + + auto *layout = new QVBoxLayout(&dialog); + + auto *details = new QPlainTextEdit(&dialog); + details->setReadOnly(true); + // A monospaced font keeps a long Received chain readable as the wrapped + // record it is. + details->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + details->setLineWrapMode(QPlainTextEdit::NoWrap); + + // setPlainText, and a QPlainTextEdit rather than a label: this dialog shows + // header values verbatim, and those come from strangers. Plain text cannot + // interpret markup, so there is nothing here to escape and nothing that + // could render. + QString text; + for (int i = 0; i < m_items.size(); ++i) { + const ParsedMessage &message = m_items.at(i).message; + + if (i > 0) + text += QLatin1Char('\n'); + if (m_items.size() > 1) + text += tr("--- Message %1 of %2 ---") + .arg(i + 1).arg(m_items.size()) + QLatin1Char('\n'); + + auto line = [&text](const QString &label, const QString &value) { + if (!value.isEmpty()) + text += label + QLatin1Char(' ') + value + QLatin1Char('\n'); + }; + + line(tr("Subject:"), message.subject); + line(tr("From:"), message.from); + line(tr("To:"), message.to); + line(tr("Cc:"), message.cc); + line(tr("Date:"), message.date); + line(tr("Message-Id:"), message.messageId); + } + details->setPlainText(text); + + layout->addWidget(details); + + auto *buttons = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); + connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout->addWidget(buttons); + + dialog.resize(700, 400); + dialog.exec(); } void MessageView::render() diff --git a/src/messageview.h b/src/messageview.h index 244a1ba..1be17f8 100644 --- a/src/messageview.h +++ b/src/messageview.h @@ -65,6 +65,15 @@ public: /// Tags of the thread on display, shown as chips along the bottom. void setTags(const QStringList &tags); + /// The full headers of every message in the thread, read-only. Also + /// reachable from the button beside the header; public so the window's + /// message_details action can call it. + /// + /// Plain text, not rich: header values are attacker-controlled and this + /// dialog exists to show them verbatim, so the format that cannot + /// interpret markup is the right one. + void showDetailsDialog(); + /// The body zoom factor. Chromium's own range is roughly 0.25 to 5.0; /// these are tighter, since a pane at either extreme is unusable and the /// only visible way back is a menu entry the user cannot read. @@ -126,6 +135,7 @@ private: /// Every attachment in the thread, in the order the messages render. QList<Attachment> allAttachments() const; + QList<ThreadRenderItem> m_items; bool m_preferHtml = true; @@ -137,6 +147,7 @@ private: QLabel *m_headerLabel = nullptr; QLabel *m_blockedLabel = nullptr; QPushButton *m_loadRemoteButton = nullptr; + QPushButton *m_detailsButton = nullptr; QWidget *m_attachmentBar = nullptr; TagStrip *m_tagStrip = nullptr; }; |
