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