1 // SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
2 // SPDX-FileCopyrightText: 2021 Nheko Contributors
3 //
4 // SPDX-License-Identifier: GPL-3.0-or-later
5 
6 #include <QApplication>
7 #include <QLayout>
8 #include <QMessageBox>
9 #include <QPluginLoader>
10 #include <QShortcut>
11 
12 #include <mtx/requests.hpp>
13 #include <mtx/responses/login.hpp>
14 
15 #include "Cache.h"
16 #include "Cache_p.h"
17 #include "ChatPage.h"
18 #include "Config.h"
19 #include "JdenticonProvider.h"
20 #include "Logging.h"
21 #include "LoginPage.h"
22 #include "MainWindow.h"
23 #include "MatrixClient.h"
24 #include "MemberList.h"
25 #include "RegisterPage.h"
26 #include "TrayIcon.h"
27 #include "UserSettingsPage.h"
28 #include "Utils.h"
29 #include "WelcomePage.h"
30 #include "ui/LoadingIndicator.h"
31 #include "ui/OverlayModal.h"
32 #include "ui/SnackBar.h"
33 #include "voip/WebRTCSession.h"
34 
35 #include "dialogs/CreateRoom.h"
36 
37 MainWindow *MainWindow::instance_ = nullptr;
38 
MainWindow(QWidget * parent)39 MainWindow::MainWindow(QWidget *parent)
40   : QMainWindow(parent)
41   , userSettings_{UserSettings::instance()}
42 {
43     instance_ = this;
44 
45     setWindowTitle(0);
46     setObjectName("MainWindow");
47 
48     modal_ = new OverlayModal(this);
49 
50     restoreWindowSize();
51 
52     QFont font;
53     font.setStyleStrategy(QFont::PreferAntialias);
54     setFont(font);
55 
56     trayIcon_ = new TrayIcon(":/logos/nheko.svg", this);
57 
58     welcome_page_     = new WelcomePage(this);
59     login_page_       = new LoginPage(this);
60     register_page_    = new RegisterPage(this);
61     chat_page_        = new ChatPage(userSettings_, this);
62     userSettingsPage_ = new UserSettingsPage(userSettings_, this);
63 
64     // Initialize sliding widget manager.
65     pageStack_ = new QStackedWidget(this);
66     pageStack_->addWidget(welcome_page_);
67     pageStack_->addWidget(login_page_);
68     pageStack_->addWidget(register_page_);
69     pageStack_->addWidget(chat_page_);
70     pageStack_->addWidget(userSettingsPage_);
71 
72     setCentralWidget(pageStack_);
73 
74     connect(welcome_page_, SIGNAL(userLogin()), this, SLOT(showLoginPage()));
75     connect(welcome_page_, SIGNAL(userRegister()), this, SLOT(showRegisterPage()));
76 
77     connect(login_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage()));
78     connect(login_page_, &LoginPage::loggingIn, this, &MainWindow::showOverlayProgressBar);
79     connect(register_page_, &RegisterPage::registering, this, &MainWindow::showOverlayProgressBar);
__anon01415c920102() 80     connect(login_page_, &LoginPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); });
81     connect(
__anon01415c920202() 82       register_page_, &RegisterPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); });
83     connect(register_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage()));
84 
85     connect(chat_page_, &ChatPage::closing, this, &MainWindow::showWelcomePage);
86     connect(
87       chat_page_, &ChatPage::showOverlayProgressBar, this, &MainWindow::showOverlayProgressBar);
88     connect(chat_page_, &ChatPage::unreadMessages, this, &MainWindow::setWindowTitle);
89     connect(chat_page_, SIGNAL(unreadMessages(int)), trayIcon_, SLOT(setUnreadCount(int)));
__anon01415c920302(const QString &msg) 90     connect(chat_page_, &ChatPage::showLoginPage, this, [this](const QString &msg) {
91         login_page_->showError(msg);
92         showLoginPage();
93     });
94 
__anon01415c920402() 95     connect(userSettingsPage_, &UserSettingsPage::moveBack, this, [this]() {
96         pageStack_->setCurrentWidget(chat_page_);
97     });
98 
99     connect(userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool)));
100     connect(
101       userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged);
102     connect(trayIcon_,
103             SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
104             this,
105             SLOT(iconActivated(QSystemTrayIcon::ActivationReason)));
106 
107     connect(chat_page_, SIGNAL(contentLoaded()), this, SLOT(removeOverlayProgressBar()));
108 
109     connect(this, &MainWindow::focusChanged, chat_page_, &ChatPage::chatFocusChanged);
110 
111     connect(chat_page_, &ChatPage::showUserSettingsPage, this, &MainWindow::showUserSettingsPage);
112 
__anon01415c920502(const mtx::responses::Login &res) 113     connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) {
114         http::client()->set_user(res.user_id);
115         showChatPage();
116     });
117 
118     connect(register_page_, &RegisterPage::registerOk, this, &MainWindow::showChatPage);
119 
120     QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this);
121     connect(quitShortcut, &QShortcut::activated, this, QApplication::quit);
122 
123     trayIcon_->setVisible(userSettings_->tray());
124 
125     // load cache on event loop
__anon01415c920602null126     QTimer::singleShot(0, this, [this] {
127         if (hasActiveUser()) {
128             QString token       = userSettings_->accessToken();
129             QString home_server = userSettings_->homeserver();
130             QString user_id     = userSettings_->userId();
131             QString device_id   = userSettings_->deviceId();
132 
133             http::client()->set_access_token(token.toStdString());
134             http::client()->set_server(home_server.toStdString());
135             http::client()->set_device_id(device_id.toStdString());
136 
137             try {
138                 using namespace mtx::identifiers;
139                 http::client()->set_user(parse<User>(user_id.toStdString()));
140             } catch (const std::invalid_argument &) {
141                 nhlog::ui()->critical("bootstrapped with invalid user_id: {}",
142                                       user_id.toStdString());
143             }
144 
145             showChatPage();
146         }
147     });
148 }
149 
150 void
setWindowTitle(int notificationCount)151 MainWindow::setWindowTitle(int notificationCount)
152 {
153     QString name = "nheko";
154 
155     if (!userSettings_.data()->profile().isEmpty())
156         name += " | " + userSettings_.data()->profile();
157     if (notificationCount > 0) {
158         name.append(QString{" (%1)"}.arg(notificationCount));
159     }
160     QMainWindow::setWindowTitle(name);
161 }
162 
163 bool
event(QEvent * event)164 MainWindow::event(QEvent *event)
165 {
166     auto type = event->type();
167     if (type == QEvent::WindowActivate) {
168         emit focusChanged(true);
169     } else if (type == QEvent::WindowDeactivate) {
170         emit focusChanged(false);
171     }
172 
173     return QMainWindow::event(event);
174 }
175 
176 void
restoreWindowSize()177 MainWindow::restoreWindowSize()
178 {
179     int savedWidth  = userSettings_->qsettings()->value("window/width").toInt();
180     int savedheight = userSettings_->qsettings()->value("window/height").toInt();
181 
182     nhlog::ui()->info("Restoring window size {}x{}", savedWidth, savedheight);
183 
184     if (savedWidth == 0 || savedheight == 0)
185         resize(conf::window::width, conf::window::height);
186     else
187         resize(savedWidth, savedheight);
188 }
189 
190 void
saveCurrentWindowSize()191 MainWindow::saveCurrentWindowSize()
192 {
193     auto settings = userSettings_->qsettings();
194     QSize current = size();
195 
196     settings->setValue("window/width", current.width());
197     settings->setValue("window/height", current.height());
198 }
199 
200 void
removeOverlayProgressBar()201 MainWindow::removeOverlayProgressBar()
202 {
203     QTimer *timer = new QTimer(this);
204     timer->setSingleShot(true);
205 
206     connect(timer, &QTimer::timeout, [this, timer]() {
207         timer->deleteLater();
208 
209         if (modal_)
210             modal_->hide();
211 
212         if (spinner_)
213             spinner_->stop();
214     });
215 
216     // FIXME:  Snackbar doesn't work if it's initialized in the constructor.
217     QTimer::singleShot(0, this, [this]() {
218         snackBar_ = new SnackBar(this);
219         connect(chat_page_, &ChatPage::showNotification, snackBar_, &SnackBar::showMessage);
220     });
221 
222     timer->start(50);
223 }
224 
225 void
showChatPage()226 MainWindow::showChatPage()
227 {
228     auto userid     = QString::fromStdString(http::client()->user_id().to_string());
229     auto device_id  = QString::fromStdString(http::client()->device_id());
230     auto homeserver = QString::fromStdString(http::client()->server() + ":" +
231                                              std::to_string(http::client()->port()));
232     auto token      = QString::fromStdString(http::client()->access_token());
233 
234     userSettings_.data()->setUserId(userid);
235     userSettings_.data()->setAccessToken(token);
236     userSettings_.data()->setDeviceId(device_id);
237     userSettings_.data()->setHomeserver(homeserver);
238 
239     showOverlayProgressBar();
240 
241     pageStack_->setCurrentWidget(chat_page_);
242 
243     pageStack_->removeWidget(welcome_page_);
244     pageStack_->removeWidget(login_page_);
245     pageStack_->removeWidget(register_page_);
246 
247     login_page_->reset();
248     chat_page_->bootstrap(userid, homeserver, token);
249     connect(cache::client(),
250             &Cache::databaseReady,
251             userSettingsPage_,
252             &UserSettingsPage::updateSecretStatus);
253     connect(cache::client(),
254             &Cache::secretChanged,
255             userSettingsPage_,
256             &UserSettingsPage::updateSecretStatus);
257     emit reload();
258 }
259 
260 void
closeEvent(QCloseEvent * event)261 MainWindow::closeEvent(QCloseEvent *event)
262 {
263     if (WebRTCSession::instance().state() != webrtc::State::DISCONNECTED) {
264         if (QMessageBox::question(this, "nheko", "A call is in progress. Quit?") !=
265             QMessageBox::Yes) {
266             event->ignore();
267             return;
268         }
269     }
270 
271     if (!qApp->isSavingSession() && isVisible() && pageSupportsTray() && userSettings_->tray()) {
272         event->ignore();
273         hide();
274     }
275 }
276 
277 void
iconActivated(QSystemTrayIcon::ActivationReason reason)278 MainWindow::iconActivated(QSystemTrayIcon::ActivationReason reason)
279 {
280     switch (reason) {
281     case QSystemTrayIcon::Trigger:
282         if (!isVisible()) {
283             show();
284         } else {
285             hide();
286         }
287         break;
288     default:
289         break;
290     }
291 }
292 
293 bool
hasActiveUser()294 MainWindow::hasActiveUser()
295 {
296     auto settings = userSettings_->qsettings();
297     QString prefix;
298     if (userSettings_->profile() != "")
299         prefix = "profile/" + userSettings_->profile() + "/";
300 
301     return settings->contains(prefix + "auth/access_token") &&
302            settings->contains(prefix + "auth/home_server") &&
303            settings->contains(prefix + "auth/user_id");
304 }
305 
306 void
showOverlayProgressBar()307 MainWindow::showOverlayProgressBar()
308 {
309     spinner_ = new LoadingIndicator(this);
310     spinner_->setFixedHeight(100);
311     spinner_->setFixedWidth(100);
312     spinner_->setObjectName("ChatPageLoadSpinner");
313     spinner_->start();
314 
315     showSolidOverlayModal(spinner_);
316 }
317 
318 void
openCreateRoomDialog(std::function<void (const mtx::requests::CreateRoom & request)> callback)319 MainWindow::openCreateRoomDialog(
320   std::function<void(const mtx::requests::CreateRoom &request)> callback)
321 {
322     auto dialog = new dialogs::CreateRoom(this);
323     connect(dialog,
324             &dialogs::CreateRoom::createRoom,
325             this,
326             [callback](const mtx::requests::CreateRoom &request) { callback(request); });
327 
328     showDialog(dialog);
329 }
330 
331 void
showTransparentOverlayModal(QWidget * content,QFlags<Qt::AlignmentFlag> flags)332 MainWindow::showTransparentOverlayModal(QWidget *content, QFlags<Qt::AlignmentFlag> flags)
333 {
334     modal_->setWidget(content);
335     modal_->setColor(QColor(30, 30, 30, 150));
336     modal_->setDismissible(true);
337     modal_->setContentAlignment(flags);
338     modal_->raise();
339     modal_->show();
340 }
341 
342 void
showSolidOverlayModal(QWidget * content,QFlags<Qt::AlignmentFlag> flags)343 MainWindow::showSolidOverlayModal(QWidget *content, QFlags<Qt::AlignmentFlag> flags)
344 {
345     modal_->setWidget(content);
346     modal_->setColor(QColor(30, 30, 30));
347     modal_->setDismissible(false);
348     modal_->setContentAlignment(flags);
349     modal_->raise();
350     modal_->show();
351 }
352 
353 bool
hasActiveDialogs() const354 MainWindow::hasActiveDialogs() const
355 {
356     return !modal_ && modal_->isVisible();
357 }
358 
359 bool
pageSupportsTray() const360 MainWindow::pageSupportsTray() const
361 {
362     return !welcome_page_->isVisible() && !login_page_->isVisible() && !register_page_->isVisible();
363 }
364 
365 void
hideOverlay()366 MainWindow::hideOverlay()
367 {
368     if (modal_)
369         modal_->hide();
370 }
371 
372 inline void
showDialog(QWidget * dialog)373 MainWindow::showDialog(QWidget *dialog)
374 {
375     utils::centerWidget(dialog, this);
376     dialog->raise();
377     dialog->show();
378 }
379 
380 void
showWelcomePage()381 MainWindow::showWelcomePage()
382 {
383     removeOverlayProgressBar();
384     pageStack_->addWidget(welcome_page_);
385     pageStack_->setCurrentWidget(welcome_page_);
386 }
387 
388 void
showLoginPage()389 MainWindow::showLoginPage()
390 {
391     if (modal_)
392         modal_->hide();
393 
394     pageStack_->addWidget(login_page_);
395     pageStack_->setCurrentWidget(login_page_);
396 }
397 
398 void
showRegisterPage()399 MainWindow::showRegisterPage()
400 {
401     pageStack_->addWidget(register_page_);
402     pageStack_->setCurrentWidget(register_page_);
403 }
404 
405 void
showUserSettingsPage()406 MainWindow::showUserSettingsPage()
407 {
408     pageStack_->setCurrentWidget(userSettingsPage_);
409 }
410