1 // For license of this file, see <project-root-folder>/LICENSE.md.
2 
3 #include "gui/messagebrowser.h"
4 
5 #include "gui/messagebox.h"
6 #include "gui/messagetextbrowser.h"
7 #include "gui/reusable/searchtextwidget.h"
8 #include "miscellaneous/application.h"
9 #include "miscellaneous/settings.h"
10 #include "network-web/webfactory.h"
11 #include "services/abstract/serviceroot.h"
12 
13 #include <QKeyEvent>
14 #include <QRegularExpression>
15 #include <QScrollBar>
16 #include <QToolTip>
17 #include <QVBoxLayout>
18 
MessageBrowser(bool should_resize_to_fit,QWidget * parent)19 MessageBrowser::MessageBrowser(bool should_resize_to_fit, QWidget* parent)
20   : QWidget(parent), m_txtBrowser(new MessageTextBrowser(this)), m_searchWidget(new SearchTextWidget(this)),
21   m_layout(new QVBoxLayout(this)) {
22   m_layout->setContentsMargins(3, 3, 3, 3);
23   m_layout->addWidget(m_txtBrowser, 1);
24   m_layout->addWidget(m_searchWidget);
25 
26   if (should_resize_to_fit) {
27     m_txtBrowser->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::MinimumExpanding);
28   }
29 
30   connect(m_searchWidget, &SearchTextWidget::searchCancelled, this, [this]() {
31     m_txtBrowser->textCursor().clearSelection();
32     m_txtBrowser->moveCursor(QTextCursor::MoveOperation::Left);
33   });
34   connect(m_searchWidget, &SearchTextWidget::searchForText, this, [this](const QString& text, bool backwards) {
35     if (backwards) {
36       m_txtBrowser->find(text, QTextDocument::FindFlag::FindBackward);
37     }
38     else {
39       m_txtBrowser->find(text);
40     }
41   });
42 
43   connect(m_txtBrowser, &QTextBrowser::anchorClicked, [=](const QUrl& url) {
44     if (url.toString().startsWith(INTERNAL_URL_PASSATTACHMENT) &&
45         m_root != nullptr &&
46         m_root->getParentServiceRoot()->downloadAttachmentOnMyOwn(url)) {
47       return;
48     }
49 
50     if (!url.isEmpty()) {
51       bool open_externally_now = qApp->settings()->value(GROUP(Browser),
52                                                          SETTING(Browser::OpenLinksInExternalBrowserRightAway)).toBool();
53 
54       if (open_externally_now) {
55         qApp->web()->openUrlInExternalBrowser(url.toString());
56       }
57       else {
58         // User clicked some URL. Open it in external browser or download?
59         MessageBox box(qApp->mainFormWidget());
60         box.setText(tr("You clicked some link. You can download the link contents or open it in external web browser."));
61         box.setInformativeText(tr("What action do you want to take?"));
62         box.setDetailedText(url.toString());
63 
64         QAbstractButton* btn_open = box.addButton(tr("Open in external browser"), QMessageBox::ButtonRole::ActionRole);
65         QAbstractButton* btn_download = box.addButton(tr("Download"), QMessageBox::ButtonRole::ActionRole);
66         QAbstractButton* btn_cancel = box.addButton(QMessageBox::StandardButton::Cancel);
67         bool always;
68         MessageBox::setCheckBox(&box, tr("Always open links in external browser."), &always);
69 
70         box.setDefaultButton(QMessageBox::StandardButton::Cancel);
71         box.exec();
72 
73         if (box.clickedButton() != box.button(QMessageBox::StandardButton::Cancel)) {
74           // Store selected checkbox value.
75           qApp->settings()->setValue(GROUP(Browser), Browser::OpenLinksInExternalBrowserRightAway, always);
76         }
77 
78         if (box.clickedButton() == btn_open) {
79           qApp->web()->openUrlInExternalBrowser(url.toString());
80         }
81         else if (box.clickedButton() == btn_download) {
82           qApp->downloadManager()->download(url);
83         }
84 
85         btn_download->deleteLater();
86         btn_open->deleteLater();
87         btn_cancel->deleteLater();
88       }
89     }
90     else {
91       MessageBox::show(qApp->mainFormWidget(), QMessageBox::Warning, tr("Incorrect link"), tr("Selected hyperlink is invalid."));
92     }
93   });
94   connect(m_txtBrowser,
95           QOverload<const QUrl&>::of(&QTextBrowser::highlighted),
96           [=](const QUrl& url) {
97     Q_UNUSED(url)
98     QToolTip::showText(QCursor::pos(), tr("Click this link to download it or open it with external browser."), this);
99   });
100 
101   m_searchWidget->hide();
102   installEventFilter(this);
103 
104   reloadFontSettings();
105 }
106 
clear(bool also_hide)107 void MessageBrowser::clear(bool also_hide) {
108   m_txtBrowser->clear();
109   m_pictures.clear();
110   m_searchWidget->hide();
111 
112   if (also_hide) {
113     hide();
114   }
115 }
116 
prepareHtmlForMessage(const Message & message)117 QString MessageBrowser::prepareHtmlForMessage(const Message& message) {
118   QString html = QString("<h2 align=\"center\">%1</h2>").arg(message.m_title);
119 
120   if (!message.m_url.isEmpty()) {
121     html += QString("[url] <a href=\"%1\">%1</a><br/>").arg(message.m_url);
122   }
123 
124   for (const Enclosure& enc : message.m_enclosures) {
125     QString enc_url;
126 
127     if (!enc.m_url.contains(QRegularExpression(QSL("^(http|ftp|\\/)")))) {
128       enc_url = QString(INTERNAL_URL_PASSATTACHMENT) + QL1S("/?") + enc.m_url;
129     }
130     else {
131       enc_url = enc.m_url;
132     }
133 
134     html += QString("[%2] <a href=\"%1\">%1</a><br/>").arg(enc_url, enc.m_mimeType);
135   }
136 
137   QRegularExpression imgTagRegex("\\<img[^\\>]*src\\s*=\\s*[\"\']([^\"\']*)[\"\'][^\\>]*\\>",
138                                  QRegularExpression::PatternOption::CaseInsensitiveOption |
139                                  QRegularExpression::PatternOption::InvertedGreedinessOption);
140   QRegularExpressionMatchIterator i = imgTagRegex.globalMatch(message.m_contents);
141   QString pictures_html;
142 
143   while (i.hasNext()) {
144     QRegularExpressionMatch match = i.next();
145 
146     m_pictures.append(match.captured(1));
147     pictures_html += QString("<br/>[%1] <a href=\"%2\">%2</a>").arg(tr("image"), match.captured(1));
148   }
149 
150   if (qApp->settings()->value(GROUP(Messages), SETTING(Messages::DisplayImagePlaceholders)).toBool()) {
151     html += message.m_contents;
152   }
153   else {
154     QString cnts = message.m_contents;
155 
156     html += cnts.replace(imgTagRegex, QString());
157   }
158 
159   html += pictures_html;
160 
161   /*html = html
162          .replace(QSL("\r\n"), QSL("\n"))
163          .replace(QL1C('\r'), QL1C('\n'))
164          .remove(QL1C('\n'));*/
165 
166   return QSL("<html>"
167              "<head><style>"
168              "a { color: %2; }"
169              "</style></head>"
170              "<body>%1</body>"
171              "</html>").arg(html,
172                             qApp->skins()->currentSkin().m_colorPalette[Skin::PaletteColors::Highlight].name());
173 }
174 
eventFilter(QObject * watched,QEvent * event)175 bool MessageBrowser::eventFilter(QObject* watched, QEvent* event) {
176   Q_UNUSED(watched)
177 
178   if (event->type() == QEvent::Type::KeyPress) {
179     auto* key_event = static_cast<QKeyEvent*>(event);
180 
181     if (key_event->matches(QKeySequence::StandardKey::Find)) {
182 
183       m_searchWidget->clear();
184       m_searchWidget->show();
185       m_searchWidget->setFocus();
186       return true;
187     }
188     else if (key_event->key() == Qt::Key::Key_Escape) {
189       m_searchWidget->cancelSearch();
190       return true;
191     }
192   }
193 
194   return false;
195 }
196 
reloadFontSettings()197 void MessageBrowser::reloadFontSettings() {
198   const Settings* settings = qApp->settings();
199   QFont fon;
200 
201   fon.fromString(settings->value(GROUP(Messages), SETTING(Messages::PreviewerFontStandard)).toString());
202   m_txtBrowser->setFont(fon);
203 }
204 
loadMessage(const Message & message,RootItem * root)205 void MessageBrowser::loadMessage(const Message& message, RootItem* root) {
206   Q_UNUSED(root)
207 
208   auto html = prepareHtmlForMessage(message);
209 
210   m_txtBrowser->setHtml(html);
211   m_txtBrowser->verticalScrollBar()->triggerAction(QScrollBar::SliderAction::SliderToMinimum);
212   m_searchWidget->hide();
213 }
214 
verticalScrollBarPosition() const215 double MessageBrowser::verticalScrollBarPosition() const {
216   return m_txtBrowser->verticalScrollBar()->value();
217 }
218 
setVerticalScrollBarPosition(double pos)219 void MessageBrowser::setVerticalScrollBarPosition(double pos) {
220   m_txtBrowser->verticalScrollBar()->setValue(pos);
221 }
222