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 "widget.h"
21 
22 #include <cassert>
23 
24 #include <QClipboard>
25 #include <QDebug>
26 #include <QDesktopServices>
27 #include <QDesktopWidget>
28 #include <QMessageBox>
29 #include <QMouseEvent>
30 #include <QPainter>
31 #include <QShortcut>
32 #include <QString>
33 #include <QSvgRenderer>
34 #include <QWindow>
35 #ifdef Q_OS_MAC
36 #include <QMenuBar>
37 #include <QSignalMapper>
38 #include <QWindow>
39 #endif
40 
41 #include "circlewidget.h"
42 #include "contentdialog.h"
43 #include "contentlayout.h"
44 #include "friendlistwidget.h"
45 #include "friendwidget.h"
46 #include "groupwidget.h"
47 #include "maskablepixmapwidget.h"
48 #include "splitterrestorer.h"
49 #include "form/groupchatform.h"
50 #include "src/audio/audio.h"
51 #include "src/chatlog/content/filetransferwidget.h"
52 #include "src/core/core.h"
53 #include "src/core/coreav.h"
54 #include "src/core/corefile.h"
55 #include "src/friendlist.h"
56 #include "src/grouplist.h"
57 #include "src/model/chathistory.h"
58 #include "src/model/chatroom/friendchatroom.h"
59 #include "src/model/chatroom/groupchatroom.h"
60 #include "src/model/friend.h"
61 #include "src/model/group.h"
62 #include "src/model/groupinvite.h"
63 #include "src/model/profile/profileinfo.h"
64 #include "src/model/status.h"
65 #include "src/net/updatecheck.h"
66 #include "src/nexus.h"
67 #include "src/persistence/offlinemsgengine.h"
68 #include "src/persistence/profile.h"
69 #include "src/persistence/settings.h"
70 #include "src/platform/timer.h"
71 #include "src/widget/contentdialogmanager.h"
72 #include "src/widget/form/addfriendform.h"
73 #include "src/widget/form/chatform.h"
74 #include "src/widget/form/filesform.h"
75 #include "src/widget/form/groupinviteform.h"
76 #include "src/widget/form/profileform.h"
77 #include "src/widget/form/settingswidget.h"
78 #include "src/widget/gui.h"
79 #include "src/widget/style.h"
80 #include "src/widget/translator.h"
81 #include "tool/removefrienddialog.h"
82 
toxActivateEventHandler(const QByteArray &)83 bool toxActivateEventHandler(const QByteArray&)
84 {
85     Widget* widget = Nexus::getDesktopGUI();
86     if (!widget) {
87         return true;
88     }
89 
90     qDebug() << "Handling [activate] event from other instance";
91     widget->forceShow();
92 
93     return true;
94 }
95 
96 namespace {
97 
98 /**
99  * @brief Dangerous way to find out if a path is writable.
100  * @param filepath Path to file which should be deleted.
101  * @return True, if file writeable, false otherwise.
102  */
tryRemoveFile(const QString & filepath)103 bool tryRemoveFile(const QString& filepath)
104 {
105     QFile tmp(filepath);
106     bool writable = tmp.open(QIODevice::WriteOnly);
107     tmp.remove();
108     return writable;
109 }
110 
acceptFileTransfer(const ToxFile & file,const QString & path)111 void acceptFileTransfer(const ToxFile& file, const QString& path)
112 {
113     QString filepath;
114     int number = 0;
115 
116     QString suffix = QFileInfo(file.fileName).completeSuffix();
117     QString base = QFileInfo(file.fileName).baseName();
118 
119     do {
120         filepath = QString("%1/%2%3.%4")
121                        .arg(path, base,
122                             number > 0 ? QString(" (%1)").arg(QString::number(number)) : QString(),
123                             suffix);
124         ++number;
125     } while (QFileInfo(filepath).exists());
126 
127     // Do not automatically accept the file-transfer if the path is not writable.
128     // The user can still accept it manually.
129     if (tryRemoveFile(filepath)) {
130         CoreFile* coreFile = Core::getInstance()->getCoreFile();
131         coreFile->acceptFileRecvRequest(file.friendId, file.fileNum, filepath);
132     } else {
133         qWarning() << "Cannot write to " << filepath;
134     }
135 }
136 } // namespace
137 
138 Widget* Widget::instance{nullptr};
139 
Widget(IAudioControl & audio,QWidget * parent)140 Widget::Widget(IAudioControl& audio, QWidget* parent)
141     : QMainWindow(parent)
142     , icon{nullptr}
143     , trayMenu{nullptr}
144     , ui(new Ui::MainWindow)
145     , activeChatroomWidget{nullptr}
146     , eventFlag(false)
147     , eventIcon(false)
148     , audio(audio)
149     , settings(Settings::getInstance())
150 {
151     installEventFilter(this);
152     QString locale = settings.getTranslation();
153     Translator::translate(locale);
154 }
155 
init()156 void Widget::init()
157 {
158     ui->setupUi(this);
159 
160     QIcon themeIcon = QIcon::fromTheme("qtox");
161     if (!themeIcon.isNull()) {
162         setWindowIcon(themeIcon);
163     }
164 
165     timer = new QTimer();
166     timer->start(1000);
167 
168     icon_size = 15;
169 
170     actionShow = new QAction(this);
171     connect(actionShow, &QAction::triggered, this, &Widget::forceShow);
172 
173     // Preparing icons and set their size
174     statusOnline = new QAction(this);
175     statusOnline->setIcon(
176         prepareIcon(Status::getIconPath(Status::Status::Online), icon_size, icon_size));
177     connect(statusOnline, &QAction::triggered, this, &Widget::setStatusOnline);
178 
179     statusAway = new QAction(this);
180     statusAway->setIcon(prepareIcon(Status::getIconPath(Status::Status::Away), icon_size, icon_size));
181     connect(statusAway, &QAction::triggered, this, &Widget::setStatusAway);
182 
183     statusBusy = new QAction(this);
184     statusBusy->setIcon(prepareIcon(Status::getIconPath(Status::Status::Busy), icon_size, icon_size));
185     connect(statusBusy, &QAction::triggered, this, &Widget::setStatusBusy);
186 
187     actionLogout = new QAction(this);
188     actionLogout->setIcon(prepareIcon(":/img/others/logout-icon.svg", icon_size, icon_size));
189 
190     actionQuit = new QAction(this);
191 #ifndef Q_OS_OSX
192     actionQuit->setMenuRole(QAction::QuitRole);
193 #endif
194 
195     actionQuit->setIcon(
196         prepareIcon(Style::getImagePath("rejectCall/rejectCall.svg"), icon_size, icon_size));
197     connect(actionQuit, &QAction::triggered, qApp, &QApplication::quit);
198 
199     layout()->setContentsMargins(0, 0, 0, 0);
200 
201     profilePicture = new MaskablePixmapWidget(this, QSize(40, 40), ":/img/avatar_mask.svg");
202     profilePicture->setPixmap(QPixmap(":/img/contact_dark.svg"));
203     profilePicture->setClickable(true);
204     profilePicture->setObjectName("selfAvatar");
205     ui->myProfile->insertWidget(0, profilePicture);
206     ui->myProfile->insertSpacing(1, 7);
207 
208     filterMenu = new QMenu(this);
209     filterGroup = new QActionGroup(this);
210     filterDisplayGroup = new QActionGroup(this);
211 
212     filterDisplayName = new QAction(this);
213     filterDisplayName->setCheckable(true);
214     filterDisplayName->setChecked(true);
215     filterDisplayGroup->addAction(filterDisplayName);
216     filterMenu->addAction(filterDisplayName);
217     filterDisplayActivity = new QAction(this);
218     filterDisplayActivity->setCheckable(true);
219     filterDisplayGroup->addAction(filterDisplayActivity);
220     filterMenu->addAction(filterDisplayActivity);
221     settings.getFriendSortingMode() == FriendListWidget::SortingMode::Name
222         ? filterDisplayName->setChecked(true)
223         : filterDisplayActivity->setChecked(true);
224     filterMenu->addSeparator();
225 
226     filterAllAction = new QAction(this);
227     filterAllAction->setCheckable(true);
228     filterAllAction->setChecked(true);
229     filterGroup->addAction(filterAllAction);
230     filterMenu->addAction(filterAllAction);
231     filterOnlineAction = new QAction(this);
232     filterOnlineAction->setCheckable(true);
233     filterGroup->addAction(filterOnlineAction);
234     filterMenu->addAction(filterOnlineAction);
235     filterOfflineAction = new QAction(this);
236     filterOfflineAction->setCheckable(true);
237     filterGroup->addAction(filterOfflineAction);
238     filterMenu->addAction(filterOfflineAction);
239     filterFriendsAction = new QAction(this);
240     filterFriendsAction->setCheckable(true);
241     filterGroup->addAction(filterFriendsAction);
242     filterMenu->addAction(filterFriendsAction);
243     filterGroupsAction = new QAction(this);
244     filterGroupsAction->setCheckable(true);
245     filterGroup->addAction(filterGroupsAction);
246     filterMenu->addAction(filterGroupsAction);
247 
248     ui->searchContactFilterBox->setMenu(filterMenu);
249 
250     contactListWidget = new FriendListWidget(this, settings.getGroupchatPosition());
251     connect(contactListWidget, &FriendListWidget::searchCircle, this, &Widget::searchCircle);
252     connect(contactListWidget, &FriendListWidget::connectCircleWidget, this,
253             &Widget::connectCircleWidget);
254     ui->friendList->setWidget(contactListWidget);
255     ui->friendList->setLayoutDirection(Qt::RightToLeft);
256     ui->friendList->setContextMenuPolicy(Qt::CustomContextMenu);
257 
258     ui->statusLabel->setEditable(true);
259 
260     QMenu* statusButtonMenu = new QMenu(ui->statusButton);
261     statusButtonMenu->addAction(statusOnline);
262     statusButtonMenu->addAction(statusAway);
263     statusButtonMenu->addAction(statusBusy);
264     ui->statusButton->setMenu(statusButtonMenu);
265 
266     // disable proportional scaling
267     ui->mainSplitter->setStretchFactor(0, 0);
268     ui->mainSplitter->setStretchFactor(1, 1);
269 
270     onStatusSet(Status::Status::Offline);
271 
272     // Disable some widgets until we're connected to the DHT
273     ui->statusButton->setEnabled(false);
274 
275     Style::setThemeColor(settings.getThemeColor());
276 
277     filesForm = new FilesForm();
278     addFriendForm = new AddFriendForm;
279     groupInviteForm = new GroupInviteForm;
280 #if UPDATE_CHECK_ENABLED
281     updateCheck = std::unique_ptr<UpdateCheck>(new UpdateCheck(settings));
282     connect(updateCheck.get(), &UpdateCheck::updateAvailable, this, &Widget::onUpdateAvailable);
283 #endif
284     settingsWidget = new SettingsWidget(updateCheck.get(), audio, this);
285 #if UPDATE_CHECK_ENABLED
286     updateCheck->checkForUpdate();
287 #endif
288 
289     core = Nexus::getCore();
290     CoreFile* coreFile = core->getCoreFile();
291     Profile* profile = Nexus::getProfile();
292     profileInfo = new ProfileInfo(core, profile);
293     profileForm = new ProfileForm(profileInfo);
294 
295     // connect logout tray menu action
296     connect(actionLogout, &QAction::triggered, profileForm, &ProfileForm::onLogoutClicked);
297 
298     connect(profile, &Profile::selfAvatarChanged, profileForm, &ProfileForm::onSelfAvatarLoaded);
299 
300     connect(coreFile, &CoreFile::fileReceiveRequested, this, &Widget::onFileReceiveRequested);
301     connect(coreFile, &CoreFile::fileDownloadFinished, filesForm, &FilesForm::onFileDownloadComplete);
302     connect(coreFile, &CoreFile::fileUploadFinished, filesForm, &FilesForm::onFileUploadComplete);
303     connect(ui->addButton, &QPushButton::clicked, this, &Widget::onAddClicked);
304     connect(ui->groupButton, &QPushButton::clicked, this, &Widget::onGroupClicked);
305     connect(ui->transferButton, &QPushButton::clicked, this, &Widget::onTransferClicked);
306     connect(ui->settingsButton, &QPushButton::clicked, this, &Widget::onShowSettings);
307     connect(profilePicture, &MaskablePixmapWidget::clicked, this, &Widget::showProfile);
308     connect(ui->nameLabel, &CroppingLabel::clicked, this, &Widget::showProfile);
309     connect(ui->statusLabel, &CroppingLabel::editFinished, this, &Widget::onStatusMessageChanged);
310     connect(ui->mainSplitter, &QSplitter::splitterMoved, this, &Widget::onSplitterMoved);
311     connect(addFriendForm, &AddFriendForm::friendRequested, this, &Widget::friendRequested);
312     connect(groupInviteForm, &GroupInviteForm::groupCreate, core, &Core::createGroup);
313     connect(timer, &QTimer::timeout, this, &Widget::onUserAwayCheck);
314     connect(timer, &QTimer::timeout, this, &Widget::onEventIconTick);
315     connect(timer, &QTimer::timeout, this, &Widget::onTryCreateTrayIcon);
316     connect(ui->searchContactText, &QLineEdit::textChanged, this, &Widget::searchContacts);
317     connect(filterGroup, &QActionGroup::triggered, this, &Widget::searchContacts);
318     connect(filterDisplayGroup, &QActionGroup::triggered, this, &Widget::changeDisplayMode);
319     connect(ui->friendList, &QWidget::customContextMenuRequested, this, &Widget::friendListContextMenu);
320 
321     connect(coreFile, &CoreFile::fileSendStarted, this, &Widget::dispatchFile);
322     connect(coreFile, &CoreFile::fileReceiveRequested, this, &Widget::dispatchFile);
323     connect(coreFile, &CoreFile::fileTransferAccepted, this, &Widget::dispatchFile);
324     connect(coreFile, &CoreFile::fileTransferCancelled, this, &Widget::dispatchFile);
325     connect(coreFile, &CoreFile::fileTransferFinished, this, &Widget::dispatchFile);
326     connect(coreFile, &CoreFile::fileTransferPaused, this, &Widget::dispatchFile);
327     connect(coreFile, &CoreFile::fileTransferInfo, this, &Widget::dispatchFile);
328     connect(coreFile, &CoreFile::fileTransferRemotePausedUnpaused, this, &Widget::dispatchFileWithBool);
329     connect(coreFile, &CoreFile::fileTransferBrokenUnbroken, this, &Widget::dispatchFileWithBool);
330     connect(coreFile, &CoreFile::fileSendFailed, this, &Widget::dispatchFileSendFailed);
331     // NOTE: We intentionally do not connect the fileUploadFinished and fileDownloadFinished signals
332     // because they are duplicates of fileTransferFinished NOTE: We don't hook up the
333     // fileNameChanged signal since it is only emitted before a fileReceiveRequest. We get the
334     // initial request with the sanitized name so there is no work for us to do
335 
336     // keyboard shortcuts
337     new QShortcut(Qt::CTRL + Qt::Key_Q, this, SLOT(close()));
338     new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_Tab, this, SLOT(previousContact()));
339     new QShortcut(Qt::CTRL + Qt::Key_Tab, this, SLOT(nextContact()));
340     new QShortcut(Qt::CTRL + Qt::Key_PageUp, this, SLOT(previousContact()));
341     new QShortcut(Qt::CTRL + Qt::Key_PageDown, this, SLOT(nextContact()));
342     new QShortcut(Qt::Key_F11, this, SLOT(toggleFullscreen()));
343 
344 #ifdef Q_OS_MAC
345     QMenuBar* globalMenu = Nexus::getInstance().globalMenuBar;
346     QAction* windowMenu = Nexus::getInstance().windowMenu->menuAction();
347     QAction* viewMenu = Nexus::getInstance().viewMenu->menuAction();
348     QAction* frontAction = Nexus::getInstance().frontAction;
349 
350     fileMenu = globalMenu->insertMenu(viewMenu, new QMenu(this));
351 
352     editProfileAction = fileMenu->menu()->addAction(QString());
353     connect(editProfileAction, &QAction::triggered, this, &Widget::showProfile);
354 
355     changeStatusMenu = fileMenu->menu()->addMenu(QString());
356     fileMenu->menu()->addAction(changeStatusMenu->menuAction());
357     changeStatusMenu->addAction(statusOnline);
358     changeStatusMenu->addSeparator();
359     changeStatusMenu->addAction(statusAway);
360     changeStatusMenu->addAction(statusBusy);
361 
362     fileMenu->menu()->addSeparator();
363     logoutAction = fileMenu->menu()->addAction(QString());
364     connect(logoutAction, &QAction::triggered, [this]() { Nexus::getInstance().showLogin(); });
365 
366     editMenu = globalMenu->insertMenu(viewMenu, new QMenu(this));
367     editMenu->menu()->addSeparator();
368 
369     viewMenu->menu()->insertMenu(Nexus::getInstance().fullscreenAction, filterMenu);
370 
371     viewMenu->menu()->insertSeparator(Nexus::getInstance().fullscreenAction);
372 
373     contactMenu = globalMenu->insertMenu(windowMenu, new QMenu(this));
374 
375     addContactAction = contactMenu->menu()->addAction(QString());
376     connect(addContactAction, &QAction::triggered, this, &Widget::onAddClicked);
377 
378     nextConversationAction = new QAction(this);
379     Nexus::getInstance().windowMenu->insertAction(frontAction, nextConversationAction);
380     nextConversationAction->setShortcut(QKeySequence::SelectNextPage);
381     connect(nextConversationAction, &QAction::triggered, [this]() {
382         if (ContentDialogManager::getInstance()->current() == QApplication::activeWindow())
383             ContentDialogManager::getInstance()->current()->cycleContacts(true);
384         else if (QApplication::activeWindow() == this)
385             cycleContacts(true);
386     });
387 
388     previousConversationAction = new QAction(this);
389     Nexus::getInstance().windowMenu->insertAction(frontAction, previousConversationAction);
390     previousConversationAction->setShortcut(QKeySequence::SelectPreviousPage);
391     connect(previousConversationAction, &QAction::triggered, [this] {
392         if (ContentDialogManager::getInstance()->current() == QApplication::activeWindow())
393             ContentDialogManager::getInstance()->current()->cycleContacts(false);
394         else if (QApplication::activeWindow() == this)
395             cycleContacts(false);
396     });
397 
398     windowMenu->menu()->insertSeparator(frontAction);
399 
400     QAction* preferencesAction = viewMenu->menu()->addAction(QString());
401     preferencesAction->setMenuRole(QAction::PreferencesRole);
402     connect(preferencesAction, &QAction::triggered, this, &Widget::onShowSettings);
403 
404     QAction* aboutAction = viewMenu->menu()->addAction(QString());
405     aboutAction->setMenuRole(QAction::AboutRole);
406     connect(aboutAction, &QAction::triggered, [this]() {
407         onShowSettings();
408         settingsWidget->showAbout();
409     });
410 
411     QMenu* dockChangeStatusMenu = new QMenu(tr("Status"), this);
412     dockChangeStatusMenu->addAction(statusOnline);
413     statusOnline->setIconVisibleInMenu(true);
414     dockChangeStatusMenu->addSeparator();
415     dockChangeStatusMenu->addAction(statusAway);
416     dockChangeStatusMenu->addAction(statusBusy);
417     Nexus::getInstance().dockMenu->addAction(dockChangeStatusMenu->menuAction());
418 
419     connect(this, &Widget::windowStateChanged, &Nexus::getInstance(), &Nexus::onWindowStateChanged);
420 #endif
421 
422     contentLayout = nullptr;
423     onSeparateWindowChanged(settings.getSeparateWindow(), false);
424 
425     ui->addButton->setCheckable(true);
426     ui->groupButton->setCheckable(true);
427     ui->transferButton->setCheckable(true);
428     ui->settingsButton->setCheckable(true);
429 
430     if (contentLayout) {
431         onAddClicked();
432     }
433 
434     // restore window state
435     restoreGeometry(settings.getWindowGeometry());
436     restoreState(settings.getWindowState());
437     SplitterRestorer restorer(ui->mainSplitter);
438     restorer.restore(settings.getSplitterState(), size());
439 
440     friendRequestsButton = nullptr;
441     groupInvitesButton = nullptr;
442     unreadGroupInvites = 0;
443 
444     connect(addFriendForm, &AddFriendForm::friendRequested, this, &Widget::friendRequestsUpdate);
445     connect(addFriendForm, &AddFriendForm::friendRequestsSeen, this, &Widget::friendRequestsUpdate);
446     connect(addFriendForm, &AddFriendForm::friendRequestAccepted, this, &Widget::friendRequestAccepted);
447     connect(groupInviteForm, &GroupInviteForm::groupInvitesSeen, this, &Widget::groupInvitesClear);
448     connect(groupInviteForm, &GroupInviteForm::groupInviteAccepted, this,
449             &Widget::onGroupInviteAccepted);
450 
451     // settings
452     connect(&settings, &Settings::showSystemTrayChanged, this, &Widget::onSetShowSystemTray);
453     connect(&settings, &Settings::separateWindowChanged, this, &Widget::onSeparateWindowClicked);
454     connect(&settings, &Settings::compactLayoutChanged, contactListWidget,
455             &FriendListWidget::onCompactChanged);
456     connect(&settings, &Settings::groupchatPositionChanged, contactListWidget,
457             &FriendListWidget::onGroupchatPositionChanged);
458 
459     reloadTheme();
460     updateIcons();
461     retranslateUi();
462     Translator::registerHandler(std::bind(&Widget::retranslateUi, this), this);
463 
464     if (!settings.getShowSystemTray()) {
465         show();
466     }
467 
468 #ifdef Q_OS_MAC
469     Nexus::getInstance().updateWindows();
470 #endif
471 }
472 
eventFilter(QObject * obj,QEvent * event)473 bool Widget::eventFilter(QObject* obj, QEvent* event)
474 {
475     QWindowStateChangeEvent* ce = nullptr;
476     Qt::WindowStates state = windowState();
477 
478     switch (event->type()) {
479     case QEvent::Close:
480         // It's needed if user enable `Close to tray`
481         wasMaximized = state.testFlag(Qt::WindowMaximized);
482         break;
483 
484     case QEvent::WindowStateChange:
485         ce = static_cast<QWindowStateChangeEvent*>(event);
486         if (state.testFlag(Qt::WindowMinimized) && obj) {
487             wasMaximized = ce->oldState().testFlag(Qt::WindowMaximized);
488         }
489 
490 #ifdef Q_OS_MAC
491         emit windowStateChanged(windowState());
492 #endif
493         break;
494     default:
495         break;
496     }
497 
498     return false;
499 }
500 
updateIcons()501 void Widget::updateIcons()
502 {
503     if (!icon) {
504         return;
505     }
506 
507     const QString assetSuffix = Status::getAssetSuffix(static_cast<Status::Status>(
508                                     ui->statusButton->property("status").toInt()))
509                                 + (eventIcon ? "_event" : "");
510 
511     // Some builds of Qt appear to have a bug in icon loading:
512     // QIcon::hasThemeIcon is sometimes unaware that the icon returned
513     // from QIcon::fromTheme was a fallback icon, causing hasThemeIcon to
514     // incorrectly return true.
515     //
516     // In qTox this leads to the tray and window icons using the static qTox logo
517     // icon instead of an icon based on the current presence status.
518     //
519     // This workaround checks for an icon that definitely does not exist to
520     // determine if hasThemeIcon can be trusted.
521     //
522     // On systems with the Qt bug, this workaround will always use our included
523     // icons but user themes will be unable to override them.
524     static bool checkedHasThemeIcon = false;
525     static bool hasThemeIconBug = false;
526 
527     if (!checkedHasThemeIcon) {
528         hasThemeIconBug = QIcon::hasThemeIcon("qtox-asjkdfhawjkeghdfjgh");
529         checkedHasThemeIcon = true;
530 
531         if (hasThemeIconBug) {
532             qDebug()
533                 << "Detected buggy QIcon::hasThemeIcon. Icon overrides from theme will be ignored.";
534         }
535     }
536 
537     QIcon ico;
538     if (!hasThemeIconBug && QIcon::hasThemeIcon("qtox-" + assetSuffix)) {
539         ico = QIcon::fromTheme("qtox-" + assetSuffix);
540     } else {
541         QString color = settings.getLightTrayIcon() ? "light" : "dark";
542         QString path = ":/img/taskbar/" + color + "/taskbar_" + assetSuffix + ".svg";
543         QSvgRenderer renderer(path);
544 
545         // Prepare a QImage with desired characteritisc
546         QImage image = QImage(250, 250, QImage::Format_ARGB32);
547         image.fill(Qt::transparent);
548         QPainter painter(&image);
549         renderer.render(&painter);
550         ico = QIcon(QPixmap::fromImage(image));
551     }
552 
553     setWindowIcon(ico);
554     if (icon) {
555         icon->setIcon(ico);
556     }
557 }
558 
~Widget()559 Widget::~Widget()
560 {
561     QWidgetList windowList = QApplication::topLevelWidgets();
562 
563     for (QWidget* window : windowList) {
564         if (window != this) {
565             window->close();
566         }
567     }
568 
569     Translator::unregister(this);
570     if (icon) {
571         icon->hide();
572     }
573 
574     for (Group* g : GroupList::getAllGroups()) {
575         removeGroup(g, true);
576     }
577 
578     for (Friend* f : FriendList::getAllFriends()) {
579         removeFriend(f, true);
580     }
581 
582     for (auto form : chatForms) {
583         delete form;
584     }
585 
586     delete profileForm;
587     delete profileInfo;
588     delete addFriendForm;
589     delete groupInviteForm;
590     delete filesForm;
591     delete timer;
592     delete contentLayout;
593     delete settingsWidget;
594 
595     FriendList::clear();
596     GroupList::clear();
597     delete trayMenu;
598     delete ui;
599     instance = nullptr;
600 }
601 
602 /**
603  * @brief Switches to the About settings page.
604  */
showUpdateDownloadProgress()605 void Widget::showUpdateDownloadProgress()
606 {
607     onShowSettings();
608     settingsWidget->showAbout();
609 }
610 
moveEvent(QMoveEvent * event)611 void Widget::moveEvent(QMoveEvent* event)
612 {
613     if (event->type() == QEvent::Move) {
614         saveWindowGeometry();
615         saveSplitterGeometry();
616     }
617 
618     QWidget::moveEvent(event);
619 }
620 
closeEvent(QCloseEvent * event)621 void Widget::closeEvent(QCloseEvent* event)
622 {
623     if (settings.getShowSystemTray() && settings.getCloseToTray()) {
624         QWidget::closeEvent(event);
625     } else {
626         if (autoAwayActive) {
627             emit statusSet(Status::Status::Online);
628             autoAwayActive = false;
629         }
630         saveWindowGeometry();
631         saveSplitterGeometry();
632         QWidget::closeEvent(event);
633         qApp->quit();
634     }
635 }
636 
changeEvent(QEvent * event)637 void Widget::changeEvent(QEvent* event)
638 {
639     if (event->type() == QEvent::WindowStateChange) {
640         if (isMinimized() && settings.getShowSystemTray() && settings.getMinimizeToTray()) {
641             this->hide();
642         }
643     }
644 }
645 
resizeEvent(QResizeEvent * event)646 void Widget::resizeEvent(QResizeEvent* event)
647 {
648     saveWindowGeometry();
649     QMainWindow::resizeEvent(event);
650 }
651 
getUsername()652 QString Widget::getUsername()
653 {
654     return core->getUsername();
655 }
656 
onSelfAvatarLoaded(const QPixmap & pic)657 void Widget::onSelfAvatarLoaded(const QPixmap& pic)
658 {
659     profilePicture->setPixmap(pic);
660 }
661 
onCoreChanged(Core & core)662 void Widget::onCoreChanged(Core& core)
663 {
664 
665     connect(&core, &Core::connected, this, &Widget::onConnected);
666     connect(&core, &Core::disconnected, this, &Widget::onDisconnected);
667     connect(&core, &Core::statusSet, this, &Widget::onStatusSet);
668     connect(&core, &Core::usernameSet, this, &Widget::setUsername);
669     connect(&core, &Core::statusMessageSet, this, &Widget::setStatusMessage);
670     connect(&core, &Core::friendAdded, this, &Widget::addFriend);
671     connect(&core, &Core::failedToAddFriend, this, &Widget::addFriendFailed);
672     connect(&core, &Core::friendUsernameChanged, this, &Widget::onFriendUsernameChanged);
673     connect(&core, &Core::friendStatusChanged, this, &Widget::onFriendStatusChanged);
674     connect(&core, &Core::friendStatusMessageChanged, this, &Widget::onFriendStatusMessageChanged);
675     connect(&core, &Core::friendRequestReceived, this, &Widget::onFriendRequestReceived);
676     connect(&core, &Core::friendMessageReceived, this, &Widget::onFriendMessageReceived);
677     connect(&core, &Core::receiptRecieved, this, &Widget::onReceiptReceived);
678     connect(&core, &Core::groupInviteReceived, this, &Widget::onGroupInviteReceived);
679     connect(&core, &Core::groupMessageReceived, this, &Widget::onGroupMessageReceived);
680     connect(&core, &Core::groupPeerlistChanged, this, &Widget::onGroupPeerlistChanged);
681     connect(&core, &Core::groupPeerNameChanged, this, &Widget::onGroupPeerNameChanged);
682     connect(&core, &Core::groupTitleChanged, this, &Widget::onGroupTitleChanged);
683     connect(&core, &Core::groupPeerAudioPlaying, this, &Widget::onGroupPeerAudioPlaying);
684     connect(&core, &Core::emptyGroupCreated, this, &Widget::onEmptyGroupCreated);
685     connect(&core, &Core::groupJoined, this, &Widget::onGroupJoined);
686     connect(&core, &Core::friendTypingChanged, this, &Widget::onFriendTypingChanged);
687     connect(&core, &Core::groupSentFailed, this, &Widget::onGroupSendFailed);
688     connect(&core, &Core::usernameSet, this, &Widget::refreshPeerListsLocal);
689     connect(this, &Widget::statusSet, &core, &Core::setStatus);
690     connect(this, &Widget::friendRequested, &core, &Core::requestFriendship);
691     connect(this, &Widget::friendRequestAccepted, &core, &Core::acceptFriendRequest);
692     connect(this, &Widget::changeGroupTitle, &core, &Core::changeGroupTitle);
693 
694     sharedMessageProcessorParams.setPublicKey(core.getSelfPublicKey().toString());
695 }
696 
onConnected()697 void Widget::onConnected()
698 {
699     ui->statusButton->setEnabled(true);
700     emit core->statusSet(core->getStatus());
701 }
702 
onDisconnected()703 void Widget::onDisconnected()
704 {
705     ui->statusButton->setEnabled(false);
706     emit core->statusSet(Status::Status::Offline);
707 }
708 
onFailedToStartCore()709 void Widget::onFailedToStartCore()
710 {
711     QMessageBox critical(this);
712     critical.setText(tr(
713         "toxcore failed to start, the application will terminate after you close this message."));
714     critical.setIcon(QMessageBox::Critical);
715     critical.exec();
716     qApp->exit(EXIT_FAILURE);
717 }
718 
onBadProxyCore()719 void Widget::onBadProxyCore()
720 {
721     settings.setProxyType(Settings::ProxyType::ptNone);
722     QMessageBox critical(this);
723     critical.setText(tr("toxcore failed to start with your proxy settings. "
724                         "qTox cannot run; please modify your "
725                         "settings and restart.",
726                         "popup text"));
727     critical.setIcon(QMessageBox::Critical);
728     critical.exec();
729     onShowSettings();
730 }
731 
onStatusSet(Status::Status status)732 void Widget::onStatusSet(Status::Status status)
733 {
734     ui->statusButton->setProperty("status", static_cast<int>(status));
735     ui->statusButton->setIcon(prepareIcon(getIconPath(status), icon_size, icon_size));
736     updateIcons();
737 }
738 
onSeparateWindowClicked(bool separate)739 void Widget::onSeparateWindowClicked(bool separate)
740 {
741     onSeparateWindowChanged(separate, true);
742 }
743 
onSeparateWindowChanged(bool separate,bool clicked)744 void Widget::onSeparateWindowChanged(bool separate, bool clicked)
745 {
746     if (!separate) {
747         QWindowList windowList = QGuiApplication::topLevelWindows();
748 
749         for (QWindow* window : windowList) {
750             if (window->objectName() == "detachedWindow") {
751                 window->close();
752             }
753         }
754 
755         QWidget* contentWidget = new QWidget(this);
756         contentWidget->setObjectName("contentWidget");
757 
758         contentLayout = new ContentLayout(contentWidget);
759         ui->mainSplitter->addWidget(contentWidget);
760 
761         setMinimumWidth(775);
762 
763         SplitterRestorer restorer(ui->mainSplitter);
764         restorer.restore(settings.getSplitterState(), size());
765 
766         onShowSettings();
767     } else {
768         int width = ui->friendList->size().width();
769         QSize size;
770         QPoint pos;
771 
772         if (contentLayout) {
773             pos = mapToGlobal(ui->mainSplitter->widget(1)->pos());
774             size = ui->mainSplitter->widget(1)->size();
775         }
776 
777         if (contentLayout) {
778             contentLayout->clear();
779             contentLayout->parentWidget()->setParent(nullptr); // Remove from splitter.
780             contentLayout->parentWidget()->hide();
781             contentLayout->parentWidget()->deleteLater();
782             contentLayout->deleteLater();
783             contentLayout = nullptr;
784         }
785 
786         setMinimumWidth(ui->tooliconsZone->sizeHint().width());
787 
788         if (clicked) {
789             showNormal();
790             resize(width, height());
791 
792             if (settingsWidget) {
793                 ContentLayout* contentLayout = createContentDialog((DialogType::SettingDialog));
794                 contentLayout->parentWidget()->resize(size);
795                 contentLayout->parentWidget()->move(pos);
796                 settingsWidget->show(contentLayout);
797                 setActiveToolMenuButton(ActiveToolMenuButton::None);
798             }
799         }
800 
801         setWindowTitle(QString());
802         setActiveToolMenuButton(ActiveToolMenuButton::None);
803     }
804 }
805 
setWindowTitle(const QString & title)806 void Widget::setWindowTitle(const QString& title)
807 {
808     if (title.isEmpty()) {
809         QMainWindow::setWindowTitle(QApplication::applicationName());
810     } else {
811         QString tmp = title;
812         /// <[^>]*> Regexp to remove HTML tags, in case someone used them in title
813         QMainWindow::setWindowTitle(QApplication::applicationName() + QStringLiteral(" - ")
814                                     + tmp.remove(QRegExp("<[^>]*>")));
815     }
816 }
817 
forceShow()818 void Widget::forceShow()
819 {
820     hide(); // Workaround to force minimized window to be restored
821     show();
822     activateWindow();
823 }
824 
onAddClicked()825 void Widget::onAddClicked()
826 {
827     if (settings.getSeparateWindow()) {
828         if (!addFriendForm->isShown()) {
829             addFriendForm->show(createContentDialog(DialogType::AddDialog));
830         }
831 
832         setActiveToolMenuButton(ActiveToolMenuButton::None);
833     } else {
834         hideMainForms(nullptr);
835         addFriendForm->show(contentLayout);
836         setWindowTitle(fromDialogType(DialogType::AddDialog));
837         setActiveToolMenuButton(ActiveToolMenuButton::AddButton);
838     }
839 }
840 
onGroupClicked()841 void Widget::onGroupClicked()
842 {
843     if (settings.getSeparateWindow()) {
844         if (!groupInviteForm->isShown()) {
845             groupInviteForm->show(createContentDialog(DialogType::GroupDialog));
846         }
847 
848         setActiveToolMenuButton(ActiveToolMenuButton::None);
849     } else {
850         hideMainForms(nullptr);
851         groupInviteForm->show(contentLayout);
852         setWindowTitle(fromDialogType(DialogType::GroupDialog));
853         setActiveToolMenuButton(ActiveToolMenuButton::GroupButton);
854     }
855 }
856 
onTransferClicked()857 void Widget::onTransferClicked()
858 {
859     if (settings.getSeparateWindow()) {
860         if (!filesForm->isShown()) {
861             filesForm->show(createContentDialog(DialogType::TransferDialog));
862         }
863 
864         setActiveToolMenuButton(ActiveToolMenuButton::None);
865     } else {
866         hideMainForms(nullptr);
867         filesForm->show(contentLayout);
868         setWindowTitle(fromDialogType(DialogType::TransferDialog));
869         setActiveToolMenuButton(ActiveToolMenuButton::TransferButton);
870     }
871 }
872 
confirmExecutableOpen(const QFileInfo & file)873 void Widget::confirmExecutableOpen(const QFileInfo& file)
874 {
875     static const QStringList dangerousExtensions = {"app",  "bat",     "com",    "cpl",  "dmg",
876                                                     "exe",  "hta",     "jar",    "js",   "jse",
877                                                     "lnk",  "msc",     "msh",    "msh1", "msh1xml",
878                                                     "msh2", "msh2xml", "mshxml", "msi",  "msp",
879                                                     "pif",  "ps1",     "ps1xml", "ps2",  "ps2xml",
880                                                     "psc1", "psc2",    "py",     "reg",  "scf",
881                                                     "sh",   "src",     "vb",     "vbe",  "vbs",
882                                                     "ws",   "wsc",     "wsf",    "wsh"};
883 
884     if (dangerousExtensions.contains(file.suffix())) {
885         bool answer = GUI::askQuestion(tr("Executable file", "popup title"),
886                                        tr("You have asked qTox to open an executable file. "
887                                           "Executable files can potentially damage your computer. "
888                                           "Are you sure want to open this file?",
889                                           "popup text"),
890                                        false, true);
891         if (!answer) {
892             return;
893         }
894 
895         // The user wants to run this file, so make it executable and run it
896         QFile(file.filePath())
897             .setPermissions(file.permissions() | QFile::ExeOwner | QFile::ExeUser | QFile::ExeGroup
898                             | QFile::ExeOther);
899     }
900 
901     QDesktopServices::openUrl(QUrl::fromLocalFile(file.filePath()));
902 }
903 
onIconClick(QSystemTrayIcon::ActivationReason reason)904 void Widget::onIconClick(QSystemTrayIcon::ActivationReason reason)
905 {
906     if (reason == QSystemTrayIcon::Trigger) {
907         if (isHidden() || isMinimized()) {
908             if (wasMaximized) {
909                 showMaximized();
910             } else {
911                 showNormal();
912             }
913 
914             activateWindow();
915         } else if (!isActiveWindow()) {
916             activateWindow();
917         } else {
918             wasMaximized = isMaximized();
919             hide();
920         }
921     } else if (reason == QSystemTrayIcon::Unknown) {
922         if (isHidden()) {
923             forceShow();
924         }
925     }
926 }
927 
onShowSettings()928 void Widget::onShowSettings()
929 {
930     if (settings.getSeparateWindow()) {
931         if (!settingsWidget->isShown()) {
932             settingsWidget->show(createContentDialog(DialogType::SettingDialog));
933         }
934 
935         setActiveToolMenuButton(ActiveToolMenuButton::None);
936     } else {
937         hideMainForms(nullptr);
938         settingsWidget->show(contentLayout);
939         setWindowTitle(fromDialogType(DialogType::SettingDialog));
940         setActiveToolMenuButton(ActiveToolMenuButton::SettingButton);
941     }
942 }
943 
showProfile()944 void Widget::showProfile() // onAvatarClicked, onUsernameClicked
945 {
946     if (settings.getSeparateWindow()) {
947         if (!profileForm->isShown()) {
948             profileForm->show(createContentDialog(DialogType::ProfileDialog));
949         }
950 
951         setActiveToolMenuButton(ActiveToolMenuButton::None);
952     } else {
953         hideMainForms(nullptr);
954         profileForm->show(contentLayout);
955         setWindowTitle(fromDialogType(DialogType::ProfileDialog));
956         setActiveToolMenuButton(ActiveToolMenuButton::None);
957     }
958 }
959 
hideMainForms(GenericChatroomWidget * chatroomWidget)960 void Widget::hideMainForms(GenericChatroomWidget* chatroomWidget)
961 {
962     setActiveToolMenuButton(ActiveToolMenuButton::None);
963 
964     if (contentLayout != nullptr) {
965         contentLayout->clear();
966     }
967 
968     if (activeChatroomWidget != nullptr) {
969         activeChatroomWidget->setAsInactiveChatroom();
970     }
971 
972     activeChatroomWidget = chatroomWidget;
973 }
974 
setUsername(const QString & username)975 void Widget::setUsername(const QString& username)
976 {
977     if (username.isEmpty()) {
978         ui->nameLabel->setText(tr("Your name"));
979         ui->nameLabel->setToolTip(tr("Your name"));
980     } else {
981         ui->nameLabel->setText(username);
982         ui->nameLabel->setToolTip(
983             Qt::convertFromPlainText(username, Qt::WhiteSpaceNormal)); // for overlength names
984     }
985 
986     sharedMessageProcessorParams.onUserNameSet(username);
987 }
988 
onStatusMessageChanged(const QString & newStatusMessage)989 void Widget::onStatusMessageChanged(const QString& newStatusMessage)
990 {
991     // Keep old status message until Core tells us to set it.
992     core->setStatusMessage(newStatusMessage);
993 }
994 
setStatusMessage(const QString & statusMessage)995 void Widget::setStatusMessage(const QString& statusMessage)
996 {
997     ui->statusLabel->setText(statusMessage);
998     // escape HTML from tooltips and preserve newlines
999     // TODO: move newspace preservance to a generic function
1000     ui->statusLabel->setToolTip("<p style='white-space:pre'>" + statusMessage.toHtmlEscaped() + "</p>");
1001 }
1002 
1003 /**
1004  * @brief Plays a sound via the audioNotification AudioSink
1005  * @param sound Sound to play
1006  * @param loop if true, loop the sound until onStopNotification() is called
1007  */
playNotificationSound(IAudioSink::Sound sound,bool loop)1008 void Widget::playNotificationSound(IAudioSink::Sound sound, bool loop)
1009 {
1010     if (!settings.getAudioOutDevEnabled()) {
1011         // don't try to play sounds if audio is disabled
1012         return;
1013     }
1014 
1015     if (audioNotification == nullptr) {
1016         audioNotification = std::unique_ptr<IAudioSink>(audio.makeSink());
1017         if (audioNotification == nullptr) {
1018             qDebug() << "Failed to allocate AudioSink";
1019             return;
1020         }
1021     }
1022 
1023     audioNotification->connectTo_finishedPlaying(this, [this](){ cleanupNotificationSound(); });
1024 
1025     audioNotification->playMono16Sound(sound);
1026 
1027     if (loop) {
1028         audioNotification->startLoop();
1029     }
1030 }
1031 
cleanupNotificationSound()1032 void Widget::cleanupNotificationSound()
1033 {
1034     audioNotification.reset();
1035 }
1036 
incomingNotification(uint32_t friendnumber)1037 void Widget::incomingNotification(uint32_t friendnumber)
1038 {
1039     const auto& friendId = FriendList::id2Key(friendnumber);
1040     newFriendMessageAlert(friendId, {}, false);
1041 
1042     // loop until call answered or rejected
1043     playNotificationSound(IAudioSink::Sound::IncomingCall, true);
1044 }
1045 
outgoingNotification()1046 void Widget::outgoingNotification()
1047 {
1048     // loop until call answered or rejected
1049     playNotificationSound(IAudioSink::Sound::OutgoingCall, true);
1050 }
1051 
onCallEnd()1052 void Widget::onCallEnd()
1053 {
1054     playNotificationSound(IAudioSink::Sound::CallEnd);
1055 }
1056 
1057 /**
1058  * @brief Widget::onStopNotification Stop the notification sound.
1059  */
onStopNotification()1060 void Widget::onStopNotification()
1061 {
1062     audioNotification.reset();
1063 }
1064 
1065 /**
1066  * @brief Dispatches file to the appropriate chatlog and accepts the transfer if necessary
1067  */
dispatchFile(ToxFile file)1068 void Widget::dispatchFile(ToxFile file)
1069 {
1070     const auto& friendId = FriendList::id2Key(file.friendId);
1071     Friend* f = FriendList::findFriend(friendId);
1072     if (!f) {
1073         return;
1074     }
1075 
1076     auto pk = f->getPublicKey();
1077 
1078     if (file.status == ToxFile::INITIALIZING && file.direction == ToxFile::RECEIVING) {
1079         auto sender =
1080             (file.direction == ToxFile::SENDING) ? Core::getInstance()->getSelfPublicKey() : pk;
1081 
1082         const Settings& settings = Settings::getInstance();
1083         QString autoAcceptDir = settings.getAutoAcceptDir(f->getPublicKey());
1084 
1085         if (autoAcceptDir.isEmpty() && settings.getAutoSaveEnabled()) {
1086             autoAcceptDir = settings.getGlobalAutoAcceptDir();
1087         }
1088 
1089         auto maxAutoAcceptSize = settings.getMaxAutoAcceptSize();
1090         bool autoAcceptSizeCheckPassed = maxAutoAcceptSize == 0 || maxAutoAcceptSize >= file.filesize;
1091 
1092         if (!autoAcceptDir.isEmpty() && autoAcceptSizeCheckPassed) {
1093             acceptFileTransfer(file, autoAcceptDir);
1094         }
1095     }
1096 
1097     const auto senderPk = (file.direction == ToxFile::SENDING) ? core->getSelfPublicKey() : pk;
1098     friendChatLogs[pk]->onFileUpdated(senderPk, file);
1099 }
1100 
dispatchFileWithBool(ToxFile file,bool)1101 void Widget::dispatchFileWithBool(ToxFile file, bool)
1102 {
1103     dispatchFile(file);
1104 }
1105 
dispatchFileSendFailed(uint32_t friendId,const QString & fileName)1106 void Widget::dispatchFileSendFailed(uint32_t friendId, const QString& fileName)
1107 {
1108     const auto& friendPk = FriendList::id2Key(friendId);
1109 
1110     auto chatForm = chatForms.find(friendPk);
1111     if (chatForm == chatForms.end()) {
1112         return;
1113     }
1114 
1115     chatForm.value()->addSystemInfoMessage(tr("Failed to send file \"%1\"").arg(fileName),
1116                                            ChatMessage::ERROR, QDateTime::currentDateTime());
1117 }
1118 
onRejectCall(uint32_t friendId)1119 void Widget::onRejectCall(uint32_t friendId)
1120 {
1121     CoreAV* const av = core->getAv();
1122     av->cancelCall(friendId);
1123 }
1124 
addFriend(uint32_t friendId,const ToxPk & friendPk)1125 void Widget::addFriend(uint32_t friendId, const ToxPk& friendPk)
1126 {
1127     settings.updateFriendAddress(friendPk.toString());
1128 
1129     Friend* newfriend = FriendList::addFriend(friendId, friendPk);
1130     auto dialogManager = ContentDialogManager::getInstance();
1131     auto rawChatroom = new FriendChatroom(newfriend, dialogManager);
1132     std::shared_ptr<FriendChatroom> chatroom(rawChatroom);
1133     const auto compact = settings.getCompactLayout();
1134     auto widget = new FriendWidget(chatroom, compact);
1135     connectFriendWidget(*widget);
1136     auto history = Nexus::getProfile()->getHistory();
1137 
1138     auto messageProcessor = MessageProcessor(sharedMessageProcessorParams);
1139     auto friendMessageDispatcher =
1140         std::make_shared<FriendMessageDispatcher>(*newfriend, std::move(messageProcessor), *core);
1141 
1142     // Note: We do not have to connect the message dispatcher signals since
1143     // ChatHistory hooks them up in a very specific order
1144     auto chatHistory =
1145         std::make_shared<ChatHistory>(*newfriend, history, *core, Settings::getInstance(),
1146                                       *friendMessageDispatcher);
1147     auto friendForm = new ChatForm(newfriend, *chatHistory, *friendMessageDispatcher);
1148     connect(friendForm, &ChatForm::updateFriendActivity, this, &Widget::updateFriendActivity);
1149 
1150     friendMessageDispatchers[friendPk] = friendMessageDispatcher;
1151     friendChatLogs[friendPk] = chatHistory;
1152     friendChatrooms[friendPk] = chatroom;
1153     friendWidgets[friendPk] = widget;
1154     chatForms[friendPk] = friendForm;
1155 
1156     const auto activityTime = settings.getFriendActivity(friendPk);
1157     const auto chatTime = friendForm->getLatestTime();
1158     if (chatTime > activityTime && chatTime.isValid()) {
1159         settings.setFriendActivity(friendPk, chatTime);
1160     }
1161 
1162     contactListWidget->addFriendWidget(widget, Status::Status::Offline,
1163                                        settings.getFriendCircleID(friendPk));
1164 
1165 
1166     auto notifyReceivedCallback = [this, friendPk](const ToxPk& author, const Message& message) {
1167         auto isTargeted = std::any_of(message.metadata.begin(), message.metadata.end(),
1168                                       [](MessageMetadata metadata) {
1169                                           return metadata.type == MessageMetadataType::selfMention;
1170                                       });
1171         newFriendMessageAlert(friendPk, message.content);
1172     };
1173 
1174     auto notifyReceivedConnection =
1175         connect(friendMessageDispatcher.get(), &IMessageDispatcher::messageReceived,
1176                 notifyReceivedCallback);
1177 
1178     friendAlertConnections.insert(friendPk, notifyReceivedConnection);
1179     connect(newfriend, &Friend::aliasChanged, this, &Widget::onFriendAliasChanged);
1180     connect(newfriend, &Friend::displayedNameChanged, this, &Widget::onFriendDisplayedNameChanged);
1181 
1182     connect(friendForm, &ChatForm::incomingNotification, this, &Widget::incomingNotification);
1183     connect(friendForm, &ChatForm::outgoingNotification, this, &Widget::outgoingNotification);
1184     connect(friendForm, &ChatForm::stopNotification, this, &Widget::onStopNotification);
1185     connect(friendForm, &ChatForm::endCallNotification, this, &Widget::onCallEnd);
1186     connect(friendForm, &ChatForm::rejectCall, this, &Widget::onRejectCall);
1187 
1188     connect(widget, &FriendWidget::newWindowOpened, this, &Widget::openNewDialog);
1189     connect(widget, &FriendWidget::chatroomWidgetClicked, this, &Widget::onChatroomWidgetClicked);
1190     connect(widget, &FriendWidget::chatroomWidgetClicked, friendForm, &ChatForm::focusInput);
1191     connect(widget, &FriendWidget::friendHistoryRemoved, friendForm, &ChatForm::clearChatArea);
1192     connect(widget, &FriendWidget::copyFriendIdToClipboard, this, &Widget::copyFriendIdToClipboard);
1193     connect(widget, &FriendWidget::contextMenuCalled, widget, &FriendWidget::onContextMenuCalled);
1194     connect(widget, SIGNAL(removeFriend(const ToxPk&)), this, SLOT(removeFriend(const ToxPk&)));
1195 
1196     Profile* profile = Nexus::getProfile();
1197     connect(profile, &Profile::friendAvatarSet, widget, &FriendWidget::onAvatarSet);
1198     connect(profile, &Profile::friendAvatarRemoved, widget, &FriendWidget::onAvatarRemoved);
1199 
1200     // Try to get the avatar from the cache
1201     QPixmap avatar = Nexus::getProfile()->loadAvatar(friendPk);
1202     if (!avatar.isNull()) {
1203         friendForm->onAvatarChanged(friendPk, avatar);
1204         widget->onAvatarSet(friendPk, avatar);
1205     }
1206 
1207     FilterCriteria filter = getFilterCriteria();
1208     widget->search(ui->searchContactText->text(), filterOffline(filter));
1209 }
1210 
addFriendFailed(const ToxPk &,const QString & errorInfo)1211 void Widget::addFriendFailed(const ToxPk&, const QString& errorInfo)
1212 {
1213     QString info = QString(tr("Couldn't request friendship"));
1214     if (!errorInfo.isEmpty()) {
1215         info = info + QStringLiteral(": ") + errorInfo;
1216     }
1217 
1218     QMessageBox::critical(nullptr, "Error", info);
1219 }
1220 
onFriendStatusChanged(int friendId,Status::Status status)1221 void Widget::onFriendStatusChanged(int friendId, Status::Status status)
1222 {
1223     const auto& friendPk = FriendList::id2Key(friendId);
1224     Friend* f = FriendList::findFriend(friendPk);
1225     if (!f) {
1226         return;
1227     }
1228 
1229     bool isActualChange = f->getStatus() != status;
1230 
1231     FriendWidget* widget = friendWidgets[f->getPublicKey()];
1232     if (isActualChange) {
1233         if (!Status::isOnline(f->getStatus())) {
1234             contactListWidget->moveWidget(widget, Status::Status::Online);
1235         } else if (status == Status::Status::Offline) {
1236             contactListWidget->moveWidget(widget, Status::Status::Offline);
1237         }
1238     }
1239 
1240     f->setStatus(status);
1241     widget->updateStatusLight();
1242     if (widget->isActive()) {
1243         setWindowTitle(widget->getTitle());
1244     }
1245 
1246     ContentDialogManager::getInstance()->updateFriendStatus(friendPk);
1247 }
1248 
onFriendStatusMessageChanged(int friendId,const QString & message)1249 void Widget::onFriendStatusMessageChanged(int friendId, const QString& message)
1250 {
1251     const auto& friendPk = FriendList::id2Key(friendId);
1252     Friend* f = FriendList::findFriend(friendPk);
1253     if (!f) {
1254         return;
1255     }
1256 
1257     QString str = message;
1258     str.replace('\n', ' ').remove('\r').remove(QChar('\0'));
1259     f->setStatusMessage(str);
1260 
1261     friendWidgets[friendPk]->setStatusMsg(str);
1262     chatForms[friendPk]->setStatusMessage(str);
1263 }
1264 
onFriendDisplayedNameChanged(const QString & displayed)1265 void Widget::onFriendDisplayedNameChanged(const QString& displayed)
1266 {
1267     Friend* f = qobject_cast<Friend*>(sender());
1268     const auto& friendPk = f->getPublicKey();
1269     for (Group* g : GroupList::getAllGroups()) {
1270         if (g->getPeerList().contains(friendPk)) {
1271             g->updateUsername(friendPk, displayed);
1272         }
1273     }
1274 
1275     FriendWidget* friendWidget = friendWidgets[f->getPublicKey()];
1276     if (friendWidget->isActive()) {
1277         GUI::setWindowTitle(displayed);
1278     }
1279 }
1280 
onFriendUsernameChanged(int friendId,const QString & username)1281 void Widget::onFriendUsernameChanged(int friendId, const QString& username)
1282 {
1283     const auto& friendPk = FriendList::id2Key(friendId);
1284     Friend* f = FriendList::findFriend(friendPk);
1285     if (!f) {
1286         return;
1287     }
1288 
1289     QString str = username;
1290     str.replace('\n', ' ').remove('\r').remove(QChar('\0'));
1291     f->setName(str);
1292 }
1293 
onFriendAliasChanged(const ToxPk & friendId,const QString & alias)1294 void Widget::onFriendAliasChanged(const ToxPk& friendId, const QString& alias)
1295 {
1296     Friend* f = qobject_cast<Friend*>(sender());
1297 
1298     // TODO(sudden6): don't update the contact list here, make it update itself
1299     FriendWidget* friendWidget = friendWidgets[friendId];
1300     Status::Status status = f->getStatus();
1301     contactListWidget->moveWidget(friendWidget, status);
1302     FilterCriteria criteria = getFilterCriteria();
1303     bool filter = status == Status::Status::Offline ? filterOffline(criteria) : filterOnline(criteria);
1304     friendWidget->searchName(ui->searchContactText->text(), filter);
1305 
1306     settings.setFriendAlias(friendId, alias);
1307     settings.savePersonal();
1308 }
1309 
onChatroomWidgetClicked(GenericChatroomWidget * widget)1310 void Widget::onChatroomWidgetClicked(GenericChatroomWidget* widget)
1311 {
1312     openDialog(widget, /* newWindow = */ false);
1313 }
1314 
openNewDialog(GenericChatroomWidget * widget)1315 void Widget::openNewDialog(GenericChatroomWidget* widget)
1316 {
1317     openDialog(widget, /* newWindow = */ true);
1318 }
1319 
openDialog(GenericChatroomWidget * widget,bool newWindow)1320 void Widget::openDialog(GenericChatroomWidget* widget, bool newWindow)
1321 {
1322     widget->resetEventFlags();
1323     widget->updateStatusLight();
1324 
1325     GenericChatForm* form;
1326     GroupId id;
1327     const Friend* frnd = widget->getFriend();
1328     const Group* group = widget->getGroup();
1329     if (frnd) {
1330         form = chatForms[frnd->getPublicKey()];
1331     } else {
1332         id = group->getPersistentId();
1333         form = groupChatForms[id].data();
1334     }
1335     bool chatFormIsSet;
1336     ContentDialogManager::getInstance()->focusContact(id);
1337     chatFormIsSet = ContentDialogManager::getInstance()->contactWidgetExists(id);
1338 
1339 
1340     if ((chatFormIsSet || form->isVisible()) && !newWindow) {
1341         return;
1342     }
1343 
1344     if (settings.getSeparateWindow() || newWindow) {
1345         ContentDialog* dialog = nullptr;
1346 
1347         if (!settings.getDontGroupWindows() && !newWindow) {
1348             dialog = ContentDialogManager::getInstance()->current();
1349         }
1350 
1351         if (dialog == nullptr) {
1352             dialog = createContentDialog();
1353         }
1354 
1355         dialog->show();
1356 
1357         if (frnd) {
1358             addFriendDialog(frnd, dialog);
1359         } else {
1360             Group* group = widget->getGroup();
1361             addGroupDialog(group, dialog);
1362         }
1363 
1364         dialog->raise();
1365         dialog->activateWindow();
1366     } else {
1367         hideMainForms(widget);
1368         if (frnd) {
1369             chatForms[frnd->getPublicKey()]->show(contentLayout);
1370         } else {
1371             groupChatForms[group->getPersistentId()]->show(contentLayout);
1372         }
1373         widget->setAsActiveChatroom();
1374         setWindowTitle(widget->getTitle());
1375     }
1376 }
1377 
onFriendMessageReceived(uint32_t friendnumber,const QString & message,bool isAction)1378 void Widget::onFriendMessageReceived(uint32_t friendnumber, const QString& message, bool isAction)
1379 {
1380     const auto& friendId = FriendList::id2Key(friendnumber);
1381     Friend* f = FriendList::findFriend(friendId);
1382     if (!f) {
1383         return;
1384     }
1385 
1386     friendMessageDispatchers[f->getPublicKey()]->onMessageReceived(isAction, message);
1387 }
1388 
onReceiptReceived(int friendId,ReceiptNum receipt)1389 void Widget::onReceiptReceived(int friendId, ReceiptNum receipt)
1390 {
1391     const auto& friendKey = FriendList::id2Key(friendId);
1392     Friend* f = FriendList::findFriend(friendKey);
1393     if (!f) {
1394         return;
1395     }
1396 
1397     friendMessageDispatchers[f->getPublicKey()]->onReceiptReceived(receipt);
1398 }
1399 
addFriendDialog(const Friend * frnd,ContentDialog * dialog)1400 void Widget::addFriendDialog(const Friend* frnd, ContentDialog* dialog)
1401 {
1402     uint32_t friendId = frnd->getId();
1403     const ToxPk& friendPk = frnd->getPublicKey();
1404     ContentDialog* contentDialog = ContentDialogManager::getInstance()->getFriendDialog(friendPk);
1405     bool isSeparate = settings.getSeparateWindow();
1406     FriendWidget* widget = friendWidgets[friendPk];
1407     bool isCurrent = activeChatroomWidget == widget;
1408     if (!contentDialog && !isSeparate && isCurrent) {
1409         onAddClicked();
1410     }
1411 
1412     auto form = chatForms[friendPk];
1413     auto chatroom = friendChatrooms[friendPk];
1414     FriendWidget* friendWidget =
1415         ContentDialogManager::getInstance()->addFriendToDialog(dialog, chatroom, form);
1416 
1417     friendWidget->setStatusMsg(widget->getStatusMsg());
1418 
1419 #if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0))
1420     auto widgetRemoveFriend = QOverload<const ToxPk&>::of(&Widget::removeFriend);
1421 #else
1422     auto widgetRemoveFriend = static_cast<void (Widget::*)(const ToxPk&)>(&Widget::removeFriend);
1423 #endif
1424     connect(friendWidget, &FriendWidget::removeFriend, this, widgetRemoveFriend);
1425     connect(friendWidget, &FriendWidget::middleMouseClicked, dialog,
1426             [=]() { dialog->removeFriend(friendPk); });
1427     connect(friendWidget, &FriendWidget::copyFriendIdToClipboard, this,
1428             &Widget::copyFriendIdToClipboard);
1429     connect(friendWidget, &FriendWidget::newWindowOpened, this, &Widget::openNewDialog);
1430 
1431     // Signal transmission from the created `friendWidget` (which shown in
1432     // ContentDialog) to the `widget` (which shown in main widget)
1433     // FIXME: emit should be removed
1434     connect(friendWidget, &FriendWidget::contextMenuCalled, widget,
1435             [=](QContextMenuEvent* event) { emit widget->contextMenuCalled(event); });
1436 
1437     connect(friendWidget, &FriendWidget::chatroomWidgetClicked, [=](GenericChatroomWidget* w) {
1438         Q_UNUSED(w);
1439         emit widget->chatroomWidgetClicked(widget);
1440     });
1441     connect(friendWidget, &FriendWidget::newWindowOpened, [=](GenericChatroomWidget* w) {
1442         Q_UNUSED(w);
1443         emit widget->newWindowOpened(widget);
1444     });
1445     // FIXME: emit should be removed
1446     emit widget->chatroomWidgetClicked(widget);
1447 
1448     Profile* profile = Nexus::getProfile();
1449     connect(profile, &Profile::friendAvatarSet, friendWidget, &FriendWidget::onAvatarSet);
1450     connect(profile, &Profile::friendAvatarRemoved, friendWidget, &FriendWidget::onAvatarRemoved);
1451 
1452     QPixmap avatar = Nexus::getProfile()->loadAvatar(frnd->getPublicKey());
1453     if (!avatar.isNull()) {
1454         friendWidget->onAvatarSet(frnd->getPublicKey(), avatar);
1455     }
1456 }
1457 
addGroupDialog(Group * group,ContentDialog * dialog)1458 void Widget::addGroupDialog(Group* group, ContentDialog* dialog)
1459 {
1460     const GroupId& groupId = group->getPersistentId();
1461     ContentDialog* groupDialog = ContentDialogManager::getInstance()->getGroupDialog(groupId);
1462     bool separated = settings.getSeparateWindow();
1463     GroupWidget* widget = groupWidgets[groupId];
1464     bool isCurrentWindow = activeChatroomWidget == widget;
1465     if (!groupDialog && !separated && isCurrentWindow) {
1466         onAddClicked();
1467     }
1468 
1469     auto chatForm = groupChatForms[groupId].data();
1470     auto chatroom = groupChatrooms[groupId];
1471     auto groupWidget =
1472         ContentDialogManager::getInstance()->addGroupToDialog(dialog, chatroom, chatForm);
1473 
1474 #if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0))
1475     auto removeGroup = QOverload<const GroupId&>::of(&Widget::removeGroup);
1476 #else
1477     auto removeGroup = static_cast<void (Widget::*)(const GroupId&)>(&Widget::removeGroup);
1478 #endif
1479     connect(groupWidget, &GroupWidget::removeGroup, this, removeGroup);
1480     connect(groupWidget, &GroupWidget::chatroomWidgetClicked, chatForm, &GroupChatForm::focusInput);
1481     connect(groupWidget, &GroupWidget::middleMouseClicked, dialog,
1482             [=]() { dialog->removeGroup(groupId); });
1483     connect(groupWidget, &GroupWidget::chatroomWidgetClicked, chatForm, &ChatForm::focusInput);
1484     connect(groupWidget, &GroupWidget::newWindowOpened, this, &Widget::openNewDialog);
1485 
1486     // Signal transmission from the created `groupWidget` (which shown in
1487     // ContentDialog) to the `widget` (which shown in main widget)
1488     // FIXME: emit should be removed
1489     connect(groupWidget, &GroupWidget::chatroomWidgetClicked, [=](GenericChatroomWidget* w) {
1490         Q_UNUSED(w);
1491         emit widget->chatroomWidgetClicked(widget);
1492     });
1493 
1494     connect(groupWidget, &GroupWidget::newWindowOpened, [=](GenericChatroomWidget* w) {
1495         Q_UNUSED(w);
1496         emit widget->newWindowOpened(widget);
1497     });
1498 
1499     // FIXME: emit should be removed
1500     emit widget->chatroomWidgetClicked(widget);
1501 }
1502 
newFriendMessageAlert(const ToxPk & friendId,const QString & text,bool sound,bool file)1503 bool Widget::newFriendMessageAlert(const ToxPk& friendId, const QString& text, bool sound, bool file)
1504 {
1505     bool hasActive;
1506     QWidget* currentWindow;
1507     ContentDialog* contentDialog = ContentDialogManager::getInstance()->getFriendDialog(friendId);
1508     Friend* f = FriendList::findFriend(friendId);
1509 
1510     if (contentDialog != nullptr) {
1511         currentWindow = contentDialog->window();
1512         hasActive = ContentDialogManager::getInstance()->isContactActive(friendId);
1513     } else {
1514         if (settings.getSeparateWindow() && settings.getShowWindow()) {
1515             if (settings.getDontGroupWindows()) {
1516                 contentDialog = createContentDialog();
1517             } else {
1518                 contentDialog = ContentDialogManager::getInstance()->current();
1519                 if (!contentDialog) {
1520                     contentDialog = createContentDialog();
1521                 }
1522             }
1523 
1524             addFriendDialog(f, contentDialog);
1525             currentWindow = contentDialog->window();
1526             hasActive = ContentDialogManager::getInstance()->isContactActive(friendId);
1527         } else {
1528             currentWindow = window();
1529             FriendWidget* widget = friendWidgets[friendId];
1530             hasActive = widget == activeChatroomWidget;
1531         }
1532     }
1533 
1534     if (newMessageAlert(currentWindow, hasActive, sound)) {
1535         FriendWidget* widget = friendWidgets[friendId];
1536         f->setEventFlag(true);
1537         widget->updateStatusLight();
1538         ui->friendList->trackWidget(widget);
1539 #if DESKTOP_NOTIFICATIONS
1540         if (settings.getNotifyHide()) {
1541             notifier.notifyMessageSimple(file ? DesktopNotify::MessageType::FRIEND_FILE
1542                                               : DesktopNotify::MessageType::FRIEND);
1543         } else {
1544             QString title = f->getDisplayedName();
1545             if (file) {
1546                 title += " - " + tr("File sent");
1547             }
1548             notifier.notifyMessagePixmap(title, text,
1549                                          Nexus::getProfile()->loadAvatar(f->getPublicKey()));
1550         }
1551 #endif
1552 
1553         if (contentDialog == nullptr) {
1554             if (hasActive) {
1555                 setWindowTitle(widget->getTitle());
1556             }
1557         } else {
1558             ContentDialogManager::getInstance()->updateFriendStatus(friendId);
1559         }
1560 
1561         return true;
1562     }
1563 
1564     return false;
1565 }
1566 
newGroupMessageAlert(const GroupId & groupId,const ToxPk & authorPk,const QString & message,bool notify)1567 bool Widget::newGroupMessageAlert(const GroupId& groupId, const ToxPk& authorPk,
1568                                   const QString& message, bool notify)
1569 {
1570     bool hasActive;
1571     QWidget* currentWindow;
1572     ContentDialog* contentDialog = ContentDialogManager::getInstance()->getGroupDialog(groupId);
1573     Group* g = GroupList::findGroup(groupId);
1574     GroupWidget* widget = groupWidgets[groupId];
1575 
1576     if (contentDialog != nullptr) {
1577         currentWindow = contentDialog->window();
1578         hasActive = ContentDialogManager::getInstance()->isContactActive(groupId);
1579     } else {
1580         currentWindow = window();
1581         hasActive = widget == activeChatroomWidget;
1582     }
1583 
1584     if (!newMessageAlert(currentWindow, hasActive, true, notify)) {
1585         return false;
1586     }
1587 
1588     g->setEventFlag(true);
1589     widget->updateStatusLight();
1590 #if DESKTOP_NOTIFICATIONS
1591     if (settings.getNotifyHide()) {
1592         notifier.notifyMessageSimple(DesktopNotify::MessageType::GROUP);
1593     } else {
1594         Friend* f = FriendList::findFriend(authorPk);
1595         QString title = g->getPeerList().value(authorPk) + " (" + g->getDisplayedName() + ")";
1596         if (!f) {
1597             notifier.notifyMessage(title, message);
1598         } else {
1599             notifier.notifyMessagePixmap(title, message,
1600                                          Nexus::getProfile()->loadAvatar(f->getPublicKey()));
1601         }
1602     }
1603 #endif
1604 
1605     if (contentDialog == nullptr) {
1606         if (hasActive) {
1607             setWindowTitle(widget->getTitle());
1608         }
1609     } else {
1610         ContentDialogManager::getInstance()->updateGroupStatus(groupId);
1611     }
1612 
1613     return true;
1614 }
1615 
fromDialogType(DialogType type)1616 QString Widget::fromDialogType(DialogType type)
1617 {
1618     switch (type) {
1619     case DialogType::AddDialog:
1620         return tr("Add friend", "title of the window");
1621     case DialogType::GroupDialog:
1622         return tr("Group invites", "title of the window");
1623     case DialogType::TransferDialog:
1624         return tr("File transfers", "title of the window");
1625     case DialogType::SettingDialog:
1626         return tr("Settings", "title of the window");
1627     case DialogType::ProfileDialog:
1628         return tr("My profile", "title of the window");
1629     }
1630 
1631     assert(false);
1632     return QString();
1633 }
1634 
newMessageAlert(QWidget * currentWindow,bool isActive,bool sound,bool notify)1635 bool Widget::newMessageAlert(QWidget* currentWindow, bool isActive, bool sound, bool notify)
1636 {
1637     bool inactiveWindow = isMinimized() || !currentWindow->isActiveWindow();
1638 
1639     if (!inactiveWindow && isActive) {
1640         return false;
1641     }
1642 
1643     if (notify) {
1644         if (settings.getShowWindow()) {
1645             currentWindow->show();
1646         }
1647 
1648         if (settings.getNotify()) {
1649             if (inactiveWindow) {
1650 #if DESKTOP_NOTIFICATIONS
1651                 if (!settings.getDesktopNotify()) {
1652                     QApplication::alert(currentWindow);
1653                 }
1654 #else
1655                 QApplication::alert(currentWindow);
1656 #endif
1657                 eventFlag = true;
1658             }
1659             bool isBusy = core->getStatus() == Status::Status::Busy;
1660             bool busySound = settings.getBusySound();
1661             bool notifySound = settings.getNotifySound();
1662 
1663             if (notifySound && sound && (!isBusy || busySound)) {
1664                 playNotificationSound(IAudioSink::Sound::NewMessage);
1665             }
1666         }
1667     }
1668 
1669     return true;
1670 }
1671 
onFriendRequestReceived(const ToxPk & friendPk,const QString & message)1672 void Widget::onFriendRequestReceived(const ToxPk& friendPk, const QString& message)
1673 {
1674     if (addFriendForm->addFriendRequest(friendPk.toString(), message)) {
1675         friendRequestsUpdate();
1676         newMessageAlert(window(), isActiveWindow(), true, true);
1677 #if DESKTOP_NOTIFICATIONS
1678         if (settings.getNotifyHide()) {
1679             notifier.notifyMessageSimple(DesktopNotify::MessageType::FRIEND_REQUEST);
1680         } else {
1681             notifier.notifyMessage(friendPk.toString() + tr(" sent you a friend request."), message);
1682         }
1683 #endif
1684     }
1685 }
1686 
onFileReceiveRequested(const ToxFile & file)1687 void Widget::onFileReceiveRequested(const ToxFile& file)
1688 {
1689     const ToxPk& friendPk = FriendList::id2Key(file.friendId);
1690     newFriendMessageAlert(friendPk,
1691                           file.fileName + " ("
1692                               + FileTransferWidget::getHumanReadableSize(file.filesize) + ")",
1693                           true, true);
1694 }
1695 
updateFriendActivity(const Friend & frnd)1696 void Widget::updateFriendActivity(const Friend& frnd)
1697 {
1698     const ToxPk& pk = frnd.getPublicKey();
1699     const auto oldTime = settings.getFriendActivity(pk);
1700     const auto newTime = QDateTime::currentDateTime();
1701     settings.setFriendActivity(pk, newTime);
1702     FriendWidget* widget = friendWidgets[frnd.getPublicKey()];
1703     contactListWidget->moveWidget(widget, frnd.getStatus());
1704     contactListWidget->updateActivityTime(oldTime); // update old category widget
1705 }
1706 
removeFriend(Friend * f,bool fake)1707 void Widget::removeFriend(Friend* f, bool fake)
1708 {
1709     if (!fake) {
1710         RemoveFriendDialog ask(this, f);
1711         ask.exec();
1712 
1713         if (!ask.accepted()) {
1714             return;
1715         }
1716 
1717         if (ask.removeHistory()) {
1718             Nexus::getProfile()->getHistory()->removeFriendHistory(f->getPublicKey().toString());
1719         }
1720     }
1721 
1722     const ToxPk friendPk = f->getPublicKey();
1723     auto widget = friendWidgets[friendPk];
1724     widget->setAsInactiveChatroom();
1725     if (widget == activeChatroomWidget) {
1726         activeChatroomWidget = nullptr;
1727         onAddClicked();
1728     }
1729 
1730     friendAlertConnections.remove(friendPk);
1731 
1732     contactListWidget->removeFriendWidget(widget);
1733 
1734     ContentDialog* lastDialog = ContentDialogManager::getInstance()->getFriendDialog(friendPk);
1735     if (lastDialog != nullptr) {
1736         lastDialog->removeFriend(friendPk);
1737     }
1738 
1739     FriendList::removeFriend(friendPk, fake);
1740     if (!fake) {
1741         core->removeFriend(f->getId());
1742         // aliases aren't supported for non-friend peers in groups, revert to basic username
1743         for (Group* g : GroupList::getAllGroups()) {
1744             if (g->getPeerList().contains(friendPk)) {
1745                 g->updateUsername(friendPk, f->getUserName());
1746             }
1747         }
1748     }
1749 
1750     friendWidgets.remove(friendPk);
1751     delete widget;
1752 
1753     auto chatForm = chatForms[friendPk];
1754     chatForms.remove(friendPk);
1755     delete chatForm;
1756 
1757     delete f;
1758     if (contentLayout && contentLayout->mainHead->layout()->isEmpty()) {
1759         onAddClicked();
1760     }
1761 
1762     contactListWidget->reDraw();
1763 }
1764 
removeFriend(const ToxPk & friendId)1765 void Widget::removeFriend(const ToxPk& friendId)
1766 {
1767     removeFriend(FriendList::findFriend(friendId), false);
1768 }
1769 
onDialogShown(GenericChatroomWidget * widget)1770 void Widget::onDialogShown(GenericChatroomWidget* widget)
1771 {
1772     widget->resetEventFlags();
1773     widget->updateStatusLight();
1774 
1775     ui->friendList->updateTracking(widget);
1776     resetIcon();
1777 }
1778 
onFriendDialogShown(const Friend * f)1779 void Widget::onFriendDialogShown(const Friend* f)
1780 {
1781     onDialogShown(friendWidgets[f->getPublicKey()]);
1782 }
1783 
onGroupDialogShown(Group * g)1784 void Widget::onGroupDialogShown(Group* g)
1785 {
1786     const GroupId& groupId = g->getPersistentId();
1787     onDialogShown(groupWidgets[groupId]);
1788 }
1789 
toggleFullscreen()1790 void Widget::toggleFullscreen()
1791 {
1792     if (windowState().testFlag(Qt::WindowFullScreen)) {
1793         setWindowState(windowState() & ~Qt::WindowFullScreen);
1794     } else {
1795         setWindowState(windowState() | Qt::WindowFullScreen);
1796     }
1797 }
1798 
onUpdateAvailable()1799 void Widget::onUpdateAvailable()
1800 {
1801     ui->settingsButton->setProperty("update-available", true);
1802     ui->settingsButton->style()->unpolish(ui->settingsButton);
1803     ui->settingsButton->style()->polish(ui->settingsButton);
1804 }
1805 
createContentDialog() const1806 ContentDialog* Widget::createContentDialog() const
1807 {
1808     ContentDialog* contentDialog = new ContentDialog();
1809 
1810     registerContentDialog(*contentDialog);
1811     return contentDialog;
1812 }
1813 
registerContentDialog(ContentDialog & contentDialog) const1814 void Widget::registerContentDialog(ContentDialog& contentDialog) const
1815 {
1816     ContentDialogManager::getInstance()->addContentDialog(contentDialog);
1817     connect(&contentDialog, &ContentDialog::friendDialogShown, this, &Widget::onFriendDialogShown);
1818     connect(&contentDialog, &ContentDialog::groupDialogShown, this, &Widget::onGroupDialogShown);
1819     connect(core, &Core::usernameSet, &contentDialog, &ContentDialog::setUsername);
1820     connect(&settings, &Settings::groupchatPositionChanged, &contentDialog,
1821             &ContentDialog::reorderLayouts);
1822     connect(&contentDialog, &ContentDialog::addFriendDialog, this, &Widget::addFriendDialog);
1823     connect(&contentDialog, &ContentDialog::addGroupDialog, this, &Widget::addGroupDialog);
1824     connect(&contentDialog, &ContentDialog::connectFriendWidget, this, &Widget::connectFriendWidget);
1825 
1826 #ifdef Q_OS_MAC
1827     Nexus& n = Nexus::getInstance();
1828     connect(&contentDialog, &ContentDialog::destroyed, &n, &Nexus::updateWindowsClosed);
1829     connect(&contentDialog, &ContentDialog::windowStateChanged, &n, &Nexus::onWindowStateChanged);
1830     connect(contentDialog.windowHandle(), &QWindow::windowTitleChanged, &n, &Nexus::updateWindows);
1831     n.updateWindows();
1832 #endif
1833 }
1834 
createContentDialog(DialogType type) const1835 ContentLayout* Widget::createContentDialog(DialogType type) const
1836 {
1837     class Dialog : public ActivateDialog
1838     {
1839     public:
1840         explicit Dialog(DialogType type, Settings& settings, Core* core)
1841             : ActivateDialog(nullptr, Qt::Window)
1842             , type(type)
1843             , settings(settings)
1844             , core{core}
1845         {
1846             restoreGeometry(settings.getDialogSettingsGeometry());
1847             Translator::registerHandler(std::bind(&Dialog::retranslateUi, this), this);
1848             retranslateUi();
1849             setWindowIcon(QIcon(":/img/icons/qtox.svg"));
1850             setStyleSheet(Style::getStylesheet("window/general.css"));
1851 
1852             connect(core, &Core::usernameSet, this, &Dialog::retranslateUi);
1853         }
1854 
1855         ~Dialog()
1856         {
1857             Translator::unregister(this);
1858         }
1859 
1860     public slots:
1861 
1862         void retranslateUi()
1863         {
1864             setWindowTitle(core->getUsername() + QStringLiteral(" - ") + Widget::fromDialogType(type));
1865         }
1866 
1867     protected:
1868         void resizeEvent(QResizeEvent* event) override
1869         {
1870             settings.setDialogSettingsGeometry(saveGeometry());
1871             QDialog::resizeEvent(event);
1872         }
1873 
1874         void moveEvent(QMoveEvent* event) override
1875         {
1876             settings.setDialogSettingsGeometry(saveGeometry());
1877             QDialog::moveEvent(event);
1878         }
1879 
1880     private:
1881         DialogType type;
1882         Settings& settings;
1883         Core* core;
1884     };
1885 
1886     Dialog* dialog = new Dialog(type, settings, core);
1887     dialog->setAttribute(Qt::WA_DeleteOnClose);
1888     ContentLayout* contentLayoutDialog = new ContentLayout(dialog);
1889 
1890     dialog->setObjectName("detached");
1891     dialog->setLayout(contentLayoutDialog);
1892     dialog->layout()->setMargin(0);
1893     dialog->layout()->setSpacing(0);
1894     dialog->setMinimumSize(720, 400);
1895     dialog->setAttribute(Qt::WA_DeleteOnClose);
1896     dialog->show();
1897 
1898 #ifdef Q_OS_MAC
1899     connect(dialog, &Dialog::destroyed, &Nexus::getInstance(), &Nexus::updateWindowsClosed);
1900     connect(dialog, &ActivateDialog::windowStateChanged, &Nexus::getInstance(),
1901             &Nexus::updateWindowsStates);
1902     connect(dialog->windowHandle(), &QWindow::windowTitleChanged, &Nexus::getInstance(),
1903             &Nexus::updateWindows);
1904     Nexus::getInstance().updateWindows();
1905 #endif
1906 
1907     return contentLayoutDialog;
1908 }
1909 
copyFriendIdToClipboard(const ToxPk & friendId)1910 void Widget::copyFriendIdToClipboard(const ToxPk& friendId)
1911 {
1912     Friend* f = FriendList::findFriend(friendId);
1913     if (f != nullptr) {
1914         QClipboard* clipboard = QApplication::clipboard();
1915         clipboard->setText(friendId.toString(), QClipboard::Clipboard);
1916     }
1917 }
1918 
onGroupInviteReceived(const GroupInvite & inviteInfo)1919 void Widget::onGroupInviteReceived(const GroupInvite& inviteInfo)
1920 {
1921     const uint32_t friendId = inviteInfo.getFriendId();
1922     const ToxPk& friendPk = FriendList::id2Key(friendId);
1923     const Friend* f = FriendList::findFriend(friendPk);
1924     updateFriendActivity(*f);
1925 
1926     const uint8_t confType = inviteInfo.getType();
1927     if (confType == TOX_CONFERENCE_TYPE_TEXT || confType == TOX_CONFERENCE_TYPE_AV) {
1928         if (settings.getAutoGroupInvite(f->getPublicKey())) {
1929             onGroupInviteAccepted(inviteInfo);
1930         } else {
1931             if (!groupInviteForm->addGroupInvite(inviteInfo)) {
1932                 return;
1933             }
1934 
1935             ++unreadGroupInvites;
1936             groupInvitesUpdate();
1937             newMessageAlert(window(), isActiveWindow(), true, true);
1938 #if DESKTOP_NOTIFICATIONS
1939             if (settings.getNotifyHide()) {
1940                 notifier.notifyMessageSimple(DesktopNotify::MessageType::GROUP_INVITE);
1941             } else {
1942                 notifier.notifyMessagePixmap(f->getDisplayedName() + tr(" invites you to join a group."),
1943                                              {}, Nexus::getProfile()->loadAvatar(f->getPublicKey()));
1944             }
1945 #endif
1946         }
1947     } else {
1948         qWarning() << "onGroupInviteReceived: Unknown groupchat type:" << confType;
1949         return;
1950     }
1951 }
1952 
onGroupInviteAccepted(const GroupInvite & inviteInfo)1953 void Widget::onGroupInviteAccepted(const GroupInvite& inviteInfo)
1954 {
1955     const uint32_t groupId = core->joinGroupchat(inviteInfo);
1956     if (groupId == std::numeric_limits<uint32_t>::max()) {
1957         qWarning() << "onGroupInviteAccepted: Unable to accept group invite";
1958         return;
1959     }
1960 }
1961 
onGroupMessageReceived(int groupnumber,int peernumber,const QString & message,bool isAction)1962 void Widget::onGroupMessageReceived(int groupnumber, int peernumber, const QString& message,
1963                                     bool isAction)
1964 {
1965     const GroupId& groupId = GroupList::id2Key(groupnumber);
1966     Group* g = GroupList::findGroup(groupId);
1967     assert(g);
1968 
1969     ToxPk author = core->getGroupPeerPk(groupnumber, peernumber);
1970 
1971     groupMessageDispatchers[groupId]->onMessageReceived(author, isAction, message);
1972 }
1973 
onGroupPeerlistChanged(uint32_t groupnumber)1974 void Widget::onGroupPeerlistChanged(uint32_t groupnumber)
1975 {
1976     const GroupId& groupId = GroupList::id2Key(groupnumber);
1977     Group* g = GroupList::findGroup(groupId);
1978     assert(g);
1979     g->regeneratePeerList();
1980 }
1981 
onGroupPeerNameChanged(uint32_t groupnumber,const ToxPk & peerPk,const QString & newName)1982 void Widget::onGroupPeerNameChanged(uint32_t groupnumber, const ToxPk& peerPk, const QString& newName)
1983 {
1984     const GroupId& groupId = GroupList::id2Key(groupnumber);
1985     Group* g = GroupList::findGroup(groupId);
1986     assert(g);
1987 
1988     const QString setName = FriendList::decideNickname(peerPk, newName);
1989     g->updateUsername(peerPk, newName);
1990 }
1991 
onGroupTitleChanged(uint32_t groupnumber,const QString & author,const QString & title)1992 void Widget::onGroupTitleChanged(uint32_t groupnumber, const QString& author, const QString& title)
1993 {
1994     const GroupId& groupId = GroupList::id2Key(groupnumber);
1995     Group* g = GroupList::findGroup(groupId);
1996     assert(g);
1997 
1998     GroupWidget* widget = groupWidgets[groupId];
1999     if (widget->isActive()) {
2000         GUI::setWindowTitle(title);
2001     }
2002 
2003     g->setTitle(author, title);
2004     FilterCriteria filter = getFilterCriteria();
2005     widget->searchName(ui->searchContactText->text(), filterGroups(filter));
2006 }
2007 
titleChangedByUser(const QString & title)2008 void Widget::titleChangedByUser(const QString& title)
2009 {
2010     const auto* group = qobject_cast<Group*>(sender());
2011     assert(group != nullptr);
2012     emit changeGroupTitle(group->getId(), title);
2013 }
2014 
onGroupPeerAudioPlaying(int groupnumber,ToxPk peerPk)2015 void Widget::onGroupPeerAudioPlaying(int groupnumber, ToxPk peerPk)
2016 {
2017     const GroupId& groupId = GroupList::id2Key(groupnumber);
2018     Group* g = GroupList::findGroup(groupId);
2019     assert(g);
2020 
2021     auto form = groupChatForms[groupId].data();
2022     form->peerAudioPlaying(peerPk);
2023 }
2024 
removeGroup(Group * g,bool fake)2025 void Widget::removeGroup(Group* g, bool fake)
2026 {
2027     const auto& groupId = g->getPersistentId();
2028     const auto groupnumber = g->getId();
2029     auto groupWidgetIt = groupWidgets.find(groupId);
2030     if (groupWidgetIt == groupWidgets.end()) {
2031         qWarning() << "Tried to remove group" << groupnumber << "but GroupWidget doesn't exist";
2032         return;
2033     }
2034     auto widget = groupWidgetIt.value();
2035     widget->setAsInactiveChatroom();
2036     if (static_cast<GenericChatroomWidget*>(widget) == activeChatroomWidget) {
2037         activeChatroomWidget = nullptr;
2038         onAddClicked();
2039     }
2040 
2041     GroupList::removeGroup(groupId, fake);
2042     ContentDialog* contentDialog = ContentDialogManager::getInstance()->getGroupDialog(groupId);
2043     if (contentDialog != nullptr) {
2044         contentDialog->removeGroup(groupId);
2045     }
2046 
2047     if (!fake) {
2048         core->removeGroup(groupnumber);
2049     }
2050     contactListWidget->removeGroupWidget(widget); // deletes widget
2051 
2052     groupWidgets.remove(groupId);
2053     auto groupChatFormIt = groupChatForms.find(groupId);
2054     if (groupChatFormIt == groupChatForms.end()) {
2055         qWarning() << "Tried to remove group" << groupnumber << "but GroupChatForm doesn't exist";
2056         return;
2057     }
2058     groupChatForms.erase(groupChatFormIt);
2059     delete g;
2060     if (contentLayout && contentLayout->mainHead->layout()->isEmpty()) {
2061         onAddClicked();
2062     }
2063 
2064     groupAlertConnections.remove(groupId);
2065 
2066     contactListWidget->reDraw();
2067 }
2068 
removeGroup(const GroupId & groupId)2069 void Widget::removeGroup(const GroupId& groupId)
2070 {
2071     removeGroup(GroupList::findGroup(groupId));
2072 }
2073 
createGroup(uint32_t groupnumber,const GroupId & groupId)2074 Group* Widget::createGroup(uint32_t groupnumber, const GroupId& groupId)
2075 {
2076     Group* g = GroupList::findGroup(groupId);
2077     if (g) {
2078         qWarning() << "Group already exists";
2079         return g;
2080     }
2081 
2082     const auto groupName = tr("Groupchat #%1").arg(groupnumber);
2083     const bool enabled = core->getGroupAvEnabled(groupnumber);
2084     Group* newgroup =
2085         GroupList::addGroup(groupnumber, groupId, groupName, enabled, core->getUsername());
2086     auto dialogManager = ContentDialogManager::getInstance();
2087     auto rawChatroom = new GroupChatroom(newgroup, dialogManager);
2088     std::shared_ptr<GroupChatroom> chatroom(rawChatroom);
2089 
2090     const auto compact = settings.getCompactLayout();
2091     auto widget = new GroupWidget(chatroom, compact);
2092     auto messageProcessor = MessageProcessor(sharedMessageProcessorParams);
2093     auto messageDispatcher =
2094         std::make_shared<GroupMessageDispatcher>(*newgroup, std::move(messageProcessor), *core,
2095                                                  *core, Settings::getInstance());
2096     auto groupChatLog = std::make_shared<SessionChatLog>(*core);
2097 
2098     connect(messageDispatcher.get(), &IMessageDispatcher::messageReceived, groupChatLog.get(),
2099             &SessionChatLog::onMessageReceived);
2100     connect(messageDispatcher.get(), &IMessageDispatcher::messageSent, groupChatLog.get(),
2101             &SessionChatLog::onMessageSent);
2102     connect(messageDispatcher.get(), &IMessageDispatcher::messageComplete, groupChatLog.get(),
2103             &SessionChatLog::onMessageComplete);
2104 
2105     auto notifyReceivedCallback = [this, groupId](const ToxPk& author, const Message& message) {
2106         auto isTargeted = std::any_of(message.metadata.begin(), message.metadata.end(),
2107                                       [](MessageMetadata metadata) {
2108                                           return metadata.type == MessageMetadataType::selfMention;
2109                                       });
2110         newGroupMessageAlert(groupId, author, message.content,
2111                              isTargeted || settings.getGroupAlwaysNotify());
2112     };
2113 
2114     auto notifyReceivedConnection =
2115         connect(messageDispatcher.get(), &IMessageDispatcher::messageReceived, notifyReceivedCallback);
2116     groupAlertConnections.insert(groupId, notifyReceivedConnection);
2117 
2118     auto form = new GroupChatForm(newgroup, *groupChatLog, *messageDispatcher);
2119     connect(&settings, &Settings::nameColorsChanged, form, &GenericChatForm::setColorizedNames);
2120     form->setColorizedNames(settings.getEnableGroupChatsColor());
2121     groupMessageDispatchers[groupId] = messageDispatcher;
2122     groupChatLogs[groupId] = groupChatLog;
2123     groupWidgets[groupId] = widget;
2124     groupChatrooms[groupId] = chatroom;
2125     groupChatForms[groupId] = QSharedPointer<GroupChatForm>(form);
2126 
2127     contactListWidget->addGroupWidget(widget);
2128     widget->updateStatusLight();
2129     contactListWidget->activateWindow();
2130 
2131     connect(widget, &GroupWidget::chatroomWidgetClicked, this, &Widget::onChatroomWidgetClicked);
2132     connect(widget, &GroupWidget::newWindowOpened, this, &Widget::openNewDialog);
2133 #if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0))
2134     auto widgetRemoveGroup = QOverload<const GroupId&>::of(&Widget::removeGroup);
2135 #else
2136     auto widgetRemoveGroup = static_cast<void (Widget::*)(const GroupId&)>(&Widget::removeGroup);
2137 #endif
2138     connect(widget, &GroupWidget::removeGroup, this, widgetRemoveGroup);
2139     connect(widget, &GroupWidget::middleMouseClicked, this, [=]() { removeGroup(groupId); });
2140     connect(widget, &GroupWidget::chatroomWidgetClicked, form, &ChatForm::focusInput);
2141     connect(newgroup, &Group::titleChangedByUser, this, &Widget::titleChangedByUser);
2142     connect(core, &Core::usernameSet, newgroup, &Group::setSelfName);
2143 
2144     FilterCriteria filter = getFilterCriteria();
2145     widget->searchName(ui->searchContactText->text(), filterGroups(filter));
2146 
2147     return newgroup;
2148 }
2149 
onEmptyGroupCreated(uint32_t groupnumber,const GroupId & groupId,const QString & title)2150 void Widget::onEmptyGroupCreated(uint32_t groupnumber, const GroupId& groupId, const QString& title)
2151 {
2152     Group* group = createGroup(groupnumber, groupId);
2153     if (!group) {
2154         return;
2155     }
2156     if (title.isEmpty()) {
2157         // Only rename group if groups are visible.
2158         if (groupsVisible()) {
2159             groupWidgets[groupId]->editName();
2160         }
2161     } else {
2162         group->setTitle(QString(), title);
2163     }
2164 }
2165 
onGroupJoined(int groupId,const GroupId & groupPersistentId)2166 void Widget::onGroupJoined(int groupId, const GroupId& groupPersistentId)
2167 {
2168     createGroup(groupId, groupPersistentId);
2169 }
2170 
2171 /**
2172  * @brief Used to reset the blinking icon.
2173  */
resetIcon()2174 void Widget::resetIcon()
2175 {
2176     eventIcon = false;
2177     eventFlag = false;
2178     updateIcons();
2179 }
2180 
event(QEvent * e)2181 bool Widget::event(QEvent* e)
2182 {
2183     switch (e->type()) {
2184     case QEvent::MouseButtonPress:
2185     case QEvent::MouseButtonDblClick:
2186         focusChatInput();
2187         break;
2188     case QEvent::Paint:
2189         ui->friendList->updateVisualTracking();
2190         break;
2191     case QEvent::WindowActivate:
2192         if (activeChatroomWidget) {
2193             activeChatroomWidget->resetEventFlags();
2194             activeChatroomWidget->updateStatusLight();
2195             setWindowTitle(activeChatroomWidget->getTitle());
2196         }
2197 
2198         if (eventFlag) {
2199             resetIcon();
2200         }
2201 
2202         focusChatInput();
2203 
2204 #ifdef Q_OS_MAC
2205         emit windowStateChanged(windowState());
2206 
2207     case QEvent::WindowStateChange:
2208         Nexus::getInstance().updateWindowsStates();
2209 #endif
2210         break;
2211     default:
2212         break;
2213     }
2214 
2215     return QMainWindow::event(e);
2216 }
2217 
onUserAwayCheck()2218 void Widget::onUserAwayCheck()
2219 {
2220 #ifdef QTOX_PLATFORM_EXT
2221     uint32_t autoAwayTime = settings.getAutoAwayTime() * 60 * 1000;
2222     bool online = static_cast<Status::Status>(ui->statusButton->property("status").toInt())
2223                   == Status::Status::Online;
2224     bool away = autoAwayTime && Platform::getIdleTime() >= autoAwayTime;
2225 
2226     if (online && away) {
2227         qDebug() << "auto away activated at" << QTime::currentTime().toString();
2228         emit statusSet(Status::Status::Away);
2229         autoAwayActive = true;
2230     } else if (autoAwayActive && !away) {
2231         qDebug() << "auto away deactivated at" << QTime::currentTime().toString();
2232         emit statusSet(Status::Status::Online);
2233         autoAwayActive = false;
2234     }
2235 #endif
2236 }
2237 
onEventIconTick()2238 void Widget::onEventIconTick()
2239 {
2240     if (eventFlag) {
2241         eventIcon ^= true;
2242         updateIcons();
2243     }
2244 }
2245 
onTryCreateTrayIcon()2246 void Widget::onTryCreateTrayIcon()
2247 {
2248     static int32_t tries = 15;
2249     if (!icon && tries--) {
2250         if (QSystemTrayIcon::isSystemTrayAvailable()) {
2251             icon = std::unique_ptr<QSystemTrayIcon>(new QSystemTrayIcon);
2252             updateIcons();
2253             trayMenu = new QMenu(this);
2254 
2255             // adding activate to the top, avoids accidentally clicking quit
2256             trayMenu->addAction(actionShow);
2257             trayMenu->addSeparator();
2258             trayMenu->addAction(statusOnline);
2259             trayMenu->addAction(statusAway);
2260             trayMenu->addAction(statusBusy);
2261             trayMenu->addSeparator();
2262             trayMenu->addAction(actionLogout);
2263             trayMenu->addAction(actionQuit);
2264             icon->setContextMenu(trayMenu);
2265 
2266             connect(icon.get(), &QSystemTrayIcon::activated, this, &Widget::onIconClick);
2267 
2268             if (settings.getShowSystemTray()) {
2269                 icon->show();
2270                 setHidden(settings.getAutostartInTray());
2271             } else {
2272                 show();
2273             }
2274 
2275 #ifdef Q_OS_MAC
2276             Nexus::getInstance().dockMenu->setAsDockMenu();
2277 #endif
2278         } else if (!isVisible()) {
2279             show();
2280         }
2281     } else {
2282         disconnect(timer, &QTimer::timeout, this, &Widget::onTryCreateTrayIcon);
2283         if (!icon) {
2284             qWarning() << "No system tray detected!";
2285             show();
2286         }
2287     }
2288 }
2289 
setStatusOnline()2290 void Widget::setStatusOnline()
2291 {
2292     if (!ui->statusButton->isEnabled()) {
2293         return;
2294     }
2295 
2296     core->setStatus(Status::Status::Online);
2297 }
2298 
setStatusAway()2299 void Widget::setStatusAway()
2300 {
2301     if (!ui->statusButton->isEnabled()) {
2302         return;
2303     }
2304 
2305     core->setStatus(Status::Status::Away);
2306 }
2307 
setStatusBusy()2308 void Widget::setStatusBusy()
2309 {
2310     if (!ui->statusButton->isEnabled()) {
2311         return;
2312     }
2313 
2314     core->setStatus(Status::Status::Busy);
2315 }
2316 
onGroupSendFailed(uint32_t groupnumber)2317 void Widget::onGroupSendFailed(uint32_t groupnumber)
2318 {
2319     const auto& groupId = GroupList::id2Key(groupnumber);
2320     Group* g = GroupList::findGroup(groupId);
2321     assert(g);
2322 
2323     const auto message = tr("Message failed to send");
2324     const auto curTime = QDateTime::currentDateTime();
2325     auto form = groupChatForms[groupId].data();
2326     form->addSystemInfoMessage(message, ChatMessage::INFO, curTime);
2327 }
2328 
onFriendTypingChanged(uint32_t friendnumber,bool isTyping)2329 void Widget::onFriendTypingChanged(uint32_t friendnumber, bool isTyping)
2330 {
2331     const auto& friendId = FriendList::id2Key(friendnumber);
2332     Friend* f = FriendList::findFriend(friendId);
2333     if (!f) {
2334         return;
2335     }
2336 
2337     chatForms[f->getPublicKey()]->setFriendTyping(isTyping);
2338 }
2339 
onSetShowSystemTray(bool newValue)2340 void Widget::onSetShowSystemTray(bool newValue)
2341 {
2342     if (icon) {
2343         icon->setVisible(newValue);
2344     }
2345 }
2346 
saveWindowGeometry()2347 void Widget::saveWindowGeometry()
2348 {
2349     settings.setWindowGeometry(saveGeometry());
2350     settings.setWindowState(saveState());
2351 }
2352 
saveSplitterGeometry()2353 void Widget::saveSplitterGeometry()
2354 {
2355     if (!settings.getSeparateWindow()) {
2356         settings.setSplitterState(ui->mainSplitter->saveState());
2357     }
2358 }
2359 
onSplitterMoved(int pos,int index)2360 void Widget::onSplitterMoved(int pos, int index)
2361 {
2362     Q_UNUSED(pos);
2363     Q_UNUSED(index);
2364     saveSplitterGeometry();
2365 }
2366 
cycleContacts(bool forward)2367 void Widget::cycleContacts(bool forward)
2368 {
2369     contactListWidget->cycleContacts(activeChatroomWidget, forward);
2370 }
2371 
filterGroups(FilterCriteria index)2372 bool Widget::filterGroups(FilterCriteria index)
2373 {
2374     switch (index) {
2375     case FilterCriteria::Offline:
2376     case FilterCriteria::Friends:
2377         return true;
2378     default:
2379         return false;
2380     }
2381 }
2382 
filterOffline(FilterCriteria index)2383 bool Widget::filterOffline(FilterCriteria index)
2384 {
2385     switch (index) {
2386     case FilterCriteria::Online:
2387     case FilterCriteria::Groups:
2388         return true;
2389     default:
2390         return false;
2391     }
2392 }
2393 
filterOnline(FilterCriteria index)2394 bool Widget::filterOnline(FilterCriteria index)
2395 {
2396     switch (index) {
2397     case FilterCriteria::Offline:
2398     case FilterCriteria::Groups:
2399         return true;
2400     default:
2401         return false;
2402     }
2403 }
2404 
clearAllReceipts()2405 void Widget::clearAllReceipts()
2406 {
2407     QList<Friend*> frnds = FriendList::getAllFriends();
2408     for (Friend* f : frnds) {
2409         friendMessageDispatchers[f->getPublicKey()]->clearOutgoingMessages();
2410     }
2411 }
2412 
reloadTheme()2413 void Widget::reloadTheme()
2414 {
2415     this->setStyleSheet(Style::getStylesheet("window/general.css"));
2416     QString statusPanelStyle = Style::getStylesheet("window/statusPanel.css");
2417     ui->tooliconsZone->setStyleSheet(Style::getStylesheet("tooliconsZone/tooliconsZone.css"));
2418     ui->statusPanel->setStyleSheet(statusPanelStyle);
2419     ui->statusHead->setStyleSheet(statusPanelStyle);
2420     ui->friendList->setStyleSheet(Style::getStylesheet("friendList/friendList.css"));
2421     ui->statusButton->setStyleSheet(Style::getStylesheet("statusButton/statusButton.css"));
2422     contactListWidget->reDraw();
2423 
2424     profilePicture->setStyleSheet(Style::getStylesheet("window/profile.css"));
2425 
2426     if (contentLayout != nullptr) {
2427         contentLayout->reloadTheme();
2428     }
2429 
2430     for (Friend* f : FriendList::getAllFriends()) {
2431         friendWidgets[f->getPublicKey()]->reloadTheme();
2432     }
2433 
2434     for (Group* g : GroupList::getAllGroups()) {
2435         groupWidgets[g->getPersistentId()]->reloadTheme();
2436     }
2437 
2438 
2439     for (auto f : FriendList::getAllFriends()) {
2440         chatForms[f->getPublicKey()]->reloadTheme();
2441     }
2442 
2443     for (auto g : GroupList::getAllGroups()) {
2444         groupChatForms[g->getPersistentId()]->reloadTheme();
2445     }
2446 }
2447 
nextContact()2448 void Widget::nextContact()
2449 {
2450     cycleContacts(true);
2451 }
2452 
previousContact()2453 void Widget::previousContact()
2454 {
2455     cycleContacts(false);
2456 }
2457 
2458 // Preparing needed to set correct size of icons for GTK tray backend
prepareIcon(QString path,int w,int h)2459 inline QIcon Widget::prepareIcon(QString path, int w, int h)
2460 {
2461 #ifdef Q_OS_LINUX
2462 
2463     QString desktop = getenv("XDG_CURRENT_DESKTOP");
2464     if (desktop.isEmpty()) {
2465         desktop = getenv("DESKTOP_SESSION");
2466     }
2467 
2468     desktop = desktop.toLower();
2469     if (desktop == "xfce" || desktop.contains("gnome") || desktop == "mate" || desktop == "x-cinnamon") {
2470         if (w > 0 && h > 0) {
2471             QSvgRenderer renderer(path);
2472 
2473             QPixmap pm(w, h);
2474             pm.fill(Qt::transparent);
2475             QPainter painter(&pm);
2476             renderer.render(&painter, pm.rect());
2477 
2478             return QIcon(pm);
2479         }
2480     }
2481 #endif
2482     return QIcon(path);
2483 }
2484 
searchContacts()2485 void Widget::searchContacts()
2486 {
2487     QString searchString = ui->searchContactText->text();
2488     FilterCriteria filter = getFilterCriteria();
2489 
2490     contactListWidget->searchChatrooms(searchString, filterOnline(filter), filterOffline(filter),
2491                                        filterGroups(filter));
2492 
2493     updateFilterText();
2494 
2495     contactListWidget->reDraw();
2496 }
2497 
changeDisplayMode()2498 void Widget::changeDisplayMode()
2499 {
2500     filterDisplayGroup->setEnabled(false);
2501 
2502     if (filterDisplayGroup->checkedAction() == filterDisplayActivity) {
2503         contactListWidget->setMode(FriendListWidget::SortingMode::Activity);
2504     } else if (filterDisplayGroup->checkedAction() == filterDisplayName) {
2505         contactListWidget->setMode(FriendListWidget::SortingMode::Name);
2506     }
2507 
2508     searchContacts();
2509     filterDisplayGroup->setEnabled(true);
2510 
2511     updateFilterText();
2512 }
2513 
updateFilterText()2514 void Widget::updateFilterText()
2515 {
2516     QString action = filterDisplayGroup->checkedAction()->text();
2517     QString text = filterGroup->checkedAction()->text();
2518     text = action + QStringLiteral(" | ") + text;
2519     ui->searchContactFilterBox->setText(text);
2520 }
2521 
getFilterCriteria() const2522 Widget::FilterCriteria Widget::getFilterCriteria() const
2523 {
2524     QAction* checked = filterGroup->checkedAction();
2525 
2526     if (checked == filterOnlineAction)
2527         return FilterCriteria::Online;
2528     else if (checked == filterOfflineAction)
2529         return FilterCriteria::Offline;
2530     else if (checked == filterFriendsAction)
2531         return FilterCriteria::Friends;
2532     else if (checked == filterGroupsAction)
2533         return FilterCriteria::Groups;
2534 
2535     return FilterCriteria::All;
2536 }
2537 
searchCircle(CircleWidget & circleWidget)2538 void Widget::searchCircle(CircleWidget& circleWidget)
2539 {
2540     FilterCriteria filter = getFilterCriteria();
2541     QString text = ui->searchContactText->text();
2542     circleWidget.search(text, true, filterOnline(filter), filterOffline(filter));
2543 }
2544 
groupsVisible() const2545 bool Widget::groupsVisible() const
2546 {
2547     FilterCriteria filter = getFilterCriteria();
2548     return !filterGroups(filter);
2549 }
2550 
friendListContextMenu(const QPoint & pos)2551 void Widget::friendListContextMenu(const QPoint& pos)
2552 {
2553     QMenu menu(this);
2554     QAction* createGroupAction = menu.addAction(tr("Create new group..."));
2555     QAction* addCircleAction = menu.addAction(tr("Add new circle..."));
2556     QAction* chosenAction = menu.exec(ui->friendList->mapToGlobal(pos));
2557 
2558     if (chosenAction == addCircleAction) {
2559         contactListWidget->addCircleWidget();
2560     } else if (chosenAction == createGroupAction) {
2561         core->createGroup();
2562     }
2563 }
2564 
friendRequestsUpdate()2565 void Widget::friendRequestsUpdate()
2566 {
2567     unsigned int unreadFriendRequests = settings.getUnreadFriendRequests();
2568 
2569     if (unreadFriendRequests == 0) {
2570         delete friendRequestsButton;
2571         friendRequestsButton = nullptr;
2572     } else if (!friendRequestsButton) {
2573         friendRequestsButton = new QPushButton(this);
2574         friendRequestsButton->setObjectName("green");
2575         ui->statusLayout->insertWidget(2, friendRequestsButton);
2576 
2577         connect(friendRequestsButton, &QPushButton::released, [this]() {
2578             onAddClicked();
2579             addFriendForm->setMode(AddFriendForm::Mode::FriendRequest);
2580         });
2581     }
2582 
2583     if (friendRequestsButton) {
2584         friendRequestsButton->setText(tr("%n New Friend Request(s)", "", unreadFriendRequests));
2585     }
2586 }
2587 
groupInvitesUpdate()2588 void Widget::groupInvitesUpdate()
2589 {
2590     if (unreadGroupInvites == 0) {
2591         delete groupInvitesButton;
2592         groupInvitesButton = nullptr;
2593     } else if (!groupInvitesButton) {
2594         groupInvitesButton = new QPushButton(this);
2595         groupInvitesButton->setObjectName("green");
2596         ui->statusLayout->insertWidget(2, groupInvitesButton);
2597 
2598         connect(groupInvitesButton, &QPushButton::released, this, &Widget::onGroupClicked);
2599     }
2600 
2601     if (groupInvitesButton) {
2602         groupInvitesButton->setText(tr("%n New Group Invite(s)", "", unreadGroupInvites));
2603     }
2604 }
2605 
groupInvitesClear()2606 void Widget::groupInvitesClear()
2607 {
2608     unreadGroupInvites = 0;
2609     groupInvitesUpdate();
2610 }
2611 
setActiveToolMenuButton(ActiveToolMenuButton newActiveButton)2612 void Widget::setActiveToolMenuButton(ActiveToolMenuButton newActiveButton)
2613 {
2614     ui->addButton->setChecked(newActiveButton == ActiveToolMenuButton::AddButton);
2615     ui->addButton->setDisabled(newActiveButton == ActiveToolMenuButton::AddButton);
2616     ui->groupButton->setChecked(newActiveButton == ActiveToolMenuButton::GroupButton);
2617     ui->groupButton->setDisabled(newActiveButton == ActiveToolMenuButton::GroupButton);
2618     ui->transferButton->setChecked(newActiveButton == ActiveToolMenuButton::TransferButton);
2619     ui->transferButton->setDisabled(newActiveButton == ActiveToolMenuButton::TransferButton);
2620     ui->settingsButton->setChecked(newActiveButton == ActiveToolMenuButton::SettingButton);
2621     ui->settingsButton->setDisabled(newActiveButton == ActiveToolMenuButton::SettingButton);
2622 }
2623 
retranslateUi()2624 void Widget::retranslateUi()
2625 {
2626     ui->retranslateUi(this);
2627     setUsername(core->getUsername());
2628     setStatusMessage(core->getStatusMessage());
2629 
2630     filterDisplayName->setText(tr("By Name"));
2631     filterDisplayActivity->setText(tr("By Activity"));
2632     filterAllAction->setText(tr("All"));
2633     filterOnlineAction->setText(tr("Online"));
2634     filterOfflineAction->setText(tr("Offline"));
2635     filterFriendsAction->setText(tr("Friends"));
2636     filterGroupsAction->setText(tr("Groups"));
2637     ui->searchContactText->setPlaceholderText(tr("Search Contacts"));
2638     updateFilterText();
2639 
2640     ui->searchContactText->setPlaceholderText(tr("Search Contacts"));
2641     statusOnline->setText(tr("Online", "Button to set your status to 'Online'"));
2642     statusAway->setText(tr("Away", "Button to set your status to 'Away'"));
2643     statusBusy->setText(tr("Busy", "Button to set your status to 'Busy'"));
2644     actionLogout->setText(tr("Logout", "Tray action menu to logout user"));
2645     actionQuit->setText(tr("Exit", "Tray action menu to exit tox"));
2646     actionShow->setText(tr("Show", "Tray action menu to show qTox window"));
2647 
2648     if (!settings.getSeparateWindow() && (settingsWidget && settingsWidget->isShown())) {
2649         setWindowTitle(fromDialogType(DialogType::SettingDialog));
2650     }
2651 
2652     friendRequestsUpdate();
2653     groupInvitesUpdate();
2654 
2655 
2656 #ifdef Q_OS_MAC
2657     Nexus::getInstance().retranslateUi();
2658 
2659     filterMenu->menuAction()->setText(tr("Filter..."));
2660 
2661     fileMenu->setText(tr("File"));
2662     editMenu->setText(tr("Edit"));
2663     contactMenu->setText(tr("Contacts"));
2664     changeStatusMenu->menuAction()->setText(tr("Change Status"));
2665     editProfileAction->setText(tr("Edit Profile"));
2666     logoutAction->setText(tr("Log out"));
2667     addContactAction->setText(tr("Add Contact..."));
2668     nextConversationAction->setText(tr("Next Conversation"));
2669     previousConversationAction->setText(tr("Previous Conversation"));
2670 #endif
2671 }
2672 
focusChatInput()2673 void Widget::focusChatInput()
2674 {
2675     if (activeChatroomWidget) {
2676         if (const Friend* f = activeChatroomWidget->getFriend()) {
2677             chatForms[f->getPublicKey()]->focusInput();
2678         } else if (Group* g = activeChatroomWidget->getGroup()) {
2679             groupChatForms[g->getPersistentId()]->focusInput();
2680         }
2681     }
2682 }
2683 
refreshPeerListsLocal(const QString & username)2684 void Widget::refreshPeerListsLocal(const QString& username)
2685 {
2686     for (Group* g : GroupList::getAllGroups()) {
2687         g->updateUsername(core->getSelfPublicKey(), username);
2688     }
2689 }
2690 
connectCircleWidget(CircleWidget & circleWidget)2691 void Widget::connectCircleWidget(CircleWidget& circleWidget)
2692 {
2693     connect(&circleWidget, &CircleWidget::searchCircle, this, &Widget::searchCircle);
2694     connect(&circleWidget, &CircleWidget::newContentDialog, this, &Widget::registerContentDialog);
2695 }
2696 
connectFriendWidget(FriendWidget & friendWidget)2697 void Widget::connectFriendWidget(FriendWidget& friendWidget)
2698 {
2699     connect(&friendWidget, &FriendWidget::searchCircle, this, &Widget::searchCircle);
2700     connect(&friendWidget, &FriendWidget::updateFriendActivity, this, &Widget::updateFriendActivity);
2701 }
2702