1 /*
2     Copyright © 2014-2019 by The qTox Project Contributors
3 
4     This file is part of qTox, a Qt-based graphical interface for Tox.
5 
6     qTox is libre software: you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation, either version 3 of the License, or
9     (at your option) any later version.
10 
11     qTox is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with qTox.  If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 #include "addfriendform.h"
21 #include "src/core/core.h"
22 #include "src/nexus.h"
23 #include "src/persistence/settings.h"
24 #include "src/widget/contentlayout.h"
25 #include "src/widget/gui.h"
26 #include "src/widget/tool/croppinglabel.h"
27 #include "src/widget/style.h"
28 #include "src/widget/translator.h"
29 #include <QApplication>
30 #include <QClipboard>
31 #include <QErrorMessage>
32 #include <QFileDialog>
33 #include <QFont>
34 #include <QMessageBox>
35 #include <QRegularExpression>
36 #include <QScrollArea>
37 #include <QSignalMapper>
38 #include <QTabWidget>
39 #include <QWindow>
40 
41 namespace
42 {
getToxId(const QString & id)43     QString getToxId(const QString& id)
44     {
45         const QString toxUriPrefix{"tox:"};
46         QString strippedId = id.trimmed();
47         if (strippedId.startsWith(toxUriPrefix)) {
48             strippedId.remove(0, toxUriPrefix.length());
49         }
50         return strippedId;
51     }
52 
checkIsValidId(const QString & id)53     bool checkIsValidId(const QString& id)
54     {
55         return ToxId::isToxId(id);
56     }
57 }
58 
59 /**
60  * @var QString AddFriendForm::lastUsername
61  * @brief Cached username so we can retranslate the invite message
62  */
63 
AddFriendForm()64 AddFriendForm::AddFriendForm()
65 {
66     tabWidget = new QTabWidget();
67     main = new QWidget(tabWidget), head = new QWidget();
68     QFont bold;
69     bold.setBold(true);
70     headLabel.setFont(bold);
71     toxIdLabel.setTextFormat(Qt::RichText);
72 
73     main->setLayout(&layout);
74     layout.addWidget(&toxIdLabel);
75     layout.addWidget(&toxId);
76     layout.addWidget(&messageLabel);
77     layout.addWidget(&message);
78     layout.addWidget(&sendButton);
79     tabWidget->addTab(main, QString());
80 
81     importContacts = new QWidget(tabWidget);
82     importContacts->setLayout(&importContactsLayout);
83     importFileLine.addWidget(&importFileLabel);
84     importFileLine.addStretch();
85     importFileLine.addWidget(&importFileButton);
86     importContactsLayout.addLayout(&importFileLine);
87     importContactsLayout.addWidget(&importMessageLabel);
88     importContactsLayout.addWidget(&importMessage);
89     importContactsLayout.addWidget(&importSendButton);
90     tabWidget->addTab(importContacts, QString());
91 
92     QScrollArea* scrollArea = new QScrollArea(tabWidget);
93     QWidget* requestWidget = new QWidget(tabWidget);
94     scrollArea->setWidget(requestWidget);
95     scrollArea->setWidgetResizable(true);
96     requestsLayout = new QVBoxLayout(requestWidget);
97     requestsLayout->addStretch(1);
98     tabWidget->addTab(scrollArea, QString());
99 
100     head->setLayout(&headLayout);
101     headLayout.addWidget(&headLabel);
102 
103     connect(&toxId, &QLineEdit::returnPressed, this, &AddFriendForm::onSendTriggered);
104     connect(&toxId, &QLineEdit::textChanged, this, &AddFriendForm::onIdChanged);
105     connect(tabWidget, &QTabWidget::currentChanged, this, &AddFriendForm::onCurrentChanged);
106     connect(&sendButton, &QPushButton::clicked, this, &AddFriendForm::onSendTriggered);
107     connect(&importSendButton, &QPushButton::clicked, this, &AddFriendForm::onImportSendClicked);
108     connect(&importFileButton, &QPushButton::clicked, this, &AddFriendForm::onImportOpenClicked);
109     connect(Nexus::getCore(), &Core::usernameSet, this, &AddFriendForm::onUsernameSet);
110 
111     // accessibility stuff
112     toxIdLabel.setAccessibleDescription(
113         tr("Tox ID, 76 hexadecimal characters"));
114     toxId.setAccessibleDescription(tr("Type in Tox ID of your friend"));
115     messageLabel.setAccessibleDescription(tr("Friend request message"));
116     message.setAccessibleDescription(tr(
117         "Type message to send with the friend request or leave empty to send a default message"));
118     message.setTabChangesFocus(true);
119 
120     retranslateUi();
121     Translator::registerHandler(std::bind(&AddFriendForm::retranslateUi, this), this);
122 
123     const int size = Settings::getInstance().getFriendRequestSize();
124     for (int i = 0; i < size; ++i) {
125         Settings::Request request = Settings::getInstance().getFriendRequest(i);
126         addFriendRequestWidget(request.address, request.message);
127     }
128 }
129 
~AddFriendForm()130 AddFriendForm::~AddFriendForm()
131 {
132     Translator::unregister(this);
133     head->deleteLater();
134     tabWidget->deleteLater();
135 }
136 
isShown() const137 bool AddFriendForm::isShown() const
138 {
139     if (head->isVisible()) {
140         head->window()->windowHandle()->alert(0);
141         return true;
142     }
143 
144     return false;
145 }
146 
show(ContentLayout * contentLayout)147 void AddFriendForm::show(ContentLayout* contentLayout)
148 {
149     contentLayout->mainContent->layout()->addWidget(tabWidget);
150     contentLayout->mainHead->layout()->addWidget(head);
151     tabWidget->show();
152     head->show();
153     setIdFromClipboard();
154     toxId.setFocus();
155 
156     // Fix #3421
157     // Needed to update tab after opening window
158     const int index = tabWidget->currentIndex();
159     onCurrentChanged(index);
160 }
161 
getMessage() const162 QString AddFriendForm::getMessage() const
163 {
164     const QString msg = message.toPlainText();
165     return !msg.isEmpty() ? msg : message.placeholderText();
166 }
167 
getImportMessage() const168 QString AddFriendForm::getImportMessage() const
169 {
170     const QString msg = importMessage.toPlainText();
171     return msg.isEmpty() ? importMessage.placeholderText() : msg;
172 }
173 
setMode(Mode mode)174 void AddFriendForm::setMode(Mode mode)
175 {
176     tabWidget->setCurrentIndex(mode);
177 }
178 
addFriendRequest(const QString & friendAddress,const QString & message)179 bool AddFriendForm::addFriendRequest(const QString& friendAddress, const QString& message)
180 {
181     if (Settings::getInstance().addFriendRequest(friendAddress, message)) {
182         addFriendRequestWidget(friendAddress, message);
183         if (isShown()) {
184             onCurrentChanged(tabWidget->currentIndex());
185         }
186 
187         return true;
188     }
189     return false;
190 }
191 
onUsernameSet(const QString & username)192 void AddFriendForm::onUsernameSet(const QString& username)
193 {
194     lastUsername = username;
195     retranslateUi();
196 }
197 
addFriend(const QString & idText)198 void AddFriendForm::addFriend(const QString& idText)
199 {
200     ToxId friendId(idText);
201 
202     if (!friendId.isValid()) {
203         GUI::showWarning(tr("Couldn't add friend"),
204                          tr("%1 Tox ID is invalid", "Tox address error").arg(idText));
205         return;
206     }
207 
208     deleteFriendRequest(friendId);
209     if (friendId == Core::getInstance()->getSelfId()) {
210         GUI::showWarning(tr("Couldn't add friend"),
211                          //: When trying to add your own Tox ID as friend
212                          tr("You can't add yourself as a friend!"));
213     } else {
214         emit friendRequested(friendId, getMessage());
215     }
216 }
217 
onSendTriggered()218 void AddFriendForm::onSendTriggered()
219 {
220     const QString id = getToxId(toxId.text());
221     addFriend(id);
222 
223     this->toxId.clear();
224     this->message.clear();
225 }
226 
onImportSendClicked()227 void AddFriendForm::onImportSendClicked()
228 {
229     for (const QString& id : contactsToImport) {
230         addFriend(id);
231     }
232 
233     contactsToImport.clear();
234     importMessage.clear();
235     retranslateUi(); // Update the importFileLabel
236 }
237 
onImportOpenClicked()238 void AddFriendForm::onImportOpenClicked()
239 {
240     const QString path = QFileDialog::getOpenFileName(Q_NULLPTR, tr("Open contact list"));
241     if (path.isEmpty()) {
242         return;
243     }
244 
245     QFile contactFile(path);
246     if (!contactFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
247         GUI::showWarning(tr("Couldn't open file"),
248                          //: Error message when trying to open a contact list file to import
249                          tr("Couldn't open the contact file"));
250         return;
251     }
252 
253     contactsToImport = QString::fromUtf8(contactFile.readAll()).split('\n');
254     qDebug() << "Import list:";
255     for (auto it = contactsToImport.begin(); it != contactsToImport.end();) {
256         const QString id = getToxId(*it);
257         if (checkIsValidId(id)) {
258             *it = id;
259             qDebug() << *it;
260             ++it;
261         } else {
262             if (!id.isEmpty()) {
263                 qDebug() << "Invalid ID:" << *it;
264             }
265             it = contactsToImport.erase(it);
266         }
267     }
268 
269     if (contactsToImport.isEmpty()) {
270         GUI::showWarning(tr("Invalid file"),
271                          tr("We couldn't find any contacts to import in this file!"));
272     }
273 
274     retranslateUi(); // Update the importFileLabel to show how many contacts we have
275 }
276 
onIdChanged(const QString & id)277 void AddFriendForm::onIdChanged(const QString& id)
278 {
279     const QString strippedId = getToxId(id);
280 
281     const bool isValidId = checkIsValidId(strippedId);
282     const bool isValidOrEmpty = strippedId.isEmpty() || isValidId;
283 
284     //: Tox ID of the person you're sending a friend request to
285     const QString toxIdText(tr("Tox ID"));
286     //: Tox ID format description
287     const QString toxIdComment(tr("76 hexadecimal characters"));
288 
289     const QString labelText =
290         isValidId ? QStringLiteral("%1 (%2)") : QStringLiteral("%1 <font color='red'>(%2)</font>");
291     toxIdLabel.setText(labelText.arg(toxIdText, toxIdComment));
292     toxId.setStyleSheet(isValidOrEmpty ? QStringLiteral("")
293                                   : Style::getStylesheet("addFriendForm/toxId.css"));
294     toxId.setToolTip(isValidOrEmpty ? QStringLiteral("") : tr("Invalid Tox ID format"));
295 
296     sendButton.setEnabled(isValidId);
297 }
298 
setIdFromClipboard()299 void AddFriendForm::setIdFromClipboard()
300 {
301     const QClipboard* clipboard = QApplication::clipboard();
302     const QString trimmedId = clipboard->text().trimmed();
303     const QString strippedId = getToxId(trimmedId);
304     const Core* core = Core::getInstance();
305     const bool isSelf = ToxId::isToxId(strippedId) && ToxId(strippedId) != core->getSelfId();
306     if (!strippedId.isEmpty() && ToxId::isToxId(strippedId) && isSelf) {
307         toxId.setText(trimmedId);
308     }
309 }
310 
deleteFriendRequest(const ToxId & toxId)311 void AddFriendForm::deleteFriendRequest(const ToxId& toxId)
312 {
313     const int size = Settings::getInstance().getFriendRequestSize();
314     for (int i = 0; i < size; ++i) {
315         Settings::Request request = Settings::getInstance().getFriendRequest(i);
316         if (toxId == ToxId(request.address)) {
317             Settings::getInstance().removeFriendRequest(i);
318             return;
319         }
320     }
321 }
322 
onFriendRequestAccepted()323 void AddFriendForm::onFriendRequestAccepted()
324 {
325     QPushButton* acceptButton = static_cast<QPushButton*>(sender());
326     QWidget* friendWidget = acceptButton->parentWidget();
327     const int index = requestsLayout->indexOf(friendWidget);
328     removeFriendRequestWidget(friendWidget);
329     const int indexFromEnd = requestsLayout->count() - index - 1;
330     const Settings::Request request = Settings::getInstance().getFriendRequest(indexFromEnd);
331     emit friendRequestAccepted(ToxId(request.address).getPublicKey());
332     Settings::getInstance().removeFriendRequest(indexFromEnd);
333     Settings::getInstance().savePersonal();
334 }
335 
onFriendRequestRejected()336 void AddFriendForm::onFriendRequestRejected()
337 {
338     QPushButton* rejectButton = static_cast<QPushButton*>(sender());
339     QWidget* friendWidget = rejectButton->parentWidget();
340     const int index = requestsLayout->indexOf(friendWidget);
341     removeFriendRequestWidget(friendWidget);
342     const int indexFromEnd = requestsLayout->count() - index - 1;
343     Settings::getInstance().removeFriendRequest(indexFromEnd);
344     Settings::getInstance().savePersonal();
345 }
346 
onCurrentChanged(int index)347 void AddFriendForm::onCurrentChanged(int index)
348 {
349     if (index == FriendRequest && Settings::getInstance().getUnreadFriendRequests() != 0) {
350         Settings::getInstance().clearUnreadFriendRequests();
351         Settings::getInstance().savePersonal();
352         emit friendRequestsSeen();
353     }
354 }
355 
retranslateUi()356 void AddFriendForm::retranslateUi()
357 {
358     headLabel.setText(tr("Add Friends"));
359     //: The message you send in friend requests
360     static const QString messageLabelText = tr("Message");
361     messageLabel.setText(messageLabelText);
362     importMessageLabel.setText(messageLabelText);
363     //: Button to choose a file with a list of contacts to import
364     importFileButton.setText(tr("Open"));
365     importSendButton.setText(tr("Send friend requests"));
366     sendButton.setText(tr("Send friend request"));
367     //: Default message in friend requests if the field is left blank. Write something appropriate!
368     message.setPlaceholderText(tr("%1 here! Tox me maybe?").arg(lastUsername));
369     importMessage.setPlaceholderText(message.placeholderText());
370 
371     importFileLabel.setText(
372         contactsToImport.isEmpty()
373             ? tr("Import a list of contacts, one Tox ID per line")
374             //: Shows the number of contacts we're about to import from a file (at least one)
375             : tr("Ready to import %n contact(s), click send to confirm", "", contactsToImport.size()));
376 
377     onIdChanged(toxId.text());
378 
379     tabWidget->setTabText(AddFriend, tr("Add a friend"));
380     tabWidget->setTabText(ImportContacts, tr("Import contacts"));
381     tabWidget->setTabText(FriendRequest, tr("Friend requests"));
382 
383     for (QPushButton* acceptButton : acceptButtons) {
384         retranslateAcceptButton(acceptButton);
385     }
386 
387     for (QPushButton* rejectButton : rejectButtons) {
388         retranslateRejectButton(rejectButton);
389     }
390 }
391 
addFriendRequestWidget(const QString & friendAddress,const QString & message)392 void AddFriendForm::addFriendRequestWidget(const QString& friendAddress, const QString& message)
393 {
394     QWidget* friendWidget = new QWidget(tabWidget);
395     QHBoxLayout* friendLayout = new QHBoxLayout(friendWidget);
396     QVBoxLayout* horLayout = new QVBoxLayout();
397     horLayout->setMargin(0);
398     friendLayout->addLayout(horLayout);
399 
400     CroppingLabel* friendLabel = new CroppingLabel(friendWidget);
401     friendLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
402     friendLabel->setText("<b>" + friendAddress + "</b>");
403     horLayout->addWidget(friendLabel);
404 
405     QLabel* messageLabel = new QLabel(message);
406     // allow to select text, but treat links as plaintext to prevent phishing
407     messageLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
408     messageLabel->setTextFormat(Qt::PlainText);
409     messageLabel->setWordWrap(true);
410     horLayout->addWidget(messageLabel, 1);
411 
412     QPushButton* acceptButton = new QPushButton(friendWidget);
413     acceptButtons.append(acceptButton);
414     connect(acceptButton, &QPushButton::released, this, &AddFriendForm::onFriendRequestAccepted);
415     friendLayout->addWidget(acceptButton);
416     retranslateAcceptButton(acceptButton);
417 
418     QPushButton* rejectButton = new QPushButton(friendWidget);
419     rejectButtons.append(rejectButton);
420     connect(rejectButton, &QPushButton::released, this, &AddFriendForm::onFriendRequestRejected);
421     friendLayout->addWidget(rejectButton);
422     retranslateRejectButton(rejectButton);
423 
424     requestsLayout->insertWidget(0, friendWidget);
425 }
426 
removeFriendRequestWidget(QWidget * friendWidget)427 void AddFriendForm::removeFriendRequestWidget(QWidget* friendWidget)
428 {
429     int index = requestsLayout->indexOf(friendWidget);
430     requestsLayout->removeWidget(friendWidget);
431     acceptButtons.removeAt(index);
432     rejectButtons.removeAt(index);
433     friendWidget->deleteLater();
434 }
435 
retranslateAcceptButton(QPushButton * acceptButton)436 void AddFriendForm::retranslateAcceptButton(QPushButton* acceptButton)
437 {
438     acceptButton->setText(tr("Accept"));
439 }
440 
retranslateRejectButton(QPushButton * rejectButton)441 void AddFriendForm::retranslateRejectButton(QPushButton* rejectButton)
442 {
443     rejectButton->setText(tr("Reject"));
444 }
445