From 0e814da91c8e041897a4c3f7e6e9234bbc7c6f7a Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Tue, 17 Jul 2018 16:37:25 +0300 Subject: Move all files under src/ --- src/AvatarProvider.cc | 72 -- src/AvatarProvider.cpp | 72 ++ src/AvatarProvider.h | 36 + src/Cache.cc | 1786 ---------------------------------- src/Cache.cpp | 1785 +++++++++++++++++++++++++++++++++ src/Cache.h | 661 +++++++++++++ src/ChatPage.cc | 1347 ------------------------- src/ChatPage.cpp | 1347 +++++++++++++++++++++++++ src/ChatPage.h | 268 +++++ src/CommunitiesList.cc | 195 ---- src/CommunitiesList.cpp | 195 ++++ src/CommunitiesList.h | 50 + src/CommunitiesListItem.cc | 108 -- src/CommunitiesListItem.cpp | 108 ++ src/CommunitiesListItem.h | 88 ++ src/Config.h | 106 ++ src/InviteeItem.cc | 37 - src/InviteeItem.cpp | 37 + src/InviteeItem.h | 27 + src/Logging.cpp | 2 +- src/Logging.h | 21 + src/LoginPage.cc | 318 ------ src/LoginPage.cpp | 318 ++++++ src/LoginPage.h | 124 +++ src/MainWindow.cc | 511 ---------- src/MainWindow.cpp | 511 ++++++++++ src/MainWindow.h | 174 ++++ src/MatrixClient.cc | 38 - src/MatrixClient.cpp | 38 + src/MatrixClient.h | 30 + src/Olm.cpp | 4 +- src/Olm.h | 86 ++ src/QuickSwitcher.cc | 138 --- src/QuickSwitcher.cpp | 139 +++ src/QuickSwitcher.h | 79 ++ src/RegisterPage.cc | 267 ----- src/RegisterPage.cpp | 267 +++++ src/RegisterPage.h | 84 ++ src/RoomInfoListItem.cc | 390 -------- src/RoomInfoListItem.cpp | 390 ++++++++ src/RoomInfoListItem.h | 204 ++++ src/RoomList.cc | 440 --------- src/RoomList.cpp | 440 +++++++++ src/RoomList.h | 108 ++ src/RunGuard.cc | 84 -- src/RunGuard.cpp | 84 ++ src/RunGuard.h | 31 + src/SideBarActions.cc | 103 -- src/SideBarActions.cpp | 105 ++ src/SideBarActions.h | 50 + src/Splitter.cc | 168 ---- src/Splitter.cpp | 168 ++++ src/Splitter.h | 46 + src/SuggestionsPopup.cpp | 13 +- src/SuggestionsPopup.h | 147 +++ src/TextInputWidget.cc | 629 ------------ src/TextInputWidget.cpp | 631 ++++++++++++ src/TextInputWidget.h | 183 ++++ src/TopRoomBar.cc | 184 ---- src/TopRoomBar.cpp | 184 ++++ src/TopRoomBar.h | 107 ++ src/TrayIcon.cc | 153 --- src/TrayIcon.cpp | 153 +++ src/TrayIcon.h | 59 ++ src/TypingDisplay.cc | 54 - src/TypingDisplay.cpp | 54 + src/TypingDisplay.h | 21 + src/UserInfoWidget.cc | 165 ---- src/UserInfoWidget.cpp | 165 ++++ src/UserInfoWidget.h | 73 ++ src/UserSettingsPage.cc | 323 ------ src/UserSettingsPage.cpp | 323 ++++++ src/UserSettingsPage.h | 148 +++ src/Utils.cc | 188 ---- src/Utils.cpp | 188 ++++ src/Utils.h | 194 ++++ src/WelcomePage.cc | 95 -- src/WelcomePage.cpp | 95 ++ src/WelcomePage.h | 44 + src/dialogs/CreateRoom.cc | 162 --- src/dialogs/CreateRoom.cpp | 162 +++ src/dialogs/CreateRoom.h | 45 + src/dialogs/ImageOverlay.cc | 105 -- src/dialogs/ImageOverlay.cpp | 106 ++ src/dialogs/ImageOverlay.h | 47 + src/dialogs/InviteUsers.cc | 157 --- src/dialogs/InviteUsers.cpp | 157 +++ src/dialogs/InviteUsers.h | 42 + src/dialogs/JoinRoom.cc | 69 -- src/dialogs/JoinRoom.cpp | 69 ++ src/dialogs/JoinRoom.h | 30 + src/dialogs/LeaveRoom.cc | 56 -- src/dialogs/LeaveRoom.cpp | 56 ++ src/dialogs/LeaveRoom.h | 25 + src/dialogs/Logout.cc | 74 -- src/dialogs/Logout.cpp | 74 ++ src/dialogs/Logout.h | 42 + src/dialogs/MemberList.cpp | 10 +- src/dialogs/MemberList.h | 61 ++ src/dialogs/PreviewUploadOverlay.cc | 177 ---- src/dialogs/PreviewUploadOverlay.cpp | 177 ++++ src/dialogs/PreviewUploadOverlay.h | 61 ++ src/dialogs/ReCaptcha.cpp | 10 +- src/dialogs/ReCaptcha.h | 28 + src/dialogs/ReadReceipts.cc | 134 --- src/dialogs/ReadReceipts.cpp | 134 +++ src/dialogs/ReadReceipts.h | 58 ++ src/dialogs/RoomSettings.cpp | 27 +- src/dialogs/RoomSettings.h | 126 +++ src/emoji/Category.cc | 90 -- src/emoji/Category.cpp | 90 ++ src/emoji/Category.h | 59 ++ src/emoji/ItemDelegate.cc | 49 - src/emoji/ItemDelegate.cpp | 49 + src/emoji/ItemDelegate.h | 43 + src/emoji/Panel.cc | 236 ----- src/emoji/Panel.cpp | 236 +++++ src/emoji/Panel.h | 66 ++ src/emoji/PickButton.cc | 72 -- src/emoji/PickButton.cpp | 72 ++ src/emoji/PickButton.h | 53 + src/emoji/Provider.cc | 1397 -------------------------- src/emoji/Provider.cpp | 1397 ++++++++++++++++++++++++++ src/emoji/Provider.h | 45 + src/main.cc | 213 ---- src/main.cpp | 213 ++++ src/notifications/Manager.h | 55 ++ src/timeline/TimelineItem.cc | 734 -------------- src/timeline/TimelineItem.cpp | 734 ++++++++++++++ src/timeline/TimelineItem.h | 380 ++++++++ src/timeline/TimelineView.cc | 1459 --------------------------- src/timeline/TimelineView.cpp | 1459 +++++++++++++++++++++++++++ src/timeline/TimelineView.h | 426 ++++++++ src/timeline/TimelineViewManager.cc | 318 ------ src/timeline/TimelineViewManager.cpp | 318 ++++++ src/timeline/TimelineViewManager.h | 94 ++ src/timeline/widgets/AudioItem.cc | 230 ----- src/timeline/widgets/AudioItem.cpp | 230 +++++ src/timeline/widgets/AudioItem.h | 107 ++ src/timeline/widgets/FileItem.cc | 216 ---- src/timeline/widgets/FileItem.cpp | 216 ++++ src/timeline/widgets/FileItem.h | 82 ++ src/timeline/widgets/ImageItem.cc | 259 ----- src/timeline/widgets/ImageItem.cpp | 259 +++++ src/timeline/widgets/ImageItem.h | 108 ++ src/timeline/widgets/VideoItem.cc | 66 -- src/timeline/widgets/VideoItem.cpp | 66 ++ src/timeline/widgets/VideoItem.h | 51 + src/ui/Avatar.cc | 147 --- src/ui/Avatar.cpp | 147 +++ src/ui/Avatar.h | 47 + src/ui/Badge.cc | 218 ----- src/ui/Badge.cpp | 218 +++++ src/ui/Badge.h | 62 ++ src/ui/DropShadow.h | 111 +++ src/ui/FlatButton.cc | 719 -------------- src/ui/FlatButton.cpp | 719 ++++++++++++++ src/ui/FlatButton.h | 185 ++++ src/ui/FloatingButton.cc | 95 -- src/ui/FloatingButton.cpp | 95 ++ src/ui/FloatingButton.h | 26 + src/ui/InfoMessage.cpp | 2 +- src/ui/InfoMessage.h | 47 + src/ui/Label.cc | 44 - src/ui/Label.cpp | 44 + src/ui/Label.h | 25 + src/ui/LoadingIndicator.cc | 85 -- src/ui/LoadingIndicator.cpp | 85 ++ src/ui/LoadingIndicator.h | 38 + src/ui/Menu.h | 32 + src/ui/OverlayModal.cc | 60 -- src/ui/OverlayModal.cpp | 60 ++ src/ui/OverlayModal.h | 45 + src/ui/OverlayWidget.cc | 72 -- src/ui/OverlayWidget.cpp | 72 ++ src/ui/OverlayWidget.h | 21 + src/ui/Painter.h | 161 +++ src/ui/RaisedButton.cc | 89 -- src/ui/RaisedButton.cpp | 89 ++ src/ui/RaisedButton.h | 28 + src/ui/Ripple.cc | 107 -- src/ui/Ripple.cpp | 107 ++ src/ui/Ripple.h | 145 +++ src/ui/RippleOverlay.cc | 62 -- src/ui/RippleOverlay.cpp | 62 ++ src/ui/RippleOverlay.h | 57 ++ src/ui/ScrollBar.cc | 59 -- src/ui/ScrollBar.cpp | 59 ++ src/ui/ScrollBar.h | 54 + src/ui/SnackBar.cc | 141 --- src/ui/SnackBar.cpp | 141 +++ src/ui/SnackBar.h | 79 ++ src/ui/TextField.cc | 363 ------- src/ui/TextField.cpp | 363 +++++++ src/ui/TextField.h | 174 ++++ src/ui/Theme.cc | 73 -- src/ui/Theme.cpp | 73 ++ src/ui/Theme.h | 97 ++ src/ui/ThemeManager.cc | 19 - src/ui/ThemeManager.cpp | 19 + src/ui/ThemeManager.h | 31 + src/ui/ToggleButton.cc | 211 ---- src/ui/ToggleButton.cpp | 211 ++++ src/ui/ToggleButton.h | 110 +++ 204 files changed, 23627 insertions(+), 16664 deletions(-) delete mode 100644 src/AvatarProvider.cc create mode 100644 src/AvatarProvider.cpp create mode 100644 src/AvatarProvider.h delete mode 100644 src/Cache.cc create mode 100644 src/Cache.cpp create mode 100644 src/Cache.h delete mode 100644 src/ChatPage.cc create mode 100644 src/ChatPage.cpp create mode 100644 src/ChatPage.h delete mode 100644 src/CommunitiesList.cc create mode 100644 src/CommunitiesList.cpp create mode 100644 src/CommunitiesList.h delete mode 100644 src/CommunitiesListItem.cc create mode 100644 src/CommunitiesListItem.cpp create mode 100644 src/CommunitiesListItem.h create mode 100644 src/Config.h delete mode 100644 src/InviteeItem.cc create mode 100644 src/InviteeItem.cpp create mode 100644 src/InviteeItem.h create mode 100644 src/Logging.h delete mode 100644 src/LoginPage.cc create mode 100644 src/LoginPage.cpp create mode 100644 src/LoginPage.h delete mode 100644 src/MainWindow.cc create mode 100644 src/MainWindow.cpp create mode 100644 src/MainWindow.h delete mode 100644 src/MatrixClient.cc create mode 100644 src/MatrixClient.cpp create mode 100644 src/MatrixClient.h create mode 100644 src/Olm.h delete mode 100644 src/QuickSwitcher.cc create mode 100644 src/QuickSwitcher.cpp create mode 100644 src/QuickSwitcher.h delete mode 100644 src/RegisterPage.cc create mode 100644 src/RegisterPage.cpp create mode 100644 src/RegisterPage.h delete mode 100644 src/RoomInfoListItem.cc create mode 100644 src/RoomInfoListItem.cpp create mode 100644 src/RoomInfoListItem.h delete mode 100644 src/RoomList.cc create mode 100644 src/RoomList.cpp create mode 100644 src/RoomList.h delete mode 100644 src/RunGuard.cc create mode 100644 src/RunGuard.cpp create mode 100644 src/RunGuard.h delete mode 100644 src/SideBarActions.cc create mode 100644 src/SideBarActions.cpp create mode 100644 src/SideBarActions.h delete mode 100644 src/Splitter.cc create mode 100644 src/Splitter.cpp create mode 100644 src/Splitter.h create mode 100644 src/SuggestionsPopup.h delete mode 100644 src/TextInputWidget.cc create mode 100644 src/TextInputWidget.cpp create mode 100644 src/TextInputWidget.h delete mode 100644 src/TopRoomBar.cc create mode 100644 src/TopRoomBar.cpp create mode 100644 src/TopRoomBar.h delete mode 100644 src/TrayIcon.cc create mode 100644 src/TrayIcon.cpp create mode 100644 src/TrayIcon.h delete mode 100644 src/TypingDisplay.cc create mode 100644 src/TypingDisplay.cpp create mode 100644 src/TypingDisplay.h delete mode 100644 src/UserInfoWidget.cc create mode 100644 src/UserInfoWidget.cpp create mode 100644 src/UserInfoWidget.h delete mode 100644 src/UserSettingsPage.cc create mode 100644 src/UserSettingsPage.cpp create mode 100644 src/UserSettingsPage.h delete mode 100644 src/Utils.cc create mode 100644 src/Utils.cpp create mode 100644 src/Utils.h delete mode 100644 src/WelcomePage.cc create mode 100644 src/WelcomePage.cpp create mode 100644 src/WelcomePage.h delete mode 100644 src/dialogs/CreateRoom.cc create mode 100644 src/dialogs/CreateRoom.cpp create mode 100644 src/dialogs/CreateRoom.h delete mode 100644 src/dialogs/ImageOverlay.cc create mode 100644 src/dialogs/ImageOverlay.cpp create mode 100644 src/dialogs/ImageOverlay.h delete mode 100644 src/dialogs/InviteUsers.cc create mode 100644 src/dialogs/InviteUsers.cpp create mode 100644 src/dialogs/InviteUsers.h delete mode 100644 src/dialogs/JoinRoom.cc create mode 100644 src/dialogs/JoinRoom.cpp create mode 100644 src/dialogs/JoinRoom.h delete mode 100644 src/dialogs/LeaveRoom.cc create mode 100644 src/dialogs/LeaveRoom.cpp create mode 100644 src/dialogs/LeaveRoom.h delete mode 100644 src/dialogs/Logout.cc create mode 100644 src/dialogs/Logout.cpp create mode 100644 src/dialogs/Logout.h create mode 100644 src/dialogs/MemberList.h delete mode 100644 src/dialogs/PreviewUploadOverlay.cc create mode 100644 src/dialogs/PreviewUploadOverlay.cpp create mode 100644 src/dialogs/PreviewUploadOverlay.h create mode 100644 src/dialogs/ReCaptcha.h delete mode 100644 src/dialogs/ReadReceipts.cc create mode 100644 src/dialogs/ReadReceipts.cpp create mode 100644 src/dialogs/ReadReceipts.h create mode 100644 src/dialogs/RoomSettings.h delete mode 100644 src/emoji/Category.cc create mode 100644 src/emoji/Category.cpp create mode 100644 src/emoji/Category.h delete mode 100644 src/emoji/ItemDelegate.cc create mode 100644 src/emoji/ItemDelegate.cpp create mode 100644 src/emoji/ItemDelegate.h delete mode 100644 src/emoji/Panel.cc create mode 100644 src/emoji/Panel.cpp create mode 100644 src/emoji/Panel.h delete mode 100644 src/emoji/PickButton.cc create mode 100644 src/emoji/PickButton.cpp create mode 100644 src/emoji/PickButton.h delete mode 100644 src/emoji/Provider.cc create mode 100644 src/emoji/Provider.cpp create mode 100644 src/emoji/Provider.h delete mode 100644 src/main.cc create mode 100644 src/main.cpp create mode 100644 src/notifications/Manager.h delete mode 100644 src/timeline/TimelineItem.cc create mode 100644 src/timeline/TimelineItem.cpp create mode 100644 src/timeline/TimelineItem.h delete mode 100644 src/timeline/TimelineView.cc create mode 100644 src/timeline/TimelineView.cpp create mode 100644 src/timeline/TimelineView.h delete mode 100644 src/timeline/TimelineViewManager.cc create mode 100644 src/timeline/TimelineViewManager.cpp create mode 100644 src/timeline/TimelineViewManager.h delete mode 100644 src/timeline/widgets/AudioItem.cc create mode 100644 src/timeline/widgets/AudioItem.cpp create mode 100644 src/timeline/widgets/AudioItem.h delete mode 100644 src/timeline/widgets/FileItem.cc create mode 100644 src/timeline/widgets/FileItem.cpp create mode 100644 src/timeline/widgets/FileItem.h delete mode 100644 src/timeline/widgets/ImageItem.cc create mode 100644 src/timeline/widgets/ImageItem.cpp create mode 100644 src/timeline/widgets/ImageItem.h delete mode 100644 src/timeline/widgets/VideoItem.cc create mode 100644 src/timeline/widgets/VideoItem.cpp create mode 100644 src/timeline/widgets/VideoItem.h delete mode 100644 src/ui/Avatar.cc create mode 100644 src/ui/Avatar.cpp create mode 100644 src/ui/Avatar.h delete mode 100644 src/ui/Badge.cc create mode 100644 src/ui/Badge.cpp create mode 100644 src/ui/Badge.h create mode 100644 src/ui/DropShadow.h delete mode 100644 src/ui/FlatButton.cc create mode 100644 src/ui/FlatButton.cpp create mode 100644 src/ui/FlatButton.h delete mode 100644 src/ui/FloatingButton.cc create mode 100644 src/ui/FloatingButton.cpp create mode 100644 src/ui/FloatingButton.h create mode 100644 src/ui/InfoMessage.h delete mode 100644 src/ui/Label.cc create mode 100644 src/ui/Label.cpp create mode 100644 src/ui/Label.h delete mode 100644 src/ui/LoadingIndicator.cc create mode 100644 src/ui/LoadingIndicator.cpp create mode 100644 src/ui/LoadingIndicator.h create mode 100644 src/ui/Menu.h delete mode 100644 src/ui/OverlayModal.cc create mode 100644 src/ui/OverlayModal.cpp create mode 100644 src/ui/OverlayModal.h delete mode 100644 src/ui/OverlayWidget.cc create mode 100644 src/ui/OverlayWidget.cpp create mode 100644 src/ui/OverlayWidget.h create mode 100644 src/ui/Painter.h delete mode 100644 src/ui/RaisedButton.cc create mode 100644 src/ui/RaisedButton.cpp create mode 100644 src/ui/RaisedButton.h delete mode 100644 src/ui/Ripple.cc create mode 100644 src/ui/Ripple.cpp create mode 100644 src/ui/Ripple.h delete mode 100644 src/ui/RippleOverlay.cc create mode 100644 src/ui/RippleOverlay.cpp create mode 100644 src/ui/RippleOverlay.h delete mode 100644 src/ui/ScrollBar.cc create mode 100644 src/ui/ScrollBar.cpp create mode 100644 src/ui/ScrollBar.h delete mode 100644 src/ui/SnackBar.cc create mode 100644 src/ui/SnackBar.cpp create mode 100644 src/ui/SnackBar.h delete mode 100644 src/ui/TextField.cc create mode 100644 src/ui/TextField.cpp create mode 100644 src/ui/TextField.h delete mode 100644 src/ui/Theme.cc create mode 100644 src/ui/Theme.cpp create mode 100644 src/ui/Theme.h delete mode 100644 src/ui/ThemeManager.cc create mode 100644 src/ui/ThemeManager.cpp create mode 100644 src/ui/ThemeManager.h delete mode 100644 src/ui/ToggleButton.cc create mode 100644 src/ui/ToggleButton.cpp create mode 100644 src/ui/ToggleButton.h (limited to 'src') diff --git a/src/AvatarProvider.cc b/src/AvatarProvider.cc deleted file mode 100644 index b4c1188a..00000000 --- a/src/AvatarProvider.cc +++ /dev/null @@ -1,72 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include - -#include "AvatarProvider.h" -#include "Cache.h" -#include "Logging.hpp" -#include "MatrixClient.h" - -namespace AvatarProvider { - -void -resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback callback) -{ - const auto key = QString("%1 %2").arg(room_id).arg(user_id); - const auto avatarUrl = Cache::avatarUrl(room_id, user_id); - - if (!Cache::AvatarUrls.contains(key) || !cache::client()) - return; - - if (avatarUrl.isEmpty()) - return; - - auto data = cache::client()->image(avatarUrl); - if (!data.isNull()) { - callback(QImage::fromData(data)); - return; - } - - auto proxy = std::make_shared(); - QObject::connect(proxy.get(), - &AvatarProxy::avatarDownloaded, - receiver, - [callback](const QByteArray &data) { callback(QImage::fromData(data)); }); - - mtx::http::ThumbOpts opts; - opts.mxc_url = avatarUrl.toStdString(); - - http::client()->get_thumbnail( - opts, - [opts, proxy = std::move(proxy)](const std::string &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to download avatar: {} - ({} {})", - opts.mxc_url, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - cache::client()->saveImage(opts.mxc_url, res); - - auto data = QByteArray(res.data(), res.size()); - emit proxy->avatarDownloaded(data); - }); -} -} diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp new file mode 100644 index 00000000..dbfc1945 --- /dev/null +++ b/src/AvatarProvider.cpp @@ -0,0 +1,72 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "AvatarProvider.h" +#include "Cache.h" +#include "Logging.h" +#include "MatrixClient.h" + +namespace AvatarProvider { + +void +resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback callback) +{ + const auto key = QString("%1 %2").arg(room_id).arg(user_id); + const auto avatarUrl = Cache::avatarUrl(room_id, user_id); + + if (!Cache::AvatarUrls.contains(key) || !cache::client()) + return; + + if (avatarUrl.isEmpty()) + return; + + auto data = cache::client()->image(avatarUrl); + if (!data.isNull()) { + callback(QImage::fromData(data)); + return; + } + + auto proxy = std::make_shared(); + QObject::connect(proxy.get(), + &AvatarProxy::avatarDownloaded, + receiver, + [callback](const QByteArray &data) { callback(QImage::fromData(data)); }); + + mtx::http::ThumbOpts opts; + opts.mxc_url = avatarUrl.toStdString(); + + http::client()->get_thumbnail( + opts, + [opts, proxy = std::move(proxy)](const std::string &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to download avatar: {} - ({} {})", + opts.mxc_url, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } + + cache::client()->saveImage(opts.mxc_url, res); + + auto data = QByteArray(res.data(), res.size()); + emit proxy->avatarDownloaded(data); + }); +} +} diff --git a/src/AvatarProvider.h b/src/AvatarProvider.h new file mode 100644 index 00000000..4b4e15e9 --- /dev/null +++ b/src/AvatarProvider.h @@ -0,0 +1,36 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +class AvatarProxy : public QObject +{ + Q_OBJECT + +signals: + void avatarDownloaded(const QByteArray &data); +}; + +using AvatarCallback = std::function; + +namespace AvatarProvider { +void +resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback cb); +} diff --git a/src/Cache.cc b/src/Cache.cc deleted file mode 100644 index 614e8a90..00000000 --- a/src/Cache.cc +++ /dev/null @@ -1,1786 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include - -#include -#include -#include -#include -#include - -#include -#include - -#include "Cache.h" -#include "Logging.hpp" -#include "Utils.h" - -//! Should be changed when a breaking change occurs in the cache format. -//! This will reset client's data. -static const std::string CURRENT_CACHE_FORMAT_VERSION("2018.06.10"); -static const std::string SECRET("secret"); - -static const lmdb::val NEXT_BATCH_KEY("next_batch"); -static const lmdb::val OLM_ACCOUNT_KEY("olm_account"); -static const lmdb::val CACHE_FORMAT_VERSION_KEY("cache_format_version"); - -constexpr size_t MAX_RESTORED_MESSAGES = 30; - -//! Cache databases and their format. -//! -//! Contains UI information for the joined rooms. (i.e name, topic, avatar url etc). -//! Format: room_id -> RoomInfo -constexpr auto ROOMS_DB("rooms"); -constexpr auto INVITES_DB("invites"); -//! Keeps already downloaded media for reuse. -//! Format: matrix_url -> binary data. -constexpr auto MEDIA_DB("media"); -//! Information that must be kept between sync requests. -constexpr auto SYNC_STATE_DB("sync_state"); -//! Read receipts per room/event. -constexpr auto READ_RECEIPTS_DB("read_receipts"); -constexpr auto NOTIFICATIONS_DB("sent_notifications"); - -//! Encryption related databases. - -//! user_id -> list of devices -constexpr auto DEVICES_DB("devices"); -//! device_id -> device keys -constexpr auto DEVICE_KEYS_DB("device_keys"); -//! room_ids that have encryption enabled. -constexpr auto ENCRYPTED_ROOMS_DB("encrypted_rooms"); - -//! room_id -> pickled OlmInboundGroupSession -constexpr auto INBOUND_MEGOLM_SESSIONS_DB("inbound_megolm_sessions"); -//! MegolmSessionIndex -> pickled OlmOutboundGroupSession -constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions"); - -using CachedReceipts = std::multimap>; -using Receipts = std::map>; - -namespace { -std::unique_ptr instance_ = nullptr; -} - -namespace cache { -void -init(const QString &user_id) -{ - qRegisterMetaType(); - qRegisterMetaType>(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType>(); - qRegisterMetaType>(); - qRegisterMetaType>(); - - instance_ = std::make_unique(user_id); -} - -Cache * -client() -{ - return instance_.get(); -} -} // namespace cache - -Cache::Cache(const QString &userId, QObject *parent) - : QObject{parent} - , env_{nullptr} - , syncStateDb_{0} - , roomsDb_{0} - , invitesDb_{0} - , mediaDb_{0} - , readReceiptsDb_{0} - , notificationsDb_{0} - , devicesDb_{0} - , deviceKeysDb_{0} - , inboundMegolmSessionDb_{0} - , outboundMegolmSessionDb_{0} - , localUserId_{userId} -{ - setup(); -} - -void -Cache::setup() -{ - nhlog::db()->debug("setting up cache"); - - auto statePath = QString("%1/%2") - .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(QString::fromUtf8(localUserId_.toUtf8().toHex())); - - cacheDirectory_ = QString("%1/%2") - .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(QString::fromUtf8(localUserId_.toUtf8().toHex())); - - bool isInitial = !QFile::exists(statePath); - - env_ = lmdb::env::create(); - env_.set_mapsize(256UL * 1024UL * 1024UL); /* 256 MB */ - env_.set_max_dbs(1024UL); - - if (isInitial) { - nhlog::db()->info("initializing LMDB"); - - if (!QDir().mkpath(statePath)) { - throw std::runtime_error( - ("Unable to create state directory:" + statePath).toStdString().c_str()); - } - } - - try { - env_.open(statePath.toStdString().c_str()); - } catch (const lmdb::error &e) { - if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) { - throw std::runtime_error("LMDB initialization failed" + - std::string(e.what())); - } - - nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what()); - - QDir stateDir(statePath); - - for (const auto &file : stateDir.entryList(QDir::NoDotAndDotDot)) { - if (!stateDir.remove(file)) - throw std::runtime_error( - ("Unable to delete file " + file).toStdString().c_str()); - } - - env_.open(statePath.toStdString().c_str()); - } - - auto txn = lmdb::txn::begin(env_); - syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE); - roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE); - invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE); - mediaDb_ = lmdb::dbi::open(txn, MEDIA_DB, MDB_CREATE); - readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE); - notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE); - - // Device management - devicesDb_ = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE); - deviceKeysDb_ = lmdb::dbi::open(txn, DEVICE_KEYS_DB, MDB_CREATE); - - // Session management - inboundMegolmSessionDb_ = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); - outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); - - txn.commit(); -} - -void -Cache::setEncryptedRoom(lmdb::txn &txn, const std::string &room_id) -{ - nhlog::db()->info("mark room {} as encrypted", room_id); - - auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); - lmdb::dbi_put(txn, db, lmdb::val(room_id), lmdb::val("0")); -} - -bool -Cache::isRoomEncrypted(const std::string &room_id) -{ - lmdb::val unused; - - auto txn = lmdb::txn::begin(env_); - auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); - auto res = lmdb::dbi_get(txn, db, lmdb::val(room_id), unused); - txn.commit(); - - return res; -} - -// -// Device Management -// - -// -// Session Management -// - -void -Cache::saveInboundMegolmSession(const MegolmSessionIndex &index, - mtx::crypto::InboundGroupSessionPtr session) -{ - using namespace mtx::crypto; - const auto key = index.to_hash(); - const auto pickled = pickle(session.get(), SECRET); - - auto txn = lmdb::txn::begin(env_); - lmdb::dbi_put(txn, inboundMegolmSessionDb_, lmdb::val(key), lmdb::val(pickled)); - txn.commit(); - - { - std::unique_lock lock(session_storage.group_inbound_mtx); - session_storage.group_inbound_sessions[key] = std::move(session); - } -} - -OlmInboundGroupSession * -Cache::getInboundMegolmSession(const MegolmSessionIndex &index) -{ - std::unique_lock lock(session_storage.group_inbound_mtx); - return session_storage.group_inbound_sessions[index.to_hash()].get(); -} - -bool -Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept -{ - std::unique_lock lock(session_storage.group_inbound_mtx); - return session_storage.group_inbound_sessions.find(index.to_hash()) != - session_storage.group_inbound_sessions.end(); -} - -void -Cache::updateOutboundMegolmSession(const std::string &room_id, int message_index) -{ - using namespace mtx::crypto; - - if (!outboundMegolmSessionExists(room_id)) - return; - - OutboundGroupSessionData data; - OlmOutboundGroupSession *session; - { - std::unique_lock lock(session_storage.group_outbound_mtx); - data = session_storage.group_outbound_session_data[room_id]; - session = session_storage.group_outbound_sessions[room_id].get(); - - // Update with the current message. - data.message_index = message_index; - session_storage.group_outbound_session_data[room_id] = data; - } - - // Save the updated pickled data for the session. - json j; - j["data"] = data; - j["session"] = pickle(session, SECRET); - - auto txn = lmdb::txn::begin(env_); - lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(room_id), lmdb::val(j.dump())); - txn.commit(); -} - -void -Cache::saveOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, - mtx::crypto::OutboundGroupSessionPtr session) -{ - using namespace mtx::crypto; - const auto pickled = pickle(session.get(), SECRET); - - json j; - j["data"] = data; - j["session"] = pickled; - - auto txn = lmdb::txn::begin(env_); - lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(room_id), lmdb::val(j.dump())); - txn.commit(); - - { - std::unique_lock lock(session_storage.group_outbound_mtx); - session_storage.group_outbound_session_data[room_id] = data; - session_storage.group_outbound_sessions[room_id] = std::move(session); - } -} - -bool -Cache::outboundMegolmSessionExists(const std::string &room_id) noexcept -{ - std::unique_lock lock(session_storage.group_outbound_mtx); - return (session_storage.group_outbound_sessions.find(room_id) != - session_storage.group_outbound_sessions.end()) && - (session_storage.group_outbound_session_data.find(room_id) != - session_storage.group_outbound_session_data.end()); -} - -OutboundGroupSessionDataRef -Cache::getOutboundMegolmSession(const std::string &room_id) -{ - std::unique_lock lock(session_storage.group_outbound_mtx); - return OutboundGroupSessionDataRef{session_storage.group_outbound_sessions[room_id].get(), - session_storage.group_outbound_session_data[room_id]}; -} - -// -// OLM sessions. -// - -void -Cache::saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session) -{ - using namespace mtx::crypto; - - auto txn = lmdb::txn::begin(env_); - auto db = getOlmSessionsDb(txn, curve25519); - - const auto pickled = pickle(session.get(), SECRET); - const auto session_id = mtx::crypto::session_id(session.get()); - - lmdb::dbi_put(txn, db, lmdb::val(session_id), lmdb::val(pickled)); - - txn.commit(); -} - -boost::optional -Cache::getOlmSession(const std::string &curve25519, const std::string &session_id) -{ - using namespace mtx::crypto; - - auto txn = lmdb::txn::begin(env_); - auto db = getOlmSessionsDb(txn, curve25519); - - lmdb::val pickled; - bool found = lmdb::dbi_get(txn, db, lmdb::val(session_id), pickled); - - txn.commit(); - - if (found) { - auto data = std::string(pickled.data(), pickled.size()); - return unpickle(data, SECRET); - } - - return boost::none; -} - -std::vector -Cache::getOlmSessions(const std::string &curve25519) -{ - using namespace mtx::crypto; - - auto txn = lmdb::txn::begin(env_); - auto db = getOlmSessionsDb(txn, curve25519); - - std::string session_id, unused; - std::vector res; - - auto cursor = lmdb::cursor::open(txn, db); - while (cursor.get(session_id, unused, MDB_NEXT)) - res.emplace_back(session_id); - cursor.close(); - - txn.commit(); - - return res; -} - -void -Cache::saveOlmAccount(const std::string &data) -{ - auto txn = lmdb::txn::begin(env_); - lmdb::dbi_put(txn, syncStateDb_, OLM_ACCOUNT_KEY, lmdb::val(data)); - txn.commit(); -} - -void -Cache::restoreSessions() -{ - using namespace mtx::crypto; - - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - std::string key, value; - - // - // Inbound Megolm Sessions - // - { - auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_); - while (cursor.get(key, value, MDB_NEXT)) { - auto session = unpickle(value, SECRET); - session_storage.group_inbound_sessions[key] = std::move(session); - } - cursor.close(); - } - - // - // Outbound Megolm Sessions - // - { - auto cursor = lmdb::cursor::open(txn, outboundMegolmSessionDb_); - while (cursor.get(key, value, MDB_NEXT)) { - json obj; - - try { - obj = json::parse(value); - - session_storage.group_outbound_session_data[key] = - obj.at("data").get(); - - auto session = - unpickle(obj.at("session"), SECRET); - session_storage.group_outbound_sessions[key] = std::move(session); - } catch (const nlohmann::json::exception &e) { - nhlog::db()->critical( - "failed to parse outbound megolm session data: {}", e.what()); - } - } - cursor.close(); - } - - txn.commit(); - - nhlog::db()->info("sessions restored"); -} - -std::string -Cache::restoreOlmAccount() -{ - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - lmdb::val pickled; - lmdb::dbi_get(txn, syncStateDb_, OLM_ACCOUNT_KEY, pickled); - txn.commit(); - - return std::string(pickled.data(), pickled.size()); -} - -// -// Media Management -// - -void -Cache::saveImage(const std::string &url, const std::string &img_data) -{ - if (url.empty() || img_data.empty()) - return; - - try { - auto txn = lmdb::txn::begin(env_); - - lmdb::dbi_put(txn, - mediaDb_, - lmdb::val(url.data(), url.size()), - lmdb::val(img_data.data(), img_data.size())); - - txn.commit(); - } catch (const lmdb::error &e) { - nhlog::db()->critical("saveImage: {}", e.what()); - } -} - -void -Cache::saveImage(const QString &url, const QByteArray &image) -{ - saveImage(url.toStdString(), std::string(image.constData(), image.length())); -} - -QByteArray -Cache::image(lmdb::txn &txn, const std::string &url) const -{ - if (url.empty()) - return QByteArray(); - - try { - lmdb::val image; - bool res = lmdb::dbi_get(txn, mediaDb_, lmdb::val(url), image); - - if (!res) - return QByteArray(); - - return QByteArray(image.data(), image.size()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("image: {}, {}", e.what(), url); - } - - return QByteArray(); -} - -QByteArray -Cache::image(const QString &url) const -{ - if (url.isEmpty()) - return QByteArray(); - - auto key = url.toUtf8(); - - try { - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - - lmdb::val image; - - bool res = lmdb::dbi_get(txn, mediaDb_, lmdb::val(key.data(), key.size()), image); - - txn.commit(); - - if (!res) - return QByteArray(); - - return QByteArray(image.data(), image.size()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("image: {} {}", e.what(), url.toStdString()); - } - - return QByteArray(); -} - -void -Cache::removeInvite(lmdb::txn &txn, const std::string &room_id) -{ - lmdb::dbi_del(txn, invitesDb_, lmdb::val(room_id), nullptr); - lmdb::dbi_drop(txn, getInviteStatesDb(txn, room_id), true); - lmdb::dbi_drop(txn, getInviteMembersDb(txn, room_id), true); -} - -void -Cache::removeInvite(const std::string &room_id) -{ - auto txn = lmdb::txn::begin(env_); - removeInvite(txn, room_id); - txn.commit(); -} - -void -Cache::removeRoom(lmdb::txn &txn, const std::string &roomid) -{ - lmdb::dbi_del(txn, roomsDb_, lmdb::val(roomid), nullptr); - lmdb::dbi_drop(txn, getStatesDb(txn, roomid), true); - lmdb::dbi_drop(txn, getMembersDb(txn, roomid), true); -} - -void -Cache::removeRoom(const std::string &roomid) -{ - auto txn = lmdb::txn::begin(env_, nullptr, 0); - lmdb::dbi_del(txn, roomsDb_, lmdb::val(roomid), nullptr); - txn.commit(); -} - -void -Cache::setNextBatchToken(lmdb::txn &txn, const std::string &token) -{ - lmdb::dbi_put(txn, syncStateDb_, NEXT_BATCH_KEY, lmdb::val(token.data(), token.size())); -} - -void -Cache::setNextBatchToken(lmdb::txn &txn, const QString &token) -{ - setNextBatchToken(txn, token.toStdString()); -} - -bool -Cache::isInitialized() const -{ - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - lmdb::val token; - - bool res = lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token); - - txn.commit(); - - return res; -} - -std::string -Cache::nextBatchToken() const -{ - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - lmdb::val token; - - lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token); - - txn.commit(); - - return std::string(token.data(), token.size()); -} - -void -Cache::deleteData() -{ - // TODO: We need to remove the env_ while not accepting new requests. - if (!cacheDirectory_.isEmpty()) { - QDir(cacheDirectory_).removeRecursively(); - nhlog::db()->info("deleted cache files from disk"); - } -} - -bool -Cache::isFormatValid() -{ - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - - lmdb::val current_version; - bool res = lmdb::dbi_get(txn, syncStateDb_, CACHE_FORMAT_VERSION_KEY, current_version); - - txn.commit(); - - if (!res) - return true; - - std::string stored_version(current_version.data(), current_version.size()); - - if (stored_version != CURRENT_CACHE_FORMAT_VERSION) { - nhlog::db()->warn("breaking changes in the cache format. stored: {}, current: {}", - stored_version, - CURRENT_CACHE_FORMAT_VERSION); - return false; - } - - return true; -} - -void -Cache::setCurrentFormat() -{ - auto txn = lmdb::txn::begin(env_); - - lmdb::dbi_put( - txn, - syncStateDb_, - CACHE_FORMAT_VERSION_KEY, - lmdb::val(CURRENT_CACHE_FORMAT_VERSION.data(), CURRENT_CACHE_FORMAT_VERSION.size())); - - txn.commit(); -} - -CachedReceipts -Cache::readReceipts(const QString &event_id, const QString &room_id) -{ - CachedReceipts receipts; - - ReadReceiptKey receipt_key{event_id.toStdString(), room_id.toStdString()}; - nlohmann::json json_key = receipt_key; - - try { - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - auto key = json_key.dump(); - - lmdb::val value; - - bool res = - lmdb::dbi_get(txn, readReceiptsDb_, lmdb::val(key.data(), key.size()), value); - - txn.commit(); - - if (res) { - auto json_response = json::parse(std::string(value.data(), value.size())); - auto values = json_response.get>(); - - for (const auto &v : values) - // timestamp, user_id - receipts.emplace(v.second, v.first); - } - - } catch (const lmdb::error &e) { - nhlog::db()->critical("readReceipts: {}", e.what()); - } - - return receipts; -} - -void -Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts) -{ - for (const auto &receipt : receipts) { - const auto event_id = receipt.first; - auto event_receipts = receipt.second; - - ReadReceiptKey receipt_key{event_id, room_id}; - nlohmann::json json_key = receipt_key; - - try { - const auto key = json_key.dump(); - - lmdb::val prev_value; - - bool exists = lmdb::dbi_get( - txn, readReceiptsDb_, lmdb::val(key.data(), key.size()), prev_value); - - std::map saved_receipts; - - // If an entry for the event id already exists, we would - // merge the existing receipts with the new ones. - if (exists) { - auto json_value = - json::parse(std::string(prev_value.data(), prev_value.size())); - - // Retrieve the saved receipts. - saved_receipts = json_value.get>(); - } - - // Append the new ones. - for (const auto &event_receipt : event_receipts) - saved_receipts.emplace(event_receipt.first, event_receipt.second); - - // Save back the merged (or only the new) receipts. - nlohmann::json json_updated_value = saved_receipts; - std::string merged_receipts = json_updated_value.dump(); - - lmdb::dbi_put(txn, - readReceiptsDb_, - lmdb::val(key.data(), key.size()), - lmdb::val(merged_receipts.data(), merged_receipts.size())); - - } catch (const lmdb::error &e) { - nhlog::db()->critical("updateReadReceipts: {}", e.what()); - } - } -} - -void -Cache::saveState(const mtx::responses::Sync &res) -{ - auto txn = lmdb::txn::begin(env_); - - setNextBatchToken(txn, res.next_batch); - - // Save joined rooms - for (const auto &room : res.rooms.join) { - auto statesdb = getStatesDb(txn, room.first); - auto membersdb = getMembersDb(txn, room.first); - - saveStateEvents(txn, statesdb, membersdb, room.first, room.second.state.events); - saveStateEvents(txn, statesdb, membersdb, room.first, room.second.timeline.events); - - saveTimelineMessages(txn, room.first, room.second.timeline); - - RoomInfo updatedInfo; - updatedInfo.name = getRoomName(txn, statesdb, membersdb).toStdString(); - updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString(); - updatedInfo.avatar_url = - getRoomAvatarUrl(txn, statesdb, membersdb, QString::fromStdString(room.first)) - .toStdString(); - - lmdb::dbi_put( - txn, roomsDb_, lmdb::val(room.first), lmdb::val(json(updatedInfo).dump())); - - updateReadReceipt(txn, room.first, room.second.ephemeral.receipts); - - // Clean up non-valid invites. - removeInvite(txn, room.first); - } - - saveInvites(txn, res.rooms.invite); - - removeLeftRooms(txn, res.rooms.leave); - - txn.commit(); -} - -void -Cache::saveInvites(lmdb::txn &txn, const std::map &rooms) -{ - for (const auto &room : rooms) { - auto statesdb = getInviteStatesDb(txn, room.first); - auto membersdb = getInviteMembersDb(txn, room.first); - - saveInvite(txn, statesdb, membersdb, room.second); - - RoomInfo updatedInfo; - updatedInfo.name = getInviteRoomName(txn, statesdb, membersdb).toStdString(); - updatedInfo.topic = getInviteRoomTopic(txn, statesdb).toStdString(); - updatedInfo.avatar_url = - getInviteRoomAvatarUrl(txn, statesdb, membersdb).toStdString(); - updatedInfo.is_invite = true; - - lmdb::dbi_put( - txn, invitesDb_, lmdb::val(room.first), lmdb::val(json(updatedInfo).dump())); - } -} - -void -Cache::saveInvite(lmdb::txn &txn, - lmdb::dbi &statesdb, - lmdb::dbi &membersdb, - const mtx::responses::InvitedRoom &room) -{ - using namespace mtx::events; - using namespace mtx::events::state; - - for (const auto &e : room.invite_state) { - if (mpark::holds_alternative>(e)) { - auto msg = mpark::get>(e); - - auto display_name = msg.content.display_name.empty() - ? msg.state_key - : msg.content.display_name; - - MemberInfo tmp{display_name, msg.content.avatar_url}; - - lmdb::dbi_put( - txn, membersdb, lmdb::val(msg.state_key), lmdb::val(json(tmp).dump())); - } else { - mpark::visit( - [&txn, &statesdb](auto msg) { - bool res = lmdb::dbi_put(txn, - statesdb, - lmdb::val(to_string(msg.type)), - lmdb::val(json(msg).dump())); - - if (!res) - std::cout << "couldn't save data" << json(msg).dump() - << '\n'; - }, - e); - } - } -} - -std::vector -Cache::roomsWithStateUpdates(const mtx::responses::Sync &res) -{ - std::vector rooms; - for (const auto &room : res.rooms.join) { - bool hasUpdates = false; - for (const auto &s : room.second.state.events) { - if (containsStateUpdates(s)) { - hasUpdates = true; - break; - } - } - - for (const auto &s : room.second.timeline.events) { - if (containsStateUpdates(s)) { - hasUpdates = true; - break; - } - } - - if (hasUpdates) - rooms.emplace_back(room.first); - } - - for (const auto &room : res.rooms.invite) { - for (const auto &s : room.second.invite_state) { - if (containsStateUpdates(s)) { - rooms.emplace_back(room.first); - break; - } - } - } - - return rooms; -} - -RoomInfo -Cache::singleRoomInfo(const std::string &room_id) -{ - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - auto statesdb = getStatesDb(txn, room_id); - - lmdb::val data; - - // Check if the room is joined. - if (lmdb::dbi_get(txn, roomsDb_, lmdb::val(room_id), data)) { - try { - RoomInfo tmp = json::parse(std::string(data.data(), data.size())); - tmp.member_count = getMembersDb(txn, room_id).size(txn); - tmp.join_rule = getRoomJoinRule(txn, statesdb); - tmp.guest_access = getRoomGuestAccess(txn, statesdb); - - txn.commit(); - - return tmp; - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse room info: room_id ({}), {}", - room_id, - std::string(data.data(), data.size())); - } - } - - txn.commit(); - - return RoomInfo(); -} - -std::map -Cache::getRoomInfo(const std::vector &rooms) -{ - std::map room_info; - - // TODO This should be read only. - auto txn = lmdb::txn::begin(env_); - - for (const auto &room : rooms) { - lmdb::val data; - auto statesdb = getStatesDb(txn, room); - - // Check if the room is joined. - if (lmdb::dbi_get(txn, roomsDb_, lmdb::val(room), data)) { - try { - RoomInfo tmp = json::parse(std::string(data.data(), data.size())); - tmp.member_count = getMembersDb(txn, room).size(txn); - tmp.join_rule = getRoomJoinRule(txn, statesdb); - tmp.guest_access = getRoomGuestAccess(txn, statesdb); - - room_info.emplace(QString::fromStdString(room), std::move(tmp)); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse room info: room_id ({}), {}", - room, - std::string(data.data(), data.size())); - } - } else { - // Check if the room is an invite. - if (lmdb::dbi_get(txn, invitesDb_, lmdb::val(room), data)) { - try { - RoomInfo tmp = - json::parse(std::string(data.data(), data.size())); - tmp.member_count = getInviteMembersDb(txn, room).size(txn); - - room_info.emplace(QString::fromStdString(room), - std::move(tmp)); - } catch (const json::exception &e) { - nhlog::db()->warn( - "failed to parse room info for invite: room_id ({}), {}", - room, - std::string(data.data(), data.size())); - } - } - } - } - - txn.commit(); - - return room_info; -} - -std::map -Cache::roomMessages() -{ - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - - std::map msgs; - std::string room_id, unused; - - auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); - while (roomsCursor.get(room_id, unused, MDB_NEXT)) - msgs.emplace(QString::fromStdString(room_id), mtx::responses::Timeline()); - - roomsCursor.close(); - txn.commit(); - - return msgs; -} - -mtx::responses::Timeline -Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id) -{ - auto db = getMessagesDb(txn, room_id); - - mtx::responses::Timeline timeline; - std::string timestamp, msg; - - auto cursor = lmdb::cursor::open(txn, db); - - size_t index = 0; - - while (cursor.get(timestamp, msg, MDB_NEXT) && index < MAX_RESTORED_MESSAGES) { - auto obj = json::parse(msg); - - if (obj.count("event") == 0 || obj.count("token") == 0) - continue; - - mtx::events::collections::TimelineEvent event; - mtx::events::collections::from_json(obj.at("event"), event); - - index += 1; - - timeline.events.push_back(event.data); - timeline.prev_batch = obj.at("token").get(); - } - cursor.close(); - - std::reverse(timeline.events.begin(), timeline.events.end()); - - return timeline; -} - -QMap -Cache::roomInfo(bool withInvites) -{ - QMap result; - - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - - std::string room_id; - std::string room_data; - - // Gather info about the joined rooms. - auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); - while (roomsCursor.get(room_id, room_data, MDB_NEXT)) { - RoomInfo tmp = json::parse(std::move(room_data)); - tmp.member_count = getMembersDb(txn, room_id).size(txn); - tmp.msgInfo = getLastMessageInfo(txn, room_id); - - result.insert(QString::fromStdString(std::move(room_id)), std::move(tmp)); - } - roomsCursor.close(); - - if (withInvites) { - // Gather info about the invites. - auto invitesCursor = lmdb::cursor::open(txn, invitesDb_); - while (invitesCursor.get(room_id, room_data, MDB_NEXT)) { - RoomInfo tmp = json::parse(room_data); - tmp.member_count = getInviteMembersDb(txn, room_id).size(txn); - result.insert(QString::fromStdString(std::move(room_id)), std::move(tmp)); - } - invitesCursor.close(); - } - - txn.commit(); - - return result; -} - -DescInfo -Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id) -{ - auto db = getMessagesDb(txn, room_id); - - if (db.size(txn) == 0) - return DescInfo{}; - - std::string timestamp, msg; - - QSettings settings; - auto local_user = settings.value("auth/user_id").toString(); - - auto cursor = lmdb::cursor::open(txn, db); - while (cursor.get(timestamp, msg, MDB_NEXT)) { - auto obj = json::parse(msg); - - if (obj.count("event") == 0) - continue; - - mtx::events::collections::TimelineEvent event; - mtx::events::collections::from_json(obj.at("event"), event); - - cursor.close(); - return utils::getMessageDescription( - event.data, local_user, QString::fromStdString(room_id)); - } - cursor.close(); - - return DescInfo{}; -} - -std::map -Cache::invites() -{ - std::map result; - - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - auto cursor = lmdb::cursor::open(txn, invitesDb_); - - std::string room_id, unused; - - while (cursor.get(room_id, unused, MDB_NEXT)) - result.emplace(QString::fromStdString(std::move(room_id)), true); - - cursor.close(); - txn.commit(); - - return result; -} - -QString -Cache::getRoomAvatarUrl(lmdb::txn &txn, - lmdb::dbi &statesdb, - lmdb::dbi &membersdb, - const QString &room_id) -{ - using namespace mtx::events; - using namespace mtx::events::state; - - lmdb::val event; - bool res = lmdb::dbi_get( - txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomAvatar)), event); - - if (res) { - try { - StateEvent msg = - json::parse(std::string(event.data(), event.size())); - - return QString::fromStdString(msg.content.url); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what()); - } - } - - // We don't use an avatar for group chats. - if (membersdb.size(txn) > 2) - return QString(); - - auto cursor = lmdb::cursor::open(txn, membersdb); - std::string user_id; - std::string member_data; - - // Resolve avatar for 1-1 chats. - while (cursor.get(user_id, member_data, MDB_NEXT)) { - if (user_id == localUserId_.toStdString()) - continue; - - try { - MemberInfo m = json::parse(member_data); - - cursor.close(); - return QString::fromStdString(m.avatar_url); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse member info: {}", e.what()); - } - } - - cursor.close(); - - // Default case when there is only one member. - return avatarUrl(room_id, localUserId_); -} - -QString -Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) -{ - using namespace mtx::events; - using namespace mtx::events::state; - - lmdb::val event; - bool res = lmdb::dbi_get( - txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomName)), event); - - if (res) { - try { - StateEvent msg = json::parse(std::string(event.data(), event.size())); - - if (!msg.content.name.empty()) - return QString::fromStdString(msg.content.name); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.name event: {}", e.what()); - } - } - - res = lmdb::dbi_get( - txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomCanonicalAlias)), event); - - if (res) { - try { - StateEvent msg = - json::parse(std::string(event.data(), event.size())); - - if (!msg.content.alias.empty()) - return QString::fromStdString(msg.content.alias); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}", - e.what()); - } - } - - auto cursor = lmdb::cursor::open(txn, membersdb); - const int total = membersdb.size(txn); - - std::size_t ii = 0; - std::string user_id; - std::string member_data; - std::map members; - - while (cursor.get(user_id, member_data, MDB_NEXT) && ii < 3) { - try { - members.emplace(user_id, json::parse(member_data)); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse member info: {}", e.what()); - } - - ii++; - } - - cursor.close(); - - if (total == 1 && !members.empty()) - return QString::fromStdString(members.begin()->second.name); - - auto first_member = [&members, this]() { - for (const auto &m : members) { - if (m.first != localUserId_.toStdString()) - return QString::fromStdString(m.second.name); - } - - return localUserId_; - }(); - - if (total == 2) - return first_member; - else if (total > 2) - return QString("%1 and %2 others").arg(first_member).arg(total); - - return "Empty Room"; -} - -JoinRule -Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb) -{ - using namespace mtx::events; - using namespace mtx::events::state; - - lmdb::val event; - bool res = lmdb::dbi_get( - txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomJoinRules)), event); - - if (res) { - try { - StateEvent msg = - json::parse(std::string(event.data(), event.size())); - return msg.content.join_rule; - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what()); - } - } - return JoinRule::Knock; -} - -bool -Cache::getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb) -{ - using namespace mtx::events; - using namespace mtx::events::state; - - lmdb::val event; - bool res = lmdb::dbi_get( - txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomGuestAccess)), event); - - if (res) { - try { - StateEvent msg = - json::parse(std::string(event.data(), event.size())); - return msg.content.guest_access == AccessState::CanJoin; - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.guest_access event: {}", - e.what()); - } - } - return false; -} - -QString -Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb) -{ - using namespace mtx::events; - using namespace mtx::events::state; - - lmdb::val event; - bool res = lmdb::dbi_get( - txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomTopic)), event); - - if (res) { - try { - StateEvent msg = - json::parse(std::string(event.data(), event.size())); - - if (!msg.content.topic.empty()) - return QString::fromStdString(msg.content.topic); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); - } - } - - return QString(); -} - -QString -Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) -{ - using namespace mtx::events; - using namespace mtx::events::state; - - lmdb::val event; - bool res = lmdb::dbi_get( - txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomName)), event); - - if (res) { - try { - StrippedEvent msg = - json::parse(std::string(event.data(), event.size())); - return QString::fromStdString(msg.content.name); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.name event: {}", e.what()); - } - } - - auto cursor = lmdb::cursor::open(txn, membersdb); - std::string user_id, member_data; - - while (cursor.get(user_id, member_data, MDB_NEXT)) { - if (user_id == localUserId_.toStdString()) - continue; - - try { - MemberInfo tmp = json::parse(member_data); - cursor.close(); - - return QString::fromStdString(tmp.name); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse member info: {}", e.what()); - } - } - - cursor.close(); - - return QString("Empty Room"); -} - -QString -Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) -{ - using namespace mtx::events; - using namespace mtx::events::state; - - lmdb::val event; - bool res = lmdb::dbi_get( - txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomAvatar)), event); - - if (res) { - try { - StrippedEvent msg = - json::parse(std::string(event.data(), event.size())); - return QString::fromStdString(msg.content.url); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what()); - } - } - - auto cursor = lmdb::cursor::open(txn, membersdb); - std::string user_id, member_data; - - while (cursor.get(user_id, member_data, MDB_NEXT)) { - if (user_id == localUserId_.toStdString()) - continue; - - try { - MemberInfo tmp = json::parse(member_data); - cursor.close(); - - return QString::fromStdString(tmp.avatar_url); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse member info: {}", e.what()); - } - } - - cursor.close(); - - return QString(); -} - -QString -Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db) -{ - using namespace mtx::events; - using namespace mtx::events::state; - - lmdb::val event; - bool res = - lmdb::dbi_get(txn, db, lmdb::val(to_string(mtx::events::EventType::RoomTopic)), event); - - if (res) { - try { - StrippedEvent msg = - json::parse(std::string(event.data(), event.size())); - return QString::fromStdString(msg.content.topic); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); - } - } - - return QString(); -} - -QImage -Cache::getRoomAvatar(const QString &room_id) -{ - return getRoomAvatar(room_id.toStdString()); -} - -QImage -Cache::getRoomAvatar(const std::string &room_id) -{ - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - - lmdb::val response; - - if (!lmdb::dbi_get(txn, roomsDb_, lmdb::val(room_id), response)) { - txn.commit(); - return QImage(); - } - - std::string media_url; - - try { - RoomInfo info = json::parse(std::string(response.data(), response.size())); - media_url = std::move(info.avatar_url); - - if (media_url.empty()) { - txn.commit(); - return QImage(); - } - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse room info: {}, {}", - e.what(), - std::string(response.data(), response.size())); - } - - if (!lmdb::dbi_get(txn, mediaDb_, lmdb::val(media_url), response)) { - txn.commit(); - return QImage(); - } - - txn.commit(); - - return QImage::fromData(QByteArray(response.data(), response.size())); -} - -std::vector -Cache::joinedRooms() -{ - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); - - std::string id, data; - std::vector room_ids; - - // Gather the room ids for the joined rooms. - while (roomsCursor.get(id, data, MDB_NEXT)) - room_ids.emplace_back(id); - - roomsCursor.close(); - txn.commit(); - - return room_ids; -} - -void -Cache::populateMembers() -{ - auto rooms = joinedRooms(); - nhlog::db()->info("loading {} rooms", rooms.size()); - - auto txn = lmdb::txn::begin(env_); - - for (const auto &room : rooms) { - const auto roomid = QString::fromStdString(room); - - auto membersdb = getMembersDb(txn, room); - auto cursor = lmdb::cursor::open(txn, membersdb); - - std::string user_id, info; - while (cursor.get(user_id, info, MDB_NEXT)) { - MemberInfo m = json::parse(info); - - const auto userid = QString::fromStdString(user_id); - - insertDisplayName(roomid, userid, QString::fromStdString(m.name)); - insertAvatarUrl(roomid, userid, QString::fromStdString(m.avatar_url)); - } - - cursor.close(); - } - - txn.commit(); -} - -std::vector -Cache::searchRooms(const std::string &query, std::uint8_t max_items) -{ - std::multimap> items; - - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - auto cursor = lmdb::cursor::open(txn, roomsDb_); - - std::string room_id, room_data; - while (cursor.get(room_id, room_data, MDB_NEXT)) { - RoomInfo tmp = json::parse(std::move(room_data)); - - const int score = utils::levenshtein_distance( - query, QString::fromStdString(tmp.name).toLower().toStdString()); - items.emplace(score, std::make_pair(room_id, tmp)); - } - - cursor.close(); - - auto end = items.begin(); - - if (items.size() >= max_items) - std::advance(end, max_items); - else if (items.size() > 0) - std::advance(end, items.size()); - - std::vector results; - for (auto it = items.begin(); it != end; it++) { - results.push_back( - RoomSearchResult{it->second.first, - it->second.second, - QImage::fromData(image(txn, it->second.second.avatar_url))}); - } - - txn.commit(); - - return results; -} - -QVector -Cache::searchUsers(const std::string &room_id, const std::string &query, std::uint8_t max_items) -{ - std::multimap> items; - - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - auto cursor = lmdb::cursor::open(txn, getMembersDb(txn, room_id)); - - std::string user_id, user_data; - while (cursor.get(user_id, user_data, MDB_NEXT)) { - const auto display_name = displayName(room_id, user_id); - const int score = utils::levenshtein_distance(query, display_name); - - items.emplace(score, std::make_pair(user_id, display_name)); - } - - auto end = items.begin(); - - if (items.size() >= max_items) - std::advance(end, max_items); - else if (items.size() > 0) - std::advance(end, items.size()); - - QVector results; - for (auto it = items.begin(); it != end; it++) { - const auto user = it->second; - results.push_back(SearchResult{QString::fromStdString(user.first), - QString::fromStdString(user.second)}); - } - - return results; -} - -std::vector -Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len) -{ - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - auto db = getMembersDb(txn, room_id); - auto cursor = lmdb::cursor::open(txn, db); - - std::size_t currentIndex = 0; - - const auto endIndex = std::min(startIndex + len, db.size(txn)); - - std::vector members; - - std::string user_id, user_data; - while (cursor.get(user_id, user_data, MDB_NEXT)) { - if (currentIndex < startIndex) { - currentIndex += 1; - continue; - } - - if (currentIndex >= endIndex) - break; - - try { - MemberInfo tmp = json::parse(user_data); - members.emplace_back( - RoomMember{QString::fromStdString(user_id), - QString::fromStdString(tmp.name), - QImage::fromData(image(txn, tmp.avatar_url))}); - } catch (const json::exception &e) { - nhlog::db()->warn("{}", e.what()); - } - - currentIndex += 1; - } - - cursor.close(); - txn.commit(); - - return members; -} - -void -Cache::saveTimelineMessages(lmdb::txn &txn, - const std::string &room_id, - const mtx::responses::Timeline &res) -{ - auto db = getMessagesDb(txn, room_id); - - using namespace mtx::events; - using namespace mtx::events::state; - - for (const auto &e : res.events) { - if (isStateEvent(e)) - continue; - - if (mpark::holds_alternative>(e)) - continue; - - json obj = json::object(); - - obj["event"] = utils::serialize_event(e); - obj["token"] = res.prev_batch; - - lmdb::dbi_put(txn, - db, - lmdb::val(std::to_string(utils::event_timestamp(e))), - lmdb::val(obj.dump())); - } -} - -void -Cache::markSentNotification(const std::string &event_id) -{ - auto txn = lmdb::txn::begin(env_); - lmdb::dbi_put(txn, notificationsDb_, lmdb::val(event_id), lmdb::val(std::string(""))); - txn.commit(); -} - -void -Cache::removeReadNotification(const std::string &event_id) -{ - auto txn = lmdb::txn::begin(env_); - - lmdb::dbi_del(txn, notificationsDb_, lmdb::val(event_id), nullptr); - - txn.commit(); -} - -bool -Cache::isNotificationSent(const std::string &event_id) -{ - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - - lmdb::val value; - bool res = lmdb::dbi_get(txn, notificationsDb_, lmdb::val(event_id), value); - txn.commit(); - - return res; -} - -bool -Cache::hasEnoughPowerLevel(const std::vector &eventTypes, - const std::string &room_id, - const std::string &user_id) -{ - using namespace mtx::events; - using namespace mtx::events::state; - - auto txn = lmdb::txn::begin(env_); - auto db = getStatesDb(txn, room_id); - - uint16_t min_event_level = std::numeric_limits::max(); - uint16_t user_level = std::numeric_limits::min(); - - lmdb::val event; - bool res = lmdb::dbi_get(txn, db, lmdb::val(to_string(EventType::RoomPowerLevels)), event); - - if (res) { - try { - StateEvent msg = - json::parse(std::string(event.data(), event.size())); - - user_level = msg.content.user_level(user_id); - - for (const auto &ty : eventTypes) - min_event_level = - std::min(min_event_level, - (uint16_t)msg.content.state_level(to_string(ty))); - } catch (const json::exception &e) { - nhlog::db()->warn("failed to parse m.room.power_levels event: {}", - e.what()); - } - } - - txn.commit(); - - return user_level >= min_event_level; -} - -std::vector -Cache::roomMembers(const std::string &room_id) -{ - auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); - - std::vector members; - std::string user_id, unused; - - auto db = getMembersDb(txn, room_id); - - auto cursor = lmdb::cursor::open(txn, db); - while (cursor.get(user_id, unused, MDB_NEXT)) - members.emplace_back(std::move(user_id)); - cursor.close(); - - txn.commit(); - - return members; -} - -QHash Cache::DisplayNames; -QHash Cache::AvatarUrls; - -QString -Cache::displayName(const QString &room_id, const QString &user_id) -{ - auto fmt = QString("%1 %2").arg(room_id).arg(user_id); - if (DisplayNames.contains(fmt)) - return DisplayNames[fmt]; - - return user_id; -} - -std::string -Cache::displayName(const std::string &room_id, const std::string &user_id) -{ - auto fmt = QString::fromStdString(room_id + " " + user_id); - if (DisplayNames.contains(fmt)) - return DisplayNames[fmt].toStdString(); - - return user_id; -} - -QString -Cache::avatarUrl(const QString &room_id, const QString &user_id) -{ - auto fmt = QString("%1 %2").arg(room_id).arg(user_id); - if (AvatarUrls.contains(fmt)) - return AvatarUrls[fmt]; - - return QString(); -} - -void -Cache::insertDisplayName(const QString &room_id, - const QString &user_id, - const QString &display_name) -{ - auto fmt = QString("%1 %2").arg(room_id).arg(user_id); - DisplayNames.insert(fmt, display_name); -} - -void -Cache::removeDisplayName(const QString &room_id, const QString &user_id) -{ - auto fmt = QString("%1 %2").arg(room_id).arg(user_id); - DisplayNames.remove(fmt); -} - -void -Cache::insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url) -{ - auto fmt = QString("%1 %2").arg(room_id).arg(user_id); - AvatarUrls.insert(fmt, avatar_url); -} - -void -Cache::removeAvatarUrl(const QString &room_id, const QString &user_id) -{ - auto fmt = QString("%1 %2").arg(room_id).arg(user_id); - AvatarUrls.remove(fmt); -} diff --git a/src/Cache.cpp b/src/Cache.cpp new file mode 100644 index 00000000..6f71b746 --- /dev/null +++ b/src/Cache.cpp @@ -0,0 +1,1785 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "Cache.h" +#include "Utils.h" + +//! Should be changed when a breaking change occurs in the cache format. +//! This will reset client's data. +static const std::string CURRENT_CACHE_FORMAT_VERSION("2018.06.10"); +static const std::string SECRET("secret"); + +static const lmdb::val NEXT_BATCH_KEY("next_batch"); +static const lmdb::val OLM_ACCOUNT_KEY("olm_account"); +static const lmdb::val CACHE_FORMAT_VERSION_KEY("cache_format_version"); + +constexpr size_t MAX_RESTORED_MESSAGES = 30; + +//! Cache databases and their format. +//! +//! Contains UI information for the joined rooms. (i.e name, topic, avatar url etc). +//! Format: room_id -> RoomInfo +constexpr auto ROOMS_DB("rooms"); +constexpr auto INVITES_DB("invites"); +//! Keeps already downloaded media for reuse. +//! Format: matrix_url -> binary data. +constexpr auto MEDIA_DB("media"); +//! Information that must be kept between sync requests. +constexpr auto SYNC_STATE_DB("sync_state"); +//! Read receipts per room/event. +constexpr auto READ_RECEIPTS_DB("read_receipts"); +constexpr auto NOTIFICATIONS_DB("sent_notifications"); + +//! Encryption related databases. + +//! user_id -> list of devices +constexpr auto DEVICES_DB("devices"); +//! device_id -> device keys +constexpr auto DEVICE_KEYS_DB("device_keys"); +//! room_ids that have encryption enabled. +constexpr auto ENCRYPTED_ROOMS_DB("encrypted_rooms"); + +//! room_id -> pickled OlmInboundGroupSession +constexpr auto INBOUND_MEGOLM_SESSIONS_DB("inbound_megolm_sessions"); +//! MegolmSessionIndex -> pickled OlmOutboundGroupSession +constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions"); + +using CachedReceipts = std::multimap>; +using Receipts = std::map>; + +namespace { +std::unique_ptr instance_ = nullptr; +} + +namespace cache { +void +init(const QString &user_id) +{ + qRegisterMetaType(); + qRegisterMetaType>(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType>(); + qRegisterMetaType>(); + qRegisterMetaType>(); + + instance_ = std::make_unique(user_id); +} + +Cache * +client() +{ + return instance_.get(); +} +} // namespace cache + +Cache::Cache(const QString &userId, QObject *parent) + : QObject{parent} + , env_{nullptr} + , syncStateDb_{0} + , roomsDb_{0} + , invitesDb_{0} + , mediaDb_{0} + , readReceiptsDb_{0} + , notificationsDb_{0} + , devicesDb_{0} + , deviceKeysDb_{0} + , inboundMegolmSessionDb_{0} + , outboundMegolmSessionDb_{0} + , localUserId_{userId} +{ + setup(); +} + +void +Cache::setup() +{ + nhlog::db()->debug("setting up cache"); + + auto statePath = QString("%1/%2") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(QString::fromUtf8(localUserId_.toUtf8().toHex())); + + cacheDirectory_ = QString("%1/%2") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(QString::fromUtf8(localUserId_.toUtf8().toHex())); + + bool isInitial = !QFile::exists(statePath); + + env_ = lmdb::env::create(); + env_.set_mapsize(256UL * 1024UL * 1024UL); /* 256 MB */ + env_.set_max_dbs(1024UL); + + if (isInitial) { + nhlog::db()->info("initializing LMDB"); + + if (!QDir().mkpath(statePath)) { + throw std::runtime_error( + ("Unable to create state directory:" + statePath).toStdString().c_str()); + } + } + + try { + env_.open(statePath.toStdString().c_str()); + } catch (const lmdb::error &e) { + if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) { + throw std::runtime_error("LMDB initialization failed" + + std::string(e.what())); + } + + nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what()); + + QDir stateDir(statePath); + + for (const auto &file : stateDir.entryList(QDir::NoDotAndDotDot)) { + if (!stateDir.remove(file)) + throw std::runtime_error( + ("Unable to delete file " + file).toStdString().c_str()); + } + + env_.open(statePath.toStdString().c_str()); + } + + auto txn = lmdb::txn::begin(env_); + syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE); + roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE); + invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE); + mediaDb_ = lmdb::dbi::open(txn, MEDIA_DB, MDB_CREATE); + readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE); + notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE); + + // Device management + devicesDb_ = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE); + deviceKeysDb_ = lmdb::dbi::open(txn, DEVICE_KEYS_DB, MDB_CREATE); + + // Session management + inboundMegolmSessionDb_ = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); + outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); + + txn.commit(); +} + +void +Cache::setEncryptedRoom(lmdb::txn &txn, const std::string &room_id) +{ + nhlog::db()->info("mark room {} as encrypted", room_id); + + auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); + lmdb::dbi_put(txn, db, lmdb::val(room_id), lmdb::val("0")); +} + +bool +Cache::isRoomEncrypted(const std::string &room_id) +{ + lmdb::val unused; + + auto txn = lmdb::txn::begin(env_); + auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE); + auto res = lmdb::dbi_get(txn, db, lmdb::val(room_id), unused); + txn.commit(); + + return res; +} + +// +// Device Management +// + +// +// Session Management +// + +void +Cache::saveInboundMegolmSession(const MegolmSessionIndex &index, + mtx::crypto::InboundGroupSessionPtr session) +{ + using namespace mtx::crypto; + const auto key = index.to_hash(); + const auto pickled = pickle(session.get(), SECRET); + + auto txn = lmdb::txn::begin(env_); + lmdb::dbi_put(txn, inboundMegolmSessionDb_, lmdb::val(key), lmdb::val(pickled)); + txn.commit(); + + { + std::unique_lock lock(session_storage.group_inbound_mtx); + session_storage.group_inbound_sessions[key] = std::move(session); + } +} + +OlmInboundGroupSession * +Cache::getInboundMegolmSession(const MegolmSessionIndex &index) +{ + std::unique_lock lock(session_storage.group_inbound_mtx); + return session_storage.group_inbound_sessions[index.to_hash()].get(); +} + +bool +Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept +{ + std::unique_lock lock(session_storage.group_inbound_mtx); + return session_storage.group_inbound_sessions.find(index.to_hash()) != + session_storage.group_inbound_sessions.end(); +} + +void +Cache::updateOutboundMegolmSession(const std::string &room_id, int message_index) +{ + using namespace mtx::crypto; + + if (!outboundMegolmSessionExists(room_id)) + return; + + OutboundGroupSessionData data; + OlmOutboundGroupSession *session; + { + std::unique_lock lock(session_storage.group_outbound_mtx); + data = session_storage.group_outbound_session_data[room_id]; + session = session_storage.group_outbound_sessions[room_id].get(); + + // Update with the current message. + data.message_index = message_index; + session_storage.group_outbound_session_data[room_id] = data; + } + + // Save the updated pickled data for the session. + json j; + j["data"] = data; + j["session"] = pickle(session, SECRET); + + auto txn = lmdb::txn::begin(env_); + lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(room_id), lmdb::val(j.dump())); + txn.commit(); +} + +void +Cache::saveOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr session) +{ + using namespace mtx::crypto; + const auto pickled = pickle(session.get(), SECRET); + + json j; + j["data"] = data; + j["session"] = pickled; + + auto txn = lmdb::txn::begin(env_); + lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(room_id), lmdb::val(j.dump())); + txn.commit(); + + { + std::unique_lock lock(session_storage.group_outbound_mtx); + session_storage.group_outbound_session_data[room_id] = data; + session_storage.group_outbound_sessions[room_id] = std::move(session); + } +} + +bool +Cache::outboundMegolmSessionExists(const std::string &room_id) noexcept +{ + std::unique_lock lock(session_storage.group_outbound_mtx); + return (session_storage.group_outbound_sessions.find(room_id) != + session_storage.group_outbound_sessions.end()) && + (session_storage.group_outbound_session_data.find(room_id) != + session_storage.group_outbound_session_data.end()); +} + +OutboundGroupSessionDataRef +Cache::getOutboundMegolmSession(const std::string &room_id) +{ + std::unique_lock lock(session_storage.group_outbound_mtx); + return OutboundGroupSessionDataRef{session_storage.group_outbound_sessions[room_id].get(), + session_storage.group_outbound_session_data[room_id]}; +} + +// +// OLM sessions. +// + +void +Cache::saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session) +{ + using namespace mtx::crypto; + + auto txn = lmdb::txn::begin(env_); + auto db = getOlmSessionsDb(txn, curve25519); + + const auto pickled = pickle(session.get(), SECRET); + const auto session_id = mtx::crypto::session_id(session.get()); + + lmdb::dbi_put(txn, db, lmdb::val(session_id), lmdb::val(pickled)); + + txn.commit(); +} + +boost::optional +Cache::getOlmSession(const std::string &curve25519, const std::string &session_id) +{ + using namespace mtx::crypto; + + auto txn = lmdb::txn::begin(env_); + auto db = getOlmSessionsDb(txn, curve25519); + + lmdb::val pickled; + bool found = lmdb::dbi_get(txn, db, lmdb::val(session_id), pickled); + + txn.commit(); + + if (found) { + auto data = std::string(pickled.data(), pickled.size()); + return unpickle(data, SECRET); + } + + return boost::none; +} + +std::vector +Cache::getOlmSessions(const std::string &curve25519) +{ + using namespace mtx::crypto; + + auto txn = lmdb::txn::begin(env_); + auto db = getOlmSessionsDb(txn, curve25519); + + std::string session_id, unused; + std::vector res; + + auto cursor = lmdb::cursor::open(txn, db); + while (cursor.get(session_id, unused, MDB_NEXT)) + res.emplace_back(session_id); + cursor.close(); + + txn.commit(); + + return res; +} + +void +Cache::saveOlmAccount(const std::string &data) +{ + auto txn = lmdb::txn::begin(env_); + lmdb::dbi_put(txn, syncStateDb_, OLM_ACCOUNT_KEY, lmdb::val(data)); + txn.commit(); +} + +void +Cache::restoreSessions() +{ + using namespace mtx::crypto; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + std::string key, value; + + // + // Inbound Megolm Sessions + // + { + auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_); + while (cursor.get(key, value, MDB_NEXT)) { + auto session = unpickle(value, SECRET); + session_storage.group_inbound_sessions[key] = std::move(session); + } + cursor.close(); + } + + // + // Outbound Megolm Sessions + // + { + auto cursor = lmdb::cursor::open(txn, outboundMegolmSessionDb_); + while (cursor.get(key, value, MDB_NEXT)) { + json obj; + + try { + obj = json::parse(value); + + session_storage.group_outbound_session_data[key] = + obj.at("data").get(); + + auto session = + unpickle(obj.at("session"), SECRET); + session_storage.group_outbound_sessions[key] = std::move(session); + } catch (const nlohmann::json::exception &e) { + nhlog::db()->critical( + "failed to parse outbound megolm session data: {}", e.what()); + } + } + cursor.close(); + } + + txn.commit(); + + nhlog::db()->info("sessions restored"); +} + +std::string +Cache::restoreOlmAccount() +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + lmdb::val pickled; + lmdb::dbi_get(txn, syncStateDb_, OLM_ACCOUNT_KEY, pickled); + txn.commit(); + + return std::string(pickled.data(), pickled.size()); +} + +// +// Media Management +// + +void +Cache::saveImage(const std::string &url, const std::string &img_data) +{ + if (url.empty() || img_data.empty()) + return; + + try { + auto txn = lmdb::txn::begin(env_); + + lmdb::dbi_put(txn, + mediaDb_, + lmdb::val(url.data(), url.size()), + lmdb::val(img_data.data(), img_data.size())); + + txn.commit(); + } catch (const lmdb::error &e) { + nhlog::db()->critical("saveImage: {}", e.what()); + } +} + +void +Cache::saveImage(const QString &url, const QByteArray &image) +{ + saveImage(url.toStdString(), std::string(image.constData(), image.length())); +} + +QByteArray +Cache::image(lmdb::txn &txn, const std::string &url) const +{ + if (url.empty()) + return QByteArray(); + + try { + lmdb::val image; + bool res = lmdb::dbi_get(txn, mediaDb_, lmdb::val(url), image); + + if (!res) + return QByteArray(); + + return QByteArray(image.data(), image.size()); + } catch (const lmdb::error &e) { + nhlog::db()->critical("image: {}, {}", e.what(), url); + } + + return QByteArray(); +} + +QByteArray +Cache::image(const QString &url) const +{ + if (url.isEmpty()) + return QByteArray(); + + auto key = url.toUtf8(); + + try { + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + lmdb::val image; + + bool res = lmdb::dbi_get(txn, mediaDb_, lmdb::val(key.data(), key.size()), image); + + txn.commit(); + + if (!res) + return QByteArray(); + + return QByteArray(image.data(), image.size()); + } catch (const lmdb::error &e) { + nhlog::db()->critical("image: {} {}", e.what(), url.toStdString()); + } + + return QByteArray(); +} + +void +Cache::removeInvite(lmdb::txn &txn, const std::string &room_id) +{ + lmdb::dbi_del(txn, invitesDb_, lmdb::val(room_id), nullptr); + lmdb::dbi_drop(txn, getInviteStatesDb(txn, room_id), true); + lmdb::dbi_drop(txn, getInviteMembersDb(txn, room_id), true); +} + +void +Cache::removeInvite(const std::string &room_id) +{ + auto txn = lmdb::txn::begin(env_); + removeInvite(txn, room_id); + txn.commit(); +} + +void +Cache::removeRoom(lmdb::txn &txn, const std::string &roomid) +{ + lmdb::dbi_del(txn, roomsDb_, lmdb::val(roomid), nullptr); + lmdb::dbi_drop(txn, getStatesDb(txn, roomid), true); + lmdb::dbi_drop(txn, getMembersDb(txn, roomid), true); +} + +void +Cache::removeRoom(const std::string &roomid) +{ + auto txn = lmdb::txn::begin(env_, nullptr, 0); + lmdb::dbi_del(txn, roomsDb_, lmdb::val(roomid), nullptr); + txn.commit(); +} + +void +Cache::setNextBatchToken(lmdb::txn &txn, const std::string &token) +{ + lmdb::dbi_put(txn, syncStateDb_, NEXT_BATCH_KEY, lmdb::val(token.data(), token.size())); +} + +void +Cache::setNextBatchToken(lmdb::txn &txn, const QString &token) +{ + setNextBatchToken(txn, token.toStdString()); +} + +bool +Cache::isInitialized() const +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + lmdb::val token; + + bool res = lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token); + + txn.commit(); + + return res; +} + +std::string +Cache::nextBatchToken() const +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + lmdb::val token; + + lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token); + + txn.commit(); + + return std::string(token.data(), token.size()); +} + +void +Cache::deleteData() +{ + // TODO: We need to remove the env_ while not accepting new requests. + if (!cacheDirectory_.isEmpty()) { + QDir(cacheDirectory_).removeRecursively(); + nhlog::db()->info("deleted cache files from disk"); + } +} + +bool +Cache::isFormatValid() +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + lmdb::val current_version; + bool res = lmdb::dbi_get(txn, syncStateDb_, CACHE_FORMAT_VERSION_KEY, current_version); + + txn.commit(); + + if (!res) + return true; + + std::string stored_version(current_version.data(), current_version.size()); + + if (stored_version != CURRENT_CACHE_FORMAT_VERSION) { + nhlog::db()->warn("breaking changes in the cache format. stored: {}, current: {}", + stored_version, + CURRENT_CACHE_FORMAT_VERSION); + return false; + } + + return true; +} + +void +Cache::setCurrentFormat() +{ + auto txn = lmdb::txn::begin(env_); + + lmdb::dbi_put( + txn, + syncStateDb_, + CACHE_FORMAT_VERSION_KEY, + lmdb::val(CURRENT_CACHE_FORMAT_VERSION.data(), CURRENT_CACHE_FORMAT_VERSION.size())); + + txn.commit(); +} + +CachedReceipts +Cache::readReceipts(const QString &event_id, const QString &room_id) +{ + CachedReceipts receipts; + + ReadReceiptKey receipt_key{event_id.toStdString(), room_id.toStdString()}; + nlohmann::json json_key = receipt_key; + + try { + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto key = json_key.dump(); + + lmdb::val value; + + bool res = + lmdb::dbi_get(txn, readReceiptsDb_, lmdb::val(key.data(), key.size()), value); + + txn.commit(); + + if (res) { + auto json_response = json::parse(std::string(value.data(), value.size())); + auto values = json_response.get>(); + + for (const auto &v : values) + // timestamp, user_id + receipts.emplace(v.second, v.first); + } + + } catch (const lmdb::error &e) { + nhlog::db()->critical("readReceipts: {}", e.what()); + } + + return receipts; +} + +void +Cache::updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts) +{ + for (const auto &receipt : receipts) { + const auto event_id = receipt.first; + auto event_receipts = receipt.second; + + ReadReceiptKey receipt_key{event_id, room_id}; + nlohmann::json json_key = receipt_key; + + try { + const auto key = json_key.dump(); + + lmdb::val prev_value; + + bool exists = lmdb::dbi_get( + txn, readReceiptsDb_, lmdb::val(key.data(), key.size()), prev_value); + + std::map saved_receipts; + + // If an entry for the event id already exists, we would + // merge the existing receipts with the new ones. + if (exists) { + auto json_value = + json::parse(std::string(prev_value.data(), prev_value.size())); + + // Retrieve the saved receipts. + saved_receipts = json_value.get>(); + } + + // Append the new ones. + for (const auto &event_receipt : event_receipts) + saved_receipts.emplace(event_receipt.first, event_receipt.second); + + // Save back the merged (or only the new) receipts. + nlohmann::json json_updated_value = saved_receipts; + std::string merged_receipts = json_updated_value.dump(); + + lmdb::dbi_put(txn, + readReceiptsDb_, + lmdb::val(key.data(), key.size()), + lmdb::val(merged_receipts.data(), merged_receipts.size())); + + } catch (const lmdb::error &e) { + nhlog::db()->critical("updateReadReceipts: {}", e.what()); + } + } +} + +void +Cache::saveState(const mtx::responses::Sync &res) +{ + auto txn = lmdb::txn::begin(env_); + + setNextBatchToken(txn, res.next_batch); + + // Save joined rooms + for (const auto &room : res.rooms.join) { + auto statesdb = getStatesDb(txn, room.first); + auto membersdb = getMembersDb(txn, room.first); + + saveStateEvents(txn, statesdb, membersdb, room.first, room.second.state.events); + saveStateEvents(txn, statesdb, membersdb, room.first, room.second.timeline.events); + + saveTimelineMessages(txn, room.first, room.second.timeline); + + RoomInfo updatedInfo; + updatedInfo.name = getRoomName(txn, statesdb, membersdb).toStdString(); + updatedInfo.topic = getRoomTopic(txn, statesdb).toStdString(); + updatedInfo.avatar_url = + getRoomAvatarUrl(txn, statesdb, membersdb, QString::fromStdString(room.first)) + .toStdString(); + + lmdb::dbi_put( + txn, roomsDb_, lmdb::val(room.first), lmdb::val(json(updatedInfo).dump())); + + updateReadReceipt(txn, room.first, room.second.ephemeral.receipts); + + // Clean up non-valid invites. + removeInvite(txn, room.first); + } + + saveInvites(txn, res.rooms.invite); + + removeLeftRooms(txn, res.rooms.leave); + + txn.commit(); +} + +void +Cache::saveInvites(lmdb::txn &txn, const std::map &rooms) +{ + for (const auto &room : rooms) { + auto statesdb = getInviteStatesDb(txn, room.first); + auto membersdb = getInviteMembersDb(txn, room.first); + + saveInvite(txn, statesdb, membersdb, room.second); + + RoomInfo updatedInfo; + updatedInfo.name = getInviteRoomName(txn, statesdb, membersdb).toStdString(); + updatedInfo.topic = getInviteRoomTopic(txn, statesdb).toStdString(); + updatedInfo.avatar_url = + getInviteRoomAvatarUrl(txn, statesdb, membersdb).toStdString(); + updatedInfo.is_invite = true; + + lmdb::dbi_put( + txn, invitesDb_, lmdb::val(room.first), lmdb::val(json(updatedInfo).dump())); + } +} + +void +Cache::saveInvite(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const mtx::responses::InvitedRoom &room) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + for (const auto &e : room.invite_state) { + if (mpark::holds_alternative>(e)) { + auto msg = mpark::get>(e); + + auto display_name = msg.content.display_name.empty() + ? msg.state_key + : msg.content.display_name; + + MemberInfo tmp{display_name, msg.content.avatar_url}; + + lmdb::dbi_put( + txn, membersdb, lmdb::val(msg.state_key), lmdb::val(json(tmp).dump())); + } else { + mpark::visit( + [&txn, &statesdb](auto msg) { + bool res = lmdb::dbi_put(txn, + statesdb, + lmdb::val(to_string(msg.type)), + lmdb::val(json(msg).dump())); + + if (!res) + std::cout << "couldn't save data" << json(msg).dump() + << '\n'; + }, + e); + } + } +} + +std::vector +Cache::roomsWithStateUpdates(const mtx::responses::Sync &res) +{ + std::vector rooms; + for (const auto &room : res.rooms.join) { + bool hasUpdates = false; + for (const auto &s : room.second.state.events) { + if (containsStateUpdates(s)) { + hasUpdates = true; + break; + } + } + + for (const auto &s : room.second.timeline.events) { + if (containsStateUpdates(s)) { + hasUpdates = true; + break; + } + } + + if (hasUpdates) + rooms.emplace_back(room.first); + } + + for (const auto &room : res.rooms.invite) { + for (const auto &s : room.second.invite_state) { + if (containsStateUpdates(s)) { + rooms.emplace_back(room.first); + break; + } + } + } + + return rooms; +} + +RoomInfo +Cache::singleRoomInfo(const std::string &room_id) +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto statesdb = getStatesDb(txn, room_id); + + lmdb::val data; + + // Check if the room is joined. + if (lmdb::dbi_get(txn, roomsDb_, lmdb::val(room_id), data)) { + try { + RoomInfo tmp = json::parse(std::string(data.data(), data.size())); + tmp.member_count = getMembersDb(txn, room_id).size(txn); + tmp.join_rule = getRoomJoinRule(txn, statesdb); + tmp.guest_access = getRoomGuestAccess(txn, statesdb); + + txn.commit(); + + return tmp; + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse room info: room_id ({}), {}", + room_id, + std::string(data.data(), data.size())); + } + } + + txn.commit(); + + return RoomInfo(); +} + +std::map +Cache::getRoomInfo(const std::vector &rooms) +{ + std::map room_info; + + // TODO This should be read only. + auto txn = lmdb::txn::begin(env_); + + for (const auto &room : rooms) { + lmdb::val data; + auto statesdb = getStatesDb(txn, room); + + // Check if the room is joined. + if (lmdb::dbi_get(txn, roomsDb_, lmdb::val(room), data)) { + try { + RoomInfo tmp = json::parse(std::string(data.data(), data.size())); + tmp.member_count = getMembersDb(txn, room).size(txn); + tmp.join_rule = getRoomJoinRule(txn, statesdb); + tmp.guest_access = getRoomGuestAccess(txn, statesdb); + + room_info.emplace(QString::fromStdString(room), std::move(tmp)); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse room info: room_id ({}), {}", + room, + std::string(data.data(), data.size())); + } + } else { + // Check if the room is an invite. + if (lmdb::dbi_get(txn, invitesDb_, lmdb::val(room), data)) { + try { + RoomInfo tmp = + json::parse(std::string(data.data(), data.size())); + tmp.member_count = getInviteMembersDb(txn, room).size(txn); + + room_info.emplace(QString::fromStdString(room), + std::move(tmp)); + } catch (const json::exception &e) { + nhlog::db()->warn( + "failed to parse room info for invite: room_id ({}), {}", + room, + std::string(data.data(), data.size())); + } + } + } + } + + txn.commit(); + + return room_info; +} + +std::map +Cache::roomMessages() +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + std::map msgs; + std::string room_id, unused; + + auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); + while (roomsCursor.get(room_id, unused, MDB_NEXT)) + msgs.emplace(QString::fromStdString(room_id), mtx::responses::Timeline()); + + roomsCursor.close(); + txn.commit(); + + return msgs; +} + +mtx::responses::Timeline +Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id) +{ + auto db = getMessagesDb(txn, room_id); + + mtx::responses::Timeline timeline; + std::string timestamp, msg; + + auto cursor = lmdb::cursor::open(txn, db); + + size_t index = 0; + + while (cursor.get(timestamp, msg, MDB_NEXT) && index < MAX_RESTORED_MESSAGES) { + auto obj = json::parse(msg); + + if (obj.count("event") == 0 || obj.count("token") == 0) + continue; + + mtx::events::collections::TimelineEvent event; + mtx::events::collections::from_json(obj.at("event"), event); + + index += 1; + + timeline.events.push_back(event.data); + timeline.prev_batch = obj.at("token").get(); + } + cursor.close(); + + std::reverse(timeline.events.begin(), timeline.events.end()); + + return timeline; +} + +QMap +Cache::roomInfo(bool withInvites) +{ + QMap result; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + std::string room_id; + std::string room_data; + + // Gather info about the joined rooms. + auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); + while (roomsCursor.get(room_id, room_data, MDB_NEXT)) { + RoomInfo tmp = json::parse(std::move(room_data)); + tmp.member_count = getMembersDb(txn, room_id).size(txn); + tmp.msgInfo = getLastMessageInfo(txn, room_id); + + result.insert(QString::fromStdString(std::move(room_id)), std::move(tmp)); + } + roomsCursor.close(); + + if (withInvites) { + // Gather info about the invites. + auto invitesCursor = lmdb::cursor::open(txn, invitesDb_); + while (invitesCursor.get(room_id, room_data, MDB_NEXT)) { + RoomInfo tmp = json::parse(room_data); + tmp.member_count = getInviteMembersDb(txn, room_id).size(txn); + result.insert(QString::fromStdString(std::move(room_id)), std::move(tmp)); + } + invitesCursor.close(); + } + + txn.commit(); + + return result; +} + +DescInfo +Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id) +{ + auto db = getMessagesDb(txn, room_id); + + if (db.size(txn) == 0) + return DescInfo{}; + + std::string timestamp, msg; + + QSettings settings; + auto local_user = settings.value("auth/user_id").toString(); + + auto cursor = lmdb::cursor::open(txn, db); + while (cursor.get(timestamp, msg, MDB_NEXT)) { + auto obj = json::parse(msg); + + if (obj.count("event") == 0) + continue; + + mtx::events::collections::TimelineEvent event; + mtx::events::collections::from_json(obj.at("event"), event); + + cursor.close(); + return utils::getMessageDescription( + event.data, local_user, QString::fromStdString(room_id)); + } + cursor.close(); + + return DescInfo{}; +} + +std::map +Cache::invites() +{ + std::map result; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto cursor = lmdb::cursor::open(txn, invitesDb_); + + std::string room_id, unused; + + while (cursor.get(room_id, unused, MDB_NEXT)) + result.emplace(QString::fromStdString(std::move(room_id)), true); + + cursor.close(); + txn.commit(); + + return result; +} + +QString +Cache::getRoomAvatarUrl(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const QString &room_id) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomAvatar)), event); + + if (res) { + try { + StateEvent msg = + json::parse(std::string(event.data(), event.size())); + + return QString::fromStdString(msg.content.url); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what()); + } + } + + // We don't use an avatar for group chats. + if (membersdb.size(txn) > 2) + return QString(); + + auto cursor = lmdb::cursor::open(txn, membersdb); + std::string user_id; + std::string member_data; + + // Resolve avatar for 1-1 chats. + while (cursor.get(user_id, member_data, MDB_NEXT)) { + if (user_id == localUserId_.toStdString()) + continue; + + try { + MemberInfo m = json::parse(member_data); + + cursor.close(); + return QString::fromStdString(m.avatar_url); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse member info: {}", e.what()); + } + } + + cursor.close(); + + // Default case when there is only one member. + return avatarUrl(room_id, localUserId_); +} + +QString +Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomName)), event); + + if (res) { + try { + StateEvent msg = json::parse(std::string(event.data(), event.size())); + + if (!msg.content.name.empty()) + return QString::fromStdString(msg.content.name); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.name event: {}", e.what()); + } + } + + res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomCanonicalAlias)), event); + + if (res) { + try { + StateEvent msg = + json::parse(std::string(event.data(), event.size())); + + if (!msg.content.alias.empty()) + return QString::fromStdString(msg.content.alias); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}", + e.what()); + } + } + + auto cursor = lmdb::cursor::open(txn, membersdb); + const int total = membersdb.size(txn); + + std::size_t ii = 0; + std::string user_id; + std::string member_data; + std::map members; + + while (cursor.get(user_id, member_data, MDB_NEXT) && ii < 3) { + try { + members.emplace(user_id, json::parse(member_data)); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse member info: {}", e.what()); + } + + ii++; + } + + cursor.close(); + + if (total == 1 && !members.empty()) + return QString::fromStdString(members.begin()->second.name); + + auto first_member = [&members, this]() { + for (const auto &m : members) { + if (m.first != localUserId_.toStdString()) + return QString::fromStdString(m.second.name); + } + + return localUserId_; + }(); + + if (total == 2) + return first_member; + else if (total > 2) + return QString("%1 and %2 others").arg(first_member).arg(total); + + return "Empty Room"; +} + +JoinRule +Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomJoinRules)), event); + + if (res) { + try { + StateEvent msg = + json::parse(std::string(event.data(), event.size())); + return msg.content.join_rule; + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what()); + } + } + return JoinRule::Knock; +} + +bool +Cache::getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomGuestAccess)), event); + + if (res) { + try { + StateEvent msg = + json::parse(std::string(event.data(), event.size())); + return msg.content.guest_access == AccessState::CanJoin; + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.guest_access event: {}", + e.what()); + } + } + return false; +} + +QString +Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomTopic)), event); + + if (res) { + try { + StateEvent msg = + json::parse(std::string(event.data(), event.size())); + + if (!msg.content.topic.empty()) + return QString::fromStdString(msg.content.topic); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); + } + } + + return QString(); +} + +QString +Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomName)), event); + + if (res) { + try { + StrippedEvent msg = + json::parse(std::string(event.data(), event.size())); + return QString::fromStdString(msg.content.name); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.name event: {}", e.what()); + } + } + + auto cursor = lmdb::cursor::open(txn, membersdb); + std::string user_id, member_data; + + while (cursor.get(user_id, member_data, MDB_NEXT)) { + if (user_id == localUserId_.toStdString()) + continue; + + try { + MemberInfo tmp = json::parse(member_data); + cursor.close(); + + return QString::fromStdString(tmp.name); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse member info: {}", e.what()); + } + } + + cursor.close(); + + return QString("Empty Room"); +} + +QString +Cache::getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomAvatar)), event); + + if (res) { + try { + StrippedEvent msg = + json::parse(std::string(event.data(), event.size())); + return QString::fromStdString(msg.content.url); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.avatar event: {}", e.what()); + } + } + + auto cursor = lmdb::cursor::open(txn, membersdb); + std::string user_id, member_data; + + while (cursor.get(user_id, member_data, MDB_NEXT)) { + if (user_id == localUserId_.toStdString()) + continue; + + try { + MemberInfo tmp = json::parse(member_data); + cursor.close(); + + return QString::fromStdString(tmp.avatar_url); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse member info: {}", e.what()); + } + } + + cursor.close(); + + return QString(); +} + +QString +Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = + lmdb::dbi_get(txn, db, lmdb::val(to_string(mtx::events::EventType::RoomTopic)), event); + + if (res) { + try { + StrippedEvent msg = + json::parse(std::string(event.data(), event.size())); + return QString::fromStdString(msg.content.topic); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what()); + } + } + + return QString(); +} + +QImage +Cache::getRoomAvatar(const QString &room_id) +{ + return getRoomAvatar(room_id.toStdString()); +} + +QImage +Cache::getRoomAvatar(const std::string &room_id) +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + lmdb::val response; + + if (!lmdb::dbi_get(txn, roomsDb_, lmdb::val(room_id), response)) { + txn.commit(); + return QImage(); + } + + std::string media_url; + + try { + RoomInfo info = json::parse(std::string(response.data(), response.size())); + media_url = std::move(info.avatar_url); + + if (media_url.empty()) { + txn.commit(); + return QImage(); + } + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse room info: {}, {}", + e.what(), + std::string(response.data(), response.size())); + } + + if (!lmdb::dbi_get(txn, mediaDb_, lmdb::val(media_url), response)) { + txn.commit(); + return QImage(); + } + + txn.commit(); + + return QImage::fromData(QByteArray(response.data(), response.size())); +} + +std::vector +Cache::joinedRooms() +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto roomsCursor = lmdb::cursor::open(txn, roomsDb_); + + std::string id, data; + std::vector room_ids; + + // Gather the room ids for the joined rooms. + while (roomsCursor.get(id, data, MDB_NEXT)) + room_ids.emplace_back(id); + + roomsCursor.close(); + txn.commit(); + + return room_ids; +} + +void +Cache::populateMembers() +{ + auto rooms = joinedRooms(); + nhlog::db()->info("loading {} rooms", rooms.size()); + + auto txn = lmdb::txn::begin(env_); + + for (const auto &room : rooms) { + const auto roomid = QString::fromStdString(room); + + auto membersdb = getMembersDb(txn, room); + auto cursor = lmdb::cursor::open(txn, membersdb); + + std::string user_id, info; + while (cursor.get(user_id, info, MDB_NEXT)) { + MemberInfo m = json::parse(info); + + const auto userid = QString::fromStdString(user_id); + + insertDisplayName(roomid, userid, QString::fromStdString(m.name)); + insertAvatarUrl(roomid, userid, QString::fromStdString(m.avatar_url)); + } + + cursor.close(); + } + + txn.commit(); +} + +std::vector +Cache::searchRooms(const std::string &query, std::uint8_t max_items) +{ + std::multimap> items; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto cursor = lmdb::cursor::open(txn, roomsDb_); + + std::string room_id, room_data; + while (cursor.get(room_id, room_data, MDB_NEXT)) { + RoomInfo tmp = json::parse(std::move(room_data)); + + const int score = utils::levenshtein_distance( + query, QString::fromStdString(tmp.name).toLower().toStdString()); + items.emplace(score, std::make_pair(room_id, tmp)); + } + + cursor.close(); + + auto end = items.begin(); + + if (items.size() >= max_items) + std::advance(end, max_items); + else if (items.size() > 0) + std::advance(end, items.size()); + + std::vector results; + for (auto it = items.begin(); it != end; it++) { + results.push_back( + RoomSearchResult{it->second.first, + it->second.second, + QImage::fromData(image(txn, it->second.second.avatar_url))}); + } + + txn.commit(); + + return results; +} + +QVector +Cache::searchUsers(const std::string &room_id, const std::string &query, std::uint8_t max_items) +{ + std::multimap> items; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto cursor = lmdb::cursor::open(txn, getMembersDb(txn, room_id)); + + std::string user_id, user_data; + while (cursor.get(user_id, user_data, MDB_NEXT)) { + const auto display_name = displayName(room_id, user_id); + const int score = utils::levenshtein_distance(query, display_name); + + items.emplace(score, std::make_pair(user_id, display_name)); + } + + auto end = items.begin(); + + if (items.size() >= max_items) + std::advance(end, max_items); + else if (items.size() > 0) + std::advance(end, items.size()); + + QVector results; + for (auto it = items.begin(); it != end; it++) { + const auto user = it->second; + results.push_back(SearchResult{QString::fromStdString(user.first), + QString::fromStdString(user.second)}); + } + + return results; +} + +std::vector +Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len) +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto db = getMembersDb(txn, room_id); + auto cursor = lmdb::cursor::open(txn, db); + + std::size_t currentIndex = 0; + + const auto endIndex = std::min(startIndex + len, db.size(txn)); + + std::vector members; + + std::string user_id, user_data; + while (cursor.get(user_id, user_data, MDB_NEXT)) { + if (currentIndex < startIndex) { + currentIndex += 1; + continue; + } + + if (currentIndex >= endIndex) + break; + + try { + MemberInfo tmp = json::parse(user_data); + members.emplace_back( + RoomMember{QString::fromStdString(user_id), + QString::fromStdString(tmp.name), + QImage::fromData(image(txn, tmp.avatar_url))}); + } catch (const json::exception &e) { + nhlog::db()->warn("{}", e.what()); + } + + currentIndex += 1; + } + + cursor.close(); + txn.commit(); + + return members; +} + +void +Cache::saveTimelineMessages(lmdb::txn &txn, + const std::string &room_id, + const mtx::responses::Timeline &res) +{ + auto db = getMessagesDb(txn, room_id); + + using namespace mtx::events; + using namespace mtx::events::state; + + for (const auto &e : res.events) { + if (isStateEvent(e)) + continue; + + if (mpark::holds_alternative>(e)) + continue; + + json obj = json::object(); + + obj["event"] = utils::serialize_event(e); + obj["token"] = res.prev_batch; + + lmdb::dbi_put(txn, + db, + lmdb::val(std::to_string(utils::event_timestamp(e))), + lmdb::val(obj.dump())); + } +} + +void +Cache::markSentNotification(const std::string &event_id) +{ + auto txn = lmdb::txn::begin(env_); + lmdb::dbi_put(txn, notificationsDb_, lmdb::val(event_id), lmdb::val(std::string(""))); + txn.commit(); +} + +void +Cache::removeReadNotification(const std::string &event_id) +{ + auto txn = lmdb::txn::begin(env_); + + lmdb::dbi_del(txn, notificationsDb_, lmdb::val(event_id), nullptr); + + txn.commit(); +} + +bool +Cache::isNotificationSent(const std::string &event_id) +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + lmdb::val value; + bool res = lmdb::dbi_get(txn, notificationsDb_, lmdb::val(event_id), value); + txn.commit(); + + return res; +} + +bool +Cache::hasEnoughPowerLevel(const std::vector &eventTypes, + const std::string &room_id, + const std::string &user_id) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + auto txn = lmdb::txn::begin(env_); + auto db = getStatesDb(txn, room_id); + + uint16_t min_event_level = std::numeric_limits::max(); + uint16_t user_level = std::numeric_limits::min(); + + lmdb::val event; + bool res = lmdb::dbi_get(txn, db, lmdb::val(to_string(EventType::RoomPowerLevels)), event); + + if (res) { + try { + StateEvent msg = + json::parse(std::string(event.data(), event.size())); + + user_level = msg.content.user_level(user_id); + + for (const auto &ty : eventTypes) + min_event_level = + std::min(min_event_level, + (uint16_t)msg.content.state_level(to_string(ty))); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.power_levels event: {}", + e.what()); + } + } + + txn.commit(); + + return user_level >= min_event_level; +} + +std::vector +Cache::roomMembers(const std::string &room_id) +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + std::vector members; + std::string user_id, unused; + + auto db = getMembersDb(txn, room_id); + + auto cursor = lmdb::cursor::open(txn, db); + while (cursor.get(user_id, unused, MDB_NEXT)) + members.emplace_back(std::move(user_id)); + cursor.close(); + + txn.commit(); + + return members; +} + +QHash Cache::DisplayNames; +QHash Cache::AvatarUrls; + +QString +Cache::displayName(const QString &room_id, const QString &user_id) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + if (DisplayNames.contains(fmt)) + return DisplayNames[fmt]; + + return user_id; +} + +std::string +Cache::displayName(const std::string &room_id, const std::string &user_id) +{ + auto fmt = QString::fromStdString(room_id + " " + user_id); + if (DisplayNames.contains(fmt)) + return DisplayNames[fmt].toStdString(); + + return user_id; +} + +QString +Cache::avatarUrl(const QString &room_id, const QString &user_id) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + if (AvatarUrls.contains(fmt)) + return AvatarUrls[fmt]; + + return QString(); +} + +void +Cache::insertDisplayName(const QString &room_id, + const QString &user_id, + const QString &display_name) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + DisplayNames.insert(fmt, display_name); +} + +void +Cache::removeDisplayName(const QString &room_id, const QString &user_id) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + DisplayNames.remove(fmt); +} + +void +Cache::insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + AvatarUrls.insert(fmt, avatar_url); +} + +void +Cache::removeAvatarUrl(const QString &room_id, const QString &user_id) +{ + auto fmt = QString("%1 %2").arg(room_id).arg(user_id); + AvatarUrls.remove(fmt); +} diff --git a/src/Cache.h b/src/Cache.h new file mode 100644 index 00000000..fa8355a5 --- /dev/null +++ b/src/Cache.h @@ -0,0 +1,661 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "Logging.h" + +using mtx::events::state::JoinRule; + +struct RoomMember +{ + QString user_id; + QString display_name; + QImage avatar; +}; + +struct SearchResult +{ + QString user_id; + QString display_name; +}; + +static int +numeric_key_comparison(const MDB_val *a, const MDB_val *b) +{ + auto lhs = std::stoull(std::string((char *)a->mv_data, a->mv_size)); + auto rhs = std::stoull(std::string((char *)b->mv_data, b->mv_size)); + + if (lhs < rhs) + return 1; + else if (lhs == rhs) + return 0; + + return -1; +} + +Q_DECLARE_METATYPE(SearchResult) +Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(RoomMember) +Q_DECLARE_METATYPE(mtx::responses::Timeline) + +//! Used to uniquely identify a list of read receipts. +struct ReadReceiptKey +{ + std::string event_id; + std::string room_id; +}; + +inline void +to_json(json &j, const ReadReceiptKey &key) +{ + j = json{{"event_id", key.event_id}, {"room_id", key.room_id}}; +} + +inline void +from_json(const json &j, ReadReceiptKey &key) +{ + key.event_id = j.at("event_id").get(); + key.room_id = j.at("room_id").get(); +} + +struct DescInfo +{ + QString username; + QString userid; + QString body; + QString timestamp; + QDateTime datetime; +}; + +//! UI info associated with a room. +struct RoomInfo +{ + //! The calculated name of the room. + std::string name; + //! The topic of the room. + std::string topic; + //! The calculated avatar url of the room. + std::string avatar_url; + //! Whether or not the room is an invite. + bool is_invite = false; + //! Total number of members in the room. + int16_t member_count = 0; + //! Who can access to the room. + JoinRule join_rule = JoinRule::Public; + bool guest_access = false; + //! Metadata describing the last message in the timeline. + DescInfo msgInfo; +}; + +inline void +to_json(json &j, const RoomInfo &info) +{ + j["name"] = info.name; + j["topic"] = info.topic; + j["avatar_url"] = info.avatar_url; + j["is_invite"] = info.is_invite; + j["join_rule"] = info.join_rule; + j["guest_access"] = info.guest_access; + + if (info.member_count != 0) + j["member_count"] = info.member_count; +} + +inline void +from_json(const json &j, RoomInfo &info) +{ + info.name = j.at("name"); + info.topic = j.at("topic"); + info.avatar_url = j.at("avatar_url"); + info.is_invite = j.at("is_invite"); + info.join_rule = j.at("join_rule"); + info.guest_access = j.at("guest_access"); + + if (j.count("member_count")) + info.member_count = j.at("member_count"); +} + +//! Basic information per member; +struct MemberInfo +{ + std::string name; + std::string avatar_url; +}; + +inline void +to_json(json &j, const MemberInfo &info) +{ + j["name"] = info.name; + j["avatar_url"] = info.avatar_url; +} + +inline void +from_json(const json &j, MemberInfo &info) +{ + info.name = j.at("name"); + info.avatar_url = j.at("avatar_url"); +} + +struct RoomSearchResult +{ + std::string room_id; + RoomInfo info; + QImage img; +}; + +Q_DECLARE_METATYPE(RoomSearchResult) +Q_DECLARE_METATYPE(RoomInfo) + +// Extra information associated with an outbound megolm session. +struct OutboundGroupSessionData +{ + std::string session_id; + std::string session_key; + uint64_t message_index = 0; +}; + +inline void +to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg) +{ + obj["session_id"] = msg.session_id; + obj["session_key"] = msg.session_key; + obj["message_index"] = msg.message_index; +} + +inline void +from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg) +{ + msg.session_id = obj.at("session_id"); + msg.session_key = obj.at("session_key"); + msg.message_index = obj.at("message_index"); +} + +struct OutboundGroupSessionDataRef +{ + OlmOutboundGroupSession *session; + OutboundGroupSessionData data; +}; + +struct DevicePublicKeys +{ + std::string ed25519; + std::string curve25519; +}; + +inline void +to_json(nlohmann::json &obj, const DevicePublicKeys &msg) +{ + obj["ed25519"] = msg.ed25519; + obj["curve25519"] = msg.curve25519; +} + +inline void +from_json(const nlohmann::json &obj, DevicePublicKeys &msg) +{ + msg.ed25519 = obj.at("ed25519"); + msg.curve25519 = obj.at("curve25519"); +} + +//! Represents a unique megolm session identifier. +struct MegolmSessionIndex +{ + //! The room in which this session exists. + std::string room_id; + //! The session_id of the megolm session. + std::string session_id; + //! The curve25519 public key of the sender. + std::string sender_key; + + //! Representation to be used in a hash map. + std::string to_hash() const { return room_id + session_id + sender_key; } +}; + +struct OlmSessionStorage +{ + // Megolm sessions + std::map group_inbound_sessions; + std::map group_outbound_sessions; + std::map group_outbound_session_data; + + // Guards for accessing megolm sessions. + std::mutex group_outbound_mtx; + std::mutex group_inbound_mtx; +}; + +class Cache : public QObject +{ + Q_OBJECT + +public: + Cache(const QString &userId, QObject *parent = nullptr); + + static QHash DisplayNames; + static QHash AvatarUrls; + + static std::string displayName(const std::string &room_id, const std::string &user_id); + static QString displayName(const QString &room_id, const QString &user_id); + static QString avatarUrl(const QString &room_id, const QString &user_id); + + static void removeDisplayName(const QString &room_id, const QString &user_id); + static void removeAvatarUrl(const QString &room_id, const QString &user_id); + + static void insertDisplayName(const QString &room_id, + const QString &user_id, + const QString &display_name); + static void insertAvatarUrl(const QString &room_id, + const QString &user_id, + const QString &avatar_url); + + //! Load saved data for the display names & avatars. + void populateMembers(); + std::vector joinedRooms(); + + QMap roomInfo(bool withInvites = true); + std::map invites(); + + //! Calculate & return the name of the room. + QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + //! Get room join rules + JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb); + bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb); + //! Retrieve the topic of the room if any. + QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); + //! Retrieve the room avatar's url if any. + QString getRoomAvatarUrl(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const QString &room_id); + + //! Retrieve member info from a room. + std::vector getMembers(const std::string &room_id, + std::size_t startIndex = 0, + std::size_t len = 30); + + void saveState(const mtx::responses::Sync &res); + bool isInitialized() const; + + std::string nextBatchToken() const; + + void deleteData(); + + void removeInvite(lmdb::txn &txn, const std::string &room_id); + void removeInvite(const std::string &room_id); + void removeRoom(lmdb::txn &txn, const std::string &roomid); + void removeRoom(const std::string &roomid); + void removeRoom(const QString &roomid) { removeRoom(roomid.toStdString()); }; + void setup(); + + bool isFormatValid(); + void setCurrentFormat(); + + std::map roomMessages(); + + //! Retrieve all the user ids from a room. + std::vector roomMembers(const std::string &room_id); + + //! Check if the given user has power leve greater than than + //! lowest power level of the given events. + bool hasEnoughPowerLevel(const std::vector &eventTypes, + const std::string &room_id, + const std::string &user_id); + + //! Retrieves the saved room avatar. + QImage getRoomAvatar(const QString &id); + QImage getRoomAvatar(const std::string &id); + + //! Adds a user to the read list for the given event. + //! + //! There should be only one user id present in a receipt list per room. + //! The user id should be removed from any other lists. + using Receipts = std::map>; + void updateReadReceipt(lmdb::txn &txn, + const std::string &room_id, + const Receipts &receipts); + + //! Retrieve all the read receipts for the given event id and room. + //! + //! Returns a map of user ids and the time of the read receipt in milliseconds. + using UserReceipts = std::multimap>; + UserReceipts readReceipts(const QString &event_id, const QString &room_id); + + QByteArray image(const QString &url) const; + QByteArray image(lmdb::txn &txn, const std::string &url) const; + QByteArray image(const std::string &url) const + { + return image(QString::fromStdString(url)); + } + void saveImage(const std::string &url, const std::string &data); + void saveImage(const QString &url, const QByteArray &data); + + RoomInfo singleRoomInfo(const std::string &room_id); + std::vector roomsWithStateUpdates(const mtx::responses::Sync &res); + std::map getRoomInfo(const std::vector &rooms); + std::map roomUpdates(const mtx::responses::Sync &sync) + { + return getRoomInfo(roomsWithStateUpdates(sync)); + } + + QVector searchUsers(const std::string &room_id, + const std::string &query, + std::uint8_t max_items = 5); + std::vector searchRooms(const std::string &query, + std::uint8_t max_items = 5); + + void markSentNotification(const std::string &event_id); + //! Removes an event from the sent notifications. + void removeReadNotification(const std::string &event_id); + //! Check if we have sent a desktop notification for the given event id. + bool isNotificationSent(const std::string &event_id); + + //! Mark a room that uses e2e encryption. + void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); + bool isRoomEncrypted(const std::string &room_id); + + //! Save the public keys for a device. + void saveDeviceKeys(const std::string &device_id); + void getDeviceKeys(const std::string &device_id); + + //! Save the device list for a user. + void setDeviceList(const std::string &user_id, const std::vector &devices); + std::vector getDeviceList(const std::string &user_id); + + // + // Outbound Megolm Sessions + // + void saveOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr session); + OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); + bool outboundMegolmSessionExists(const std::string &room_id) noexcept; + void updateOutboundMegolmSession(const std::string &room_id, int message_index); + + // + // Inbound Megolm Sessions + // + void saveInboundMegolmSession(const MegolmSessionIndex &index, + mtx::crypto::InboundGroupSessionPtr session); + OlmInboundGroupSession *getInboundMegolmSession(const MegolmSessionIndex &index); + bool inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept; + + // + // Olm Sessions + // + void saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session); + std::vector getOlmSessions(const std::string &curve25519); + boost::optional getOlmSession(const std::string &curve25519, + const std::string &session_id); + + void saveOlmAccount(const std::string &pickled); + std::string restoreOlmAccount(); + + void restoreSessions(); + + OlmSessionStorage session_storage; + +private: + //! Save an invited room. + void saveInvite(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const mtx::responses::InvitedRoom &room); + + QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); + QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + + DescInfo getLastMessageInfo(lmdb::txn &txn, const std::string &room_id); + void saveTimelineMessages(lmdb::txn &txn, + const std::string &room_id, + const mtx::responses::Timeline &res); + + mtx::responses::Timeline getTimelineMessages(lmdb::txn &txn, const std::string &room_id); + + //! Remove a room from the cache. + // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id); + template + void saveStateEvents(lmdb::txn &txn, + const lmdb::dbi &statesdb, + const lmdb::dbi &membersdb, + const std::string &room_id, + const std::vector &events) + { + for (const auto &e : events) + saveStateEvent(txn, statesdb, membersdb, room_id, e); + } + + template + void saveStateEvent(lmdb::txn &txn, + const lmdb::dbi &statesdb, + const lmdb::dbi &membersdb, + const std::string &room_id, + const T &event) + { + using namespace mtx::events; + using namespace mtx::events::state; + + if (mpark::holds_alternative>(event)) { + const auto e = mpark::get>(event); + + switch (e.content.membership) { + // + // We only keep users with invite or join membership. + // + case Membership::Invite: + case Membership::Join: { + auto display_name = e.content.display_name.empty() + ? e.state_key + : e.content.display_name; + + // Lightweight representation of a member. + MemberInfo tmp{display_name, e.content.avatar_url}; + + lmdb::dbi_put(txn, + membersdb, + lmdb::val(e.state_key), + lmdb::val(json(tmp).dump())); + + insertDisplayName(QString::fromStdString(room_id), + QString::fromStdString(e.state_key), + QString::fromStdString(display_name)); + + insertAvatarUrl(QString::fromStdString(room_id), + QString::fromStdString(e.state_key), + QString::fromStdString(e.content.avatar_url)); + + break; + } + default: { + lmdb::dbi_del( + txn, membersdb, lmdb::val(e.state_key), lmdb::val("")); + + removeDisplayName(QString::fromStdString(room_id), + QString::fromStdString(e.state_key)); + removeAvatarUrl(QString::fromStdString(room_id), + QString::fromStdString(e.state_key)); + + break; + } + } + + return; + } else if (mpark::holds_alternative>(event)) { + setEncryptedRoom(txn, room_id); + return; + } + + if (!isStateEvent(event)) + return; + + mpark::visit( + [&txn, &statesdb](auto e) { + lmdb::dbi_put( + txn, statesdb, lmdb::val(to_string(e.type)), lmdb::val(json(e).dump())); + }, + event); + } + + template + bool isStateEvent(const T &e) + { + using namespace mtx::events; + using namespace mtx::events::state; + + return mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e); + } + + template + bool containsStateUpdates(const T &e) + { + using namespace mtx::events; + using namespace mtx::events::state; + + return mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e); + } + + bool containsStateUpdates(const mtx::events::collections::StrippedEvents &e) + { + using namespace mtx::events; + using namespace mtx::events::state; + + return mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e) || + mpark::holds_alternative>(e); + } + + void saveInvites(lmdb::txn &txn, + const std::map &rooms); + + //! Sends signals for the rooms that are removed. + void removeLeftRooms(lmdb::txn &txn, + const std::map &rooms) + { + for (const auto &room : rooms) { + removeRoom(txn, room.first); + + // Clean up leftover invites. + removeInvite(txn, room.first); + } + } + + lmdb::dbi getMessagesDb(lmdb::txn &txn, const std::string &room_id) + { + auto db = + lmdb::dbi::open(txn, std::string(room_id + "/messages").c_str(), MDB_CREATE); + lmdb::dbi_set_compare(txn, db, numeric_key_comparison); + + return db; + } + + lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open( + txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE); + } + + lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open( + txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE); + } + + lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE); + } + + lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE); + } + + //! Retrieves or creates the database that stores the open OLM sessions between our device + //! and the given curve25519 key which represents another device. + //! + //! Each entry is a map from the session_id to the pickled representation of the session. + lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key) + { + return lmdb::dbi::open( + txn, std::string("olm_sessions/" + curve25519_key).c_str(), MDB_CREATE); + } + + QString getDisplayName(const mtx::events::StateEvent &event) + { + if (!event.content.display_name.empty()) + return QString::fromStdString(event.content.display_name); + + return QString::fromStdString(event.state_key); + } + + void setNextBatchToken(lmdb::txn &txn, const std::string &token); + void setNextBatchToken(lmdb::txn &txn, const QString &token); + + lmdb::env env_; + lmdb::dbi syncStateDb_; + lmdb::dbi roomsDb_; + lmdb::dbi invitesDb_; + lmdb::dbi mediaDb_; + lmdb::dbi readReceiptsDb_; + lmdb::dbi notificationsDb_; + + lmdb::dbi devicesDb_; + lmdb::dbi deviceKeysDb_; + + lmdb::dbi inboundMegolmSessionDb_; + lmdb::dbi outboundMegolmSessionDb_; + + QString localUserId_; + QString cacheDirectory_; +}; + +namespace cache { +void +init(const QString &user_id); + +Cache * +client(); +} diff --git a/src/ChatPage.cc b/src/ChatPage.cc deleted file mode 100644 index ff059cee..00000000 --- a/src/ChatPage.cc +++ /dev/null @@ -1,1347 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include - -#include "AvatarProvider.h" -#include "Cache.h" -#include "ChatPage.h" -#include "Logging.hpp" -#include "MainWindow.h" -#include "MatrixClient.h" -#include "Olm.hpp" -#include "OverlayModal.h" -#include "QuickSwitcher.h" -#include "RoomList.h" -#include "SideBarActions.h" -#include "Splitter.h" -#include "TextInputWidget.h" -#include "Theme.h" -#include "TopRoomBar.h" -#include "TypingDisplay.h" -#include "UserInfoWidget.h" -#include "UserSettingsPage.h" -#include "Utils.h" - -#include "notifications/Manager.h" - -#include "dialogs/ReadReceipts.h" -#include "timeline/TimelineViewManager.h" - -// TODO: Needs to be updated with an actual secret. -static const std::string STORAGE_SECRET_KEY("secret"); - -ChatPage *ChatPage::instance_ = nullptr; -constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000; -constexpr size_t MAX_ONETIME_KEYS = 50; - -ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) - : QWidget(parent) - , isConnected_(true) - , userSettings_{userSettings} - , notificationsManager(this) -{ - setObjectName("chatPage"); - - topLayout_ = new QHBoxLayout(this); - topLayout_->setSpacing(0); - topLayout_->setMargin(0); - - communitiesList_ = new CommunitiesList(this); - topLayout_->addWidget(communitiesList_); - - splitter = new Splitter(this); - splitter->setHandleWidth(0); - - topLayout_->addWidget(splitter); - - // SideBar - sideBar_ = new QFrame(this); - sideBar_->setObjectName("sideBar"); - sideBar_->setMinimumWidth(ui::sidebar::NormalSize); - sideBarLayout_ = new QVBoxLayout(sideBar_); - sideBarLayout_->setSpacing(0); - sideBarLayout_->setMargin(0); - - sideBarTopWidget_ = new QWidget(sideBar_); - sidebarActions_ = new SideBarActions(this); - connect( - sidebarActions_, &SideBarActions::showSettings, this, &ChatPage::showUserSettingsPage); - connect(sidebarActions_, &SideBarActions::joinRoom, this, &ChatPage::joinRoom); - connect(sidebarActions_, &SideBarActions::createRoom, this, &ChatPage::createRoom); - - user_info_widget_ = new UserInfoWidget(sideBar_); - room_list_ = new RoomList(userSettings_, sideBar_); - connect(room_list_, &RoomList::joinRoom, this, &ChatPage::joinRoom); - - sideBarLayout_->addWidget(user_info_widget_); - sideBarLayout_->addWidget(room_list_); - sideBarLayout_->addWidget(sidebarActions_); - - sideBarTopWidgetLayout_ = new QVBoxLayout(sideBarTopWidget_); - sideBarTopWidgetLayout_->setSpacing(0); - sideBarTopWidgetLayout_->setMargin(0); - - // Content - content_ = new QFrame(this); - content_->setObjectName("mainContent"); - contentLayout_ = new QVBoxLayout(content_); - contentLayout_->setSpacing(0); - contentLayout_->setMargin(0); - - top_bar_ = new TopRoomBar(this); - view_manager_ = new TimelineViewManager(this); - - contentLayout_->addWidget(top_bar_); - contentLayout_->addWidget(view_manager_); - - connect(this, - &ChatPage::removeTimelineEvent, - view_manager_, - &TimelineViewManager::removeTimelineEvent); - - // Splitter - splitter->addWidget(sideBar_); - splitter->addWidget(content_); - splitter->restoreSizes(parent->width()); - - text_input_ = new TextInputWidget(this); - typingDisplay_ = new TypingDisplay(this); - contentLayout_->addWidget(typingDisplay_); - contentLayout_->addWidget(text_input_); - - typingRefresher_ = new QTimer(this); - typingRefresher_->setInterval(TYPING_REFRESH_TIMEOUT); - - connect(this, &ChatPage::connectionLost, this, [this]() { - nhlog::net()->info("connectivity lost"); - isConnected_ = false; - http::client()->shutdown(); - text_input_->disableInput(); - }); - connect(this, &ChatPage::connectionRestored, this, [this]() { - nhlog::net()->info("trying to re-connect"); - text_input_->enableInput(); - isConnected_ = true; - - // Drop all pending connections. - http::client()->shutdown(); - trySync(); - }); - - connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL); - connect(&connectivityTimer_, &QTimer::timeout, this, [=]() { - if (http::client()->access_token().empty()) { - connectivityTimer_.stop(); - return; - } - - http::client()->versions( - [this](const mtx::responses::Versions &, mtx::http::RequestErr err) { - if (err) { - emit connectionLost(); - return; - } - - if (!isConnected_) - emit connectionRestored(); - }); - }); - - connect(this, &ChatPage::loggedOut, this, &ChatPage::logout); - connect(user_info_widget_, &UserInfoWidget::logout, this, [this]() { - http::client()->logout( - [this](const mtx::responses::Logout &, mtx::http::RequestErr err) { - if (err) { - // TODO: handle special errors - emit contentLoaded(); - nhlog::net()->warn( - "failed to logout: {} - {}", - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - emit loggedOut(); - }); - - emit showOverlayProgressBar(); - }); - - connect(top_bar_, &TopRoomBar::showRoomList, splitter, &Splitter::showFullRoomList); - connect(top_bar_, &TopRoomBar::inviteUsers, this, [this](QStringList users) { - const auto room_id = current_room_.toStdString(); - - for (int ii = 0; ii < users.size(); ++ii) { - QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { - const auto user = users.at(ii); - - http::client()->invite_user( - room_id, - user.toStdString(), - [this, user](const mtx::responses::RoomInvite &, - mtx::http::RequestErr err) { - if (err) { - emit showNotification( - QString("Failed to invite user: %1").arg(user)); - return; - } - - emit showNotification( - QString("Invited user: %1").arg(user)); - }); - }); - } - }); - - connect(room_list_, &RoomList::roomChanged, this, [this](const QString &roomid) { - QStringList users; - - if (!userSettings_->isTypingNotificationsEnabled()) { - typingDisplay_->setUsers(users); - return; - } - - if (typingUsers_.find(roomid) != typingUsers_.end()) - users = typingUsers_[roomid]; - - typingDisplay_->setUsers(users); - }); - connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::stopTyping); - connect(room_list_, &RoomList::roomChanged, this, &ChatPage::changeTopRoomInfo); - connect(room_list_, &RoomList::roomChanged, splitter, &Splitter::showChatView); - connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::focusLineEdit); - connect( - room_list_, &RoomList::roomChanged, view_manager_, &TimelineViewManager::setHistoryView); - - connect(room_list_, &RoomList::acceptInvite, this, [this](const QString &room_id) { - view_manager_->addRoom(room_id); - joinRoom(room_id); - room_list_->removeRoom(room_id, currentRoom() == room_id); - }); - - connect(room_list_, &RoomList::declineInvite, this, [this](const QString &room_id) { - leaveRoom(room_id); - room_list_->removeRoom(room_id, currentRoom() == room_id); - }); - - connect( - text_input_, &TextInputWidget::startedTyping, this, &ChatPage::sendTypingNotifications); - connect(typingRefresher_, &QTimer::timeout, this, &ChatPage::sendTypingNotifications); - connect(text_input_, &TextInputWidget::stoppedTyping, this, [this]() { - if (!userSettings_->isTypingNotificationsEnabled()) - return; - - typingRefresher_->stop(); - - if (current_room_.isEmpty()) - return; - - http::client()->stop_typing( - current_room_.toStdString(), [](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to stop typing notifications: {}", - err->matrix_error.error); - } - }); - }); - - connect(view_manager_, - &TimelineViewManager::updateRoomsLastMessage, - room_list_, - &RoomList::updateRoomDescription); - - connect(room_list_, - SIGNAL(totalUnreadMessageCountUpdated(int)), - this, - SLOT(showUnreadMessageNotification(int))); - - connect(text_input_, - SIGNAL(sendTextMessage(const QString &)), - view_manager_, - SLOT(queueTextMessage(const QString &))); - - connect(text_input_, - SIGNAL(sendEmoteMessage(const QString &)), - view_manager_, - SLOT(queueEmoteMessage(const QString &))); - - connect(text_input_, &TextInputWidget::sendJoinRoomRequest, this, &ChatPage::joinRoom); - - connect( - text_input_, - &TextInputWidget::uploadImage, - this, - [this](QSharedPointer dev, const QString &fn) { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(dev.data()); - - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->peek(dev->size()); - auto payload = std::string(bin.data(), bin.size()); - auto dimensions = QImageReader(dev.data()).size(); - - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - mime = mime.name(), - size = payload.size(), - dimensions](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload image. Please try again.")); - nhlog::net()->warn("failed to upload image: {} {} ({})", - err->matrix_error.error, - to_string(err->matrix_error.errcode), - static_cast(err->status_code)); - return; - } - - emit imageUploaded(room_id, - filename, - QString::fromStdString(res.content_uri), - mime, - size, - dimensions); - }); - }); - - connect(text_input_, - &TextInputWidget::uploadFile, - this, - [this](QSharedPointer dev, const QString &fn) { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(dev.data()); - - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - auto payload = std::string(bin.data(), bin.size()); - - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - mime = mime.name(), - size = payload.size()](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload file. Please try again.")); - nhlog::net()->warn("failed to upload file: {} ({})", - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - emit fileUploaded(room_id, - filename, - QString::fromStdString(res.content_uri), - mime, - size); - }); - }); - - connect(text_input_, - &TextInputWidget::uploadAudio, - this, - [this](QSharedPointer dev, const QString &fn) { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(dev.data()); - - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - auto payload = std::string(bin.data(), bin.size()); - - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - mime = mime.name(), - size = payload.size()](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload audio. Please try again.")); - nhlog::net()->warn("failed to upload audio: {} ({})", - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - emit audioUploaded(room_id, - filename, - QString::fromStdString(res.content_uri), - mime, - size); - }); - }); - connect(text_input_, - &TextInputWidget::uploadVideo, - this, - [this](QSharedPointer dev, const QString &fn) { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(dev.data()); - - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - auto payload = std::string(bin.data(), bin.size()); - - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - mime = mime.name(), - size = payload.size()](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload video. Please try again.")); - nhlog::net()->warn("failed to upload video: {} ({})", - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - emit videoUploaded(room_id, - filename, - QString::fromStdString(res.content_uri), - mime, - size); - }); - }); - - connect(this, &ChatPage::uploadFailed, this, [this](const QString &msg) { - text_input_->hideUploadSpinner(); - emit showNotification(msg); - }); - connect(this, - &ChatPage::imageUploaded, - this, - [this](QString roomid, - QString filename, - QString url, - QString mime, - qint64 dsize, - QSize dimensions) { - text_input_->hideUploadSpinner(); - view_manager_->queueImageMessage( - roomid, filename, url, mime, dsize, dimensions); - }); - connect(this, - &ChatPage::fileUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueFileMessage(roomid, filename, url, mime, dsize); - }); - connect(this, - &ChatPage::audioUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueAudioMessage(roomid, filename, url, mime, dsize); - }); - connect(this, - &ChatPage::videoUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueVideoMessage(roomid, filename, url, mime, dsize); - }); - - connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar); - - connect( - this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities); - - connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom); - connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendDesktopNotifications); - - connect(communitiesList_, - &CommunitiesList::communityChanged, - this, - [this](const QString &groupId) { - current_community_ = groupId; - - if (groupId == "world") - room_list_->setFilterRooms(false); - else - room_list_->setRoomFilter(communitiesList_->roomList(groupId)); - }); - - connect(¬ificationsManager, - &NotificationsManager::notificationClicked, - this, - [this](const QString &roomid, const QString &eventid) { - Q_UNUSED(eventid) - room_list_->highlightSelectedRoom(roomid); - activateWindow(); - }); - - setGroupViewState(userSettings_->isGroupViewEnabled()); - - connect(userSettings_.data(), - &UserSettings::groupViewStateChanged, - this, - &ChatPage::setGroupViewState); - - connect(this, &ChatPage::initializeRoomList, room_list_, &RoomList::initialize); - connect(this, - &ChatPage::initializeViews, - view_manager_, - [this](const mtx::responses::Rooms &rooms) { view_manager_->initialize(rooms); }); - connect(this, - &ChatPage::initializeEmptyViews, - view_manager_, - &TimelineViewManager::initWithMessages); - connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) { - try { - room_list_->cleanupInvites(cache::client()->invites()); - } catch (const lmdb::error &e) { - nhlog::db()->error("failed to retrieve invites: {}", e.what()); - } - - view_manager_->initialize(rooms); - removeLeftRooms(rooms.leave); - - bool hasNotifications = false; - for (const auto &room : rooms.join) { - auto room_id = QString::fromStdString(room.first); - - updateTypingUsers(room_id, room.second.ephemeral.typing); - updateRoomNotificationCount( - room_id, room.second.unread_notifications.notification_count); - - if (room.second.unread_notifications.notification_count > 0) - hasNotifications = true; - } - - if (hasNotifications) - http::client()->notifications( - 5, - [this](const mtx::responses::Notifications &res, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to retrieve notifications: {} ({})", - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - emit notificationsRetrieved(std::move(res)); - }); - }); - connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync); - connect( - this, &ChatPage::syncTopBar, this, [this](const std::map &updates) { - if (updates.find(currentRoom()) != updates.end()) - changeTopRoomInfo(currentRoom()); - }); - - // Callbacks to update the user info (top left corner of the page). - connect(this, &ChatPage::setUserAvatar, user_info_widget_, &UserInfoWidget::setAvatar); - connect(this, &ChatPage::setUserDisplayName, this, [this](const QString &name) { - QSettings settings; - auto userid = settings.value("auth/user_id").toString(); - user_info_widget_->setUserId(userid); - user_info_widget_->setDisplayName(name); - }); - - connect(this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync); - connect(this, &ChatPage::trySyncCb, this, &ChatPage::trySync); - connect(this, &ChatPage::tryDelayedSyncCb, this, [this]() { - QTimer::singleShot(5000, this, &ChatPage::trySync); - }); - - connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage); - connect(this, &ChatPage::messageReply, text_input_, &TextInputWidget::addReply); - - instance_ = this; -} - -void -ChatPage::logout() -{ - deleteConfigs(); - - resetUI(); - - emit closing(); - connectivityTimer_.stop(); -} - -void -ChatPage::dropToLoginPage(const QString &msg) -{ - deleteConfigs(); - resetUI(); - - http::client()->shutdown(); - connectivityTimer_.stop(); - - emit showLoginPage(msg); -} - -void -ChatPage::resetUI() -{ - room_list_->clear(); - top_bar_->reset(); - user_info_widget_->reset(); - view_manager_->clearAll(); - - showUnreadMessageNotification(0); -} - -void -ChatPage::deleteConfigs() -{ - QSettings settings; - settings.beginGroup("auth"); - settings.remove(""); - settings.endGroup(); - settings.beginGroup("client"); - settings.remove(""); - settings.endGroup(); - settings.beginGroup("notifications"); - settings.remove(""); - settings.endGroup(); - - cache::client()->deleteData(); - http::client()->clear(); -} - -void -ChatPage::bootstrap(QString userid, QString homeserver, QString token) -{ - using namespace mtx::identifiers; - - try { - http::client()->set_user(parse(userid.toStdString())); - } catch (const std::invalid_argument &e) { - nhlog::ui()->critical("bootstrapped with invalid user_id: {}", - userid.toStdString()); - } - - http::client()->set_server(homeserver.toStdString()); - http::client()->set_access_token(token.toStdString()); - - // The Olm client needs the user_id & device_id that will be included - // in the generated payloads & keys. - olm::client()->set_user_id(http::client()->user_id().to_string()); - olm::client()->set_device_id(http::client()->device_id()); - - try { - cache::init(userid); - - const bool isInitialized = cache::client()->isInitialized(); - const bool isValid = cache::client()->isFormatValid(); - - if (isInitialized && !isValid) { - nhlog::db()->warn("breaking changes in cache"); - // TODO: Deleting session data but keep using the - // same device doesn't work. - cache::client()->deleteData(); - - cache::init(userid); - cache::client()->setCurrentFormat(); - } else if (isInitialized) { - loadStateFromCache(); - return; - } - } catch (const lmdb::error &e) { - nhlog::db()->critical("failure during boot: {}", e.what()); - cache::client()->deleteData(); - nhlog::net()->info("falling back to initial sync"); - } - - try { - // It's the first time syncing with this device - // There isn't a saved olm account to restore. - nhlog::crypto()->info("creating new olm account"); - olm::client()->create_new_account(); - cache::client()->saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); - } catch (const lmdb::error &e) { - nhlog::crypto()->critical("failed to save olm account {}", e.what()); - emit dropToLoginPageCb(QString::fromStdString(e.what())); - return; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to create new olm account {}", e.what()); - emit dropToLoginPageCb(QString::fromStdString(e.what())); - return; - } - - getProfileInfo(); - tryInitialSync(); -} - -void -ChatPage::updateTopBarAvatar(const QString &roomid, const QPixmap &img) -{ - if (current_room_ != roomid) - return; - - top_bar_->updateRoomAvatar(img.toImage()); -} - -void -ChatPage::changeTopRoomInfo(const QString &room_id) -{ - if (room_id.isEmpty()) { - nhlog::ui()->warn("cannot switch to empty room_id"); - return; - } - - try { - auto room_info = cache::client()->getRoomInfo({room_id.toStdString()}); - - if (room_info.find(room_id) == room_info.end()) - return; - - const auto name = QString::fromStdString(room_info[room_id].name); - const auto avatar_url = QString::fromStdString(room_info[room_id].avatar_url); - - top_bar_->updateRoomName(name); - top_bar_->updateRoomTopic(QString::fromStdString(room_info[room_id].topic)); - - auto img = cache::client()->getRoomAvatar(room_id); - - if (img.isNull()) - top_bar_->updateRoomAvatarFromName(name); - else - top_bar_->updateRoomAvatar(img); - - } catch (const lmdb::error &e) { - nhlog::ui()->error("failed to change top bar room info: {}", e.what()); - } - - current_room_ = room_id; -} - -void -ChatPage::showUnreadMessageNotification(int count) -{ - emit unreadMessages(count); - - // TODO: Make the default title a const. - if (count == 0) - emit changeWindowTitle("nheko"); - else - emit changeWindowTitle(QString("nheko (%1)").arg(count)); -} - -void -ChatPage::loadStateFromCache() -{ - emit contentLoaded(); - - nhlog::db()->info("restoring state from cache"); - - getProfileInfo(); - - QtConcurrent::run([this]() { - try { - cache::client()->restoreSessions(); - olm::client()->load(cache::client()->restoreOlmAccount(), - STORAGE_SECRET_KEY); - - cache::client()->populateMembers(); - - emit initializeEmptyViews(cache::client()->roomMessages()); - emit initializeRoomList(cache::client()->roomInfo()); - - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to restore olm account: {}", e.what()); - emit dropToLoginPageCb( - tr("Failed to restore OLM account. Please login again.")); - return; - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to restore cache: {}", e.what()); - emit dropToLoginPageCb( - tr("Failed to restore save data. Please login again.")); - return; - } catch (const json::exception &e) { - nhlog::db()->critical("failed to parse cache data: {}", e.what()); - return; - } - - nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); - nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); - - // Start receiving events. - emit trySyncCb(); - }); -} - -void -ChatPage::showQuickSwitcher() -{ - if (quickSwitcher_.isNull()) { - quickSwitcher_ = QSharedPointer( - new QuickSwitcher(this), - [](QuickSwitcher *switcher) { switcher->deleteLater(); }); - - connect(quickSwitcher_.data(), - &QuickSwitcher::roomSelected, - room_list_, - &RoomList::highlightSelectedRoom); - - connect(quickSwitcher_.data(), &QuickSwitcher::closing, this, [this]() { - if (!quickSwitcherModal_.isNull()) - quickSwitcherModal_->hide(); - text_input_->setFocus(Qt::FocusReason::PopupFocusReason); - }); - } - - if (quickSwitcherModal_.isNull()) { - quickSwitcherModal_ = QSharedPointer( - new OverlayModal(MainWindow::instance(), quickSwitcher_.data()), - [](OverlayModal *modal) { modal->deleteLater(); }); - quickSwitcherModal_->setColor(QColor(30, 30, 30, 170)); - } - - quickSwitcherModal_->show(); -} - -void -ChatPage::removeRoom(const QString &room_id) -{ - try { - cache::client()->removeRoom(room_id); - cache::client()->removeInvite(room_id.toStdString()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failure while removing room: {}", e.what()); - // TODO: Notify the user. - } - - room_list_->removeRoom(room_id, room_id == current_room_); -} - -void -ChatPage::updateTypingUsers(const QString &roomid, const std::vector &user_ids) -{ - if (!userSettings_->isTypingNotificationsEnabled()) - return; - - typingUsers_[roomid] = generateTypingUsers(roomid, user_ids); - - if (current_room_ == roomid) - typingDisplay_->setUsers(typingUsers_[roomid]); -} - -QStringList -ChatPage::generateTypingUsers(const QString &room_id, const std::vector &typing_users) -{ - QStringList users; - - QSettings settings; - QString local_user = settings.value("auth/user_id").toString(); - - for (const auto &uid : typing_users) { - const auto remote_user = QString::fromStdString(uid); - - if (remote_user == local_user) - continue; - - users.append(Cache::displayName(room_id, remote_user)); - } - - users.sort(); - - return users; -} - -void -ChatPage::removeLeftRooms(const std::map &rooms) -{ - for (auto it = rooms.cbegin(); it != rooms.cend(); ++it) { - const auto room_id = QString::fromStdString(it->first); - room_list_->removeRoom(room_id, room_id == current_room_); - } -} - -void -ChatPage::showReadReceipts(const QString &event_id) -{ - if (receiptsDialog_.isNull()) { - receiptsDialog_ = QSharedPointer( - new dialogs::ReadReceipts(this), - [](dialogs::ReadReceipts *dialog) { dialog->deleteLater(); }); - } - - if (receiptsModal_.isNull()) { - receiptsModal_ = QSharedPointer( - new OverlayModal(MainWindow::instance(), receiptsDialog_.data()), - [](OverlayModal *modal) { modal->deleteLater(); }); - receiptsModal_->setColor(QColor(30, 30, 30, 170)); - } - - receiptsDialog_->addUsers(cache::client()->readReceipts(event_id, current_room_)); - receiptsModal_->show(); -} - -void -ChatPage::setGroupViewState(bool isEnabled) -{ - if (!isEnabled) { - communitiesList_->communityChanged("world"); - communitiesList_->hide(); - - return; - } - - communitiesList_->show(); -} - -void -ChatPage::updateRoomNotificationCount(const QString &room_id, uint16_t notification_count) -{ - room_list_->updateUnreadMessageCount(room_id, notification_count); -} - -void -ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res) -{ - for (const auto &item : res.notifications) { - const auto event_id = utils::event_id(item.event); - - try { - if (item.read) { - cache::client()->removeReadNotification(event_id); - continue; - } - - if (!cache::client()->isNotificationSent(event_id)) { - const auto room_id = QString::fromStdString(item.room_id); - const auto user_id = utils::event_sender(item.event); - - // We should only sent one notification per event. - cache::client()->markSentNotification(event_id); - - // Don't send a notification when the current room is opened. - if (isRoomActive(room_id)) - continue; - - notificationsManager.postNotification( - room_id, - QString::fromStdString(event_id), - QString::fromStdString( - cache::client()->singleRoomInfo(item.room_id).name), - Cache::displayName(room_id, user_id), - utils::event_body(item.event), - cache::client()->getRoomAvatar(room_id)); - } - } catch (const lmdb::error &e) { - nhlog::db()->warn("error while sending desktop notification: {}", e.what()); - } - } -} - -void -ChatPage::tryInitialSync() -{ - nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); - nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); - - // Upload one time keys for the device. - nhlog::crypto()->info("generating one time keys"); - olm::client()->generate_one_time_keys(MAX_ONETIME_KEYS); - - http::client()->upload_keys( - olm::client()->create_upload_keys_request(), - [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) { - if (err) { - const int status_code = static_cast(err->status_code); - nhlog::crypto()->critical("failed to upload one time keys: {} {}", - err->matrix_error.error, - status_code); - // TODO We should have a timeout instead of keeping hammering the server. - emit tryInitialSyncCb(); - return; - } - - olm::mark_keys_as_published(); - - for (const auto &entry : res.one_time_key_counts) - nhlog::net()->info( - "uploaded {} {} one-time keys", entry.second, entry.first); - - nhlog::net()->info("trying initial sync"); - - mtx::http::SyncOpts opts; - opts.timeout = 0; - http::client()->sync(opts, - std::bind(&ChatPage::initialSyncHandler, - this, - std::placeholders::_1, - std::placeholders::_2)); - }); -} - -void -ChatPage::trySync() -{ - mtx::http::SyncOpts opts; - - if (!connectivityTimer_.isActive()) - connectivityTimer_.start(); - - try { - opts.since = cache::client()->nextBatchToken(); - } catch (const lmdb::error &e) { - nhlog::db()->error("failed to retrieve next batch token: {}", e.what()); - return; - } - - http::client()->sync( - opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) { - if (err) { - const auto error = QString::fromStdString(err->matrix_error.error); - const auto msg = tr("Please try to login again: %1").arg(error); - const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); - const int status_code = static_cast(err->status_code); - - nhlog::net()->error("sync error: {} {}", status_code, err_code); - - if (status_code <= 0 || status_code >= 600) { - if (!http::is_logged_in()) - return; - - emit tryDelayedSyncCb(); - return; - } - - switch (status_code) { - case 502: - case 504: - case 524: { - emit trySyncCb(); - return; - } - default: { - if (!http::is_logged_in()) - return; - - if (err->matrix_error.errcode == - mtx::errors::ErrorCode::M_UNKNOWN_TOKEN) - emit dropToLoginPageCb(msg); - else - emit tryDelayedSyncCb(); - - return; - } - } - } - - nhlog::net()->debug("sync completed: {}", res.next_batch); - - // Ensure that we have enough one-time keys available. - ensureOneTimeKeyCount(res.device_one_time_keys_count); - - // TODO: fine grained error handling - try { - cache::client()->saveState(res); - olm::handle_to_device_messages(res.to_device); - - emit syncUI(res.rooms); - - auto updates = cache::client()->roomUpdates(res); - - emit syncTopBar(updates); - emit syncRoomlist(updates); - } catch (const lmdb::error &e) { - nhlog::db()->error("saving sync response: {}", e.what()); - } - - emit trySyncCb(); - }); -} - -void -ChatPage::joinRoom(const QString &room) -{ - const auto room_id = room.toStdString(); - - http::client()->join_room( - room_id, [this, room_id](const nlohmann::json &, mtx::http::RequestErr err) { - if (err) { - emit showNotification( - QString("Failed to join room: %1") - .arg(QString::fromStdString(err->matrix_error.error))); - return; - } - - emit showNotification("You joined the room"); - - // We remove any invites with the same room_id. - try { - cache::client()->removeInvite(room_id); - } catch (const lmdb::error &e) { - emit showNotification( - QString("Failed to remove invite: %1").arg(e.what())); - } - }); -} - -void -ChatPage::createRoom(const mtx::requests::CreateRoom &req) -{ - http::client()->create_room( - req, [this](const mtx::responses::CreateRoom &res, mtx::http::RequestErr err) { - if (err) { - emit showNotification( - tr("Room creation failed: %1") - .arg(QString::fromStdString(err->matrix_error.error))); - return; - } - - emit showNotification(QString("Room %1 created") - .arg(QString::fromStdString(res.room_id.to_string()))); - }); -} - -void -ChatPage::leaveRoom(const QString &room_id) -{ - http::client()->leave_room( - room_id.toStdString(), [this, room_id](const json &, mtx::http::RequestErr err) { - if (err) { - emit showNotification( - tr("Failed to leave room: %1") - .arg(QString::fromStdString(err->matrix_error.error))); - return; - } - - emit leftRoom(room_id); - }); -} - -void -ChatPage::sendTypingNotifications() -{ - if (!userSettings_->isTypingNotificationsEnabled()) - return; - - http::client()->start_typing( - current_room_.toStdString(), 10'000, [](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send typing notification: {}", - err->matrix_error.error); - } - }); -} - -void -ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::RequestErr err) -{ - if (err) { - const auto error = QString::fromStdString(err->matrix_error.error); - const auto msg = tr("Please try to login again: %1").arg(error); - const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); - const int status_code = static_cast(err->status_code); - - nhlog::net()->error("sync error: {} {}", status_code, err_code); - - switch (status_code) { - case 502: - case 504: - case 524: { - emit tryInitialSyncCb(); - return; - } - default: { - emit dropToLoginPageCb(msg); - return; - } - } - } - - nhlog::net()->info("initial sync completed"); - - try { - cache::client()->saveState(res); - - olm::handle_to_device_messages(res.to_device); - - emit initializeViews(std::move(res.rooms)); - emit initializeRoomList(cache::client()->roomInfo()); - } catch (const lmdb::error &e) { - nhlog::db()->error("{}", e.what()); - emit tryInitialSyncCb(); - return; - } - - emit trySyncCb(); - emit contentLoaded(); -} - -void -ChatPage::ensureOneTimeKeyCount(const std::map &counts) -{ - for (const auto &entry : counts) { - if (entry.second < MAX_ONETIME_KEYS) { - const int nkeys = MAX_ONETIME_KEYS - entry.second; - - nhlog::crypto()->info("uploading {} {} keys", nkeys, entry.first); - olm::client()->generate_one_time_keys(nkeys); - - http::client()->upload_keys( - olm::client()->create_upload_keys_request(), - [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) { - if (err) { - nhlog::crypto()->warn( - "failed to update one-time keys: {} {}", - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - olm::mark_keys_as_published(); - }); - } - } -} - -void -ChatPage::getProfileInfo() -{ - QSettings settings; - const auto userid = settings.value("auth/user_id").toString().toStdString(); - - http::client()->get_profile( - userid, [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve own profile info"); - return; - } - - emit setUserDisplayName(QString::fromStdString(res.display_name)); - - if (cache::client()) { - auto data = cache::client()->image(res.avatar_url); - if (!data.isNull()) { - emit setUserAvatar(QImage::fromData(data)); - return; - } - } - - if (res.avatar_url.empty()) - return; - - http::client()->download( - res.avatar_url, - [this, res](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to download user avatar: {} - {}", - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - if (cache::client()) - cache::client()->saveImage(res.avatar_url, data); - - emit setUserAvatar( - QImage::fromData(QByteArray(data.data(), data.size()))); - }); - }); - - http::client()->joined_groups( - [this](const mtx::responses::JoinedGroups &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->critical("failed to retrieve joined groups: {} {}", - static_cast(err->status_code), - err->matrix_error.error); - return; - } - - emit updateGroupsInfo(res); - }); -} - -void -ChatPage::hideSideBars() -{ - communitiesList_->hide(); - sideBar_->hide(); - top_bar_->enableBackButton(); -} - -void -ChatPage::showSideBars() -{ - if (userSettings_->isGroupViewEnabled()) - communitiesList_->show(); - - sideBar_->show(); - top_bar_->disableBackButton(); -} - -int -ChatPage::timelineWidth() -{ - int sidebarWidth = sideBar_->size().width(); - sidebarWidth += communitiesList_->size().width(); - - return size().width() - sidebarWidth; -} -bool -ChatPage::isSideBarExpanded() -{ - return sideBar_->size().width() > ui::sidebar::NormalSize; -} diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp new file mode 100644 index 00000000..cc7a5741 --- /dev/null +++ b/src/ChatPage.cpp @@ -0,0 +1,1347 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include + +#include "AvatarProvider.h" +#include "Cache.h" +#include "ChatPage.h" +#include "Logging.h" +#include "MainWindow.h" +#include "MatrixClient.h" +#include "Olm.h" +#include "QuickSwitcher.h" +#include "RoomList.h" +#include "SideBarActions.h" +#include "Splitter.h" +#include "TextInputWidget.h" +#include "TopRoomBar.h" +#include "TypingDisplay.h" +#include "UserInfoWidget.h" +#include "UserSettingsPage.h" +#include "Utils.h" +#include "ui/OverlayModal.h" +#include "ui/Theme.h" + +#include "notifications/Manager.h" + +#include "dialogs/ReadReceipts.h" +#include "timeline/TimelineViewManager.h" + +// TODO: Needs to be updated with an actual secret. +static const std::string STORAGE_SECRET_KEY("secret"); + +ChatPage *ChatPage::instance_ = nullptr; +constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000; +constexpr size_t MAX_ONETIME_KEYS = 50; + +ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) + : QWidget(parent) + , isConnected_(true) + , userSettings_{userSettings} + , notificationsManager(this) +{ + setObjectName("chatPage"); + + topLayout_ = new QHBoxLayout(this); + topLayout_->setSpacing(0); + topLayout_->setMargin(0); + + communitiesList_ = new CommunitiesList(this); + topLayout_->addWidget(communitiesList_); + + splitter = new Splitter(this); + splitter->setHandleWidth(0); + + topLayout_->addWidget(splitter); + + // SideBar + sideBar_ = new QFrame(this); + sideBar_->setObjectName("sideBar"); + sideBar_->setMinimumWidth(ui::sidebar::NormalSize); + sideBarLayout_ = new QVBoxLayout(sideBar_); + sideBarLayout_->setSpacing(0); + sideBarLayout_->setMargin(0); + + sideBarTopWidget_ = new QWidget(sideBar_); + sidebarActions_ = new SideBarActions(this); + connect( + sidebarActions_, &SideBarActions::showSettings, this, &ChatPage::showUserSettingsPage); + connect(sidebarActions_, &SideBarActions::joinRoom, this, &ChatPage::joinRoom); + connect(sidebarActions_, &SideBarActions::createRoom, this, &ChatPage::createRoom); + + user_info_widget_ = new UserInfoWidget(sideBar_); + room_list_ = new RoomList(userSettings_, sideBar_); + connect(room_list_, &RoomList::joinRoom, this, &ChatPage::joinRoom); + + sideBarLayout_->addWidget(user_info_widget_); + sideBarLayout_->addWidget(room_list_); + sideBarLayout_->addWidget(sidebarActions_); + + sideBarTopWidgetLayout_ = new QVBoxLayout(sideBarTopWidget_); + sideBarTopWidgetLayout_->setSpacing(0); + sideBarTopWidgetLayout_->setMargin(0); + + // Content + content_ = new QFrame(this); + content_->setObjectName("mainContent"); + contentLayout_ = new QVBoxLayout(content_); + contentLayout_->setSpacing(0); + contentLayout_->setMargin(0); + + top_bar_ = new TopRoomBar(this); + view_manager_ = new TimelineViewManager(this); + + contentLayout_->addWidget(top_bar_); + contentLayout_->addWidget(view_manager_); + + connect(this, + &ChatPage::removeTimelineEvent, + view_manager_, + &TimelineViewManager::removeTimelineEvent); + + // Splitter + splitter->addWidget(sideBar_); + splitter->addWidget(content_); + splitter->restoreSizes(parent->width()); + + text_input_ = new TextInputWidget(this); + typingDisplay_ = new TypingDisplay(this); + contentLayout_->addWidget(typingDisplay_); + contentLayout_->addWidget(text_input_); + + typingRefresher_ = new QTimer(this); + typingRefresher_->setInterval(TYPING_REFRESH_TIMEOUT); + + connect(this, &ChatPage::connectionLost, this, [this]() { + nhlog::net()->info("connectivity lost"); + isConnected_ = false; + http::client()->shutdown(); + text_input_->disableInput(); + }); + connect(this, &ChatPage::connectionRestored, this, [this]() { + nhlog::net()->info("trying to re-connect"); + text_input_->enableInput(); + isConnected_ = true; + + // Drop all pending connections. + http::client()->shutdown(); + trySync(); + }); + + connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL); + connect(&connectivityTimer_, &QTimer::timeout, this, [=]() { + if (http::client()->access_token().empty()) { + connectivityTimer_.stop(); + return; + } + + http::client()->versions( + [this](const mtx::responses::Versions &, mtx::http::RequestErr err) { + if (err) { + emit connectionLost(); + return; + } + + if (!isConnected_) + emit connectionRestored(); + }); + }); + + connect(this, &ChatPage::loggedOut, this, &ChatPage::logout); + connect(user_info_widget_, &UserInfoWidget::logout, this, [this]() { + http::client()->logout( + [this](const mtx::responses::Logout &, mtx::http::RequestErr err) { + if (err) { + // TODO: handle special errors + emit contentLoaded(); + nhlog::net()->warn( + "failed to logout: {} - {}", + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } + + emit loggedOut(); + }); + + emit showOverlayProgressBar(); + }); + + connect(top_bar_, &TopRoomBar::showRoomList, splitter, &Splitter::showFullRoomList); + connect(top_bar_, &TopRoomBar::inviteUsers, this, [this](QStringList users) { + const auto room_id = current_room_.toStdString(); + + for (int ii = 0; ii < users.size(); ++ii) { + QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { + const auto user = users.at(ii); + + http::client()->invite_user( + room_id, + user.toStdString(), + [this, user](const mtx::responses::RoomInvite &, + mtx::http::RequestErr err) { + if (err) { + emit showNotification( + QString("Failed to invite user: %1").arg(user)); + return; + } + + emit showNotification( + QString("Invited user: %1").arg(user)); + }); + }); + } + }); + + connect(room_list_, &RoomList::roomChanged, this, [this](const QString &roomid) { + QStringList users; + + if (!userSettings_->isTypingNotificationsEnabled()) { + typingDisplay_->setUsers(users); + return; + } + + if (typingUsers_.find(roomid) != typingUsers_.end()) + users = typingUsers_[roomid]; + + typingDisplay_->setUsers(users); + }); + connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::stopTyping); + connect(room_list_, &RoomList::roomChanged, this, &ChatPage::changeTopRoomInfo); + connect(room_list_, &RoomList::roomChanged, splitter, &Splitter::showChatView); + connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::focusLineEdit); + connect( + room_list_, &RoomList::roomChanged, view_manager_, &TimelineViewManager::setHistoryView); + + connect(room_list_, &RoomList::acceptInvite, this, [this](const QString &room_id) { + view_manager_->addRoom(room_id); + joinRoom(room_id); + room_list_->removeRoom(room_id, currentRoom() == room_id); + }); + + connect(room_list_, &RoomList::declineInvite, this, [this](const QString &room_id) { + leaveRoom(room_id); + room_list_->removeRoom(room_id, currentRoom() == room_id); + }); + + connect( + text_input_, &TextInputWidget::startedTyping, this, &ChatPage::sendTypingNotifications); + connect(typingRefresher_, &QTimer::timeout, this, &ChatPage::sendTypingNotifications); + connect(text_input_, &TextInputWidget::stoppedTyping, this, [this]() { + if (!userSettings_->isTypingNotificationsEnabled()) + return; + + typingRefresher_->stop(); + + if (current_room_.isEmpty()) + return; + + http::client()->stop_typing( + current_room_.toStdString(), [](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to stop typing notifications: {}", + err->matrix_error.error); + } + }); + }); + + connect(view_manager_, + &TimelineViewManager::updateRoomsLastMessage, + room_list_, + &RoomList::updateRoomDescription); + + connect(room_list_, + SIGNAL(totalUnreadMessageCountUpdated(int)), + this, + SLOT(showUnreadMessageNotification(int))); + + connect(text_input_, + SIGNAL(sendTextMessage(const QString &)), + view_manager_, + SLOT(queueTextMessage(const QString &))); + + connect(text_input_, + SIGNAL(sendEmoteMessage(const QString &)), + view_manager_, + SLOT(queueEmoteMessage(const QString &))); + + connect(text_input_, &TextInputWidget::sendJoinRoomRequest, this, &ChatPage::joinRoom); + + connect( + text_input_, + &TextInputWidget::uploadImage, + this, + [this](QSharedPointer dev, const QString &fn) { + QMimeDatabase db; + QMimeType mime = db.mimeTypeForData(dev.data()); + + if (!dev->open(QIODevice::ReadOnly)) { + emit uploadFailed( + QString("Error while reading media: %1").arg(dev->errorString())); + return; + } + + auto bin = dev->peek(dev->size()); + auto payload = std::string(bin.data(), bin.size()); + auto dimensions = QImageReader(dev.data()).size(); + + http::client()->upload( + payload, + mime.name().toStdString(), + QFileInfo(fn).fileName().toStdString(), + [this, + room_id = current_room_, + filename = fn, + mime = mime.name(), + size = payload.size(), + dimensions](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) { + if (err) { + emit uploadFailed( + tr("Failed to upload image. Please try again.")); + nhlog::net()->warn("failed to upload image: {} {} ({})", + err->matrix_error.error, + to_string(err->matrix_error.errcode), + static_cast(err->status_code)); + return; + } + + emit imageUploaded(room_id, + filename, + QString::fromStdString(res.content_uri), + mime, + size, + dimensions); + }); + }); + + connect(text_input_, + &TextInputWidget::uploadFile, + this, + [this](QSharedPointer dev, const QString &fn) { + QMimeDatabase db; + QMimeType mime = db.mimeTypeForData(dev.data()); + + if (!dev->open(QIODevice::ReadOnly)) { + emit uploadFailed( + QString("Error while reading media: %1").arg(dev->errorString())); + return; + } + + auto bin = dev->readAll(); + auto payload = std::string(bin.data(), bin.size()); + + http::client()->upload( + payload, + mime.name().toStdString(), + QFileInfo(fn).fileName().toStdString(), + [this, + room_id = current_room_, + filename = fn, + mime = mime.name(), + size = payload.size()](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) { + if (err) { + emit uploadFailed( + tr("Failed to upload file. Please try again.")); + nhlog::net()->warn("failed to upload file: {} ({})", + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + emit fileUploaded(room_id, + filename, + QString::fromStdString(res.content_uri), + mime, + size); + }); + }); + + connect(text_input_, + &TextInputWidget::uploadAudio, + this, + [this](QSharedPointer dev, const QString &fn) { + QMimeDatabase db; + QMimeType mime = db.mimeTypeForData(dev.data()); + + if (!dev->open(QIODevice::ReadOnly)) { + emit uploadFailed( + QString("Error while reading media: %1").arg(dev->errorString())); + return; + } + + auto bin = dev->readAll(); + auto payload = std::string(bin.data(), bin.size()); + + http::client()->upload( + payload, + mime.name().toStdString(), + QFileInfo(fn).fileName().toStdString(), + [this, + room_id = current_room_, + filename = fn, + mime = mime.name(), + size = payload.size()](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) { + if (err) { + emit uploadFailed( + tr("Failed to upload audio. Please try again.")); + nhlog::net()->warn("failed to upload audio: {} ({})", + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + emit audioUploaded(room_id, + filename, + QString::fromStdString(res.content_uri), + mime, + size); + }); + }); + connect(text_input_, + &TextInputWidget::uploadVideo, + this, + [this](QSharedPointer dev, const QString &fn) { + QMimeDatabase db; + QMimeType mime = db.mimeTypeForData(dev.data()); + + if (!dev->open(QIODevice::ReadOnly)) { + emit uploadFailed( + QString("Error while reading media: %1").arg(dev->errorString())); + return; + } + + auto bin = dev->readAll(); + auto payload = std::string(bin.data(), bin.size()); + + http::client()->upload( + payload, + mime.name().toStdString(), + QFileInfo(fn).fileName().toStdString(), + [this, + room_id = current_room_, + filename = fn, + mime = mime.name(), + size = payload.size()](const mtx::responses::ContentURI &res, + mtx::http::RequestErr err) { + if (err) { + emit uploadFailed( + tr("Failed to upload video. Please try again.")); + nhlog::net()->warn("failed to upload video: {} ({})", + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + emit videoUploaded(room_id, + filename, + QString::fromStdString(res.content_uri), + mime, + size); + }); + }); + + connect(this, &ChatPage::uploadFailed, this, [this](const QString &msg) { + text_input_->hideUploadSpinner(); + emit showNotification(msg); + }); + connect(this, + &ChatPage::imageUploaded, + this, + [this](QString roomid, + QString filename, + QString url, + QString mime, + qint64 dsize, + QSize dimensions) { + text_input_->hideUploadSpinner(); + view_manager_->queueImageMessage( + roomid, filename, url, mime, dsize, dimensions); + }); + connect(this, + &ChatPage::fileUploaded, + this, + [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { + text_input_->hideUploadSpinner(); + view_manager_->queueFileMessage(roomid, filename, url, mime, dsize); + }); + connect(this, + &ChatPage::audioUploaded, + this, + [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { + text_input_->hideUploadSpinner(); + view_manager_->queueAudioMessage(roomid, filename, url, mime, dsize); + }); + connect(this, + &ChatPage::videoUploaded, + this, + [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { + text_input_->hideUploadSpinner(); + view_manager_->queueVideoMessage(roomid, filename, url, mime, dsize); + }); + + connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar); + + connect( + this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities); + + connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom); + connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendDesktopNotifications); + + connect(communitiesList_, + &CommunitiesList::communityChanged, + this, + [this](const QString &groupId) { + current_community_ = groupId; + + if (groupId == "world") + room_list_->setFilterRooms(false); + else + room_list_->setRoomFilter(communitiesList_->roomList(groupId)); + }); + + connect(¬ificationsManager, + &NotificationsManager::notificationClicked, + this, + [this](const QString &roomid, const QString &eventid) { + Q_UNUSED(eventid) + room_list_->highlightSelectedRoom(roomid); + activateWindow(); + }); + + setGroupViewState(userSettings_->isGroupViewEnabled()); + + connect(userSettings_.data(), + &UserSettings::groupViewStateChanged, + this, + &ChatPage::setGroupViewState); + + connect(this, &ChatPage::initializeRoomList, room_list_, &RoomList::initialize); + connect(this, + &ChatPage::initializeViews, + view_manager_, + [this](const mtx::responses::Rooms &rooms) { view_manager_->initialize(rooms); }); + connect(this, + &ChatPage::initializeEmptyViews, + view_manager_, + &TimelineViewManager::initWithMessages); + connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) { + try { + room_list_->cleanupInvites(cache::client()->invites()); + } catch (const lmdb::error &e) { + nhlog::db()->error("failed to retrieve invites: {}", e.what()); + } + + view_manager_->initialize(rooms); + removeLeftRooms(rooms.leave); + + bool hasNotifications = false; + for (const auto &room : rooms.join) { + auto room_id = QString::fromStdString(room.first); + + updateTypingUsers(room_id, room.second.ephemeral.typing); + updateRoomNotificationCount( + room_id, room.second.unread_notifications.notification_count); + + if (room.second.unread_notifications.notification_count > 0) + hasNotifications = true; + } + + if (hasNotifications) + http::client()->notifications( + 5, + [this](const mtx::responses::Notifications &res, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to retrieve notifications: {} ({})", + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + emit notificationsRetrieved(std::move(res)); + }); + }); + connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync); + connect( + this, &ChatPage::syncTopBar, this, [this](const std::map &updates) { + if (updates.find(currentRoom()) != updates.end()) + changeTopRoomInfo(currentRoom()); + }); + + // Callbacks to update the user info (top left corner of the page). + connect(this, &ChatPage::setUserAvatar, user_info_widget_, &UserInfoWidget::setAvatar); + connect(this, &ChatPage::setUserDisplayName, this, [this](const QString &name) { + QSettings settings; + auto userid = settings.value("auth/user_id").toString(); + user_info_widget_->setUserId(userid); + user_info_widget_->setDisplayName(name); + }); + + connect(this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync); + connect(this, &ChatPage::trySyncCb, this, &ChatPage::trySync); + connect(this, &ChatPage::tryDelayedSyncCb, this, [this]() { + QTimer::singleShot(5000, this, &ChatPage::trySync); + }); + + connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage); + connect(this, &ChatPage::messageReply, text_input_, &TextInputWidget::addReply); + + instance_ = this; +} + +void +ChatPage::logout() +{ + deleteConfigs(); + + resetUI(); + + emit closing(); + connectivityTimer_.stop(); +} + +void +ChatPage::dropToLoginPage(const QString &msg) +{ + deleteConfigs(); + resetUI(); + + http::client()->shutdown(); + connectivityTimer_.stop(); + + emit showLoginPage(msg); +} + +void +ChatPage::resetUI() +{ + room_list_->clear(); + top_bar_->reset(); + user_info_widget_->reset(); + view_manager_->clearAll(); + + showUnreadMessageNotification(0); +} + +void +ChatPage::deleteConfigs() +{ + QSettings settings; + settings.beginGroup("auth"); + settings.remove(""); + settings.endGroup(); + settings.beginGroup("client"); + settings.remove(""); + settings.endGroup(); + settings.beginGroup("notifications"); + settings.remove(""); + settings.endGroup(); + + cache::client()->deleteData(); + http::client()->clear(); +} + +void +ChatPage::bootstrap(QString userid, QString homeserver, QString token) +{ + using namespace mtx::identifiers; + + try { + http::client()->set_user(parse(userid.toStdString())); + } catch (const std::invalid_argument &e) { + nhlog::ui()->critical("bootstrapped with invalid user_id: {}", + userid.toStdString()); + } + + http::client()->set_server(homeserver.toStdString()); + http::client()->set_access_token(token.toStdString()); + + // The Olm client needs the user_id & device_id that will be included + // in the generated payloads & keys. + olm::client()->set_user_id(http::client()->user_id().to_string()); + olm::client()->set_device_id(http::client()->device_id()); + + try { + cache::init(userid); + + const bool isInitialized = cache::client()->isInitialized(); + const bool isValid = cache::client()->isFormatValid(); + + if (isInitialized && !isValid) { + nhlog::db()->warn("breaking changes in cache"); + // TODO: Deleting session data but keep using the + // same device doesn't work. + cache::client()->deleteData(); + + cache::init(userid); + cache::client()->setCurrentFormat(); + } else if (isInitialized) { + loadStateFromCache(); + return; + } + } catch (const lmdb::error &e) { + nhlog::db()->critical("failure during boot: {}", e.what()); + cache::client()->deleteData(); + nhlog::net()->info("falling back to initial sync"); + } + + try { + // It's the first time syncing with this device + // There isn't a saved olm account to restore. + nhlog::crypto()->info("creating new olm account"); + olm::client()->create_new_account(); + cache::client()->saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); + } catch (const lmdb::error &e) { + nhlog::crypto()->critical("failed to save olm account {}", e.what()); + emit dropToLoginPageCb(QString::fromStdString(e.what())); + return; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to create new olm account {}", e.what()); + emit dropToLoginPageCb(QString::fromStdString(e.what())); + return; + } + + getProfileInfo(); + tryInitialSync(); +} + +void +ChatPage::updateTopBarAvatar(const QString &roomid, const QPixmap &img) +{ + if (current_room_ != roomid) + return; + + top_bar_->updateRoomAvatar(img.toImage()); +} + +void +ChatPage::changeTopRoomInfo(const QString &room_id) +{ + if (room_id.isEmpty()) { + nhlog::ui()->warn("cannot switch to empty room_id"); + return; + } + + try { + auto room_info = cache::client()->getRoomInfo({room_id.toStdString()}); + + if (room_info.find(room_id) == room_info.end()) + return; + + const auto name = QString::fromStdString(room_info[room_id].name); + const auto avatar_url = QString::fromStdString(room_info[room_id].avatar_url); + + top_bar_->updateRoomName(name); + top_bar_->updateRoomTopic(QString::fromStdString(room_info[room_id].topic)); + + auto img = cache::client()->getRoomAvatar(room_id); + + if (img.isNull()) + top_bar_->updateRoomAvatarFromName(name); + else + top_bar_->updateRoomAvatar(img); + + } catch (const lmdb::error &e) { + nhlog::ui()->error("failed to change top bar room info: {}", e.what()); + } + + current_room_ = room_id; +} + +void +ChatPage::showUnreadMessageNotification(int count) +{ + emit unreadMessages(count); + + // TODO: Make the default title a const. + if (count == 0) + emit changeWindowTitle("nheko"); + else + emit changeWindowTitle(QString("nheko (%1)").arg(count)); +} + +void +ChatPage::loadStateFromCache() +{ + emit contentLoaded(); + + nhlog::db()->info("restoring state from cache"); + + getProfileInfo(); + + QtConcurrent::run([this]() { + try { + cache::client()->restoreSessions(); + olm::client()->load(cache::client()->restoreOlmAccount(), + STORAGE_SECRET_KEY); + + cache::client()->populateMembers(); + + emit initializeEmptyViews(cache::client()->roomMessages()); + emit initializeRoomList(cache::client()->roomInfo()); + + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to restore olm account: {}", e.what()); + emit dropToLoginPageCb( + tr("Failed to restore OLM account. Please login again.")); + return; + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to restore cache: {}", e.what()); + emit dropToLoginPageCb( + tr("Failed to restore save data. Please login again.")); + return; + } catch (const json::exception &e) { + nhlog::db()->critical("failed to parse cache data: {}", e.what()); + return; + } + + nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); + nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); + + // Start receiving events. + emit trySyncCb(); + }); +} + +void +ChatPage::showQuickSwitcher() +{ + if (quickSwitcher_.isNull()) { + quickSwitcher_ = QSharedPointer( + new QuickSwitcher(this), + [](QuickSwitcher *switcher) { switcher->deleteLater(); }); + + connect(quickSwitcher_.data(), + &QuickSwitcher::roomSelected, + room_list_, + &RoomList::highlightSelectedRoom); + + connect(quickSwitcher_.data(), &QuickSwitcher::closing, this, [this]() { + if (!quickSwitcherModal_.isNull()) + quickSwitcherModal_->hide(); + text_input_->setFocus(Qt::FocusReason::PopupFocusReason); + }); + } + + if (quickSwitcherModal_.isNull()) { + quickSwitcherModal_ = QSharedPointer( + new OverlayModal(MainWindow::instance(), quickSwitcher_.data()), + [](OverlayModal *modal) { modal->deleteLater(); }); + quickSwitcherModal_->setColor(QColor(30, 30, 30, 170)); + } + + quickSwitcherModal_->show(); +} + +void +ChatPage::removeRoom(const QString &room_id) +{ + try { + cache::client()->removeRoom(room_id); + cache::client()->removeInvite(room_id.toStdString()); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failure while removing room: {}", e.what()); + // TODO: Notify the user. + } + + room_list_->removeRoom(room_id, room_id == current_room_); +} + +void +ChatPage::updateTypingUsers(const QString &roomid, const std::vector &user_ids) +{ + if (!userSettings_->isTypingNotificationsEnabled()) + return; + + typingUsers_[roomid] = generateTypingUsers(roomid, user_ids); + + if (current_room_ == roomid) + typingDisplay_->setUsers(typingUsers_[roomid]); +} + +QStringList +ChatPage::generateTypingUsers(const QString &room_id, const std::vector &typing_users) +{ + QStringList users; + + QSettings settings; + QString local_user = settings.value("auth/user_id").toString(); + + for (const auto &uid : typing_users) { + const auto remote_user = QString::fromStdString(uid); + + if (remote_user == local_user) + continue; + + users.append(Cache::displayName(room_id, remote_user)); + } + + users.sort(); + + return users; +} + +void +ChatPage::removeLeftRooms(const std::map &rooms) +{ + for (auto it = rooms.cbegin(); it != rooms.cend(); ++it) { + const auto room_id = QString::fromStdString(it->first); + room_list_->removeRoom(room_id, room_id == current_room_); + } +} + +void +ChatPage::showReadReceipts(const QString &event_id) +{ + if (receiptsDialog_.isNull()) { + receiptsDialog_ = QSharedPointer( + new dialogs::ReadReceipts(this), + [](dialogs::ReadReceipts *dialog) { dialog->deleteLater(); }); + } + + if (receiptsModal_.isNull()) { + receiptsModal_ = QSharedPointer( + new OverlayModal(MainWindow::instance(), receiptsDialog_.data()), + [](OverlayModal *modal) { modal->deleteLater(); }); + receiptsModal_->setColor(QColor(30, 30, 30, 170)); + } + + receiptsDialog_->addUsers(cache::client()->readReceipts(event_id, current_room_)); + receiptsModal_->show(); +} + +void +ChatPage::setGroupViewState(bool isEnabled) +{ + if (!isEnabled) { + communitiesList_->communityChanged("world"); + communitiesList_->hide(); + + return; + } + + communitiesList_->show(); +} + +void +ChatPage::updateRoomNotificationCount(const QString &room_id, uint16_t notification_count) +{ + room_list_->updateUnreadMessageCount(room_id, notification_count); +} + +void +ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res) +{ + for (const auto &item : res.notifications) { + const auto event_id = utils::event_id(item.event); + + try { + if (item.read) { + cache::client()->removeReadNotification(event_id); + continue; + } + + if (!cache::client()->isNotificationSent(event_id)) { + const auto room_id = QString::fromStdString(item.room_id); + const auto user_id = utils::event_sender(item.event); + + // We should only sent one notification per event. + cache::client()->markSentNotification(event_id); + + // Don't send a notification when the current room is opened. + if (isRoomActive(room_id)) + continue; + + notificationsManager.postNotification( + room_id, + QString::fromStdString(event_id), + QString::fromStdString( + cache::client()->singleRoomInfo(item.room_id).name), + Cache::displayName(room_id, user_id), + utils::event_body(item.event), + cache::client()->getRoomAvatar(room_id)); + } + } catch (const lmdb::error &e) { + nhlog::db()->warn("error while sending desktop notification: {}", e.what()); + } + } +} + +void +ChatPage::tryInitialSync() +{ + nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); + nhlog::crypto()->info("curve25519: {}", olm::client()->identity_keys().curve25519); + + // Upload one time keys for the device. + nhlog::crypto()->info("generating one time keys"); + olm::client()->generate_one_time_keys(MAX_ONETIME_KEYS); + + http::client()->upload_keys( + olm::client()->create_upload_keys_request(), + [this](const mtx::responses::UploadKeys &res, mtx::http::RequestErr err) { + if (err) { + const int status_code = static_cast(err->status_code); + nhlog::crypto()->critical("failed to upload one time keys: {} {}", + err->matrix_error.error, + status_code); + // TODO We should have a timeout instead of keeping hammering the server. + emit tryInitialSyncCb(); + return; + } + + olm::mark_keys_as_published(); + + for (const auto &entry : res.one_time_key_counts) + nhlog::net()->info( + "uploaded {} {} one-time keys", entry.second, entry.first); + + nhlog::net()->info("trying initial sync"); + + mtx::http::SyncOpts opts; + opts.timeout = 0; + http::client()->sync(opts, + std::bind(&ChatPage::initialSyncHandler, + this, + std::placeholders::_1, + std::placeholders::_2)); + }); +} + +void +ChatPage::trySync() +{ + mtx::http::SyncOpts opts; + + if (!connectivityTimer_.isActive()) + connectivityTimer_.start(); + + try { + opts.since = cache::client()->nextBatchToken(); + } catch (const lmdb::error &e) { + nhlog::db()->error("failed to retrieve next batch token: {}", e.what()); + return; + } + + http::client()->sync( + opts, [this](const mtx::responses::Sync &res, mtx::http::RequestErr err) { + if (err) { + const auto error = QString::fromStdString(err->matrix_error.error); + const auto msg = tr("Please try to login again: %1").arg(error); + const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); + const int status_code = static_cast(err->status_code); + + nhlog::net()->error("sync error: {} {}", status_code, err_code); + + if (status_code <= 0 || status_code >= 600) { + if (!http::is_logged_in()) + return; + + emit tryDelayedSyncCb(); + return; + } + + switch (status_code) { + case 502: + case 504: + case 524: { + emit trySyncCb(); + return; + } + default: { + if (!http::is_logged_in()) + return; + + if (err->matrix_error.errcode == + mtx::errors::ErrorCode::M_UNKNOWN_TOKEN) + emit dropToLoginPageCb(msg); + else + emit tryDelayedSyncCb(); + + return; + } + } + } + + nhlog::net()->debug("sync completed: {}", res.next_batch); + + // Ensure that we have enough one-time keys available. + ensureOneTimeKeyCount(res.device_one_time_keys_count); + + // TODO: fine grained error handling + try { + cache::client()->saveState(res); + olm::handle_to_device_messages(res.to_device); + + emit syncUI(res.rooms); + + auto updates = cache::client()->roomUpdates(res); + + emit syncTopBar(updates); + emit syncRoomlist(updates); + } catch (const lmdb::error &e) { + nhlog::db()->error("saving sync response: {}", e.what()); + } + + emit trySyncCb(); + }); +} + +void +ChatPage::joinRoom(const QString &room) +{ + const auto room_id = room.toStdString(); + + http::client()->join_room( + room_id, [this, room_id](const nlohmann::json &, mtx::http::RequestErr err) { + if (err) { + emit showNotification( + QString("Failed to join room: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit showNotification("You joined the room"); + + // We remove any invites with the same room_id. + try { + cache::client()->removeInvite(room_id); + } catch (const lmdb::error &e) { + emit showNotification( + QString("Failed to remove invite: %1").arg(e.what())); + } + }); +} + +void +ChatPage::createRoom(const mtx::requests::CreateRoom &req) +{ + http::client()->create_room( + req, [this](const mtx::responses::CreateRoom &res, mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Room creation failed: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit showNotification(QString("Room %1 created") + .arg(QString::fromStdString(res.room_id.to_string()))); + }); +} + +void +ChatPage::leaveRoom(const QString &room_id) +{ + http::client()->leave_room( + room_id.toStdString(), [this, room_id](const json &, mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Failed to leave room: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit leftRoom(room_id); + }); +} + +void +ChatPage::sendTypingNotifications() +{ + if (!userSettings_->isTypingNotificationsEnabled()) + return; + + http::client()->start_typing( + current_room_.toStdString(), 10'000, [](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to send typing notification: {}", + err->matrix_error.error); + } + }); +} + +void +ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::RequestErr err) +{ + if (err) { + const auto error = QString::fromStdString(err->matrix_error.error); + const auto msg = tr("Please try to login again: %1").arg(error); + const auto err_code = mtx::errors::to_string(err->matrix_error.errcode); + const int status_code = static_cast(err->status_code); + + nhlog::net()->error("sync error: {} {}", status_code, err_code); + + switch (status_code) { + case 502: + case 504: + case 524: { + emit tryInitialSyncCb(); + return; + } + default: { + emit dropToLoginPageCb(msg); + return; + } + } + } + + nhlog::net()->info("initial sync completed"); + + try { + cache::client()->saveState(res); + + olm::handle_to_device_messages(res.to_device); + + emit initializeViews(std::move(res.rooms)); + emit initializeRoomList(cache::client()->roomInfo()); + } catch (const lmdb::error &e) { + nhlog::db()->error("{}", e.what()); + emit tryInitialSyncCb(); + return; + } + + emit trySyncCb(); + emit contentLoaded(); +} + +void +ChatPage::ensureOneTimeKeyCount(const std::map &counts) +{ + for (const auto &entry : counts) { + if (entry.second < MAX_ONETIME_KEYS) { + const int nkeys = MAX_ONETIME_KEYS - entry.second; + + nhlog::crypto()->info("uploading {} {} keys", nkeys, entry.first); + olm::client()->generate_one_time_keys(nkeys); + + http::client()->upload_keys( + olm::client()->create_upload_keys_request(), + [](const mtx::responses::UploadKeys &, mtx::http::RequestErr err) { + if (err) { + nhlog::crypto()->warn( + "failed to update one-time keys: {} {}", + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + olm::mark_keys_as_published(); + }); + } + } +} + +void +ChatPage::getProfileInfo() +{ + QSettings settings; + const auto userid = settings.value("auth/user_id").toString().toStdString(); + + http::client()->get_profile( + userid, [this](const mtx::responses::Profile &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve own profile info"); + return; + } + + emit setUserDisplayName(QString::fromStdString(res.display_name)); + + if (cache::client()) { + auto data = cache::client()->image(res.avatar_url); + if (!data.isNull()) { + emit setUserAvatar(QImage::fromData(data)); + return; + } + } + + if (res.avatar_url.empty()) + return; + + http::client()->download( + res.avatar_url, + [this, res](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to download user avatar: {} - {}", + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } + + if (cache::client()) + cache::client()->saveImage(res.avatar_url, data); + + emit setUserAvatar( + QImage::fromData(QByteArray(data.data(), data.size()))); + }); + }); + + http::client()->joined_groups( + [this](const mtx::responses::JoinedGroups &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->critical("failed to retrieve joined groups: {} {}", + static_cast(err->status_code), + err->matrix_error.error); + return; + } + + emit updateGroupsInfo(res); + }); +} + +void +ChatPage::hideSideBars() +{ + communitiesList_->hide(); + sideBar_->hide(); + top_bar_->enableBackButton(); +} + +void +ChatPage::showSideBars() +{ + if (userSettings_->isGroupViewEnabled()) + communitiesList_->show(); + + sideBar_->show(); + top_bar_->disableBackButton(); +} + +int +ChatPage::timelineWidth() +{ + int sidebarWidth = sideBar_->size().width(); + sidebarWidth += communitiesList_->size().width(); + + return size().width() - sidebarWidth; +} +bool +ChatPage::isSideBarExpanded() +{ + return sideBar_->size().width() > ui::sidebar::NormalSize; +} diff --git a/src/ChatPage.h b/src/ChatPage.h new file mode 100644 index 00000000..6a70acf4 --- /dev/null +++ b/src/ChatPage.h @@ -0,0 +1,268 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "Cache.h" +#include "CommunitiesList.h" +#include "MatrixClient.h" +#include "notifications/Manager.h" + +class OverlayModal; +class QuickSwitcher; +class RoomList; +class SideBarActions; +class Splitter; +class TextInputWidget; +class TimelineViewManager; +class TopRoomBar; +class TypingDisplay; +class UserInfoWidget; +class UserSettings; +class NotificationsManager; + +namespace dialogs { +class ReadReceipts; +} + +constexpr int CONSENSUS_TIMEOUT = 1000; +constexpr int SHOW_CONTENT_TIMEOUT = 3000; +constexpr int TYPING_REFRESH_TIMEOUT = 10000; + +class ChatPage : public QWidget +{ + Q_OBJECT + +public: + ChatPage(QSharedPointer userSettings, QWidget *parent = 0); + + // Initialize all the components of the UI. + void bootstrap(QString userid, QString homeserver, QString token); + void showQuickSwitcher(); + void showReadReceipts(const QString &event_id); + QString currentRoom() const { return current_room_; } + + static ChatPage *instance() { return instance_; } + + QSharedPointer userSettings() { return userSettings_; } + void deleteConfigs(); + + //! Calculate the width of the message timeline. + int timelineWidth(); + bool isSideBarExpanded(); + //! Hide the room & group list (if it was visible). + void hideSideBars(); + //! Show the room/group list (if it was visible). + void showSideBars(); + +public slots: + void leaveRoom(const QString &room_id); + +signals: + void connectionLost(); + void connectionRestored(); + + void messageReply(const QString &username, const QString &msg); + + void notificationsRetrieved(const mtx::responses::Notifications &); + + void uploadFailed(const QString &msg); + void imageUploaded(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + qint64 dsize, + const QSize &dimensions); + void fileUploaded(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + qint64 dsize); + void audioUploaded(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + qint64 dsize); + void videoUploaded(const QString &roomid, + const QString &filename, + const QString &url, + const QString &mime, + qint64 dsize); + + void contentLoaded(); + void closing(); + void changeWindowTitle(const QString &msg); + void unreadMessages(int count); + void showNotification(const QString &msg); + void showLoginPage(const QString &msg); + void showUserSettingsPage(); + void showOverlayProgressBar(); + + void removeTimelineEvent(const QString &room_id, const QString &event_id); + + void ownProfileOk(); + void setUserDisplayName(const QString &name); + void setUserAvatar(const QImage &avatar); + void loggedOut(); + + void trySyncCb(); + void tryDelayedSyncCb(); + void tryInitialSyncCb(); + void leftRoom(const QString &room_id); + + void initializeRoomList(QMap); + void initializeViews(const mtx::responses::Rooms &rooms); + void initializeEmptyViews(const std::map &msgs); + void syncUI(const mtx::responses::Rooms &rooms); + void syncRoomlist(const std::map &updates); + void syncTopBar(const std::map &updates); + void dropToLoginPageCb(const QString &msg); + + void notifyMessage(const QString &roomid, + const QString &eventid, + const QString &roomname, + const QString &sender, + const QString &message, + const QImage &icon); + + void updateGroupsInfo(const mtx::responses::JoinedGroups &groups); + +private slots: + void showUnreadMessageNotification(int count); + void updateTopBarAvatar(const QString &roomid, const QPixmap &img); + void changeTopRoomInfo(const QString &room_id); + void logout(); + void removeRoom(const QString &room_id); + void dropToLoginPage(const QString &msg); + + void joinRoom(const QString &room); + void createRoom(const mtx::requests::CreateRoom &req); + void sendTypingNotifications(); + +private: + static ChatPage *instance_; + + //! Handler callback for initial sync. It doesn't run on the main thread so all + //! communication with the GUI should be done through signals. + void initialSyncHandler(const mtx::responses::Sync &res, mtx::http::RequestErr err); + void tryInitialSync(); + void trySync(); + void ensureOneTimeKeyCount(const std::map &counts); + void getProfileInfo(); + + //! Check if the given room is currently open. + bool isRoomActive(const QString &room_id) + { + return isActiveWindow() && currentRoom() == room_id; + } + + using UserID = QString; + using Membership = mtx::events::StateEvent; + using Memberships = std::map; + + using LeftRooms = std::map; + void removeLeftRooms(const LeftRooms &rooms); + + void updateTypingUsers(const QString &roomid, const std::vector &user_ids); + + void loadStateFromCache(); + void resetUI(); + //! Decides whether or not to hide the group's sidebar. + void setGroupViewState(bool isEnabled); + + template + Memberships getMemberships(const std::vector &events) const; + + //! Update the room with the new notification count. + void updateRoomNotificationCount(const QString &room_id, uint16_t notification_count); + //! Send desktop notification for the received messages. + void sendDesktopNotifications(const mtx::responses::Notifications &); + + QStringList generateTypingUsers(const QString &room_id, + const std::vector &typing_users); + + QHBoxLayout *topLayout_; + Splitter *splitter; + + QWidget *sideBar_; + QVBoxLayout *sideBarLayout_; + QWidget *sideBarTopWidget_; + QVBoxLayout *sideBarTopWidgetLayout_; + + QFrame *content_; + QVBoxLayout *contentLayout_; + + CommunitiesList *communitiesList_; + RoomList *room_list_; + + TimelineViewManager *view_manager_; + SideBarActions *sidebarActions_; + + TopRoomBar *top_bar_; + TextInputWidget *text_input_; + TypingDisplay *typingDisplay_; + + QTimer connectivityTimer_; + std::atomic_bool isConnected_; + + QString current_room_; + QString current_community_; + + UserInfoWidget *user_info_widget_; + + // Keeps track of the users currently typing on each room. + std::map> typingUsers_; + QTimer *typingRefresher_; + + QSharedPointer quickSwitcher_; + QSharedPointer quickSwitcherModal_; + + QSharedPointer receiptsDialog_; + QSharedPointer receiptsModal_; + + // Global user settings. + QSharedPointer userSettings_; + + NotificationsManager notificationsManager; +}; + +template +std::map> +ChatPage::getMemberships(const std::vector &collection) const +{ + std::map> memberships; + + using Member = mtx::events::StateEvent; + + for (const auto &event : collection) { + if (mpark::holds_alternative(event)) { + auto member = mpark::get(event); + memberships.emplace(member.state_key, member); + } + } + + return memberships; +} diff --git a/src/CommunitiesList.cc b/src/CommunitiesList.cc deleted file mode 100644 index 822ca1d2..00000000 --- a/src/CommunitiesList.cc +++ /dev/null @@ -1,195 +0,0 @@ -#include "CommunitiesList.h" -#include "Cache.h" -#include "Logging.hpp" -#include "MatrixClient.h" - -#include - -CommunitiesList::CommunitiesList(QWidget *parent) - : QWidget(parent) -{ - QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); - sizePolicy.setHorizontalStretch(0); - sizePolicy.setVerticalStretch(1); - setSizePolicy(sizePolicy); - - setStyleSheet("border-style: none;"); - - topLayout_ = new QVBoxLayout(this); - topLayout_->setSpacing(0); - topLayout_->setMargin(0); - - setFixedWidth(ui::sidebar::CommunitiesSidebarSize); - - scrollArea_ = new QScrollArea(this); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); - scrollArea_->setWidgetResizable(true); - scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter); - - scrollAreaContents_ = new QWidget(); - - contentsLayout_ = new QVBoxLayout(scrollAreaContents_); - contentsLayout_->setSpacing(0); - contentsLayout_->setMargin(0); - - addGlobalItem(); - contentsLayout_->addStretch(1); - - scrollArea_->setWidget(scrollAreaContents_); - topLayout_->addWidget(scrollArea_); - - connect( - this, &CommunitiesList::avatarRetrieved, this, &CommunitiesList::updateCommunityAvatar); -} - -void -CommunitiesList::setCommunities(const mtx::responses::JoinedGroups &response) -{ - communities_.clear(); - - addGlobalItem(); - - for (const auto &group : response.groups) - addCommunity(group); - - communities_["world"]->setPressedState(true); - emit communityChanged("world"); -} - -void -CommunitiesList::addCommunity(const std::string &group_id) -{ - const auto id = QString::fromStdString(group_id); - - CommunitiesListItem *list_item = new CommunitiesListItem(id, scrollArea_); - communities_.emplace(id, QSharedPointer(list_item)); - contentsLayout_->insertWidget(contentsLayout_->count() - 1, list_item); - - connect(this, - &CommunitiesList::groupProfileRetrieved, - this, - [this](const QString &id, const mtx::responses::GroupProfile &profile) { - if (communities_.find(id) == communities_.end()) - return; - - communities_.at(id)->setName(QString::fromStdString(profile.name)); - - if (!profile.avatar_url.empty()) - fetchCommunityAvatar(id, - QString::fromStdString(profile.avatar_url)); - }); - connect(this, - &CommunitiesList::groupRoomsRetrieved, - this, - [this](const QString &id, const std::vector &rooms) { - if (communities_.find(id) == communities_.end()) - return; - - communities_.at(id)->setRooms(rooms); - }); - connect(list_item, - &CommunitiesListItem::clicked, - this, - &CommunitiesList::highlightSelectedCommunity); - - http::client()->group_profile( - group_id, [id, this](const mtx::responses::GroupProfile &res, mtx::http::RequestErr err) { - if (err) { - return; - } - - emit groupProfileRetrieved(id, res); - }); - - http::client()->group_rooms( - group_id, [id, this](const nlohmann::json &res, mtx::http::RequestErr err) { - if (err) { - return; - } - - std::vector room_ids; - for (const auto &room : res.at("chunk")) - room_ids.push_back(QString::fromStdString(room.at("room_id"))); - - emit groupRoomsRetrieved(id, room_ids); - }); -} - -void -CommunitiesList::updateCommunityAvatar(const QString &community_id, const QPixmap &img) -{ - if (!communityExists(community_id)) { - qWarning() << "Avatar update on nonexistent community" << community_id; - return; - } - - communities_.at(community_id)->setAvatar(img.toImage()); -} - -void -CommunitiesList::highlightSelectedCommunity(const QString &community_id) -{ - if (!communityExists(community_id)) { - qDebug() << "CommunitiesList: clicked unknown community"; - return; - } - - emit communityChanged(community_id); - - for (const auto &community : communities_) { - if (community.first != community_id) { - community.second->setPressedState(false); - } else { - community.second->setPressedState(true); - scrollArea_->ensureWidgetVisible(community.second.data()); - } - } -} - -void -CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl) -{ - auto savedImgData = cache::client()->image(avatarUrl); - if (!savedImgData.isNull()) { - QPixmap pix; - pix.loadFromData(savedImgData); - emit avatarRetrieved(id, pix); - return; - } - - if (avatarUrl.isEmpty()) - return; - - mtx::http::ThumbOpts opts; - opts.mxc_url = avatarUrl.toStdString(); - http::client()->get_thumbnail( - opts, [this, opts, id](const std::string &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to download avatar: {} - ({} {})", - opts.mxc_url, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - cache::client()->saveImage(opts.mxc_url, res); - - auto data = QByteArray(res.data(), res.size()); - - QPixmap pix; - pix.loadFromData(data); - - emit avatarRetrieved(id, pix); - }); -} - -std::vector -CommunitiesList::roomList(const QString &id) const -{ - if (communityExists(id)) - return communities_.at(id)->rooms(); - - return {}; -} diff --git a/src/CommunitiesList.cpp b/src/CommunitiesList.cpp new file mode 100644 index 00000000..c271be89 --- /dev/null +++ b/src/CommunitiesList.cpp @@ -0,0 +1,195 @@ +#include "CommunitiesList.h" +#include "Cache.h" +#include "Logging.h" +#include "MatrixClient.h" + +#include + +CommunitiesList::CommunitiesList(QWidget *parent) + : QWidget(parent) +{ + QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); + sizePolicy.setHorizontalStretch(0); + sizePolicy.setVerticalStretch(1); + setSizePolicy(sizePolicy); + + setStyleSheet("border-style: none;"); + + topLayout_ = new QVBoxLayout(this); + topLayout_->setSpacing(0); + topLayout_->setMargin(0); + + setFixedWidth(ui::sidebar::CommunitiesSidebarSize); + + scrollArea_ = new QScrollArea(this); + scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + scrollArea_->setWidgetResizable(true); + scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter); + + scrollAreaContents_ = new QWidget(); + + contentsLayout_ = new QVBoxLayout(scrollAreaContents_); + contentsLayout_->setSpacing(0); + contentsLayout_->setMargin(0); + + addGlobalItem(); + contentsLayout_->addStretch(1); + + scrollArea_->setWidget(scrollAreaContents_); + topLayout_->addWidget(scrollArea_); + + connect( + this, &CommunitiesList::avatarRetrieved, this, &CommunitiesList::updateCommunityAvatar); +} + +void +CommunitiesList::setCommunities(const mtx::responses::JoinedGroups &response) +{ + communities_.clear(); + + addGlobalItem(); + + for (const auto &group : response.groups) + addCommunity(group); + + communities_["world"]->setPressedState(true); + emit communityChanged("world"); +} + +void +CommunitiesList::addCommunity(const std::string &group_id) +{ + const auto id = QString::fromStdString(group_id); + + CommunitiesListItem *list_item = new CommunitiesListItem(id, scrollArea_); + communities_.emplace(id, QSharedPointer(list_item)); + contentsLayout_->insertWidget(contentsLayout_->count() - 1, list_item); + + connect(this, + &CommunitiesList::groupProfileRetrieved, + this, + [this](const QString &id, const mtx::responses::GroupProfile &profile) { + if (communities_.find(id) == communities_.end()) + return; + + communities_.at(id)->setName(QString::fromStdString(profile.name)); + + if (!profile.avatar_url.empty()) + fetchCommunityAvatar(id, + QString::fromStdString(profile.avatar_url)); + }); + connect(this, + &CommunitiesList::groupRoomsRetrieved, + this, + [this](const QString &id, const std::vector &rooms) { + if (communities_.find(id) == communities_.end()) + return; + + communities_.at(id)->setRooms(rooms); + }); + connect(list_item, + &CommunitiesListItem::clicked, + this, + &CommunitiesList::highlightSelectedCommunity); + + http::client()->group_profile( + group_id, [id, this](const mtx::responses::GroupProfile &res, mtx::http::RequestErr err) { + if (err) { + return; + } + + emit groupProfileRetrieved(id, res); + }); + + http::client()->group_rooms( + group_id, [id, this](const nlohmann::json &res, mtx::http::RequestErr err) { + if (err) { + return; + } + + std::vector room_ids; + for (const auto &room : res.at("chunk")) + room_ids.push_back(QString::fromStdString(room.at("room_id"))); + + emit groupRoomsRetrieved(id, room_ids); + }); +} + +void +CommunitiesList::updateCommunityAvatar(const QString &community_id, const QPixmap &img) +{ + if (!communityExists(community_id)) { + qWarning() << "Avatar update on nonexistent community" << community_id; + return; + } + + communities_.at(community_id)->setAvatar(img.toImage()); +} + +void +CommunitiesList::highlightSelectedCommunity(const QString &community_id) +{ + if (!communityExists(community_id)) { + qDebug() << "CommunitiesList: clicked unknown community"; + return; + } + + emit communityChanged(community_id); + + for (const auto &community : communities_) { + if (community.first != community_id) { + community.second->setPressedState(false); + } else { + community.second->setPressedState(true); + scrollArea_->ensureWidgetVisible(community.second.data()); + } + } +} + +void +CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl) +{ + auto savedImgData = cache::client()->image(avatarUrl); + if (!savedImgData.isNull()) { + QPixmap pix; + pix.loadFromData(savedImgData); + emit avatarRetrieved(id, pix); + return; + } + + if (avatarUrl.isEmpty()) + return; + + mtx::http::ThumbOpts opts; + opts.mxc_url = avatarUrl.toStdString(); + http::client()->get_thumbnail( + opts, [this, opts, id](const std::string &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to download avatar: {} - ({} {})", + opts.mxc_url, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } + + cache::client()->saveImage(opts.mxc_url, res); + + auto data = QByteArray(res.data(), res.size()); + + QPixmap pix; + pix.loadFromData(data); + + emit avatarRetrieved(id, pix); + }); +} + +std::vector +CommunitiesList::roomList(const QString &id) const +{ + if (communityExists(id)) + return communities_.at(id)->rooms(); + + return {}; +} diff --git a/src/CommunitiesList.h b/src/CommunitiesList.h new file mode 100644 index 00000000..32a64bf2 --- /dev/null +++ b/src/CommunitiesList.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include + +#include "CommunitiesListItem.h" +#include "ui/Theme.h" + +class CommunitiesList : public QWidget +{ + Q_OBJECT + +public: + CommunitiesList(QWidget *parent = nullptr); + + void clear() { communities_.clear(); } + + void addCommunity(const std::string &id); + void removeCommunity(const QString &id) { communities_.erase(id); }; + std::vector roomList(const QString &id) const; + +signals: + void communityChanged(const QString &id); + void avatarRetrieved(const QString &id, const QPixmap &img); + void groupProfileRetrieved(const QString &group_id, const mtx::responses::GroupProfile &); + void groupRoomsRetrieved(const QString &group_id, const std::vector &res); + +public slots: + void updateCommunityAvatar(const QString &id, const QPixmap &img); + void highlightSelectedCommunity(const QString &id); + void setCommunities(const mtx::responses::JoinedGroups &groups); + +private: + void fetchCommunityAvatar(const QString &id, const QString &avatarUrl); + void addGlobalItem() { addCommunity("world"); } + + //! Check whether or not a community id is currently managed. + bool communityExists(const QString &id) const + { + return communities_.find(id) != communities_.end(); + } + + QVBoxLayout *topLayout_; + QVBoxLayout *contentsLayout_; + QWidget *scrollAreaContents_; + QScrollArea *scrollArea_; + + std::map> communities_; +}; diff --git a/src/CommunitiesListItem.cc b/src/CommunitiesListItem.cc deleted file mode 100644 index df6c5393..00000000 --- a/src/CommunitiesListItem.cc +++ /dev/null @@ -1,108 +0,0 @@ -#include "CommunitiesListItem.h" -#include "Painter.h" -#include "Ripple.h" -#include "RippleOverlay.h" -#include "Utils.h" - -CommunitiesListItem::CommunitiesListItem(QString group_id, QWidget *parent) - : QWidget(parent) - , groupId_(group_id) -{ - setMouseTracking(true); - setAttribute(Qt::WA_Hover); - - QPainterPath path; - path.addRect(0, 0, parent->width(), height()); - rippleOverlay_ = new RippleOverlay(this); - rippleOverlay_->setClipPath(path); - rippleOverlay_->setClipping(true); - - if (groupId_ == "world") - avatar_ = QPixmap(":/icons/icons/ui/world.svg"); -} - -void -CommunitiesListItem::setPressedState(bool state) -{ - if (isPressed_ != state) { - isPressed_ = state; - update(); - } -} - -void -CommunitiesListItem::mousePressEvent(QMouseEvent *event) -{ - if (event->buttons() == Qt::RightButton) { - QWidget::mousePressEvent(event); - return; - } - - emit clicked(groupId_); - - setPressedState(true); - - QPoint pos = event->pos(); - qreal radiusEndValue = static_cast(width()) / 3; - - auto ripple = new Ripple(pos); - ripple->setRadiusEndValue(radiusEndValue); - ripple->setOpacityStartValue(0.15); - ripple->setColor("white"); - ripple->radiusAnimation()->setDuration(200); - ripple->opacityAnimation()->setDuration(400); - rippleOverlay_->addRipple(ripple); -} - -void -CommunitiesListItem::paintEvent(QPaintEvent *) -{ - Painter p(this); - PainterHighQualityEnabler hq(p); - - if (isPressed_) - p.fillRect(rect(), highlightedBackgroundColor_); - else if (underMouse()) - p.fillRect(rect(), hoverBackgroundColor_); - else - p.fillRect(rect(), backgroundColor_); - - if (avatar_.isNull()) { - QFont font; - font.setPixelSize(conf::roomlist::fonts::communityBubble); - p.setFont(font); - - p.drawLetterAvatar(utils::firstChar(resolveName()), - avatarFgColor_, - avatarBgColor_, - width(), - height(), - IconSize); - } else { - p.save(); - - p.drawAvatar(avatar_, width(), height(), IconSize); - p.restore(); - } -} - -void -CommunitiesListItem::setAvatar(const QImage &img) -{ - avatar_ = utils::scaleImageToPixmap(img, IconSize); - update(); -} - -QString -CommunitiesListItem::resolveName() const -{ - if (!name_.isEmpty()) - return name_; - - if (!groupId_.startsWith("+")) - return QString("Group"); // Group with no name or id. - - // Extract the localpart of the group. - auto firstPart = groupId_.split(':').at(0); - return firstPart.right(firstPart.size() - 1); -} diff --git a/src/CommunitiesListItem.cpp b/src/CommunitiesListItem.cpp new file mode 100644 index 00000000..8afaebff --- /dev/null +++ b/src/CommunitiesListItem.cpp @@ -0,0 +1,108 @@ +#include "CommunitiesListItem.h" +#include "Utils.h" +#include "ui/Painter.h" +#include "ui/Ripple.h" +#include "ui/RippleOverlay.h" + +CommunitiesListItem::CommunitiesListItem(QString group_id, QWidget *parent) + : QWidget(parent) + , groupId_(group_id) +{ + setMouseTracking(true); + setAttribute(Qt::WA_Hover); + + QPainterPath path; + path.addRect(0, 0, parent->width(), height()); + rippleOverlay_ = new RippleOverlay(this); + rippleOverlay_->setClipPath(path); + rippleOverlay_->setClipping(true); + + if (groupId_ == "world") + avatar_ = QPixmap(":/icons/icons/ui/world.svg"); +} + +void +CommunitiesListItem::setPressedState(bool state) +{ + if (isPressed_ != state) { + isPressed_ = state; + update(); + } +} + +void +CommunitiesListItem::mousePressEvent(QMouseEvent *event) +{ + if (event->buttons() == Qt::RightButton) { + QWidget::mousePressEvent(event); + return; + } + + emit clicked(groupId_); + + setPressedState(true); + + QPoint pos = event->pos(); + qreal radiusEndValue = static_cast(width()) / 3; + + auto ripple = new Ripple(pos); + ripple->setRadiusEndValue(radiusEndValue); + ripple->setOpacityStartValue(0.15); + ripple->setColor("white"); + ripple->radiusAnimation()->setDuration(200); + ripple->opacityAnimation()->setDuration(400); + rippleOverlay_->addRipple(ripple); +} + +void +CommunitiesListItem::paintEvent(QPaintEvent *) +{ + Painter p(this); + PainterHighQualityEnabler hq(p); + + if (isPressed_) + p.fillRect(rect(), highlightedBackgroundColor_); + else if (underMouse()) + p.fillRect(rect(), hoverBackgroundColor_); + else + p.fillRect(rect(), backgroundColor_); + + if (avatar_.isNull()) { + QFont font; + font.setPixelSize(conf::roomlist::fonts::communityBubble); + p.setFont(font); + + p.drawLetterAvatar(utils::firstChar(resolveName()), + avatarFgColor_, + avatarBgColor_, + width(), + height(), + IconSize); + } else { + p.save(); + + p.drawAvatar(avatar_, width(), height(), IconSize); + p.restore(); + } +} + +void +CommunitiesListItem::setAvatar(const QImage &img) +{ + avatar_ = utils::scaleImageToPixmap(img, IconSize); + update(); +} + +QString +CommunitiesListItem::resolveName() const +{ + if (!name_.isEmpty()) + return name_; + + if (!groupId_.startsWith("+")) + return QString("Group"); // Group with no name or id. + + // Extract the localpart of the group. + auto firstPart = groupId_.split(':').at(0); + return firstPart.right(firstPart.size() - 1); +} diff --git a/src/CommunitiesListItem.h b/src/CommunitiesListItem.h new file mode 100644 index 00000000..a9b6e333 --- /dev/null +++ b/src/CommunitiesListItem.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "Config.h" +#include "ui/Theme.h" + +class RippleOverlay; + +class CommunitiesListItem : public QWidget +{ + Q_OBJECT + Q_PROPERTY(QColor highlightedBackgroundColor READ highlightedBackgroundColor WRITE + setHighlightedBackgroundColor) + Q_PROPERTY( + QColor hoverBackgroundColor READ hoverBackgroundColor WRITE setHoverBackgroundColor) + Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) + + Q_PROPERTY(QColor avatarFgColor READ avatarFgColor WRITE setAvatarFgColor) + Q_PROPERTY(QColor avatarBgColor READ avatarBgColor WRITE setAvatarBgColor) + +public: + CommunitiesListItem(QString group_id, QWidget *parent = nullptr); + + void setName(QString name) { name_ = name; } + bool isPressed() const { return isPressed_; } + void setAvatar(const QImage &img); + + void setRooms(std::vector room_ids) { room_ids_ = std::move(room_ids); } + std::vector rooms() const { return room_ids_; } + + QColor highlightedBackgroundColor() const { return highlightedBackgroundColor_; } + QColor hoverBackgroundColor() const { return hoverBackgroundColor_; } + QColor backgroundColor() const { return backgroundColor_; } + + QColor avatarFgColor() const { return avatarFgColor_; } + QColor avatarBgColor() const { return avatarBgColor_; } + + void setHighlightedBackgroundColor(QColor &color) { highlightedBackgroundColor_ = color; } + void setHoverBackgroundColor(QColor &color) { hoverBackgroundColor_ = color; } + void setBackgroundColor(QColor &color) { backgroundColor_ = color; } + + void setAvatarFgColor(QColor &color) { avatarFgColor_ = color; } + void setAvatarBgColor(QColor &color) { avatarBgColor_ = color; } + + QSize sizeHint() const override + { + return QSize(IconSize + IconSize / 3, IconSize + IconSize / 3); + } + +signals: + void clicked(const QString &group_id); + +public slots: + void setPressedState(bool state); + +protected: + void mousePressEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *event) override; + +private: + const int IconSize = 36; + + QString resolveName() const; + + std::vector room_ids_; + + QString name_; + QString groupId_; + QPixmap avatar_; + + QColor highlightedBackgroundColor_; + QColor hoverBackgroundColor_; + QColor backgroundColor_; + + QColor avatarFgColor_; + QColor avatarBgColor_; + + bool isPressed_ = false; + + RippleOverlay *rippleOverlay_; +}; diff --git a/src/Config.h b/src/Config.h new file mode 100644 index 00000000..3a3296d6 --- /dev/null +++ b/src/Config.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include + +// Non-theme app configuration. Layouts, fonts spacing etc. +// +// Font sizes are in pixels. + +namespace conf { +constexpr int sideBarCollapsePoint = 450; +// Global settings. +constexpr int fontSize = 14; +constexpr int textInputFontSize = 14; +constexpr int emojiSize = 14; +constexpr int headerFontSize = 21; +constexpr int typingNotificationFontSize = 11; + +namespace popup { +constexpr int font = fontSize; +constexpr int avatar = 28; +} + +namespace modals { +constexpr int errorFont = conf::fontSize - 2; +} + +namespace receipts { +constexpr int font = 12; +} + +namespace dialogs { +constexpr int labelSize = 15; +} + +namespace strings { +const QString url_html = "\\1"; +const QRegExp url_regex( + "((www\\.(?!\\.)|[a-z][a-z0-9+.-]*://)[^\\s<>'\"]+[^!,\\.\\s<>'\"\\]\\)\\:])"); +} + +// Window geometry. +namespace window { +constexpr int height = 600; +constexpr int width = 1066; + +constexpr int minHeight = height; +constexpr int minWidth = 950; +} // namespace window + +namespace textInput { +constexpr int height = 50; +} + +namespace sidebarActions { +constexpr int height = textInput::height; +constexpr int iconSize = 28; +} + +// Button settings. +namespace btn { +constexpr int fontSize = 20; +constexpr int cornerRadius = 3; +} // namespace btn + +// RoomList specific. +namespace roomlist { +namespace fonts { +constexpr int heading = 13; +constexpr int timestamp = heading; +constexpr int badge = 10; +constexpr int bubble = 20; +constexpr int communityBubble = bubble - 4; +} // namespace fonts +} // namespace roomlist + +namespace userInfoWidget { +namespace fonts { +constexpr int displayName = 15; +constexpr int userid = 13; +} // namespace fonts +} // namespace userInfoWidget + +namespace topRoomBar { +namespace fonts { +constexpr int roomName = 15; +constexpr int roomDescription = 14; +} // namespace fonts +} // namespace topRoomBar + +namespace timeline { +constexpr int msgAvatarTopMargin = 15; +constexpr int msgTopMargin = 2; +constexpr int msgLeftMargin = 14; +constexpr int avatarSize = 36; +constexpr int headerSpacing = 3; +constexpr int headerLeftMargin = 15; + +namespace fonts { +constexpr int timestamp = 13; +constexpr int indicator = timestamp - 2; +constexpr int dateSeparator = conf::fontSize; +} // namespace fonts +} // namespace timeline + +} // namespace conf diff --git a/src/InviteeItem.cc b/src/InviteeItem.cc deleted file mode 100644 index 5ae2a7b6..00000000 --- a/src/InviteeItem.cc +++ /dev/null @@ -1,37 +0,0 @@ -#include - -#include "FlatButton.h" -#include "InviteeItem.h" -#include "Theme.h" - -constexpr int SidePadding = 10; -constexpr int IconSize = 13; - -InviteeItem::InviteeItem(mtx::identifiers::User user, QWidget *parent) - : QWidget{parent} - , user_{QString::fromStdString(user.to_string())} -{ - auto topLayout_ = new QHBoxLayout(this); - topLayout_->setSpacing(0); - topLayout_->setContentsMargins(SidePadding, 0, 3 * SidePadding, 0); - - QFont font; - font.setPixelSize(15); - - name_ = new QLabel(user_, this); - name_->setFont(font); - - QIcon removeUserIcon; - removeUserIcon.addFile(":/icons/icons/ui/remove-symbol.png"); - - removeUserBtn_ = new FlatButton(this); - removeUserBtn_->setIcon(removeUserIcon); - removeUserBtn_->setIconSize(QSize(IconSize, IconSize)); - removeUserBtn_->setFixedSize(QSize(IconSize, IconSize)); - removeUserBtn_->setRippleStyle(ui::RippleStyle::NoRipple); - - topLayout_->addWidget(name_); - topLayout_->addWidget(removeUserBtn_); - - connect(removeUserBtn_, &FlatButton::clicked, this, &InviteeItem::removeItem); -} diff --git a/src/InviteeItem.cpp b/src/InviteeItem.cpp new file mode 100644 index 00000000..6e9be0d5 --- /dev/null +++ b/src/InviteeItem.cpp @@ -0,0 +1,37 @@ +#include + +#include "InviteeItem.h" +#include "ui/FlatButton.h" +#include "ui/Theme.h" + +constexpr int SidePadding = 10; +constexpr int IconSize = 13; + +InviteeItem::InviteeItem(mtx::identifiers::User user, QWidget *parent) + : QWidget{parent} + , user_{QString::fromStdString(user.to_string())} +{ + auto topLayout_ = new QHBoxLayout(this); + topLayout_->setSpacing(0); + topLayout_->setContentsMargins(SidePadding, 0, 3 * SidePadding, 0); + + QFont font; + font.setPixelSize(15); + + name_ = new QLabel(user_, this); + name_->setFont(font); + + QIcon removeUserIcon; + removeUserIcon.addFile(":/icons/icons/ui/remove-symbol.png"); + + removeUserBtn_ = new FlatButton(this); + removeUserBtn_->setIcon(removeUserIcon); + removeUserBtn_->setIconSize(QSize(IconSize, IconSize)); + removeUserBtn_->setFixedSize(QSize(IconSize, IconSize)); + removeUserBtn_->setRippleStyle(ui::RippleStyle::NoRipple); + + topLayout_->addWidget(name_); + topLayout_->addWidget(removeUserBtn_); + + connect(removeUserBtn_, &FlatButton::clicked, this, &InviteeItem::removeItem); +} diff --git a/src/InviteeItem.h b/src/InviteeItem.h new file mode 100644 index 00000000..f0bdbdf0 --- /dev/null +++ b/src/InviteeItem.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include "mtx.hpp" + +class FlatButton; + +class InviteeItem : public QWidget +{ + Q_OBJECT + +public: + InviteeItem(mtx::identifiers::User user, QWidget *parent = nullptr); + + QString userID() { return user_; } + +signals: + void removeItem(); + +private: + QString user_; + + QLabel *name_; + FlatButton *removeUserBtn_; +}; diff --git a/src/Logging.cpp b/src/Logging.cpp index bccbe389..1b2838f3 100644 --- a/src/Logging.cpp +++ b/src/Logging.cpp @@ -1,4 +1,4 @@ -#include "Logging.hpp" +#include "Logging.h" #include #include diff --git a/src/Logging.h b/src/Logging.h new file mode 100644 index 00000000..2feae60d --- /dev/null +++ b/src/Logging.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +namespace nhlog { +void +init(const std::string &file); + +std::shared_ptr +ui(); + +std::shared_ptr +net(); + +std::shared_ptr +db(); + +std::shared_ptr +crypto(); +} diff --git a/src/LoginPage.cc b/src/LoginPage.cc deleted file mode 100644 index 6a3b925c..00000000 --- a/src/LoginPage.cc +++ /dev/null @@ -1,318 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include - -#include - -#include "Config.h" -#include "FlatButton.h" -#include "LoadingIndicator.h" -#include "LoginPage.h" -#include "MatrixClient.h" -#include "OverlayModal.h" -#include "RaisedButton.h" -#include "TextField.h" - -using namespace mtx::identifiers; - -LoginPage::LoginPage(QWidget *parent) - : QWidget(parent) - , inferredServerAddress_() -{ - top_layout_ = new QVBoxLayout(); - - top_bar_layout_ = new QHBoxLayout(); - top_bar_layout_->setSpacing(0); - top_bar_layout_->setMargin(0); - - back_button_ = new FlatButton(this); - back_button_->setMinimumSize(QSize(30, 30)); - - top_bar_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); - top_bar_layout_->addStretch(1); - - QIcon icon; - icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); - - back_button_->setIcon(icon); - back_button_->setIconSize(QSize(32, 32)); - - QIcon logo; - logo.addFile(":/logos/login.png"); - - logo_ = new QLabel(this); - logo_->setPixmap(logo.pixmap(128)); - - logo_layout_ = new QHBoxLayout(); - logo_layout_->setContentsMargins(0, 0, 0, 20); - logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter); - - form_wrapper_ = new QHBoxLayout(); - form_widget_ = new QWidget(); - form_widget_->setMinimumSize(QSize(350, 200)); - - form_layout_ = new QVBoxLayout(); - form_layout_->setSpacing(20); - form_layout_->setContentsMargins(0, 0, 0, 30); - form_widget_->setLayout(form_layout_); - - form_wrapper_->addStretch(1); - form_wrapper_->addWidget(form_widget_); - form_wrapper_->addStretch(1); - - matrixid_input_ = new TextField(this); - matrixid_input_->setLabel(tr("Matrix ID")); - matrixid_input_->setPlaceholderText(tr("e.g @joe:matrix.org")); - - spinner_ = new LoadingIndicator(this); - spinner_->setFixedHeight(40); - spinner_->setFixedWidth(40); - spinner_->hide(); - - errorIcon_ = new QLabel(this); - errorIcon_->setPixmap(QPixmap(":/icons/icons/error.png")); - errorIcon_->hide(); - - matrixidLayout_ = new QHBoxLayout(); - matrixidLayout_->addWidget(matrixid_input_, 0, Qt::AlignVCenter); - - password_input_ = new TextField(this); - password_input_->setLabel(tr("Password")); - password_input_->setEchoMode(QLineEdit::Password); - - serverInput_ = new TextField(this); - serverInput_->setLabel("Homeserver address"); - serverInput_->setPlaceholderText("matrix.org"); - serverInput_->hide(); - - serverLayout_ = new QHBoxLayout(); - serverLayout_->addWidget(serverInput_, 0, Qt::AlignVCenter); - - form_layout_->addLayout(matrixidLayout_); - form_layout_->addWidget(password_input_, Qt::AlignHCenter, 0); - form_layout_->addLayout(serverLayout_); - - button_layout_ = new QHBoxLayout(); - button_layout_->setSpacing(0); - button_layout_->setContentsMargins(0, 0, 0, 30); - - login_button_ = new RaisedButton(tr("LOGIN"), this); - login_button_->setMinimumSize(350, 65); - login_button_->setFontSize(20); - login_button_->setCornerRadius(3); - - button_layout_->addStretch(1); - button_layout_->addWidget(login_button_); - button_layout_->addStretch(1); - - QFont font; - font.setPixelSize(conf::fontSize); - - error_label_ = new QLabel(this); - error_label_->setFont(font); - - top_layout_->addLayout(top_bar_layout_); - top_layout_->addStretch(1); - top_layout_->addLayout(logo_layout_); - top_layout_->addLayout(form_wrapper_); - top_layout_->addStretch(1); - top_layout_->addLayout(button_layout_); - top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); - top_layout_->addStretch(1); - - setLayout(top_layout_); - - connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk); - connect(this, &LoginPage::versionErrorCb, this, &LoginPage::versionError); - connect(this, &LoginPage::loginErrorCb, this, &LoginPage::loginError); - - connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); - connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked())); - connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); - connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); - connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click())); - connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered())); - connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered())); -} - -void -LoginPage::onMatrixIdEntered() -{ - error_label_->setText(""); - - User user; - - try { - user = parse(matrixid_input_->text().toStdString()); - } catch (const std::exception &e) { - return loginError("You have entered an invalid Matrix ID e.g @joe:matrix.org"); - } - - QString homeServer = QString::fromStdString(user.hostname()); - if (homeServer != inferredServerAddress_) { - serverInput_->hide(); - serverLayout_->removeWidget(errorIcon_); - errorIcon_->hide(); - if (serverInput_->isVisible()) { - matrixidLayout_->removeWidget(spinner_); - serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); - spinner_->start(); - } else { - serverLayout_->removeWidget(spinner_); - matrixidLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); - spinner_->start(); - } - - inferredServerAddress_ = homeServer; - serverInput_->setText(homeServer); - - http::client()->set_server(user.hostname()); - checkHomeserverVersion(); - } -} - -void -LoginPage::checkHomeserverVersion() -{ - http::client()->versions( - [this](const mtx::responses::Versions &, mtx::http::RequestErr err) { - if (err) { - using namespace boost::beast::http; - - if (err->status_code == status::not_found) { - emit versionErrorCb(tr("The required endpoints were not found. " - "Possibly not a Matrix server.")); - return; - } - - if (!err->parse_error.empty()) { - emit versionErrorCb(tr("Received malformed response. Make sure " - "the homeserver domain is valid.")); - return; - } - - emit versionErrorCb(tr( - "An unknown error occured. Make sure the homeserver domain is valid.")); - return; - } - - emit versionOkCb(); - }); -} - -void -LoginPage::onServerAddressEntered() -{ - error_label_->setText(""); - http::client()->set_server(serverInput_->text().toStdString()); - checkHomeserverVersion(); - - serverLayout_->removeWidget(errorIcon_); - errorIcon_->hide(); - serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); - spinner_->start(); -} - -void -LoginPage::versionError(const QString &error) -{ - error_label_->setText(error); - serverInput_->show(); - - spinner_->stop(); - serverLayout_->removeWidget(spinner_); - serverLayout_->addWidget(errorIcon_, 0, Qt::AlignVCenter | Qt::AlignRight); - errorIcon_->show(); - matrixidLayout_->removeWidget(spinner_); -} - -void -LoginPage::versionOk() -{ - serverLayout_->removeWidget(spinner_); - matrixidLayout_->removeWidget(spinner_); - spinner_->stop(); - - if (serverInput_->isVisible()) - serverInput_->hide(); -} - -void -LoginPage::onLoginButtonClicked() -{ - error_label_->setText(""); - - User user; - - try { - user = parse(matrixid_input_->text().toStdString()); - } catch (const std::exception &e) { - return loginError("You have entered an invalid Matrix ID e.g @joe:matrix.org"); - } - - if (password_input_->text().isEmpty()) - return loginError(tr("Empty password")); - - http::client()->set_server(serverInput_->text().toStdString()); - http::client()->login(user.localpart(), - password_input_->text().toStdString(), - initialDeviceName(), - [this](const mtx::responses::Login &res, mtx::http::RequestErr err) { - if (err) { - emit loginError( - QString::fromStdString(err->matrix_error.error)); - emit errorOccurred(); - return; - } - - emit loginOk(res); - }); - - emit loggingIn(); -} - -void -LoginPage::reset() -{ - matrixid_input_->clear(); - password_input_->clear(); - serverInput_->clear(); - - spinner_->stop(); - errorIcon_->hide(); - serverLayout_->removeWidget(spinner_); - serverLayout_->removeWidget(errorIcon_); - matrixidLayout_->removeWidget(spinner_); - - inferredServerAddress_.clear(); -} - -void -LoginPage::onBackButtonClicked() -{ - emit backButtonClicked(); -} - -void -LoginPage::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp new file mode 100644 index 00000000..dbf9d470 --- /dev/null +++ b/src/LoginPage.cpp @@ -0,0 +1,318 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include + +#include "Config.h" +#include "LoginPage.h" +#include "MatrixClient.h" +#include "ui/FlatButton.h" +#include "ui/LoadingIndicator.h" +#include "ui/OverlayModal.h" +#include "ui/RaisedButton.h" +#include "ui/TextField.h" + +using namespace mtx::identifiers; + +LoginPage::LoginPage(QWidget *parent) + : QWidget(parent) + , inferredServerAddress_() +{ + top_layout_ = new QVBoxLayout(); + + top_bar_layout_ = new QHBoxLayout(); + top_bar_layout_->setSpacing(0); + top_bar_layout_->setMargin(0); + + back_button_ = new FlatButton(this); + back_button_->setMinimumSize(QSize(30, 30)); + + top_bar_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); + top_bar_layout_->addStretch(1); + + QIcon icon; + icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); + + back_button_->setIcon(icon); + back_button_->setIconSize(QSize(32, 32)); + + QIcon logo; + logo.addFile(":/logos/login.png"); + + logo_ = new QLabel(this); + logo_->setPixmap(logo.pixmap(128)); + + logo_layout_ = new QHBoxLayout(); + logo_layout_->setContentsMargins(0, 0, 0, 20); + logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter); + + form_wrapper_ = new QHBoxLayout(); + form_widget_ = new QWidget(); + form_widget_->setMinimumSize(QSize(350, 200)); + + form_layout_ = new QVBoxLayout(); + form_layout_->setSpacing(20); + form_layout_->setContentsMargins(0, 0, 0, 30); + form_widget_->setLayout(form_layout_); + + form_wrapper_->addStretch(1); + form_wrapper_->addWidget(form_widget_); + form_wrapper_->addStretch(1); + + matrixid_input_ = new TextField(this); + matrixid_input_->setLabel(tr("Matrix ID")); + matrixid_input_->setPlaceholderText(tr("e.g @joe:matrix.org")); + + spinner_ = new LoadingIndicator(this); + spinner_->setFixedHeight(40); + spinner_->setFixedWidth(40); + spinner_->hide(); + + errorIcon_ = new QLabel(this); + errorIcon_->setPixmap(QPixmap(":/icons/icons/error.png")); + errorIcon_->hide(); + + matrixidLayout_ = new QHBoxLayout(); + matrixidLayout_->addWidget(matrixid_input_, 0, Qt::AlignVCenter); + + password_input_ = new TextField(this); + password_input_->setLabel(tr("Password")); + password_input_->setEchoMode(QLineEdit::Password); + + serverInput_ = new TextField(this); + serverInput_->setLabel("Homeserver address"); + serverInput_->setPlaceholderText("matrix.org"); + serverInput_->hide(); + + serverLayout_ = new QHBoxLayout(); + serverLayout_->addWidget(serverInput_, 0, Qt::AlignVCenter); + + form_layout_->addLayout(matrixidLayout_); + form_layout_->addWidget(password_input_, Qt::AlignHCenter, 0); + form_layout_->addLayout(serverLayout_); + + button_layout_ = new QHBoxLayout(); + button_layout_->setSpacing(0); + button_layout_->setContentsMargins(0, 0, 0, 30); + + login_button_ = new RaisedButton(tr("LOGIN"), this); + login_button_->setMinimumSize(350, 65); + login_button_->setFontSize(20); + login_button_->setCornerRadius(3); + + button_layout_->addStretch(1); + button_layout_->addWidget(login_button_); + button_layout_->addStretch(1); + + QFont font; + font.setPixelSize(conf::fontSize); + + error_label_ = new QLabel(this); + error_label_->setFont(font); + + top_layout_->addLayout(top_bar_layout_); + top_layout_->addStretch(1); + top_layout_->addLayout(logo_layout_); + top_layout_->addLayout(form_wrapper_); + top_layout_->addStretch(1); + top_layout_->addLayout(button_layout_); + top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); + top_layout_->addStretch(1); + + setLayout(top_layout_); + + connect(this, &LoginPage::versionOkCb, this, &LoginPage::versionOk); + connect(this, &LoginPage::versionErrorCb, this, &LoginPage::versionError); + connect(this, &LoginPage::loginErrorCb, this, &LoginPage::loginError); + + connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); + connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked())); + connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered())); + connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered())); +} + +void +LoginPage::onMatrixIdEntered() +{ + error_label_->setText(""); + + User user; + + try { + user = parse(matrixid_input_->text().toStdString()); + } catch (const std::exception &e) { + return loginError("You have entered an invalid Matrix ID e.g @joe:matrix.org"); + } + + QString homeServer = QString::fromStdString(user.hostname()); + if (homeServer != inferredServerAddress_) { + serverInput_->hide(); + serverLayout_->removeWidget(errorIcon_); + errorIcon_->hide(); + if (serverInput_->isVisible()) { + matrixidLayout_->removeWidget(spinner_); + serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); + spinner_->start(); + } else { + serverLayout_->removeWidget(spinner_); + matrixidLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); + spinner_->start(); + } + + inferredServerAddress_ = homeServer; + serverInput_->setText(homeServer); + + http::client()->set_server(user.hostname()); + checkHomeserverVersion(); + } +} + +void +LoginPage::checkHomeserverVersion() +{ + http::client()->versions( + [this](const mtx::responses::Versions &, mtx::http::RequestErr err) { + if (err) { + using namespace boost::beast::http; + + if (err->status_code == status::not_found) { + emit versionErrorCb(tr("The required endpoints were not found. " + "Possibly not a Matrix server.")); + return; + } + + if (!err->parse_error.empty()) { + emit versionErrorCb(tr("Received malformed response. Make sure " + "the homeserver domain is valid.")); + return; + } + + emit versionErrorCb(tr( + "An unknown error occured. Make sure the homeserver domain is valid.")); + return; + } + + emit versionOkCb(); + }); +} + +void +LoginPage::onServerAddressEntered() +{ + error_label_->setText(""); + http::client()->set_server(serverInput_->text().toStdString()); + checkHomeserverVersion(); + + serverLayout_->removeWidget(errorIcon_); + errorIcon_->hide(); + serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); + spinner_->start(); +} + +void +LoginPage::versionError(const QString &error) +{ + error_label_->setText(error); + serverInput_->show(); + + spinner_->stop(); + serverLayout_->removeWidget(spinner_); + serverLayout_->addWidget(errorIcon_, 0, Qt::AlignVCenter | Qt::AlignRight); + errorIcon_->show(); + matrixidLayout_->removeWidget(spinner_); +} + +void +LoginPage::versionOk() +{ + serverLayout_->removeWidget(spinner_); + matrixidLayout_->removeWidget(spinner_); + spinner_->stop(); + + if (serverInput_->isVisible()) + serverInput_->hide(); +} + +void +LoginPage::onLoginButtonClicked() +{ + error_label_->setText(""); + + User user; + + try { + user = parse(matrixid_input_->text().toStdString()); + } catch (const std::exception &e) { + return loginError("You have entered an invalid Matrix ID e.g @joe:matrix.org"); + } + + if (password_input_->text().isEmpty()) + return loginError(tr("Empty password")); + + http::client()->set_server(serverInput_->text().toStdString()); + http::client()->login(user.localpart(), + password_input_->text().toStdString(), + initialDeviceName(), + [this](const mtx::responses::Login &res, mtx::http::RequestErr err) { + if (err) { + emit loginError( + QString::fromStdString(err->matrix_error.error)); + emit errorOccurred(); + return; + } + + emit loginOk(res); + }); + + emit loggingIn(); +} + +void +LoginPage::reset() +{ + matrixid_input_->clear(); + password_input_->clear(); + serverInput_->clear(); + + spinner_->stop(); + errorIcon_->hide(); + serverLayout_->removeWidget(spinner_); + serverLayout_->removeWidget(errorIcon_); + matrixidLayout_->removeWidget(spinner_); + + inferredServerAddress_.clear(); +} + +void +LoginPage::onBackButtonClicked() +{ + emit backButtonClicked(); +} + +void +LoginPage::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} diff --git a/src/LoginPage.h b/src/LoginPage.h new file mode 100644 index 00000000..c52ccaa4 --- /dev/null +++ b/src/LoginPage.h @@ -0,0 +1,124 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +class FlatButton; +class LoadingIndicator; +class OverlayModal; +class RaisedButton; +class TextField; + +namespace mtx { +namespace responses { +struct Login; +} +} + +class LoginPage : public QWidget +{ + Q_OBJECT + +public: + LoginPage(QWidget *parent = 0); + + void reset(); + +signals: + void backButtonClicked(); + void loggingIn(); + void errorOccurred(); + + //! Used to trigger the corresponding slot outside of the main thread. + void versionErrorCb(const QString &err); + void loginErrorCb(const QString &err); + void versionOkCb(); + + void loginOk(const mtx::responses::Login &res); + +protected: + void paintEvent(QPaintEvent *event) override; + +public slots: + // Displays errors produced during the login. + void loginError(const QString &msg) { error_label_->setText(msg); } + +private slots: + // Callback for the back button. + void onBackButtonClicked(); + + // Callback for the login button. + void onLoginButtonClicked(); + + // Callback for probing the server found in the mxid + void onMatrixIdEntered(); + + // Callback for probing the manually entered server + void onServerAddressEntered(); + + // Callback for errors produced during server probing + void versionError(const QString &error_message); + // Callback for successful server probing + void versionOk(); + +private: + bool isMatrixIdValid(); + void checkHomeserverVersion(); + std::string initialDeviceName() + { +#if defined(Q_OS_MAC) + return "nheko on macOS"; +#elif defined(Q_OS_LINUX) + return "nheko on Linux"; +#elif defined(Q_OS_WIN) + return "nheko on Windows"; +#else + return "nheko"; +#endif + } + + QVBoxLayout *top_layout_; + + QHBoxLayout *top_bar_layout_; + QHBoxLayout *logo_layout_; + QHBoxLayout *button_layout_; + + QLabel *logo_; + QLabel *error_label_; + + QHBoxLayout *serverLayout_; + QHBoxLayout *matrixidLayout_; + LoadingIndicator *spinner_; + QLabel *errorIcon_; + QString inferredServerAddress_; + + FlatButton *back_button_; + RaisedButton *login_button_; + + QWidget *form_widget_; + QHBoxLayout *form_wrapper_; + QVBoxLayout *form_layout_; + + TextField *matrixid_input_; + TextField *password_input_; + TextField *serverInput_; +}; diff --git a/src/MainWindow.cc b/src/MainWindow.cc deleted file mode 100644 index 749e7caf..00000000 --- a/src/MainWindow.cc +++ /dev/null @@ -1,511 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include - -#include - -#include "ChatPage.h" -#include "Config.h" -#include "LoadingIndicator.h" -#include "Logging.hpp" -#include "LoginPage.h" -#include "MainWindow.h" -#include "MatrixClient.h" -#include "OverlayModal.h" -#include "RegisterPage.h" -#include "SnackBar.h" -#include "TrayIcon.h" -#include "UserSettingsPage.h" -#include "WelcomePage.h" - -#include "dialogs/CreateRoom.h" -#include "dialogs/InviteUsers.h" -#include "dialogs/JoinRoom.h" -#include "dialogs/LeaveRoom.h" -#include "dialogs/Logout.h" -#include "dialogs/MemberList.hpp" -#include "dialogs/RoomSettings.hpp" - -MainWindow *MainWindow::instance_ = nullptr; - -MainWindow::MainWindow(QWidget *parent) - : QMainWindow(parent) - , progressModal_{nullptr} - , spinner_{nullptr} -{ - setWindowTitle("nheko"); - setObjectName("MainWindow"); - - restoreWindowSize(); - - QFont font("Open Sans"); - font.setPixelSize(conf::fontSize); - font.setStyleStrategy(QFont::PreferAntialias); - setFont(font); - - userSettings_ = QSharedPointer(new UserSettings); - trayIcon_ = new TrayIcon(":/logos/nheko-32.png", this); - - welcome_page_ = new WelcomePage(this); - login_page_ = new LoginPage(this); - register_page_ = new RegisterPage(this); - chat_page_ = new ChatPage(userSettings_, this); - userSettingsPage_ = new UserSettingsPage(userSettings_, this); - - // Initialize sliding widget manager. - pageStack_ = new QStackedWidget(this); - pageStack_->addWidget(welcome_page_); - pageStack_->addWidget(login_page_); - pageStack_->addWidget(register_page_); - pageStack_->addWidget(chat_page_); - pageStack_->addWidget(userSettingsPage_); - - setCentralWidget(pageStack_); - - connect(welcome_page_, SIGNAL(userLogin()), this, SLOT(showLoginPage())); - connect(welcome_page_, SIGNAL(userRegister()), this, SLOT(showRegisterPage())); - - connect(login_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); - connect(login_page_, &LoginPage::loggingIn, this, &MainWindow::showOverlayProgressBar); - connect( - register_page_, &RegisterPage::registering, this, &MainWindow::showOverlayProgressBar); - connect( - login_page_, &LoginPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); }); - connect(register_page_, &RegisterPage::errorOccurred, this, [this]() { - removeOverlayProgressBar(); - }); - connect(register_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); - - connect(chat_page_, &ChatPage::closing, this, &MainWindow::showWelcomePage); - connect( - chat_page_, &ChatPage::showOverlayProgressBar, this, &MainWindow::showOverlayProgressBar); - connect( - chat_page_, SIGNAL(changeWindowTitle(QString)), this, SLOT(setWindowTitle(QString))); - connect(chat_page_, SIGNAL(unreadMessages(int)), trayIcon_, SLOT(setUnreadCount(int))); - connect(chat_page_, &ChatPage::showLoginPage, this, [this](const QString &msg) { - login_page_->loginError(msg); - showLoginPage(); - }); - - connect(userSettingsPage_, &UserSettingsPage::moveBack, this, [this]() { - pageStack_->setCurrentWidget(chat_page_); - }); - - connect( - userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool))); - - connect(trayIcon_, - SIGNAL(activated(QSystemTrayIcon::ActivationReason)), - this, - SLOT(iconActivated(QSystemTrayIcon::ActivationReason))); - - connect(chat_page_, SIGNAL(contentLoaded()), this, SLOT(removeOverlayProgressBar())); - connect( - chat_page_, &ChatPage::showUserSettingsPage, this, &MainWindow::showUserSettingsPage); - - connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) { - http::client()->set_user(res.user_id); - showChatPage(); - }); - - connect(register_page_, &RegisterPage::registerOk, this, &MainWindow::showChatPage); - - QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this); - connect(quitShortcut, &QShortcut::activated, this, QApplication::quit); - - QShortcut *quickSwitchShortcut = new QShortcut(QKeySequence("Ctrl+K"), this); - connect(quickSwitchShortcut, &QShortcut::activated, this, [this]() { - if (chat_page_->isVisible() && !hasActiveDialogs()) - chat_page_->showQuickSwitcher(); - }); - - QSettings settings; - - trayIcon_->setVisible(userSettings_->isTrayEnabled()); - - if (hasActiveUser()) { - QString token = settings.value("auth/access_token").toString(); - QString home_server = settings.value("auth/home_server").toString(); - QString user_id = settings.value("auth/user_id").toString(); - QString device_id = settings.value("auth/device_id").toString(); - - http::client()->set_access_token(token.toStdString()); - http::client()->set_server(home_server.toStdString()); - http::client()->set_device_id(device_id.toStdString()); - - try { - using namespace mtx::identifiers; - http::client()->set_user(parse(user_id.toStdString())); - } catch (const std::invalid_argument &e) { - nhlog::ui()->critical("bootstrapped with invalid user_id: {}", - user_id.toStdString()); - } - - showChatPage(); - } -} - -void -MainWindow::showEvent(QShowEvent *event) -{ - adjustSideBars(); - QMainWindow::showEvent(event); -} - -void -MainWindow::resizeEvent(QResizeEvent *event) -{ - adjustSideBars(); - QMainWindow::resizeEvent(event); -} - -void -MainWindow::adjustSideBars() -{ - const int timelineWidth = chat_page_->timelineWidth(); - const int minAvailableWidth = - conf::sideBarCollapsePoint + ui::sidebar::CommunitiesSidebarSize; - - if (timelineWidth < minAvailableWidth && !chat_page_->isSideBarExpanded()) { - chat_page_->hideSideBars(); - } else { - chat_page_->showSideBars(); - } -} - -void -MainWindow::restoreWindowSize() -{ - QSettings settings; - int savedWidth = settings.value("window/width").toInt(); - int savedheight = settings.value("window/height").toInt(); - - if (savedWidth == 0 || savedheight == 0) - resize(conf::window::width, conf::window::height); - else - resize(savedWidth, savedheight); -} - -void -MainWindow::saveCurrentWindowSize() -{ - QSettings settings; - QSize current = size(); - - settings.setValue("window/width", current.width()); - settings.setValue("window/height", current.height()); -} - -void -MainWindow::removeOverlayProgressBar() -{ - QTimer *timer = new QTimer(this); - timer->setSingleShot(true); - - connect(timer, &QTimer::timeout, [this, timer]() { - timer->deleteLater(); - - if (!progressModal_.isNull()) - progressModal_->hide(); - - if (!spinner_.isNull()) - spinner_->stop(); - - progressModal_.reset(); - spinner_.reset(); - }); - - // FIXME: Snackbar doesn't work if it's initialized in the constructor. - QTimer::singleShot(0, this, [this]() { - snackBar_ = QSharedPointer(new SnackBar(this)); - connect(chat_page_, - &ChatPage::showNotification, - snackBar_.data(), - &SnackBar::showMessage); - }); - - timer->start(500); -} - -void -MainWindow::showChatPage() -{ - auto userid = QString::fromStdString(http::client()->user_id().to_string()); - auto device_id = QString::fromStdString(http::client()->device_id()); - auto homeserver = QString::fromStdString(http::client()->server() + ":" + - std::to_string(http::client()->port())); - auto token = QString::fromStdString(http::client()->access_token()); - - QSettings settings; - settings.setValue("auth/access_token", token); - settings.setValue("auth/home_server", homeserver); - settings.setValue("auth/user_id", userid); - settings.setValue("auth/device_id", device_id); - - showOverlayProgressBar(); - - pageStack_->setCurrentWidget(chat_page_); - - pageStack_->removeWidget(welcome_page_); - pageStack_->removeWidget(login_page_); - pageStack_->removeWidget(register_page_); - - login_page_->reset(); - chat_page_->bootstrap(userid, homeserver, token); - - instance_ = this; -} - -void -MainWindow::closeEvent(QCloseEvent *event) -{ - if (!qApp->isSavingSession() && isVisible() && pageSupportsTray() && - userSettings_->isTrayEnabled()) { - event->ignore(); - hide(); - } -} - -void -MainWindow::iconActivated(QSystemTrayIcon::ActivationReason reason) -{ - switch (reason) { - case QSystemTrayIcon::Trigger: - if (!isVisible()) { - show(); - } else { - hide(); - } - break; - default: - break; - } -} - -bool -MainWindow::hasActiveUser() -{ - QSettings settings; - - return settings.contains("auth/access_token") && settings.contains("auth/home_server") && - settings.contains("auth/user_id"); -} - -void -MainWindow::openRoomSettings(const QString &room_id) -{ - const auto roomToSearch = room_id.isEmpty() ? chat_page_->currentRoom() : ""; - - roomSettingsDialog_ = - QSharedPointer(new dialogs::RoomSettings(roomToSearch, this)); - - connect(roomSettingsDialog_.data(), &dialogs::RoomSettings::closing, this, [this]() { - roomSettingsModal_->hide(); - }); - - roomSettingsModal_ = - QSharedPointer(new OverlayModal(this, roomSettingsDialog_.data())); - - roomSettingsModal_->show(); -} - -void -MainWindow::openMemberListDialog(const QString &room_id) -{ - const auto roomToSearch = room_id.isEmpty() ? chat_page_->currentRoom() : ""; - - memberListDialog_ = - QSharedPointer(new dialogs::MemberList(roomToSearch, this)); - - memberListModal_ = - QSharedPointer(new OverlayModal(this, memberListDialog_.data())); - - memberListModal_->show(); -} - -void -MainWindow::openLeaveRoomDialog(const QString &room_id) -{ - auto roomToLeave = room_id.isEmpty() ? chat_page_->currentRoom() : room_id; - - leaveRoomDialog_ = QSharedPointer(new dialogs::LeaveRoom(this)); - - connect(leaveRoomDialog_.data(), - &dialogs::LeaveRoom::closing, - this, - [this, roomToLeave](bool leaving) { - leaveRoomModal_->hide(); - - if (leaving) - chat_page_->leaveRoom(roomToLeave); - }); - - leaveRoomModal_ = - QSharedPointer(new OverlayModal(this, leaveRoomDialog_.data())); - leaveRoomModal_->setColor(QColor(30, 30, 30, 170)); - - leaveRoomModal_->show(); -} - -void -MainWindow::showOverlayProgressBar() -{ - if (spinner_.isNull()) { - spinner_ = QSharedPointer( - new LoadingIndicator(this), - [](LoadingIndicator *indicator) { indicator->deleteLater(); }); - spinner_->setFixedHeight(100); - spinner_->setFixedWidth(100); - spinner_->setObjectName("ChatPageLoadSpinner"); - spinner_->start(); - } - - if (progressModal_.isNull()) { - progressModal_ = - QSharedPointer(new OverlayModal(this, spinner_.data()), - [](OverlayModal *modal) { modal->deleteLater(); }); - progressModal_->setColor(QColor(30, 30, 30)); - progressModal_->setDismissible(false); - progressModal_->show(); - } -} - -void -MainWindow::openInviteUsersDialog(std::function callback) -{ - if (inviteUsersDialog_.isNull()) { - inviteUsersDialog_ = - QSharedPointer(new dialogs::InviteUsers(this)); - - connect(inviteUsersDialog_.data(), - &dialogs::InviteUsers::closing, - this, - [this, callback](bool isSending, QStringList invitees) { - inviteUsersModal_->hide(); - - if (isSending && !invitees.isEmpty()) - callback(invitees); - }); - } - - if (inviteUsersModal_.isNull()) { - inviteUsersModal_ = QSharedPointer( - new OverlayModal(MainWindow::instance(), inviteUsersDialog_.data())); - inviteUsersModal_->setColor(QColor(30, 30, 30, 170)); - } - - inviteUsersModal_->show(); -} - -void -MainWindow::openJoinRoomDialog(std::function callback) -{ - if (joinRoomDialog_.isNull()) { - joinRoomDialog_ = QSharedPointer(new dialogs::JoinRoom(this)); - - connect(joinRoomDialog_.data(), - &dialogs::JoinRoom::closing, - this, - [this, callback](bool isJoining, const QString &room) { - joinRoomModal_->hide(); - - if (isJoining && !room.isEmpty()) - callback(room); - }); - } - - if (joinRoomModal_.isNull()) { - joinRoomModal_ = QSharedPointer( - new OverlayModal(MainWindow::instance(), joinRoomDialog_.data())); - } - - joinRoomModal_->show(); -} - -void -MainWindow::openCreateRoomDialog( - std::function callback) -{ - if (createRoomDialog_.isNull()) { - createRoomDialog_ = - QSharedPointer(new dialogs::CreateRoom(this)); - - connect( - createRoomDialog_.data(), - &dialogs::CreateRoom::closing, - this, - [this, callback](bool isCreating, const mtx::requests::CreateRoom &request) { - createRoomModal_->hide(); - - if (isCreating) - callback(request); - }); - } - - if (createRoomModal_.isNull()) { - createRoomModal_ = QSharedPointer( - new OverlayModal(MainWindow::instance(), createRoomDialog_.data())); - } - - createRoomModal_->show(); -} - -void -MainWindow::openLogoutDialog(std::function callback) -{ - if (logoutDialog_.isNull()) { - logoutDialog_ = QSharedPointer(new dialogs::Logout(this)); - connect(logoutDialog_.data(), - &dialogs::Logout::closing, - this, - [this, callback](bool logging_out) { - logoutModal_->hide(); - - if (logging_out) - callback(); - }); - } - - if (logoutModal_.isNull()) { - logoutModal_ = QSharedPointer( - new OverlayModal(MainWindow::instance(), logoutDialog_.data())); - } - - logoutModal_->show(); -} - -bool -MainWindow::hasActiveDialogs() const -{ - return (!leaveRoomModal_.isNull() && leaveRoomModal_->isVisible()) || - (!progressModal_.isNull() && progressModal_->isVisible()) || - (!inviteUsersModal_.isNull() && inviteUsersModal_->isVisible()) || - (!joinRoomModal_.isNull() && joinRoomModal_->isVisible()) || - (!createRoomModal_.isNull() && createRoomModal_->isVisible()) || - (!logoutModal_.isNull() && logoutModal_->isVisible()); -} - -bool -MainWindow::pageSupportsTray() const -{ - return !welcome_page_->isVisible() && !login_page_->isVisible() && - !register_page_->isVisible(); -} diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp new file mode 100644 index 00000000..fdca98c3 --- /dev/null +++ b/src/MainWindow.cpp @@ -0,0 +1,511 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include + +#include + +#include "ChatPage.h" +#include "Config.h" +#include "Logging.h" +#include "LoginPage.h" +#include "MainWindow.h" +#include "MatrixClient.h" +#include "RegisterPage.h" +#include "TrayIcon.h" +#include "UserSettingsPage.h" +#include "WelcomePage.h" +#include "ui/LoadingIndicator.h" +#include "ui/OverlayModal.h" +#include "ui/SnackBar.h" + +#include "dialogs/CreateRoom.h" +#include "dialogs/InviteUsers.h" +#include "dialogs/JoinRoom.h" +#include "dialogs/LeaveRoom.h" +#include "dialogs/Logout.h" +#include "dialogs/MemberList.h" +#include "dialogs/RoomSettings.h" + +MainWindow *MainWindow::instance_ = nullptr; + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , progressModal_{nullptr} + , spinner_{nullptr} +{ + setWindowTitle("nheko"); + setObjectName("MainWindow"); + + restoreWindowSize(); + + QFont font("Open Sans"); + font.setPixelSize(conf::fontSize); + font.setStyleStrategy(QFont::PreferAntialias); + setFont(font); + + userSettings_ = QSharedPointer(new UserSettings); + trayIcon_ = new TrayIcon(":/logos/nheko-32.png", this); + + welcome_page_ = new WelcomePage(this); + login_page_ = new LoginPage(this); + register_page_ = new RegisterPage(this); + chat_page_ = new ChatPage(userSettings_, this); + userSettingsPage_ = new UserSettingsPage(userSettings_, this); + + // Initialize sliding widget manager. + pageStack_ = new QStackedWidget(this); + pageStack_->addWidget(welcome_page_); + pageStack_->addWidget(login_page_); + pageStack_->addWidget(register_page_); + pageStack_->addWidget(chat_page_); + pageStack_->addWidget(userSettingsPage_); + + setCentralWidget(pageStack_); + + connect(welcome_page_, SIGNAL(userLogin()), this, SLOT(showLoginPage())); + connect(welcome_page_, SIGNAL(userRegister()), this, SLOT(showRegisterPage())); + + connect(login_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); + connect(login_page_, &LoginPage::loggingIn, this, &MainWindow::showOverlayProgressBar); + connect( + register_page_, &RegisterPage::registering, this, &MainWindow::showOverlayProgressBar); + connect( + login_page_, &LoginPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); }); + connect(register_page_, &RegisterPage::errorOccurred, this, [this]() { + removeOverlayProgressBar(); + }); + connect(register_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); + + connect(chat_page_, &ChatPage::closing, this, &MainWindow::showWelcomePage); + connect( + chat_page_, &ChatPage::showOverlayProgressBar, this, &MainWindow::showOverlayProgressBar); + connect( + chat_page_, SIGNAL(changeWindowTitle(QString)), this, SLOT(setWindowTitle(QString))); + connect(chat_page_, SIGNAL(unreadMessages(int)), trayIcon_, SLOT(setUnreadCount(int))); + connect(chat_page_, &ChatPage::showLoginPage, this, [this](const QString &msg) { + login_page_->loginError(msg); + showLoginPage(); + }); + + connect(userSettingsPage_, &UserSettingsPage::moveBack, this, [this]() { + pageStack_->setCurrentWidget(chat_page_); + }); + + connect( + userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool))); + + connect(trayIcon_, + SIGNAL(activated(QSystemTrayIcon::ActivationReason)), + this, + SLOT(iconActivated(QSystemTrayIcon::ActivationReason))); + + connect(chat_page_, SIGNAL(contentLoaded()), this, SLOT(removeOverlayProgressBar())); + connect( + chat_page_, &ChatPage::showUserSettingsPage, this, &MainWindow::showUserSettingsPage); + + connect(login_page_, &LoginPage::loginOk, this, [this](const mtx::responses::Login &res) { + http::client()->set_user(res.user_id); + showChatPage(); + }); + + connect(register_page_, &RegisterPage::registerOk, this, &MainWindow::showChatPage); + + QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this); + connect(quitShortcut, &QShortcut::activated, this, QApplication::quit); + + QShortcut *quickSwitchShortcut = new QShortcut(QKeySequence("Ctrl+K"), this); + connect(quickSwitchShortcut, &QShortcut::activated, this, [this]() { + if (chat_page_->isVisible() && !hasActiveDialogs()) + chat_page_->showQuickSwitcher(); + }); + + QSettings settings; + + trayIcon_->setVisible(userSettings_->isTrayEnabled()); + + if (hasActiveUser()) { + QString token = settings.value("auth/access_token").toString(); + QString home_server = settings.value("auth/home_server").toString(); + QString user_id = settings.value("auth/user_id").toString(); + QString device_id = settings.value("auth/device_id").toString(); + + http::client()->set_access_token(token.toStdString()); + http::client()->set_server(home_server.toStdString()); + http::client()->set_device_id(device_id.toStdString()); + + try { + using namespace mtx::identifiers; + http::client()->set_user(parse(user_id.toStdString())); + } catch (const std::invalid_argument &e) { + nhlog::ui()->critical("bootstrapped with invalid user_id: {}", + user_id.toStdString()); + } + + showChatPage(); + } +} + +void +MainWindow::showEvent(QShowEvent *event) +{ + adjustSideBars(); + QMainWindow::showEvent(event); +} + +void +MainWindow::resizeEvent(QResizeEvent *event) +{ + adjustSideBars(); + QMainWindow::resizeEvent(event); +} + +void +MainWindow::adjustSideBars() +{ + const int timelineWidth = chat_page_->timelineWidth(); + const int minAvailableWidth = + conf::sideBarCollapsePoint + ui::sidebar::CommunitiesSidebarSize; + + if (timelineWidth < minAvailableWidth && !chat_page_->isSideBarExpanded()) { + chat_page_->hideSideBars(); + } else { + chat_page_->showSideBars(); + } +} + +void +MainWindow::restoreWindowSize() +{ + QSettings settings; + int savedWidth = settings.value("window/width").toInt(); + int savedheight = settings.value("window/height").toInt(); + + if (savedWidth == 0 || savedheight == 0) + resize(conf::window::width, conf::window::height); + else + resize(savedWidth, savedheight); +} + +void +MainWindow::saveCurrentWindowSize() +{ + QSettings settings; + QSize current = size(); + + settings.setValue("window/width", current.width()); + settings.setValue("window/height", current.height()); +} + +void +MainWindow::removeOverlayProgressBar() +{ + QTimer *timer = new QTimer(this); + timer->setSingleShot(true); + + connect(timer, &QTimer::timeout, [this, timer]() { + timer->deleteLater(); + + if (!progressModal_.isNull()) + progressModal_->hide(); + + if (!spinner_.isNull()) + spinner_->stop(); + + progressModal_.reset(); + spinner_.reset(); + }); + + // FIXME: Snackbar doesn't work if it's initialized in the constructor. + QTimer::singleShot(0, this, [this]() { + snackBar_ = QSharedPointer(new SnackBar(this)); + connect(chat_page_, + &ChatPage::showNotification, + snackBar_.data(), + &SnackBar::showMessage); + }); + + timer->start(500); +} + +void +MainWindow::showChatPage() +{ + auto userid = QString::fromStdString(http::client()->user_id().to_string()); + auto device_id = QString::fromStdString(http::client()->device_id()); + auto homeserver = QString::fromStdString(http::client()->server() + ":" + + std::to_string(http::client()->port())); + auto token = QString::fromStdString(http::client()->access_token()); + + QSettings settings; + settings.setValue("auth/access_token", token); + settings.setValue("auth/home_server", homeserver); + settings.setValue("auth/user_id", userid); + settings.setValue("auth/device_id", device_id); + + showOverlayProgressBar(); + + pageStack_->setCurrentWidget(chat_page_); + + pageStack_->removeWidget(welcome_page_); + pageStack_->removeWidget(login_page_); + pageStack_->removeWidget(register_page_); + + login_page_->reset(); + chat_page_->bootstrap(userid, homeserver, token); + + instance_ = this; +} + +void +MainWindow::closeEvent(QCloseEvent *event) +{ + if (!qApp->isSavingSession() && isVisible() && pageSupportsTray() && + userSettings_->isTrayEnabled()) { + event->ignore(); + hide(); + } +} + +void +MainWindow::iconActivated(QSystemTrayIcon::ActivationReason reason) +{ + switch (reason) { + case QSystemTrayIcon::Trigger: + if (!isVisible()) { + show(); + } else { + hide(); + } + break; + default: + break; + } +} + +bool +MainWindow::hasActiveUser() +{ + QSettings settings; + + return settings.contains("auth/access_token") && settings.contains("auth/home_server") && + settings.contains("auth/user_id"); +} + +void +MainWindow::openRoomSettings(const QString &room_id) +{ + const auto roomToSearch = room_id.isEmpty() ? chat_page_->currentRoom() : ""; + + roomSettingsDialog_ = + QSharedPointer(new dialogs::RoomSettings(roomToSearch, this)); + + connect(roomSettingsDialog_.data(), &dialogs::RoomSettings::closing, this, [this]() { + roomSettingsModal_->hide(); + }); + + roomSettingsModal_ = + QSharedPointer(new OverlayModal(this, roomSettingsDialog_.data())); + + roomSettingsModal_->show(); +} + +void +MainWindow::openMemberListDialog(const QString &room_id) +{ + const auto roomToSearch = room_id.isEmpty() ? chat_page_->currentRoom() : ""; + + memberListDialog_ = + QSharedPointer(new dialogs::MemberList(roomToSearch, this)); + + memberListModal_ = + QSharedPointer(new OverlayModal(this, memberListDialog_.data())); + + memberListModal_->show(); +} + +void +MainWindow::openLeaveRoomDialog(const QString &room_id) +{ + auto roomToLeave = room_id.isEmpty() ? chat_page_->currentRoom() : room_id; + + leaveRoomDialog_ = QSharedPointer(new dialogs::LeaveRoom(this)); + + connect(leaveRoomDialog_.data(), + &dialogs::LeaveRoom::closing, + this, + [this, roomToLeave](bool leaving) { + leaveRoomModal_->hide(); + + if (leaving) + chat_page_->leaveRoom(roomToLeave); + }); + + leaveRoomModal_ = + QSharedPointer(new OverlayModal(this, leaveRoomDialog_.data())); + leaveRoomModal_->setColor(QColor(30, 30, 30, 170)); + + leaveRoomModal_->show(); +} + +void +MainWindow::showOverlayProgressBar() +{ + if (spinner_.isNull()) { + spinner_ = QSharedPointer( + new LoadingIndicator(this), + [](LoadingIndicator *indicator) { indicator->deleteLater(); }); + spinner_->setFixedHeight(100); + spinner_->setFixedWidth(100); + spinner_->setObjectName("ChatPageLoadSpinner"); + spinner_->start(); + } + + if (progressModal_.isNull()) { + progressModal_ = + QSharedPointer(new OverlayModal(this, spinner_.data()), + [](OverlayModal *modal) { modal->deleteLater(); }); + progressModal_->setColor(QColor(30, 30, 30)); + progressModal_->setDismissible(false); + progressModal_->show(); + } +} + +void +MainWindow::openInviteUsersDialog(std::function callback) +{ + if (inviteUsersDialog_.isNull()) { + inviteUsersDialog_ = + QSharedPointer(new dialogs::InviteUsers(this)); + + connect(inviteUsersDialog_.data(), + &dialogs::InviteUsers::closing, + this, + [this, callback](bool isSending, QStringList invitees) { + inviteUsersModal_->hide(); + + if (isSending && !invitees.isEmpty()) + callback(invitees); + }); + } + + if (inviteUsersModal_.isNull()) { + inviteUsersModal_ = QSharedPointer( + new OverlayModal(MainWindow::instance(), inviteUsersDialog_.data())); + inviteUsersModal_->setColor(QColor(30, 30, 30, 170)); + } + + inviteUsersModal_->show(); +} + +void +MainWindow::openJoinRoomDialog(std::function callback) +{ + if (joinRoomDialog_.isNull()) { + joinRoomDialog_ = QSharedPointer(new dialogs::JoinRoom(this)); + + connect(joinRoomDialog_.data(), + &dialogs::JoinRoom::closing, + this, + [this, callback](bool isJoining, const QString &room) { + joinRoomModal_->hide(); + + if (isJoining && !room.isEmpty()) + callback(room); + }); + } + + if (joinRoomModal_.isNull()) { + joinRoomModal_ = QSharedPointer( + new OverlayModal(MainWindow::instance(), joinRoomDialog_.data())); + } + + joinRoomModal_->show(); +} + +void +MainWindow::openCreateRoomDialog( + std::function callback) +{ + if (createRoomDialog_.isNull()) { + createRoomDialog_ = + QSharedPointer(new dialogs::CreateRoom(this)); + + connect( + createRoomDialog_.data(), + &dialogs::CreateRoom::closing, + this, + [this, callback](bool isCreating, const mtx::requests::CreateRoom &request) { + createRoomModal_->hide(); + + if (isCreating) + callback(request); + }); + } + + if (createRoomModal_.isNull()) { + createRoomModal_ = QSharedPointer( + new OverlayModal(MainWindow::instance(), createRoomDialog_.data())); + } + + createRoomModal_->show(); +} + +void +MainWindow::openLogoutDialog(std::function callback) +{ + if (logoutDialog_.isNull()) { + logoutDialog_ = QSharedPointer(new dialogs::Logout(this)); + connect(logoutDialog_.data(), + &dialogs::Logout::closing, + this, + [this, callback](bool logging_out) { + logoutModal_->hide(); + + if (logging_out) + callback(); + }); + } + + if (logoutModal_.isNull()) { + logoutModal_ = QSharedPointer( + new OverlayModal(MainWindow::instance(), logoutDialog_.data())); + } + + logoutModal_->show(); +} + +bool +MainWindow::hasActiveDialogs() const +{ + return (!leaveRoomModal_.isNull() && leaveRoomModal_->isVisible()) || + (!progressModal_.isNull() && progressModal_->isVisible()) || + (!inviteUsersModal_.isNull() && inviteUsersModal_->isVisible()) || + (!joinRoomModal_.isNull() && joinRoomModal_->isVisible()) || + (!createRoomModal_.isNull() && createRoomModal_->isVisible()) || + (!logoutModal_.isNull() && logoutModal_->isVisible()); +} + +bool +MainWindow::pageSupportsTray() const +{ + return !welcome_page_->isVisible() && !login_page_->isVisible() && + !register_page_->isVisible(); +} diff --git a/src/MainWindow.h b/src/MainWindow.h new file mode 100644 index 00000000..92040191 --- /dev/null +++ b/src/MainWindow.h @@ -0,0 +1,174 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include "LoginPage.h" +#include "RegisterPage.h" +#include "UserSettingsPage.h" +#include "WelcomePage.h" + +class ChatPage; +class LoadingIndicator; +class OverlayModal; +class SnackBar; +class TrayIcon; +class UserSettings; + +namespace mtx { +namespace requests { +struct CreateRoom; +} +} + +namespace dialogs { +class CreateRoom; +class InviteUsers; +class JoinRoom; +class LeaveRoom; +class Logout; +class MemberList; +class ReCaptcha; +class RoomSettings; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + + static MainWindow *instance() { return instance_; }; + void saveCurrentWindowSize(); + + void openLeaveRoomDialog(const QString &room_id = ""); + void openInviteUsersDialog(std::function callback); + void openCreateRoomDialog( + std::function callback); + void openJoinRoomDialog(std::function callback); + void openLogoutDialog(std::function callback); + void openRoomSettings(const QString &room_id = ""); + void openMemberListDialog(const QString &room_id = ""); + +protected: + void closeEvent(QCloseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void showEvent(QShowEvent *event) override; + +private slots: + //! Show or hide the sidebars based on window's size. + void adjustSideBars(); + //! Handle interaction with the tray icon. + void iconActivated(QSystemTrayIcon::ActivationReason reason); + + //! Show the welcome page in the main window. + void showWelcomePage() + { + removeOverlayProgressBar(); + pageStack_->addWidget(welcome_page_); + pageStack_->setCurrentWidget(welcome_page_); + } + + //! Show the login page in the main window. + void showLoginPage() + { + pageStack_->addWidget(login_page_); + pageStack_->setCurrentWidget(login_page_); + } + + //! Show the register page in the main window. + void showRegisterPage() + { + pageStack_->addWidget(register_page_); + pageStack_->setCurrentWidget(register_page_); + } + + //! Show user settings page. + void showUserSettingsPage() { pageStack_->setCurrentWidget(userSettingsPage_); } + + //! Show the chat page and start communicating with the given access token. + void showChatPage(); + + void showOverlayProgressBar(); + void removeOverlayProgressBar(); + +private: + bool hasActiveUser(); + void restoreWindowSize(); + //! Check if there is an open dialog. + bool hasActiveDialogs() const; + //! Check if the current page supports the "minimize to tray" functionality. + bool pageSupportsTray() const; + + static MainWindow *instance_; + + //! The initial welcome screen. + WelcomePage *welcome_page_; + //! The login screen. + LoginPage *login_page_; + //! The register page. + RegisterPage *register_page_; + //! A stacked widget that handles the transitions between widgets. + QStackedWidget *pageStack_; + //! The main chat area. + ChatPage *chat_page_; + UserSettingsPage *userSettingsPage_; + QSharedPointer userSettings_; + //! Used to hide undefined states between page transitions. + QSharedPointer progressModal_; + QSharedPointer spinner_; + //! Tray icon that shows the unread message count. + TrayIcon *trayIcon_; + //! Notifications display. + QSharedPointer snackBar_; + //! Leave room modal. + QSharedPointer leaveRoomModal_; + //! Leave room dialog. + QSharedPointer leaveRoomDialog_; + //! Invite users modal. + QSharedPointer inviteUsersModal_; + //! Invite users dialog. + QSharedPointer inviteUsersDialog_; + //! Join room modal. + QSharedPointer joinRoomModal_; + //! Join room dialog. + QSharedPointer joinRoomDialog_; + //! Create room modal. + QSharedPointer createRoomModal_; + //! Create room dialog. + QSharedPointer createRoomDialog_; + //! Logout modal. + QSharedPointer logoutModal_; + //! Logout dialog. + QSharedPointer logoutDialog_; + //! Room settings modal. + QSharedPointer roomSettingsModal_; + //! Room settings dialog. + QSharedPointer roomSettingsDialog_; + //! Member list modal. + QSharedPointer memberListModal_; + //! Member list dialog. + QSharedPointer memberListDialog_; +}; diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc deleted file mode 100644 index e41c66c1..00000000 --- a/src/MatrixClient.cc +++ /dev/null @@ -1,38 +0,0 @@ -#include "MatrixClient.h" - -#include - -namespace { -auto client_ = std::make_shared(); -} - -namespace http { - -mtx::http::Client * -client() -{ - return client_.get(); -} - -bool -is_logged_in() -{ - return !client_->access_token().empty(); -} - -void -init() -{ - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType>(); - qRegisterMetaType>(); -} - -} // namespace http diff --git a/src/MatrixClient.cpp b/src/MatrixClient.cpp new file mode 100644 index 00000000..e41c66c1 --- /dev/null +++ b/src/MatrixClient.cpp @@ -0,0 +1,38 @@ +#include "MatrixClient.h" + +#include + +namespace { +auto client_ = std::make_shared(); +} + +namespace http { + +mtx::http::Client * +client() +{ + return client_.get(); +} + +bool +is_logged_in() +{ + return !client_->access_token().empty(); +} + +void +init() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType>(); + qRegisterMetaType>(); +} + +} // namespace http diff --git a/src/MatrixClient.h b/src/MatrixClient.h new file mode 100644 index 00000000..12bba889 --- /dev/null +++ b/src/MatrixClient.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include +#include + +Q_DECLARE_METATYPE(mtx::responses::Login) +Q_DECLARE_METATYPE(mtx::responses::Messages) +Q_DECLARE_METATYPE(mtx::responses::Notifications) +Q_DECLARE_METATYPE(mtx::responses::Rooms) +Q_DECLARE_METATYPE(mtx::responses::Sync) +Q_DECLARE_METATYPE(mtx::responses::JoinedGroups) +Q_DECLARE_METATYPE(mtx::responses::GroupProfile) +Q_DECLARE_METATYPE(std::string) +Q_DECLARE_METATYPE(std::vector) +Q_DECLARE_METATYPE(std::vector) + +namespace http { +mtx::http::Client * +client(); + +bool +is_logged_in(); + +//! Initialize the http module +void +init(); +} diff --git a/src/Olm.cpp b/src/Olm.cpp index b3bb4316..fe4265d7 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -1,7 +1,7 @@ -#include "Olm.hpp" +#include "Olm.h" #include "Cache.h" -#include "Logging.hpp" +#include "Logging.h" #include "MatrixClient.h" using namespace mtx::crypto; diff --git a/src/Olm.h b/src/Olm.h new file mode 100644 index 00000000..ae4e0659 --- /dev/null +++ b/src/Olm.h @@ -0,0 +1,86 @@ +#pragma once + +#include + +#include +#include +#include + +constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2"; + +namespace olm { + +struct OlmMessage +{ + std::string sender_key; + std::string sender; + + using RecipientKey = std::string; + std::map ciphertext; +}; + +inline void +from_json(const nlohmann::json &obj, OlmMessage &msg) +{ + if (obj.at("type") != "m.room.encrypted") + throw std::invalid_argument("invalid type for olm message"); + + if (obj.at("content").at("algorithm") != OLM_ALGO) + throw std::invalid_argument("invalid algorithm for olm message"); + + msg.sender = obj.at("sender"); + msg.sender_key = obj.at("content").at("sender_key"); + msg.ciphertext = obj.at("content") + .at("ciphertext") + .get>(); +} + +mtx::crypto::OlmClient * +client(); + +void +handle_to_device_messages(const std::vector &msgs); + +nlohmann::json +try_olm_decryption(const std::string &sender_key, + const mtx::events::msg::OlmCipherContent &content); + +void +handle_olm_message(const OlmMessage &msg); + +//! Establish a new inbound megolm session with the decrypted payload from olm. +void +create_inbound_megolm_session(const std::string &sender, + const std::string &sender_key, + const nlohmann::json &payload); + +void +handle_pre_key_olm_message(const std::string &sender, + const std::string &sender_key, + const mtx::events::msg::OlmCipherContent &content); + +mtx::events::msg::Encrypted +encrypt_group_message(const std::string &room_id, + const std::string &device_id, + const std::string &body); + +void +mark_keys_as_published(); + +//! Request the encryption keys from sender's device for the given event. +void +request_keys(const std::string &room_id, const std::string &event_id); + +void +send_key_request_for(const std::string &room_id, + const mtx::events::EncryptedEvent &); + +void +handle_key_request_message(const mtx::events::msg::KeyRequest &); + +void +send_megolm_key_to_device(const std::string &user_id, + const std::string &device_id, + const json &payload); + +} // namespace olm diff --git a/src/QuickSwitcher.cc b/src/QuickSwitcher.cc deleted file mode 100644 index 3c9725d1..00000000 --- a/src/QuickSwitcher.cc +++ /dev/null @@ -1,138 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include -#include - -#include "QuickSwitcher.h" - -RoomSearchInput::RoomSearchInput(QWidget *parent) - : TextField(parent) -{} - -void -RoomSearchInput::keyPressEvent(QKeyEvent *event) -{ - switch (event->key()) { - case Qt::Key_Tab: - case Qt::Key_Down: { - emit selectNextCompletion(); - event->accept(); - break; - } - case Qt::Key_Backtab: - case Qt::Key_Up: { - emit selectPreviousCompletion(); - event->accept(); - break; - } - default: - TextField::keyPressEvent(event); - } -} - -void -RoomSearchInput::hideEvent(QHideEvent *event) -{ - emit hiding(); - TextField::hideEvent(event); -} - -QuickSwitcher::QuickSwitcher(QWidget *parent) - : QWidget(parent) -{ - qRegisterMetaType>(); - setMaximumWidth(450); - - QFont font; - font.setPixelSize(20); - - roomSearch_ = new RoomSearchInput(this); - roomSearch_->setFont(font); - roomSearch_->setPlaceholderText(tr("Search for a room...")); - - topLayout_ = new QVBoxLayout(this); - topLayout_->addWidget(roomSearch_); - - connect(this, - &QuickSwitcher::queryResults, - this, - [this](const std::vector &rooms) { - auto pos = mapToGlobal(roomSearch_->geometry().bottomLeft()); - - popup_.setFixedWidth(width()); - popup_.addRooms(rooms); - popup_.move(pos.x() - topLayout_->margin(), pos.y() + topLayout_->margin()); - popup_.show(); - }); - - connect(roomSearch_, &QLineEdit::textEdited, this, [this](const QString &query) { - if (query.isEmpty()) { - popup_.hide(); - return; - } - - QtConcurrent::run([this, query = query.toLower()]() { - try { - emit queryResults( - cache::client()->searchRooms(query.toStdString())); - } catch (const lmdb::error &e) { - qWarning() << "room search failed:" << e.what(); - } - }); - }); - - connect(roomSearch_, - &RoomSearchInput::selectNextCompletion, - &popup_, - &SuggestionsPopup::selectNextSuggestion); - connect(roomSearch_, - &RoomSearchInput::selectPreviousCompletion, - &popup_, - &SuggestionsPopup::selectPreviousSuggestion); - connect(&popup_, &SuggestionsPopup::itemSelected, this, [this](const QString &room_id) { - reset(); - emit roomSelected(room_id); - }); - connect(roomSearch_, &RoomSearchInput::hiding, this, [this]() { popup_.hide(); }); - connect(roomSearch_, &QLineEdit::returnPressed, this, [this]() { - reset(); - popup_.selectHoveredSuggestion(); - }); -} - -void -QuickSwitcher::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -QuickSwitcher::keyPressEvent(QKeyEvent *event) -{ - if (event->key() == Qt::Key_Escape) { - event->accept(); - reset(); - } -} diff --git a/src/QuickSwitcher.cpp b/src/QuickSwitcher.cpp new file mode 100644 index 00000000..07460efb --- /dev/null +++ b/src/QuickSwitcher.cpp @@ -0,0 +1,139 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +#include "QuickSwitcher.h" +#include "SuggestionsPopup.h" + +RoomSearchInput::RoomSearchInput(QWidget *parent) + : TextField(parent) +{} + +void +RoomSearchInput::keyPressEvent(QKeyEvent *event) +{ + switch (event->key()) { + case Qt::Key_Tab: + case Qt::Key_Down: { + emit selectNextCompletion(); + event->accept(); + break; + } + case Qt::Key_Backtab: + case Qt::Key_Up: { + emit selectPreviousCompletion(); + event->accept(); + break; + } + default: + TextField::keyPressEvent(event); + } +} + +void +RoomSearchInput::hideEvent(QHideEvent *event) +{ + emit hiding(); + TextField::hideEvent(event); +} + +QuickSwitcher::QuickSwitcher(QWidget *parent) + : QWidget(parent) +{ + qRegisterMetaType>(); + setMaximumWidth(450); + + QFont font; + font.setPixelSize(20); + + roomSearch_ = new RoomSearchInput(this); + roomSearch_->setFont(font); + roomSearch_->setPlaceholderText(tr("Search for a room...")); + + topLayout_ = new QVBoxLayout(this); + topLayout_->addWidget(roomSearch_); + + connect(this, + &QuickSwitcher::queryResults, + this, + [this](const std::vector &rooms) { + auto pos = mapToGlobal(roomSearch_->geometry().bottomLeft()); + + popup_.setFixedWidth(width()); + popup_.addRooms(rooms); + popup_.move(pos.x() - topLayout_->margin(), pos.y() + topLayout_->margin()); + popup_.show(); + }); + + connect(roomSearch_, &QLineEdit::textEdited, this, [this](const QString &query) { + if (query.isEmpty()) { + popup_.hide(); + return; + } + + QtConcurrent::run([this, query = query.toLower()]() { + try { + emit queryResults( + cache::client()->searchRooms(query.toStdString())); + } catch (const lmdb::error &e) { + qWarning() << "room search failed:" << e.what(); + } + }); + }); + + connect(roomSearch_, + &RoomSearchInput::selectNextCompletion, + &popup_, + &SuggestionsPopup::selectNextSuggestion); + connect(roomSearch_, + &RoomSearchInput::selectPreviousCompletion, + &popup_, + &SuggestionsPopup::selectPreviousSuggestion); + connect(&popup_, &SuggestionsPopup::itemSelected, this, [this](const QString &room_id) { + reset(); + emit roomSelected(room_id); + }); + connect(roomSearch_, &RoomSearchInput::hiding, this, [this]() { popup_.hide(); }); + connect(roomSearch_, &QLineEdit::returnPressed, this, [this]() { + reset(); + popup_.selectHoveredSuggestion(); + }); +} + +void +QuickSwitcher::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} + +void +QuickSwitcher::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Escape) { + event->accept(); + reset(); + } +} diff --git a/src/QuickSwitcher.h b/src/QuickSwitcher.h new file mode 100644 index 00000000..24b9adfa --- /dev/null +++ b/src/QuickSwitcher.h @@ -0,0 +1,79 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +#include "SuggestionsPopup.h" +#include "ui/TextField.h" + +Q_DECLARE_METATYPE(std::vector) + +class RoomSearchInput : public TextField +{ + Q_OBJECT +public: + explicit RoomSearchInput(QWidget *parent = nullptr); + +signals: + void selectNextCompletion(); + void selectPreviousCompletion(); + void hiding(); + +protected: + void keyPressEvent(QKeyEvent *event) override; + void hideEvent(QHideEvent *event) override; + bool focusNextPrevChild(bool) override { return false; }; +}; + +class QuickSwitcher : public QWidget +{ + Q_OBJECT + +public: + QuickSwitcher(QWidget *parent = nullptr); + +signals: + void closing(); + void roomSelected(const QString &roomid); + void queryResults(const std::vector &rooms); + +protected: + void keyPressEvent(QKeyEvent *event) override; + void showEvent(QShowEvent *) override { roomSearch_->setFocus(); } + void paintEvent(QPaintEvent *event) override; + +private: + void reset() + { + emit closing(); + roomSearch_->clear(); + } + + // Current highlighted selection from the completer. + int selection_ = -1; + + QVBoxLayout *topLayout_; + RoomSearchInput *roomSearch_; + + //! Autocomplete popup box with the room suggestions. + SuggestionsPopup popup_; +}; diff --git a/src/RegisterPage.cc b/src/RegisterPage.cc deleted file mode 100644 index 4894d122..00000000 --- a/src/RegisterPage.cc +++ /dev/null @@ -1,267 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include - -#include "Config.h" -#include "FlatButton.h" -#include "Logging.hpp" -#include "MainWindow.h" -#include "MatrixClient.h" -#include "RaisedButton.h" -#include "RegisterPage.h" -#include "TextField.h" - -#include "dialogs/ReCaptcha.hpp" - -RegisterPage::RegisterPage(QWidget *parent) - : QWidget(parent) -{ - top_layout_ = new QVBoxLayout(); - - back_layout_ = new QHBoxLayout(); - back_layout_->setSpacing(0); - back_layout_->setContentsMargins(5, 5, -1, -1); - - back_button_ = new FlatButton(this); - back_button_->setMinimumSize(QSize(30, 30)); - - QIcon icon; - icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); - - back_button_->setIcon(icon); - back_button_->setIconSize(QSize(32, 32)); - - back_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); - back_layout_->addStretch(1); - - QIcon logo; - logo.addFile(":/logos/register.png"); - - logo_ = new QLabel(this); - logo_->setPixmap(logo.pixmap(128)); - - logo_layout_ = new QHBoxLayout(); - logo_layout_->setMargin(0); - logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter); - - form_wrapper_ = new QHBoxLayout(); - form_widget_ = new QWidget(); - form_widget_->setMinimumSize(QSize(350, 300)); - - form_layout_ = new QVBoxLayout(); - form_layout_->setSpacing(20); - form_layout_->setContentsMargins(0, 0, 0, 40); - form_widget_->setLayout(form_layout_); - - form_wrapper_->addStretch(1); - form_wrapper_->addWidget(form_widget_); - form_wrapper_->addStretch(1); - - username_input_ = new TextField(); - username_input_->setLabel(tr("Username")); - - password_input_ = new TextField(); - password_input_->setLabel(tr("Password")); - password_input_->setEchoMode(QLineEdit::Password); - - password_confirmation_ = new TextField(); - password_confirmation_->setLabel(tr("Password confirmation")); - password_confirmation_->setEchoMode(QLineEdit::Password); - - server_input_ = new TextField(); - server_input_->setLabel(tr("Home Server")); - - form_layout_->addWidget(username_input_, Qt::AlignHCenter, 0); - form_layout_->addWidget(password_input_, Qt::AlignHCenter, 0); - form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter, 0); - form_layout_->addWidget(server_input_, Qt::AlignHCenter, 0); - - button_layout_ = new QHBoxLayout(); - button_layout_->setSpacing(0); - button_layout_->setMargin(0); - - QFont font; - font.setPixelSize(conf::fontSize); - - error_label_ = new QLabel(this); - error_label_->setFont(font); - - register_button_ = new RaisedButton(tr("REGISTER"), this); - register_button_->setMinimumSize(350, 65); - register_button_->setFontSize(conf::btn::fontSize); - register_button_->setCornerRadius(conf::btn::cornerRadius); - - button_layout_->addStretch(1); - button_layout_->addWidget(register_button_); - button_layout_->addStretch(1); - - top_layout_->addLayout(back_layout_); - top_layout_->addLayout(logo_layout_); - top_layout_->addLayout(form_wrapper_); - top_layout_->addStretch(1); - top_layout_->addLayout(button_layout_); - top_layout_->addStretch(1); - top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); - - connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); - connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked())); - - connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); - connect(this, &RegisterPage::registerErrorCb, this, &RegisterPage::registerError); - connect( - this, - &RegisterPage::registrationFlow, - this, - [this](const std::string &user, const std::string &pass, const std::string &session) { - emit errorOccurred(); - - if (!captchaDialog_) { - captchaDialog_ = std::make_shared( - QString::fromStdString(session), this); - connect( - captchaDialog_.get(), - &dialogs::ReCaptcha::closing, - this, - [this, user, pass, session]() { - captchaDialog_->close(); - emit registering(); - - http::client()->flow_response( - user, - pass, - session, - "m.login.recaptcha", - [this](const mtx::responses::Register &res, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to retrieve registration flows: {}", - err->matrix_error.error); - emit errorOccurred(); - emit registerErrorCb(QString::fromStdString( - err->matrix_error.error)); - return; - } - - http::client()->set_user(res.user_id); - http::client()->set_access_token(res.access_token); - - emit registerOk(); - }); - }); - } - - QTimer::singleShot(1000, this, [this]() { captchaDialog_->show(); }); - }); - - setLayout(top_layout_); -} - -void -RegisterPage::onBackButtonClicked() -{ - emit backButtonClicked(); -} - -void -RegisterPage::registerError(const QString &msg) -{ - emit errorOccurred(); - error_label_->setText(msg); -} - -void -RegisterPage::onRegisterButtonClicked() -{ - error_label_->setText(""); - - if (!username_input_->hasAcceptableInput()) { - registerError(tr("Invalid username")); - } else if (!password_input_->hasAcceptableInput()) { - registerError(tr("Password is not long enough (min 8 chars)")); - } else if (password_input_->text() != password_confirmation_->text()) { - registerError(tr("Passwords don't match")); - } else if (!server_input_->hasAcceptableInput()) { - registerError(tr("Invalid server name")); - } else { - auto username = username_input_->text().toStdString(); - auto password = password_input_->text().toStdString(); - auto server = server_input_->text().toStdString(); - - http::client()->set_server(server); - http::client()->registration( - username, - password, - [this, username, password](const mtx::responses::Register &res, - mtx::http::RequestErr err) { - if (!err) { - http::client()->set_user(res.user_id); - http::client()->set_access_token(res.access_token); - - emit registerOk(); - return; - } - - // The server requires registration flows. - if (err->status_code == boost::beast::http::status::unauthorized) { - http::client()->flow_register( - username, - password, - [this, username, password]( - const mtx::responses::RegistrationFlows &res, - mtx::http::RequestErr err) { - if (res.session.empty() && err) { - nhlog::net()->warn( - "failed to retrieve registration flows: ({}) " - "{}", - static_cast(err->status_code), - err->matrix_error.error); - emit errorOccurred(); - emit registerErrorCb(QString::fromStdString( - err->matrix_error.error)); - return; - } - - emit registrationFlow(username, password, res.session); - }); - return; - } - - nhlog::net()->warn("failed to register: status_code ({})", - static_cast(err->status_code)); - - emit registerErrorCb(QString::fromStdString(err->matrix_error.error)); - emit errorOccurred(); - }); - - emit registering(); - } -} - -void -RegisterPage::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp new file mode 100644 index 00000000..5a02713a --- /dev/null +++ b/src/RegisterPage.cpp @@ -0,0 +1,267 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "Config.h" +#include "Logging.h" +#include "MainWindow.h" +#include "MatrixClient.h" +#include "RegisterPage.h" +#include "ui/FlatButton.h" +#include "ui/RaisedButton.h" +#include "ui/TextField.h" + +#include "dialogs/ReCaptcha.h" + +RegisterPage::RegisterPage(QWidget *parent) + : QWidget(parent) +{ + top_layout_ = new QVBoxLayout(); + + back_layout_ = new QHBoxLayout(); + back_layout_->setSpacing(0); + back_layout_->setContentsMargins(5, 5, -1, -1); + + back_button_ = new FlatButton(this); + back_button_->setMinimumSize(QSize(30, 30)); + + QIcon icon; + icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); + + back_button_->setIcon(icon); + back_button_->setIconSize(QSize(32, 32)); + + back_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); + back_layout_->addStretch(1); + + QIcon logo; + logo.addFile(":/logos/register.png"); + + logo_ = new QLabel(this); + logo_->setPixmap(logo.pixmap(128)); + + logo_layout_ = new QHBoxLayout(); + logo_layout_->setMargin(0); + logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter); + + form_wrapper_ = new QHBoxLayout(); + form_widget_ = new QWidget(); + form_widget_->setMinimumSize(QSize(350, 300)); + + form_layout_ = new QVBoxLayout(); + form_layout_->setSpacing(20); + form_layout_->setContentsMargins(0, 0, 0, 40); + form_widget_->setLayout(form_layout_); + + form_wrapper_->addStretch(1); + form_wrapper_->addWidget(form_widget_); + form_wrapper_->addStretch(1); + + username_input_ = new TextField(); + username_input_->setLabel(tr("Username")); + + password_input_ = new TextField(); + password_input_->setLabel(tr("Password")); + password_input_->setEchoMode(QLineEdit::Password); + + password_confirmation_ = new TextField(); + password_confirmation_->setLabel(tr("Password confirmation")); + password_confirmation_->setEchoMode(QLineEdit::Password); + + server_input_ = new TextField(); + server_input_->setLabel(tr("Home Server")); + + form_layout_->addWidget(username_input_, Qt::AlignHCenter, 0); + form_layout_->addWidget(password_input_, Qt::AlignHCenter, 0); + form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter, 0); + form_layout_->addWidget(server_input_, Qt::AlignHCenter, 0); + + button_layout_ = new QHBoxLayout(); + button_layout_->setSpacing(0); + button_layout_->setMargin(0); + + QFont font; + font.setPixelSize(conf::fontSize); + + error_label_ = new QLabel(this); + error_label_->setFont(font); + + register_button_ = new RaisedButton(tr("REGISTER"), this); + register_button_->setMinimumSize(350, 65); + register_button_->setFontSize(conf::btn::fontSize); + register_button_->setCornerRadius(conf::btn::cornerRadius); + + button_layout_->addStretch(1); + button_layout_->addWidget(register_button_); + button_layout_->addStretch(1); + + top_layout_->addLayout(back_layout_); + top_layout_->addLayout(logo_layout_); + top_layout_->addLayout(form_wrapper_); + top_layout_->addStretch(1); + top_layout_->addLayout(button_layout_); + top_layout_->addStretch(1); + top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); + + connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); + connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked())); + + connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click())); + connect(this, &RegisterPage::registerErrorCb, this, &RegisterPage::registerError); + connect( + this, + &RegisterPage::registrationFlow, + this, + [this](const std::string &user, const std::string &pass, const std::string &session) { + emit errorOccurred(); + + if (!captchaDialog_) { + captchaDialog_ = std::make_shared( + QString::fromStdString(session), this); + connect( + captchaDialog_.get(), + &dialogs::ReCaptcha::closing, + this, + [this, user, pass, session]() { + captchaDialog_->close(); + emit registering(); + + http::client()->flow_response( + user, + pass, + session, + "m.login.recaptcha", + [this](const mtx::responses::Register &res, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to retrieve registration flows: {}", + err->matrix_error.error); + emit errorOccurred(); + emit registerErrorCb(QString::fromStdString( + err->matrix_error.error)); + return; + } + + http::client()->set_user(res.user_id); + http::client()->set_access_token(res.access_token); + + emit registerOk(); + }); + }); + } + + QTimer::singleShot(1000, this, [this]() { captchaDialog_->show(); }); + }); + + setLayout(top_layout_); +} + +void +RegisterPage::onBackButtonClicked() +{ + emit backButtonClicked(); +} + +void +RegisterPage::registerError(const QString &msg) +{ + emit errorOccurred(); + error_label_->setText(msg); +} + +void +RegisterPage::onRegisterButtonClicked() +{ + error_label_->setText(""); + + if (!username_input_->hasAcceptableInput()) { + registerError(tr("Invalid username")); + } else if (!password_input_->hasAcceptableInput()) { + registerError(tr("Password is not long enough (min 8 chars)")); + } else if (password_input_->text() != password_confirmation_->text()) { + registerError(tr("Passwords don't match")); + } else if (!server_input_->hasAcceptableInput()) { + registerError(tr("Invalid server name")); + } else { + auto username = username_input_->text().toStdString(); + auto password = password_input_->text().toStdString(); + auto server = server_input_->text().toStdString(); + + http::client()->set_server(server); + http::client()->registration( + username, + password, + [this, username, password](const mtx::responses::Register &res, + mtx::http::RequestErr err) { + if (!err) { + http::client()->set_user(res.user_id); + http::client()->set_access_token(res.access_token); + + emit registerOk(); + return; + } + + // The server requires registration flows. + if (err->status_code == boost::beast::http::status::unauthorized) { + http::client()->flow_register( + username, + password, + [this, username, password]( + const mtx::responses::RegistrationFlows &res, + mtx::http::RequestErr err) { + if (res.session.empty() && err) { + nhlog::net()->warn( + "failed to retrieve registration flows: ({}) " + "{}", + static_cast(err->status_code), + err->matrix_error.error); + emit errorOccurred(); + emit registerErrorCb(QString::fromStdString( + err->matrix_error.error)); + return; + } + + emit registrationFlow(username, password, res.session); + }); + return; + } + + nhlog::net()->warn("failed to register: status_code ({})", + static_cast(err->status_code)); + + emit registerErrorCb(QString::fromStdString(err->matrix_error.error)); + emit errorOccurred(); + }); + + emit registering(); + } +} + +void +RegisterPage::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} diff --git a/src/RegisterPage.h b/src/RegisterPage.h new file mode 100644 index 00000000..d02de7c4 --- /dev/null +++ b/src/RegisterPage.h @@ -0,0 +1,84 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +class FlatButton; +class RaisedButton; +class TextField; + +namespace dialogs { +class ReCaptcha; +} + +class RegisterPage : public QWidget +{ + Q_OBJECT + +public: + RegisterPage(QWidget *parent = 0); + +protected: + void paintEvent(QPaintEvent *event) override; + +signals: + void backButtonClicked(); + void errorOccurred(); + void registering(); + void registerOk(); + void registerErrorCb(const QString &msg); + void registrationFlow(const std::string &user, + const std::string &pass, + const std::string &session); + +private slots: + void onBackButtonClicked(); + void onRegisterButtonClicked(); + + // Display registration specific errors to the user. + void registerError(const QString &msg); + +private: + QVBoxLayout *top_layout_; + + QHBoxLayout *back_layout_; + QHBoxLayout *logo_layout_; + QHBoxLayout *button_layout_; + + QLabel *logo_; + QLabel *error_label_; + + FlatButton *back_button_; + RaisedButton *register_button_; + + QWidget *form_widget_; + QHBoxLayout *form_wrapper_; + QVBoxLayout *form_layout_; + + TextField *username_input_; + TextField *password_input_; + TextField *password_confirmation_; + TextField *server_input_; + + //! ReCaptcha dialog. + std::shared_ptr captchaDialog_; +}; diff --git a/src/RoomInfoListItem.cc b/src/RoomInfoListItem.cc deleted file mode 100644 index 7027115f..00000000 --- a/src/RoomInfoListItem.cc +++ /dev/null @@ -1,390 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include - -#include - -#include "Cache.h" -#include "Config.h" -#include "Menu.h" -#include "Ripple.h" -#include "RippleOverlay.h" -#include "RoomInfoListItem.h" -#include "Theme.h" -#include "Utils.h" - -constexpr int MaxUnreadCountDisplayed = 99; - -constexpr int Padding = 9; -constexpr int IconSize = 44; -constexpr int MaxHeight = IconSize + 2 * Padding; - -constexpr int InviteBtnX = IconSize + 2 * Padding; -constexpr int InviteBtnY = IconSize / 2 + Padding + Padding / 3; - -void -RoomInfoListItem::init(QWidget *parent) -{ - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - setMouseTracking(true); - setAttribute(Qt::WA_Hover); - - setFixedHeight(MaxHeight); - - QPainterPath path; - path.addRect(0, 0, parent->width(), height()); - - ripple_overlay_ = new RippleOverlay(this); - ripple_overlay_->setClipPath(path); - ripple_overlay_->setClipping(true); - - font_.setPixelSize(conf::fontSize - 1); - - usernameFont_ = font_; - - bubbleFont_ = font_; - bubbleFont_.setPixelSize(conf::roomlist::fonts::bubble); - - unreadCountFont_.setPixelSize(conf::roomlist::fonts::badge); - unreadCountFont_.setBold(true); - bubbleDiameter_ = QFontMetrics(unreadCountFont_).averageCharWidth() * 3; - - timestampFont_ = font_; - timestampFont_.setPixelSize(conf::roomlist::fonts::timestamp); - timestampFont_.setBold(false); - - headingFont_ = font_; - headingFont_.setPixelSize(conf::roomlist::fonts::heading); - headingFont_.setWeight(60); - - menu_ = new Menu(this); - leaveRoom_ = new QAction(tr("Leave room"), this); - connect(leaveRoom_, &QAction::triggered, this, [this]() { emit leaveRoom(roomId_); }); - menu_->addAction(leaveRoom_); -} - -RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *parent) - : QWidget(parent) - , roomType_{info.is_invite ? RoomType::Invited : RoomType::Joined} - , roomId_(std::move(room_id)) - , roomName_{QString::fromStdString(std::move(info.name))} - , isPressed_(false) - , unreadMsgCount_(0) -{ - init(parent); - - // HACK - // We use fake message info with an old date to pin - // the invite events to the top. - // - // State events in invited rooms don't contain timestamp info, - // so we can't use them for sorting. - if (roomType_ == RoomType::Invited) - lastMsgInfo_ = {"-", "-", "-", "-", QDateTime::currentDateTime().addYears(10)}; -} - -void -RoomInfoListItem::resizeEvent(QResizeEvent *) -{ - // Update ripple's clipping path. - QPainterPath path; - path.addRect(0, 0, width(), height()); - - if (width() > ui::sidebar::SmallSize) - setToolTip(""); - else - setToolTip(roomName_); - - ripple_overlay_->setClipPath(path); - ripple_overlay_->setClipping(true); -} - -void -RoomInfoListItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter p(this); - p.setRenderHint(QPainter::TextAntialiasing); - p.setRenderHint(QPainter::SmoothPixmapTransform); - p.setRenderHint(QPainter::Antialiasing); - - QFontMetrics metrics(font_); - - QPen titlePen(titleColor_); - QPen subtitlePen(subtitleColor_); - - if (isPressed_) { - p.fillRect(rect(), highlightedBackgroundColor_); - titlePen.setColor(highlightedTitleColor_); - subtitlePen.setColor(highlightedSubtitleColor_); - } else if (underMouse()) { - p.fillRect(rect(), hoverBackgroundColor_); - } else { - p.fillRect(rect(), backgroundColor_); - } - - QRect avatarRegion(Padding, Padding, IconSize, IconSize); - - // Description line with the default font. - int bottom_y = MaxHeight - Padding - metrics.ascent() / 2; - - if (width() > ui::sidebar::SmallSize) { - p.setFont(headingFont_); - p.setPen(titlePen); - - const int msgStampWidth = - QFontMetrics(timestampFont_).width(lastMsgInfo_.timestamp) + 4; - - // We use the full width of the widget if there is no unread msg bubble. - const int bottomLineWidthLimit = (unreadMsgCount_ > 0) ? msgStampWidth : 0; - - // Name line. - QFontMetrics fontNameMetrics(headingFont_); - int top_y = 2 * Padding + fontNameMetrics.ascent() / 2; - - const auto name = - metrics.elidedText(roomName(), - Qt::ElideRight, - (width() - IconSize - 2 * Padding - msgStampWidth) * 0.8); - p.drawText(QPoint(2 * Padding + IconSize, top_y), name); - - if (roomType_ == RoomType::Joined) { - p.setFont(font_); - p.setPen(subtitlePen); - - // The limit is the space between the end of the avatar and the start of the - // timestamp. - int usernameLimit = - std::max(0, width() - 3 * Padding - msgStampWidth - IconSize - 20); - auto userName = - metrics.elidedText(lastMsgInfo_.username, Qt::ElideRight, usernameLimit); - - p.setFont(usernameFont_); - p.drawText(QPoint(2 * Padding + IconSize, bottom_y), userName); - - int nameWidth = QFontMetrics(usernameFont_).width(userName); - - p.setFont(font_); - - // The limit is the space between the end of the username and the start of - // the timestamp. - int descriptionLimit = std::max( - 0, - width() - 3 * Padding - bottomLineWidthLimit - IconSize - nameWidth - 5); - auto description = - metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit); - p.drawText(QPoint(2 * Padding + IconSize + nameWidth, bottom_y), - description); - - // We show the last message timestamp. - p.save(); - if (isPressed_) - p.setPen(QPen(highlightedTimestampColor_)); - else - p.setPen(QPen(timestampColor_)); - - p.setFont(timestampFont_); - p.drawText(QPoint(width() - Padding - msgStampWidth, top_y), - lastMsgInfo_.timestamp); - p.restore(); - } else { - int btnWidth = (width() - IconSize - 6 * Padding) / 2; - - acceptBtnRegion_ = QRectF(InviteBtnX, InviteBtnY, btnWidth, 20); - declineBtnRegion_ = - QRectF(InviteBtnX + btnWidth + 2 * Padding, InviteBtnY, btnWidth, 20); - - QPainterPath acceptPath; - acceptPath.addRoundedRect(acceptBtnRegion_, 10, 10); - - p.setPen(Qt::NoPen); - p.fillPath(acceptPath, btnColor_); - p.drawPath(acceptPath); - - QPainterPath declinePath; - declinePath.addRoundedRect(declineBtnRegion_, 10, 10); - - p.setPen(Qt::NoPen); - p.fillPath(declinePath, btnColor_); - p.drawPath(declinePath); - - p.setPen(QPen(btnTextColor_)); - p.setFont(font_); - p.drawText(acceptBtnRegion_, Qt::AlignCenter, tr("Accept")); - p.drawText(declineBtnRegion_, Qt::AlignCenter, tr("Decline")); - } - } - - p.setPen(Qt::NoPen); - - // We using the first letter of room's name. - if (roomAvatar_.isNull()) { - QBrush brush; - brush.setStyle(Qt::SolidPattern); - brush.setColor(avatarBgColor()); - - p.setPen(Qt::NoPen); - p.setBrush(brush); - - p.drawEllipse(avatarRegion.center(), IconSize / 2, IconSize / 2); - - p.setFont(bubbleFont_); - p.setPen(avatarFgColor()); - p.setBrush(Qt::NoBrush); - p.drawText( - avatarRegion.translated(0, -1), Qt::AlignCenter, utils::firstChar(roomName())); - } else { - p.save(); - - QPainterPath path; - path.addEllipse(Padding, Padding, IconSize, IconSize); - p.setClipPath(path); - - p.drawPixmap(avatarRegion, roomAvatar_); - p.restore(); - } - - if (unreadMsgCount_ > 0) { - QBrush brush; - brush.setStyle(Qt::SolidPattern); - brush.setColor(bubbleBgColor()); - - if (isPressed_) - brush.setColor(bubbleFgColor()); - - p.setBrush(brush); - p.setPen(Qt::NoPen); - p.setFont(unreadCountFont_); - - // Extra space on the x-axis to accomodate the extra character space - // inside the bubble. - const int x_width = unreadMsgCount_ > MaxUnreadCountDisplayed - ? QFontMetrics(p.font()).averageCharWidth() - : 0; - - QRectF r(width() - bubbleDiameter_ - Padding - x_width, - bottom_y - bubbleDiameter_ / 2 - 5, - bubbleDiameter_ + x_width, - bubbleDiameter_); - - if (width() == ui::sidebar::SmallSize) - r = QRectF(width() - bubbleDiameter_ - 5, - height() - bubbleDiameter_ - 5, - bubbleDiameter_ + x_width, - bubbleDiameter_); - - p.setPen(Qt::NoPen); - p.drawEllipse(r); - - p.setPen(QPen(bubbleFgColor())); - - if (isPressed_) - p.setPen(QPen(bubbleBgColor())); - - auto countTxt = unreadMsgCount_ > MaxUnreadCountDisplayed - ? QString("99+") - : QString::number(unreadMsgCount_); - - p.setBrush(Qt::NoBrush); - p.drawText(r.translated(0, -0.5), Qt::AlignCenter, countTxt); - } -} - -void -RoomInfoListItem::updateUnreadMessageCount(int count) -{ - unreadMsgCount_ = count; - update(); -} - -void -RoomInfoListItem::setPressedState(bool state) -{ - if (isPressed_ != state) { - isPressed_ = state; - update(); - } -} - -void -RoomInfoListItem::contextMenuEvent(QContextMenuEvent *event) -{ - Q_UNUSED(event); - - if (roomType_ == RoomType::Invited) - return; - - menu_->popup(event->globalPos()); -} - -void -RoomInfoListItem::mousePressEvent(QMouseEvent *event) -{ - if (event->buttons() == Qt::RightButton) { - QWidget::mousePressEvent(event); - return; - } - - if (roomType_ == RoomType::Invited) { - const auto point = event->pos(); - - if (acceptBtnRegion_.contains(point)) - emit acceptInvite(roomId_); - - if (declineBtnRegion_.contains(point)) - emit declineInvite(roomId_); - - return; - } - - emit clicked(roomId_); - - setPressedState(true); - - // Ripple on mouse position by default. - QPoint pos = event->pos(); - qreal radiusEndValue = static_cast(width()) / 3; - - Ripple *ripple = new Ripple(pos); - - ripple->setRadiusEndValue(radiusEndValue); - ripple->setOpacityStartValue(0.15); - ripple->setColor(QColor("white")); - ripple->radiusAnimation()->setDuration(200); - ripple->opacityAnimation()->setDuration(400); - - ripple_overlay_->addRipple(ripple); -} - -void -RoomInfoListItem::setAvatar(const QImage &img) -{ - roomAvatar_ = utils::scaleImageToPixmap(img, IconSize); - update(); -} - -void -RoomInfoListItem::setDescriptionMessage(const DescInfo &info) -{ - lastMsgInfo_ = info; - update(); -} diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp new file mode 100644 index 00000000..172cdb90 --- /dev/null +++ b/src/RoomInfoListItem.cpp @@ -0,0 +1,390 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include + +#include + +#include "Cache.h" +#include "Config.h" +#include "RoomInfoListItem.h" +#include "Utils.h" +#include "ui/Menu.h" +#include "ui/Ripple.h" +#include "ui/RippleOverlay.h" +#include "ui/Theme.h" + +constexpr int MaxUnreadCountDisplayed = 99; + +constexpr int Padding = 9; +constexpr int IconSize = 44; +constexpr int MaxHeight = IconSize + 2 * Padding; + +constexpr int InviteBtnX = IconSize + 2 * Padding; +constexpr int InviteBtnY = IconSize / 2 + Padding + Padding / 3; + +void +RoomInfoListItem::init(QWidget *parent) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setMouseTracking(true); + setAttribute(Qt::WA_Hover); + + setFixedHeight(MaxHeight); + + QPainterPath path; + path.addRect(0, 0, parent->width(), height()); + + ripple_overlay_ = new RippleOverlay(this); + ripple_overlay_->setClipPath(path); + ripple_overlay_->setClipping(true); + + font_.setPixelSize(conf::fontSize - 1); + + usernameFont_ = font_; + + bubbleFont_ = font_; + bubbleFont_.setPixelSize(conf::roomlist::fonts::bubble); + + unreadCountFont_.setPixelSize(conf::roomlist::fonts::badge); + unreadCountFont_.setBold(true); + bubbleDiameter_ = QFontMetrics(unreadCountFont_).averageCharWidth() * 3; + + timestampFont_ = font_; + timestampFont_.setPixelSize(conf::roomlist::fonts::timestamp); + timestampFont_.setBold(false); + + headingFont_ = font_; + headingFont_.setPixelSize(conf::roomlist::fonts::heading); + headingFont_.setWeight(60); + + menu_ = new Menu(this); + leaveRoom_ = new QAction(tr("Leave room"), this); + connect(leaveRoom_, &QAction::triggered, this, [this]() { emit leaveRoom(roomId_); }); + menu_->addAction(leaveRoom_); +} + +RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *parent) + : QWidget(parent) + , roomType_{info.is_invite ? RoomType::Invited : RoomType::Joined} + , roomId_(std::move(room_id)) + , roomName_{QString::fromStdString(std::move(info.name))} + , isPressed_(false) + , unreadMsgCount_(0) +{ + init(parent); + + // HACK + // We use fake message info with an old date to pin + // the invite events to the top. + // + // State events in invited rooms don't contain timestamp info, + // so we can't use them for sorting. + if (roomType_ == RoomType::Invited) + lastMsgInfo_ = {"-", "-", "-", "-", QDateTime::currentDateTime().addYears(10)}; +} + +void +RoomInfoListItem::resizeEvent(QResizeEvent *) +{ + // Update ripple's clipping path. + QPainterPath path; + path.addRect(0, 0, width(), height()); + + if (width() > ui::sidebar::SmallSize) + setToolTip(""); + else + setToolTip(roomName_); + + ripple_overlay_->setClipPath(path); + ripple_overlay_->setClipping(true); +} + +void +RoomInfoListItem::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + + QPainter p(this); + p.setRenderHint(QPainter::TextAntialiasing); + p.setRenderHint(QPainter::SmoothPixmapTransform); + p.setRenderHint(QPainter::Antialiasing); + + QFontMetrics metrics(font_); + + QPen titlePen(titleColor_); + QPen subtitlePen(subtitleColor_); + + if (isPressed_) { + p.fillRect(rect(), highlightedBackgroundColor_); + titlePen.setColor(highlightedTitleColor_); + subtitlePen.setColor(highlightedSubtitleColor_); + } else if (underMouse()) { + p.fillRect(rect(), hoverBackgroundColor_); + } else { + p.fillRect(rect(), backgroundColor_); + } + + QRect avatarRegion(Padding, Padding, IconSize, IconSize); + + // Description line with the default font. + int bottom_y = MaxHeight - Padding - metrics.ascent() / 2; + + if (width() > ui::sidebar::SmallSize) { + p.setFont(headingFont_); + p.setPen(titlePen); + + const int msgStampWidth = + QFontMetrics(timestampFont_).width(lastMsgInfo_.timestamp) + 4; + + // We use the full width of the widget if there is no unread msg bubble. + const int bottomLineWidthLimit = (unreadMsgCount_ > 0) ? msgStampWidth : 0; + + // Name line. + QFontMetrics fontNameMetrics(headingFont_); + int top_y = 2 * Padding + fontNameMetrics.ascent() / 2; + + const auto name = + metrics.elidedText(roomName(), + Qt::ElideRight, + (width() - IconSize - 2 * Padding - msgStampWidth) * 0.8); + p.drawText(QPoint(2 * Padding + IconSize, top_y), name); + + if (roomType_ == RoomType::Joined) { + p.setFont(font_); + p.setPen(subtitlePen); + + // The limit is the space between the end of the avatar and the start of the + // timestamp. + int usernameLimit = + std::max(0, width() - 3 * Padding - msgStampWidth - IconSize - 20); + auto userName = + metrics.elidedText(lastMsgInfo_.username, Qt::ElideRight, usernameLimit); + + p.setFont(usernameFont_); + p.drawText(QPoint(2 * Padding + IconSize, bottom_y), userName); + + int nameWidth = QFontMetrics(usernameFont_).width(userName); + + p.setFont(font_); + + // The limit is the space between the end of the username and the start of + // the timestamp. + int descriptionLimit = std::max( + 0, + width() - 3 * Padding - bottomLineWidthLimit - IconSize - nameWidth - 5); + auto description = + metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit); + p.drawText(QPoint(2 * Padding + IconSize + nameWidth, bottom_y), + description); + + // We show the last message timestamp. + p.save(); + if (isPressed_) + p.setPen(QPen(highlightedTimestampColor_)); + else + p.setPen(QPen(timestampColor_)); + + p.setFont(timestampFont_); + p.drawText(QPoint(width() - Padding - msgStampWidth, top_y), + lastMsgInfo_.timestamp); + p.restore(); + } else { + int btnWidth = (width() - IconSize - 6 * Padding) / 2; + + acceptBtnRegion_ = QRectF(InviteBtnX, InviteBtnY, btnWidth, 20); + declineBtnRegion_ = + QRectF(InviteBtnX + btnWidth + 2 * Padding, InviteBtnY, btnWidth, 20); + + QPainterPath acceptPath; + acceptPath.addRoundedRect(acceptBtnRegion_, 10, 10); + + p.setPen(Qt::NoPen); + p.fillPath(acceptPath, btnColor_); + p.drawPath(acceptPath); + + QPainterPath declinePath; + declinePath.addRoundedRect(declineBtnRegion_, 10, 10); + + p.setPen(Qt::NoPen); + p.fillPath(declinePath, btnColor_); + p.drawPath(declinePath); + + p.setPen(QPen(btnTextColor_)); + p.setFont(font_); + p.drawText(acceptBtnRegion_, Qt::AlignCenter, tr("Accept")); + p.drawText(declineBtnRegion_, Qt::AlignCenter, tr("Decline")); + } + } + + p.setPen(Qt::NoPen); + + // We using the first letter of room's name. + if (roomAvatar_.isNull()) { + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(avatarBgColor()); + + p.setPen(Qt::NoPen); + p.setBrush(brush); + + p.drawEllipse(avatarRegion.center(), IconSize / 2, IconSize / 2); + + p.setFont(bubbleFont_); + p.setPen(avatarFgColor()); + p.setBrush(Qt::NoBrush); + p.drawText( + avatarRegion.translated(0, -1), Qt::AlignCenter, utils::firstChar(roomName())); + } else { + p.save(); + + QPainterPath path; + path.addEllipse(Padding, Padding, IconSize, IconSize); + p.setClipPath(path); + + p.drawPixmap(avatarRegion, roomAvatar_); + p.restore(); + } + + if (unreadMsgCount_ > 0) { + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(bubbleBgColor()); + + if (isPressed_) + brush.setColor(bubbleFgColor()); + + p.setBrush(brush); + p.setPen(Qt::NoPen); + p.setFont(unreadCountFont_); + + // Extra space on the x-axis to accomodate the extra character space + // inside the bubble. + const int x_width = unreadMsgCount_ > MaxUnreadCountDisplayed + ? QFontMetrics(p.font()).averageCharWidth() + : 0; + + QRectF r(width() - bubbleDiameter_ - Padding - x_width, + bottom_y - bubbleDiameter_ / 2 - 5, + bubbleDiameter_ + x_width, + bubbleDiameter_); + + if (width() == ui::sidebar::SmallSize) + r = QRectF(width() - bubbleDiameter_ - 5, + height() - bubbleDiameter_ - 5, + bubbleDiameter_ + x_width, + bubbleDiameter_); + + p.setPen(Qt::NoPen); + p.drawEllipse(r); + + p.setPen(QPen(bubbleFgColor())); + + if (isPressed_) + p.setPen(QPen(bubbleBgColor())); + + auto countTxt = unreadMsgCount_ > MaxUnreadCountDisplayed + ? QString("99+") + : QString::number(unreadMsgCount_); + + p.setBrush(Qt::NoBrush); + p.drawText(r.translated(0, -0.5), Qt::AlignCenter, countTxt); + } +} + +void +RoomInfoListItem::updateUnreadMessageCount(int count) +{ + unreadMsgCount_ = count; + update(); +} + +void +RoomInfoListItem::setPressedState(bool state) +{ + if (isPressed_ != state) { + isPressed_ = state; + update(); + } +} + +void +RoomInfoListItem::contextMenuEvent(QContextMenuEvent *event) +{ + Q_UNUSED(event); + + if (roomType_ == RoomType::Invited) + return; + + menu_->popup(event->globalPos()); +} + +void +RoomInfoListItem::mousePressEvent(QMouseEvent *event) +{ + if (event->buttons() == Qt::RightButton) { + QWidget::mousePressEvent(event); + return; + } + + if (roomType_ == RoomType::Invited) { + const auto point = event->pos(); + + if (acceptBtnRegion_.contains(point)) + emit acceptInvite(roomId_); + + if (declineBtnRegion_.contains(point)) + emit declineInvite(roomId_); + + return; + } + + emit clicked(roomId_); + + setPressedState(true); + + // Ripple on mouse position by default. + QPoint pos = event->pos(); + qreal radiusEndValue = static_cast(width()) / 3; + + Ripple *ripple = new Ripple(pos); + + ripple->setRadiusEndValue(radiusEndValue); + ripple->setOpacityStartValue(0.15); + ripple->setColor(QColor("white")); + ripple->radiusAnimation()->setDuration(200); + ripple->opacityAnimation()->setDuration(400); + + ripple_overlay_->addRipple(ripple); +} + +void +RoomInfoListItem::setAvatar(const QImage &img) +{ + roomAvatar_ = utils::scaleImageToPixmap(img, IconSize); + update(); +} + +void +RoomInfoListItem::setDescriptionMessage(const DescInfo &info) +{ + lastMsgInfo_ = info; + update(); +} diff --git a/src/RoomInfoListItem.h b/src/RoomInfoListItem.h new file mode 100644 index 00000000..95db1d75 --- /dev/null +++ b/src/RoomInfoListItem.h @@ -0,0 +1,204 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +#include "Cache.h" +#include + +class Menu; +class RippleOverlay; + +class RoomInfoListItem : public QWidget +{ + Q_OBJECT + Q_PROPERTY(QColor highlightedBackgroundColor READ highlightedBackgroundColor WRITE + setHighlightedBackgroundColor) + Q_PROPERTY( + QColor hoverBackgroundColor READ hoverBackgroundColor WRITE setHoverBackgroundColor) + Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) + + Q_PROPERTY(QColor avatarBgColor READ avatarBgColor WRITE setAvatarBgColor) + Q_PROPERTY(QColor avatarFgColor READ avatarFgColor WRITE setAvatarFgColor) + + Q_PROPERTY(QColor bubbleBgColor READ bubbleBgColor WRITE setBubbleBgColor) + Q_PROPERTY(QColor bubbleFgColor READ bubbleFgColor WRITE setBubbleFgColor) + + Q_PROPERTY(QColor titleColor READ titleColor WRITE setTitleColor) + Q_PROPERTY(QColor subtitleColor READ subtitleColor WRITE setSubtitleColor) + + Q_PROPERTY(QColor timestampColor READ timestampColor WRITE setTimestampColor) + Q_PROPERTY(QColor highlightedTimestampColor READ highlightedTimestampColor WRITE + setHighlightedTimestampColor) + + Q_PROPERTY( + QColor highlightedTitleColor READ highlightedTitleColor WRITE setHighlightedTitleColor) + Q_PROPERTY(QColor highlightedSubtitleColor READ highlightedSubtitleColor WRITE + setHighlightedSubtitleColor) + + Q_PROPERTY(QColor btnColor READ btnColor WRITE setBtnColor) + Q_PROPERTY(QColor btnTextColor READ btnTextColor WRITE setBtnTextColor) + +public: + RoomInfoListItem(QString room_id, RoomInfo info, QWidget *parent = 0); + + void updateUnreadMessageCount(int count); + void clearUnreadMessageCount() { updateUnreadMessageCount(0); }; + + QString roomId() { return roomId_; } + bool isPressed() const { return isPressed_; } + int unreadMessageCount() const { return unreadMsgCount_; } + + void setAvatar(const QImage &avatar_image); + void setDescriptionMessage(const DescInfo &info); + DescInfo lastMessageInfo() const { return lastMsgInfo_; } + + QColor highlightedBackgroundColor() const { return highlightedBackgroundColor_; } + QColor hoverBackgroundColor() const { return hoverBackgroundColor_; } + QColor backgroundColor() const { return backgroundColor_; } + QColor avatarBgColor() const { return avatarBgColor_; } + QColor avatarFgColor() const { return avatarFgColor_; } + + QColor highlightedTitleColor() const { return highlightedTitleColor_; } + QColor highlightedSubtitleColor() const { return highlightedSubtitleColor_; } + QColor highlightedTimestampColor() const { return highlightedTimestampColor_; } + + QColor titleColor() const { return titleColor_; } + QColor subtitleColor() const { return subtitleColor_; } + QColor timestampColor() const { return timestampColor_; } + QColor btnColor() const { return btnColor_; } + QColor btnTextColor() const { return btnTextColor_; } + + QColor bubbleFgColor() const { return bubbleFgColor_; } + QColor bubbleBgColor() const { return bubbleBgColor_; } + + void setHighlightedBackgroundColor(QColor &color) { highlightedBackgroundColor_ = color; } + void setHoverBackgroundColor(QColor &color) { hoverBackgroundColor_ = color; } + void setBackgroundColor(QColor &color) { backgroundColor_ = color; } + void setTimestampColor(QColor &color) { timestampColor_ = color; } + void setAvatarFgColor(QColor &color) { avatarFgColor_ = color; } + void setAvatarBgColor(QColor &color) { avatarBgColor_ = color; } + + void setHighlightedTitleColor(QColor &color) { highlightedTitleColor_ = color; } + void setHighlightedSubtitleColor(QColor &color) { highlightedSubtitleColor_ = color; } + void setHighlightedTimestampColor(QColor &color) { highlightedTimestampColor_ = color; } + + void setTitleColor(QColor &color) { titleColor_ = color; } + void setSubtitleColor(QColor &color) { subtitleColor_ = color; } + + void setBtnColor(QColor &color) { btnColor_ = color; } + void setBtnTextColor(QColor &color) { btnTextColor_ = color; } + + void setBubbleFgColor(QColor &color) { bubbleFgColor_ = color; } + void setBubbleBgColor(QColor &color) { bubbleBgColor_ = color; } + + void setRoomName(const QString &name) { roomName_ = name; } + void setRoomType(bool isInvite) + { + if (isInvite) + roomType_ = RoomType::Invited; + else + roomType_ = RoomType::Joined; + } + + bool isInvite() { return roomType_ == RoomType::Invited; } + +signals: + void clicked(const QString &room_id); + void leaveRoom(const QString &room_id); + void acceptInvite(const QString &room_id); + void declineInvite(const QString &room_id); + +public slots: + void setPressedState(bool state); + +protected: + void mousePressEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void contextMenuEvent(QContextMenuEvent *event) override; + +private: + void init(QWidget *parent); + QString roomName() { return roomName_; } + + RippleOverlay *ripple_overlay_; + + enum class RoomType + { + Joined, + Invited, + }; + + RoomType roomType_ = RoomType::Joined; + + // State information for the invited rooms. + mtx::responses::InvitedRoom invitedRoom_; + + QString roomId_; + QString roomName_; + + DescInfo lastMsgInfo_; + + QPixmap roomAvatar_; + + Menu *menu_; + QAction *leaveRoom_; + + bool isPressed_ = false; + + int unreadMsgCount_ = 0; + + QColor highlightedBackgroundColor_; + QColor hoverBackgroundColor_; + QColor backgroundColor_; + + QColor highlightedTitleColor_; + QColor highlightedSubtitleColor_; + + QColor titleColor_; + QColor subtitleColor_; + + QColor btnColor_; + QColor btnTextColor_; + + QRectF acceptBtnRegion_; + QRectF declineBtnRegion_; + + // Fonts + QFont bubbleFont_; + QFont font_; + QFont headingFont_; + QFont timestampFont_; + QFont usernameFont_; + QFont unreadCountFont_; + int bubbleDiameter_; + + QColor timestampColor_; + QColor highlightedTimestampColor_; + + QColor avatarBgColor_; + QColor avatarFgColor_; + + QColor bubbleBgColor_; + QColor bubbleFgColor_; +}; diff --git a/src/RoomList.cc b/src/RoomList.cc deleted file mode 100644 index 418a5d6f..00000000 --- a/src/RoomList.cc +++ /dev/null @@ -1,440 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include - -#include "Cache.h" -#include "Logging.hpp" -#include "MainWindow.h" -#include "MatrixClient.h" -#include "OverlayModal.h" -#include "RoomInfoListItem.h" -#include "RoomList.h" -#include "UserSettingsPage.h" -#include "Utils.h" - -RoomList::RoomList(QSharedPointer userSettings, QWidget *parent) - : QWidget(parent) - , userSettings_{userSettings} -{ - setStyleSheet("border: none;"); - topLayout_ = new QVBoxLayout(this); - topLayout_->setSpacing(0); - topLayout_->setMargin(0); - - scrollArea_ = new QScrollArea(this); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); - scrollArea_->setWidgetResizable(true); - scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter); - - scrollAreaContents_ = new QWidget(this); - - contentsLayout_ = new QVBoxLayout(scrollAreaContents_); - contentsLayout_->setSpacing(0); - contentsLayout_->setMargin(0); - contentsLayout_->addStretch(1); - - scrollArea_->setWidget(scrollAreaContents_); - topLayout_->addWidget(scrollArea_); - - connect(this, &RoomList::updateRoomAvatarCb, this, &RoomList::updateRoomAvatar); -} - -void -RoomList::addRoom(const QString &room_id, const RoomInfo &info) -{ - auto room_item = new RoomInfoListItem(room_id, info, scrollArea_); - room_item->setRoomName(QString::fromStdString(std::move(info.name))); - - connect(room_item, &RoomInfoListItem::clicked, this, &RoomList::highlightSelectedRoom); - connect(room_item, &RoomInfoListItem::leaveRoom, this, [](const QString &room_id) { - MainWindow::instance()->openLeaveRoomDialog(room_id); - }); - - rooms_.emplace(room_id, QSharedPointer(room_item)); - - if (!info.avatar_url.empty()) - updateAvatar(room_id, QString::fromStdString(info.avatar_url)); - - int pos = contentsLayout_->count() - 1; - contentsLayout_->insertWidget(pos, room_item); -} - -void -RoomList::updateAvatar(const QString &room_id, const QString &url) -{ - if (url.isEmpty()) - return; - - QByteArray savedImgData; - - if (cache::client()) - savedImgData = cache::client()->image(url); - - if (savedImgData.isEmpty()) { - mtx::http::ThumbOpts opts; - opts.mxc_url = url.toStdString(); - http::client()->get_thumbnail( - opts, [room_id, opts, this](const std::string &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to download room avatar: {} {} {}", - opts.mxc_url, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - if (cache::client()) - cache::client()->saveImage(opts.mxc_url, res); - - auto data = QByteArray(res.data(), res.size()); - QPixmap pixmap; - pixmap.loadFromData(data); - - emit updateRoomAvatarCb(room_id, pixmap); - }); - } else { - QPixmap img; - img.loadFromData(savedImgData); - - updateRoomAvatar(room_id, img); - } -} - -void -RoomList::removeRoom(const QString &room_id, bool reset) -{ - rooms_.erase(room_id); - - if (rooms_.empty() || !reset) - return; - - auto room = firstRoom(); - - if (room.second.isNull()) - return; - - room.second->setPressedState(true); - emit roomChanged(room.first); -} - -void -RoomList::updateUnreadMessageCount(const QString &roomid, int count) -{ - if (!roomExists(roomid)) { - nhlog::ui()->warn("updateUnreadMessageCount: unknown room_id {}", - roomid.toStdString()); - return; - } - - rooms_[roomid]->updateUnreadMessageCount(count); - - calculateUnreadMessageCount(); -} - -void -RoomList::calculateUnreadMessageCount() -{ - int total_unread_msgs = 0; - - for (const auto &room : rooms_) { - if (!room.second.isNull()) - total_unread_msgs += room.second->unreadMessageCount(); - } - - emit totalUnreadMessageCountUpdated(total_unread_msgs); -} - -void -RoomList::initialize(const QMap &info) -{ - nhlog::ui()->info("initialize room list"); - - rooms_.clear(); - - setUpdatesEnabled(false); - - for (auto it = info.begin(); it != info.end(); it++) { - if (it.value().is_invite) - addInvitedRoom(it.key(), it.value()); - else - addRoom(it.key(), it.value()); - } - - for (auto it = info.begin(); it != info.end(); it++) - updateRoomDescription(it.key(), it.value().msgInfo); - - setUpdatesEnabled(true); - - if (rooms_.empty()) - return; - - auto room = firstRoom(); - if (room.second.isNull()) - return; - - room.second->setPressedState(true); - emit roomChanged(room.first); -} - -void -RoomList::cleanupInvites(const std::map &invites) -{ - if (invites.size() == 0) - return; - - utils::erase_if(rooms_, [invites](auto &room) { - auto room_id = room.first; - auto item = room.second; - - if (!item) - return false; - - return item->isInvite() && (invites.find(room_id) == invites.end()); - }); -} - -void -RoomList::sync(const std::map &info) - -{ - for (const auto &room : info) - updateRoom(room.first, room.second); -} - -void -RoomList::highlightSelectedRoom(const QString &room_id) -{ - emit roomChanged(room_id); - - if (!roomExists(room_id)) { - nhlog::ui()->warn("roomlist: clicked unknown room_id"); - return; - } - - for (auto const &room : rooms_) { - if (room.second.isNull()) - continue; - - if (room.first != room_id) { - room.second->setPressedState(false); - } else { - room.second->setPressedState(true); - scrollArea_->ensureWidgetVisible(room.second.data()); - } - } - - selectedRoom_ = room_id; -} - -void -RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img) -{ - if (!roomExists(roomid)) { - nhlog::ui()->warn("avatar update on non-existent room_id: {}", - roomid.toStdString()); - return; - } - - rooms_[roomid]->setAvatar(img.toImage()); - - // Used to inform other widgets for the new image data. - emit roomAvatarChanged(roomid, img); -} - -void -RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info) -{ - if (!roomExists(roomid)) { - nhlog::ui()->warn("description update on non-existent room_id: {}, {}", - roomid.toStdString(), - info.body.toStdString()); - return; - } - - rooms_[roomid]->setDescriptionMessage(info); - - if (underMouse()) { - // When the user hover out of the roomlist a sort will be triggered. - isSortPending_ = true; - return; - } - - isSortPending_ = false; - - emit sortRoomsByLastMessage(); -} - -void -RoomList::sortRoomsByLastMessage() -{ - if (!userSettings_->isOrderingEnabled()) - return; - - isSortPending_ = false; - - std::multimap> times; - - for (int ii = 0; ii < contentsLayout_->count(); ++ii) { - auto room = qobject_cast(contentsLayout_->itemAt(ii)->widget()); - - if (!room) - continue; - - // Not a room message. - if (room->lastMessageInfo().userid.isEmpty()) - times.emplace(0, room); - else - times.emplace(room->lastMessageInfo().datetime.toMSecsSinceEpoch(), room); - } - - for (auto it = times.cbegin(); it != times.cend(); ++it) { - const auto roomWidget = it->second; - const auto currentIndex = contentsLayout_->indexOf(roomWidget); - const auto newIndex = std::distance(times.cbegin(), it); - - if (currentIndex == newIndex) - continue; - - contentsLayout_->removeWidget(roomWidget); - contentsLayout_->insertWidget(newIndex, roomWidget); - } -} - -void -RoomList::leaveEvent(QEvent *event) -{ - if (isSortPending_) - QTimer::singleShot(700, this, &RoomList::sortRoomsByLastMessage); - - QWidget::leaveEvent(event); -} - -void -RoomList::closeJoinRoomDialog(bool isJoining, QString roomAlias) -{ - joinRoomModal_->hide(); - - if (isJoining) - emit joinRoom(roomAlias); -} - -void -RoomList::setFilterRooms(bool isFilteringEnabled) -{ - for (int i = 0; i < contentsLayout_->count(); i++) { - // If roomFilter_ contains the room for the current RoomInfoListItem, - // show the list item, otherwise hide it - auto listitem = - qobject_cast(contentsLayout_->itemAt(i)->widget()); - - if (!listitem) - continue; - - if (!isFilteringEnabled || filterItemExists(listitem->roomId())) - listitem->show(); - else - listitem->hide(); - } - - if (isFilteringEnabled && !filterItemExists(selectedRoom_)) { - RoomInfoListItem *firstVisibleRoom = nullptr; - - for (int i = 0; i < contentsLayout_->count(); i++) { - QWidget *item = contentsLayout_->itemAt(i)->widget(); - - if (item != nullptr && item->isVisible()) { - firstVisibleRoom = qobject_cast(item); - break; - } - } - - if (firstVisibleRoom != nullptr) - highlightSelectedRoom(firstVisibleRoom->roomId()); - } else { - scrollArea_->ensureWidgetVisible(rooms_[selectedRoom_].data()); - } -} - -void -RoomList::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -RoomList::updateRoom(const QString &room_id, const RoomInfo &info) -{ - if (!roomExists(room_id)) { - if (info.is_invite) - addInvitedRoom(room_id, info); - else - addRoom(room_id, info); - - return; - } - - auto room = rooms_[room_id]; - updateAvatar(room_id, QString::fromStdString(info.avatar_url)); - room->setRoomName(QString::fromStdString(info.name)); - room->setRoomType(info.is_invite); - room->update(); -} - -void -RoomList::setRoomFilter(std::vector room_ids) -{ - roomFilter_ = room_ids; - setFilterRooms(true); -} - -void -RoomList::addInvitedRoom(const QString &room_id, const RoomInfo &info) -{ - auto room_item = new RoomInfoListItem(room_id, info, scrollArea_); - - connect(room_item, &RoomInfoListItem::acceptInvite, this, &RoomList::acceptInvite); - connect(room_item, &RoomInfoListItem::declineInvite, this, &RoomList::declineInvite); - - rooms_.emplace(room_id, QSharedPointer(room_item)); - - updateAvatar(room_id, QString::fromStdString(info.avatar_url)); - - int pos = contentsLayout_->count() - 1; - contentsLayout_->insertWidget(pos, room_item); -} - -std::pair> -RoomList::firstRoom() const -{ - auto firstRoom = rooms_.begin(); - - while (firstRoom->second.isNull() && firstRoom != rooms_.end()) - firstRoom++; - - return std::pair>(firstRoom->first, - firstRoom->second); -} diff --git a/src/RoomList.cpp b/src/RoomList.cpp new file mode 100644 index 00000000..a9328984 --- /dev/null +++ b/src/RoomList.cpp @@ -0,0 +1,440 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include + +#include "Cache.h" +#include "Logging.h" +#include "MainWindow.h" +#include "MatrixClient.h" +#include "RoomInfoListItem.h" +#include "RoomList.h" +#include "UserSettingsPage.h" +#include "Utils.h" +#include "ui/OverlayModal.h" + +RoomList::RoomList(QSharedPointer userSettings, QWidget *parent) + : QWidget(parent) + , userSettings_{userSettings} +{ + setStyleSheet("border: none;"); + topLayout_ = new QVBoxLayout(this); + topLayout_->setSpacing(0); + topLayout_->setMargin(0); + + scrollArea_ = new QScrollArea(this); + scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + scrollArea_->setWidgetResizable(true); + scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter); + + scrollAreaContents_ = new QWidget(this); + + contentsLayout_ = new QVBoxLayout(scrollAreaContents_); + contentsLayout_->setSpacing(0); + contentsLayout_->setMargin(0); + contentsLayout_->addStretch(1); + + scrollArea_->setWidget(scrollAreaContents_); + topLayout_->addWidget(scrollArea_); + + connect(this, &RoomList::updateRoomAvatarCb, this, &RoomList::updateRoomAvatar); +} + +void +RoomList::addRoom(const QString &room_id, const RoomInfo &info) +{ + auto room_item = new RoomInfoListItem(room_id, info, scrollArea_); + room_item->setRoomName(QString::fromStdString(std::move(info.name))); + + connect(room_item, &RoomInfoListItem::clicked, this, &RoomList::highlightSelectedRoom); + connect(room_item, &RoomInfoListItem::leaveRoom, this, [](const QString &room_id) { + MainWindow::instance()->openLeaveRoomDialog(room_id); + }); + + rooms_.emplace(room_id, QSharedPointer(room_item)); + + if (!info.avatar_url.empty()) + updateAvatar(room_id, QString::fromStdString(info.avatar_url)); + + int pos = contentsLayout_->count() - 1; + contentsLayout_->insertWidget(pos, room_item); +} + +void +RoomList::updateAvatar(const QString &room_id, const QString &url) +{ + if (url.isEmpty()) + return; + + QByteArray savedImgData; + + if (cache::client()) + savedImgData = cache::client()->image(url); + + if (savedImgData.isEmpty()) { + mtx::http::ThumbOpts opts; + opts.mxc_url = url.toStdString(); + http::client()->get_thumbnail( + opts, [room_id, opts, this](const std::string &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to download room avatar: {} {} {}", + opts.mxc_url, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } + + if (cache::client()) + cache::client()->saveImage(opts.mxc_url, res); + + auto data = QByteArray(res.data(), res.size()); + QPixmap pixmap; + pixmap.loadFromData(data); + + emit updateRoomAvatarCb(room_id, pixmap); + }); + } else { + QPixmap img; + img.loadFromData(savedImgData); + + updateRoomAvatar(room_id, img); + } +} + +void +RoomList::removeRoom(const QString &room_id, bool reset) +{ + rooms_.erase(room_id); + + if (rooms_.empty() || !reset) + return; + + auto room = firstRoom(); + + if (room.second.isNull()) + return; + + room.second->setPressedState(true); + emit roomChanged(room.first); +} + +void +RoomList::updateUnreadMessageCount(const QString &roomid, int count) +{ + if (!roomExists(roomid)) { + nhlog::ui()->warn("updateUnreadMessageCount: unknown room_id {}", + roomid.toStdString()); + return; + } + + rooms_[roomid]->updateUnreadMessageCount(count); + + calculateUnreadMessageCount(); +} + +void +RoomList::calculateUnreadMessageCount() +{ + int total_unread_msgs = 0; + + for (const auto &room : rooms_) { + if (!room.second.isNull()) + total_unread_msgs += room.second->unreadMessageCount(); + } + + emit totalUnreadMessageCountUpdated(total_unread_msgs); +} + +void +RoomList::initialize(const QMap &info) +{ + nhlog::ui()->info("initialize room list"); + + rooms_.clear(); + + setUpdatesEnabled(false); + + for (auto it = info.begin(); it != info.end(); it++) { + if (it.value().is_invite) + addInvitedRoom(it.key(), it.value()); + else + addRoom(it.key(), it.value()); + } + + for (auto it = info.begin(); it != info.end(); it++) + updateRoomDescription(it.key(), it.value().msgInfo); + + setUpdatesEnabled(true); + + if (rooms_.empty()) + return; + + auto room = firstRoom(); + if (room.second.isNull()) + return; + + room.second->setPressedState(true); + emit roomChanged(room.first); +} + +void +RoomList::cleanupInvites(const std::map &invites) +{ + if (invites.size() == 0) + return; + + utils::erase_if(rooms_, [invites](auto &room) { + auto room_id = room.first; + auto item = room.second; + + if (!item) + return false; + + return item->isInvite() && (invites.find(room_id) == invites.end()); + }); +} + +void +RoomList::sync(const std::map &info) + +{ + for (const auto &room : info) + updateRoom(room.first, room.second); +} + +void +RoomList::highlightSelectedRoom(const QString &room_id) +{ + emit roomChanged(room_id); + + if (!roomExists(room_id)) { + nhlog::ui()->warn("roomlist: clicked unknown room_id"); + return; + } + + for (auto const &room : rooms_) { + if (room.second.isNull()) + continue; + + if (room.first != room_id) { + room.second->setPressedState(false); + } else { + room.second->setPressedState(true); + scrollArea_->ensureWidgetVisible(room.second.data()); + } + } + + selectedRoom_ = room_id; +} + +void +RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img) +{ + if (!roomExists(roomid)) { + nhlog::ui()->warn("avatar update on non-existent room_id: {}", + roomid.toStdString()); + return; + } + + rooms_[roomid]->setAvatar(img.toImage()); + + // Used to inform other widgets for the new image data. + emit roomAvatarChanged(roomid, img); +} + +void +RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info) +{ + if (!roomExists(roomid)) { + nhlog::ui()->warn("description update on non-existent room_id: {}, {}", + roomid.toStdString(), + info.body.toStdString()); + return; + } + + rooms_[roomid]->setDescriptionMessage(info); + + if (underMouse()) { + // When the user hover out of the roomlist a sort will be triggered. + isSortPending_ = true; + return; + } + + isSortPending_ = false; + + emit sortRoomsByLastMessage(); +} + +void +RoomList::sortRoomsByLastMessage() +{ + if (!userSettings_->isOrderingEnabled()) + return; + + isSortPending_ = false; + + std::multimap> times; + + for (int ii = 0; ii < contentsLayout_->count(); ++ii) { + auto room = qobject_cast(contentsLayout_->itemAt(ii)->widget()); + + if (!room) + continue; + + // Not a room message. + if (room->lastMessageInfo().userid.isEmpty()) + times.emplace(0, room); + else + times.emplace(room->lastMessageInfo().datetime.toMSecsSinceEpoch(), room); + } + + for (auto it = times.cbegin(); it != times.cend(); ++it) { + const auto roomWidget = it->second; + const auto currentIndex = contentsLayout_->indexOf(roomWidget); + const auto newIndex = std::distance(times.cbegin(), it); + + if (currentIndex == newIndex) + continue; + + contentsLayout_->removeWidget(roomWidget); + contentsLayout_->insertWidget(newIndex, roomWidget); + } +} + +void +RoomList::leaveEvent(QEvent *event) +{ + if (isSortPending_) + QTimer::singleShot(700, this, &RoomList::sortRoomsByLastMessage); + + QWidget::leaveEvent(event); +} + +void +RoomList::closeJoinRoomDialog(bool isJoining, QString roomAlias) +{ + joinRoomModal_->hide(); + + if (isJoining) + emit joinRoom(roomAlias); +} + +void +RoomList::setFilterRooms(bool isFilteringEnabled) +{ + for (int i = 0; i < contentsLayout_->count(); i++) { + // If roomFilter_ contains the room for the current RoomInfoListItem, + // show the list item, otherwise hide it + auto listitem = + qobject_cast(contentsLayout_->itemAt(i)->widget()); + + if (!listitem) + continue; + + if (!isFilteringEnabled || filterItemExists(listitem->roomId())) + listitem->show(); + else + listitem->hide(); + } + + if (isFilteringEnabled && !filterItemExists(selectedRoom_)) { + RoomInfoListItem *firstVisibleRoom = nullptr; + + for (int i = 0; i < contentsLayout_->count(); i++) { + QWidget *item = contentsLayout_->itemAt(i)->widget(); + + if (item != nullptr && item->isVisible()) { + firstVisibleRoom = qobject_cast(item); + break; + } + } + + if (firstVisibleRoom != nullptr) + highlightSelectedRoom(firstVisibleRoom->roomId()); + } else { + scrollArea_->ensureWidgetVisible(rooms_[selectedRoom_].data()); + } +} + +void +RoomList::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} + +void +RoomList::updateRoom(const QString &room_id, const RoomInfo &info) +{ + if (!roomExists(room_id)) { + if (info.is_invite) + addInvitedRoom(room_id, info); + else + addRoom(room_id, info); + + return; + } + + auto room = rooms_[room_id]; + updateAvatar(room_id, QString::fromStdString(info.avatar_url)); + room->setRoomName(QString::fromStdString(info.name)); + room->setRoomType(info.is_invite); + room->update(); +} + +void +RoomList::setRoomFilter(std::vector room_ids) +{ + roomFilter_ = room_ids; + setFilterRooms(true); +} + +void +RoomList::addInvitedRoom(const QString &room_id, const RoomInfo &info) +{ + auto room_item = new RoomInfoListItem(room_id, info, scrollArea_); + + connect(room_item, &RoomInfoListItem::acceptInvite, this, &RoomList::acceptInvite); + connect(room_item, &RoomInfoListItem::declineInvite, this, &RoomList::declineInvite); + + rooms_.emplace(room_id, QSharedPointer(room_item)); + + updateAvatar(room_id, QString::fromStdString(info.avatar_url)); + + int pos = contentsLayout_->count() - 1; + contentsLayout_->insertWidget(pos, room_item); +} + +std::pair> +RoomList::firstRoom() const +{ + auto firstRoom = rooms_.begin(); + + while (firstRoom->second.isNull() && firstRoom != rooms_.end()) + firstRoom++; + + return std::pair>(firstRoom->first, + firstRoom->second); +} diff --git a/src/RoomList.h b/src/RoomList.h new file mode 100644 index 00000000..59b0e865 --- /dev/null +++ b/src/RoomList.h @@ -0,0 +1,108 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +class LeaveRoomDialog; +class OverlayModal; +class RoomInfoListItem; +class Sync; +class UserSettings; +struct DescInfo; +struct RoomInfo; + +class RoomList : public QWidget +{ + Q_OBJECT + +public: + RoomList(QSharedPointer userSettings, QWidget *parent = 0); + + void initialize(const QMap &info); + void sync(const std::map &info); + + void clear() { rooms_.clear(); }; + void updateAvatar(const QString &room_id, const QString &url); + + void addRoom(const QString &room_id, const RoomInfo &info); + void addInvitedRoom(const QString &room_id, const RoomInfo &info); + void removeRoom(const QString &room_id, bool reset); + void setFilterRooms(bool filterRooms); + void setRoomFilter(std::vector room_ids); + void updateRoom(const QString &room_id, const RoomInfo &info); + void cleanupInvites(const std::map &invites); + +signals: + void roomChanged(const QString &room_id); + void totalUnreadMessageCountUpdated(int count); + void acceptInvite(const QString &room_id); + void declineInvite(const QString &room_id); + void roomAvatarChanged(const QString &room_id, const QPixmap &img); + void joinRoom(const QString &room_id); + void updateRoomAvatarCb(const QString &room_id, const QPixmap &img); + +public slots: + void updateRoomAvatar(const QString &roomid, const QPixmap &img); + void highlightSelectedRoom(const QString &room_id); + void updateUnreadMessageCount(const QString &roomid, int count); + void updateRoomDescription(const QString &roomid, const DescInfo &info); + void closeJoinRoomDialog(bool isJoining, QString roomAlias); + +protected: + void paintEvent(QPaintEvent *event) override; + void leaveEvent(QEvent *event) override; + +private slots: + void sortRoomsByLastMessage(); + +private: + //! Return the first non-null room. + std::pair> firstRoom() const; + void calculateUnreadMessageCount(); + bool roomExists(const QString &room_id) { return rooms_.find(room_id) != rooms_.end(); } + bool filterItemExists(const QString &id) + { + return std::find(roomFilter_.begin(), roomFilter_.end(), id) != roomFilter_.end(); + } + + QVBoxLayout *topLayout_; + QVBoxLayout *contentsLayout_; + QScrollArea *scrollArea_; + QWidget *scrollAreaContents_; + + QPushButton *joinRoomButton_; + + OverlayModal *joinRoomModal_; + + std::map> rooms_; + QString selectedRoom_; + + //! Which rooms to include in the room list. + std::vector roomFilter_; + + QSharedPointer userSettings_; + + bool isSortPending_ = false; +}; diff --git a/src/RunGuard.cc b/src/RunGuard.cc deleted file mode 100644 index 75833eb7..00000000 --- a/src/RunGuard.cc +++ /dev/null @@ -1,84 +0,0 @@ -#include "RunGuard.h" - -#include - -namespace { - -QString -generateKeyHash(const QString &key, const QString &salt) -{ - QByteArray data; - - data.append(key.toUtf8()); - data.append(salt.toUtf8()); - data = QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex(); - - return data; -} -} - -RunGuard::RunGuard(const QString &key) - : key(key) - , memLockKey(generateKeyHash(key, "_memLockKey")) - , sharedmemKey(generateKeyHash(key, "_sharedmemKey")) - , sharedMem(sharedmemKey) - , memLock(memLockKey, 1) -{ - memLock.acquire(); - { - // Fix for *nix: http://habrahabr.ru/post/173281/ - QSharedMemory fix(sharedmemKey); - fix.attach(); - } - - memLock.release(); -} - -RunGuard::~RunGuard() { release(); } - -bool -RunGuard::isAnotherRunning() -{ - if (sharedMem.isAttached()) - return false; - - memLock.acquire(); - const bool isRunning = sharedMem.attach(); - - if (isRunning) - sharedMem.detach(); - - memLock.release(); - - return isRunning; -} - -bool -RunGuard::tryToRun() -{ - // Extra check - if (isAnotherRunning()) - return false; - - memLock.acquire(); - const bool result = sharedMem.create(sizeof(quint64)); - memLock.release(); - - if (!result) { - release(); - return false; - } - - return true; -} - -void -RunGuard::release() -{ - memLock.acquire(); - - if (sharedMem.isAttached()) - sharedMem.detach(); - - memLock.release(); -} diff --git a/src/RunGuard.cpp b/src/RunGuard.cpp new file mode 100644 index 00000000..75833eb7 --- /dev/null +++ b/src/RunGuard.cpp @@ -0,0 +1,84 @@ +#include "RunGuard.h" + +#include + +namespace { + +QString +generateKeyHash(const QString &key, const QString &salt) +{ + QByteArray data; + + data.append(key.toUtf8()); + data.append(salt.toUtf8()); + data = QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex(); + + return data; +} +} + +RunGuard::RunGuard(const QString &key) + : key(key) + , memLockKey(generateKeyHash(key, "_memLockKey")) + , sharedmemKey(generateKeyHash(key, "_sharedmemKey")) + , sharedMem(sharedmemKey) + , memLock(memLockKey, 1) +{ + memLock.acquire(); + { + // Fix for *nix: http://habrahabr.ru/post/173281/ + QSharedMemory fix(sharedmemKey); + fix.attach(); + } + + memLock.release(); +} + +RunGuard::~RunGuard() { release(); } + +bool +RunGuard::isAnotherRunning() +{ + if (sharedMem.isAttached()) + return false; + + memLock.acquire(); + const bool isRunning = sharedMem.attach(); + + if (isRunning) + sharedMem.detach(); + + memLock.release(); + + return isRunning; +} + +bool +RunGuard::tryToRun() +{ + // Extra check + if (isAnotherRunning()) + return false; + + memLock.acquire(); + const bool result = sharedMem.create(sizeof(quint64)); + memLock.release(); + + if (!result) { + release(); + return false; + } + + return true; +} + +void +RunGuard::release() +{ + memLock.acquire(); + + if (sharedMem.isAttached()) + sharedMem.detach(); + + memLock.release(); +} diff --git a/src/RunGuard.h b/src/RunGuard.h new file mode 100644 index 00000000..f9a9641a --- /dev/null +++ b/src/RunGuard.h @@ -0,0 +1,31 @@ +#pragma once + +// +// Taken from +// https://stackoverflow.com/questions/5006547/qt-best-practice-for-a-single-instance-app-protection +// + +#include +#include +#include + +class RunGuard +{ +public: + RunGuard(const QString &key); + ~RunGuard(); + + bool isAnotherRunning(); + bool tryToRun(); + void release(); + +private: + const QString key; + const QString memLockKey; + const QString sharedmemKey; + + QSharedMemory sharedMem; + QSystemSemaphore memLock; + + Q_DISABLE_COPY(RunGuard) +}; diff --git a/src/SideBarActions.cc b/src/SideBarActions.cc deleted file mode 100644 index d65900b3..00000000 --- a/src/SideBarActions.cc +++ /dev/null @@ -1,103 +0,0 @@ -#include -#include - -#include - -#include "Config.h" -#include "MainWindow.h" -#include "OverlayModal.h" -#include "SideBarActions.h" -#include "Theme.h" - -SideBarActions::SideBarActions(QWidget *parent) - : QWidget{parent} -{ - setFixedHeight(conf::sidebarActions::height); - - layout_ = new QHBoxLayout(this); - layout_->setMargin(0); - - QIcon settingsIcon; - settingsIcon.addFile(":/icons/icons/ui/settings.png"); - - QIcon createRoomIcon; - createRoomIcon.addFile(":/icons/icons/ui/add-square-button.png"); - - QIcon joinRoomIcon; - joinRoomIcon.addFile(":/icons/icons/ui/speech-bubbles-comment-option.png"); - - settingsBtn_ = new FlatButton(this); - settingsBtn_->setIcon(settingsIcon); - settingsBtn_->setCornerRadius(conf::sidebarActions::iconSize / 2); - settingsBtn_->setIconSize( - QSize(conf::sidebarActions::iconSize, conf::sidebarActions::iconSize)); - - addMenu_ = new Menu(this); - createRoomAction_ = new QAction(tr("Create new room"), this); - joinRoomAction_ = new QAction(tr("Join a room"), this); - - connect(joinRoomAction_, &QAction::triggered, this, [this]() { - MainWindow::instance()->openJoinRoomDialog( - [this](const QString &room_id) { emit joinRoom(room_id); }); - }); - - connect(createRoomAction_, &QAction::triggered, this, [this]() { - MainWindow::instance()->openCreateRoomDialog( - [this](const mtx::requests::CreateRoom &req) { emit createRoom(req); }); - }); - - addMenu_->addAction(createRoomAction_); - addMenu_->addAction(joinRoomAction_); - - createRoomBtn_ = new FlatButton(this); - createRoomBtn_->setIcon(createRoomIcon); - createRoomBtn_->setCornerRadius(conf::sidebarActions::iconSize / 2); - createRoomBtn_->setIconSize( - QSize(conf::sidebarActions::iconSize, conf::sidebarActions::iconSize)); - - connect(createRoomBtn_, &QPushButton::clicked, this, [this]() { - auto pos = mapToGlobal(createRoomBtn_->pos()); - auto padding = conf::sidebarActions::iconSize / 2; - - addMenu_->popup( - QPoint(pos.x() + padding, pos.y() - padding - addMenu_->sizeHint().height())); - }); - - joinRoomBtn_ = new FlatButton(this); - joinRoomBtn_->setIcon(joinRoomIcon); - joinRoomBtn_->setCornerRadius(conf::sidebarActions::iconSize / 2); - joinRoomBtn_->setIconSize( - QSize(conf::sidebarActions::iconSize, conf::sidebarActions::iconSize)); - - layout_->addWidget(createRoomBtn_); - layout_->addWidget(joinRoomBtn_); - layout_->addWidget(settingsBtn_); - - connect(settingsBtn_, &QPushButton::clicked, this, &SideBarActions::showSettings); -} - -void -SideBarActions::resizeEvent(QResizeEvent *event) -{ - Q_UNUSED(event); - - if (width() <= ui::sidebar::SmallSize) { - joinRoomBtn_->hide(); - createRoomBtn_->hide(); - } else { - joinRoomBtn_->show(); - createRoomBtn_->show(); - } -} - -void -SideBarActions::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); - - p.setPen(QPen(borderColor())); - p.drawLine(QPointF(0, 0), QPointF(width(), 0)); -} diff --git a/src/SideBarActions.cpp b/src/SideBarActions.cpp new file mode 100644 index 00000000..b2a01e3e --- /dev/null +++ b/src/SideBarActions.cpp @@ -0,0 +1,105 @@ +#include +#include + +#include + +#include "Config.h" +#include "MainWindow.h" +#include "SideBarActions.h" +#include "ui/FlatButton.h" +#include "ui/Menu.h" +#include "ui/OverlayModal.h" +#include "ui/Theme.h" + +SideBarActions::SideBarActions(QWidget *parent) + : QWidget{parent} +{ + setFixedHeight(conf::sidebarActions::height); + + layout_ = new QHBoxLayout(this); + layout_->setMargin(0); + + QIcon settingsIcon; + settingsIcon.addFile(":/icons/icons/ui/settings.png"); + + QIcon createRoomIcon; + createRoomIcon.addFile(":/icons/icons/ui/add-square-button.png"); + + QIcon joinRoomIcon; + joinRoomIcon.addFile(":/icons/icons/ui/speech-bubbles-comment-option.png"); + + settingsBtn_ = new FlatButton(this); + settingsBtn_->setIcon(settingsIcon); + settingsBtn_->setCornerRadius(conf::sidebarActions::iconSize / 2); + settingsBtn_->setIconSize( + QSize(conf::sidebarActions::iconSize, conf::sidebarActions::iconSize)); + + addMenu_ = new Menu(this); + createRoomAction_ = new QAction(tr("Create new room"), this); + joinRoomAction_ = new QAction(tr("Join a room"), this); + + connect(joinRoomAction_, &QAction::triggered, this, [this]() { + MainWindow::instance()->openJoinRoomDialog( + [this](const QString &room_id) { emit joinRoom(room_id); }); + }); + + connect(createRoomAction_, &QAction::triggered, this, [this]() { + MainWindow::instance()->openCreateRoomDialog( + [this](const mtx::requests::CreateRoom &req) { emit createRoom(req); }); + }); + + addMenu_->addAction(createRoomAction_); + addMenu_->addAction(joinRoomAction_); + + createRoomBtn_ = new FlatButton(this); + createRoomBtn_->setIcon(createRoomIcon); + createRoomBtn_->setCornerRadius(conf::sidebarActions::iconSize / 2); + createRoomBtn_->setIconSize( + QSize(conf::sidebarActions::iconSize, conf::sidebarActions::iconSize)); + + connect(createRoomBtn_, &QPushButton::clicked, this, [this]() { + auto pos = mapToGlobal(createRoomBtn_->pos()); + auto padding = conf::sidebarActions::iconSize / 2; + + addMenu_->popup( + QPoint(pos.x() + padding, pos.y() - padding - addMenu_->sizeHint().height())); + }); + + joinRoomBtn_ = new FlatButton(this); + joinRoomBtn_->setIcon(joinRoomIcon); + joinRoomBtn_->setCornerRadius(conf::sidebarActions::iconSize / 2); + joinRoomBtn_->setIconSize( + QSize(conf::sidebarActions::iconSize, conf::sidebarActions::iconSize)); + + layout_->addWidget(createRoomBtn_); + layout_->addWidget(joinRoomBtn_); + layout_->addWidget(settingsBtn_); + + connect(settingsBtn_, &QPushButton::clicked, this, &SideBarActions::showSettings); +} + +void +SideBarActions::resizeEvent(QResizeEvent *event) +{ + Q_UNUSED(event); + + if (width() <= ui::sidebar::SmallSize) { + joinRoomBtn_->hide(); + createRoomBtn_->hide(); + } else { + joinRoomBtn_->show(); + createRoomBtn_->show(); + } +} + +void +SideBarActions::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + + p.setPen(QPen(borderColor())); + p.drawLine(QPointF(0, 0), QPointF(width(), 0)); +} diff --git a/src/SideBarActions.h b/src/SideBarActions.h new file mode 100644 index 00000000..f97c72de --- /dev/null +++ b/src/SideBarActions.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include + +namespace mtx { +namespace requests { +struct CreateRoom; +} +} + +class Menu; +class FlatButton; + +class SideBarActions : public QWidget +{ + Q_OBJECT + + Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor) + +public: + SideBarActions(QWidget *parent = nullptr); + + QColor borderColor() const { return borderColor_; } + void setBorderColor(QColor &color) { borderColor_ = color; } + +signals: + void showSettings(); + void joinRoom(const QString &room); + void createRoom(const mtx::requests::CreateRoom &request); + +protected: + void resizeEvent(QResizeEvent *event) override; + void paintEvent(QPaintEvent *event) override; + +private: + QHBoxLayout *layout_; + + Menu *addMenu_; + QAction *createRoomAction_; + QAction *joinRoomAction_; + + FlatButton *settingsBtn_; + FlatButton *createRoomBtn_; + FlatButton *joinRoomBtn_; + + QColor borderColor_; +}; diff --git a/src/Splitter.cc b/src/Splitter.cc deleted file mode 100644 index 7b6c9573..00000000 --- a/src/Splitter.cc +++ /dev/null @@ -1,168 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include - -#include "Config.h" -#include "Splitter.h" -#include "Theme.h" - -constexpr auto MaxWidth = (1 << 24) - 1; - -Splitter::Splitter(QWidget *parent) - : QSplitter(parent) -{ - connect(this, &QSplitter::splitterMoved, this, &Splitter::onSplitterMoved); - setChildrenCollapsible(false); - setStyleSheet("QSplitter::handle { image: none; }"); -} - -void -Splitter::restoreSizes(int fallback) -{ - QSettings settings; - int savedWidth = settings.value("sidebar/width").toInt(); - - auto left = widget(0); - if (savedWidth == 0) { - hideSidebar(); - return; - } else if (savedWidth == ui::sidebar::SmallSize) { - if (left) { - left->setMinimumWidth(ui::sidebar::SmallSize); - left->setMaximumWidth(ui::sidebar::SmallSize); - return; - } - } - - left->setMinimumWidth(ui::sidebar::NormalSize); - left->setMaximumWidth(2 * ui::sidebar::NormalSize); - setSizes({ui::sidebar::NormalSize, fallback - ui::sidebar::NormalSize}); - - setStretchFactor(0, 0); - setStretchFactor(1, 1); -} - -Splitter::~Splitter() -{ - auto left = widget(0); - - if (left) { - QSettings settings; - settings.setValue("sidebar/width", left->width()); - } -} - -void -Splitter::onSplitterMoved(int pos, int index) -{ - Q_UNUSED(pos); - Q_UNUSED(index); - - auto s = sizes(); - - if (s.count() < 2) { - qWarning() << "Splitter needs at least two children"; - return; - } - - if (s[0] == ui::sidebar::NormalSize) { - rightMoveCount_ += 1; - - if (rightMoveCount_ > moveEventLimit_) { - auto left = widget(0); - auto cursorPosition = left->mapFromGlobal(QCursor::pos()); - - // if we are coming from the right, the cursor should - // end up on the first widget. - if (left->rect().contains(cursorPosition)) { - left->setMinimumWidth(ui::sidebar::SmallSize); - left->setMaximumWidth(ui::sidebar::SmallSize); - - rightMoveCount_ = 0; - } - } - } else if (s[0] == ui::sidebar::SmallSize) { - leftMoveCount_ += 1; - - if (leftMoveCount_ > moveEventLimit_) { - auto left = widget(0); - auto right = widget(1); - auto cursorPosition = right->mapFromGlobal(QCursor::pos()); - - // We move the start a little further so the transition isn't so abrupt. - auto extended = right->rect(); - extended.translate(100, 0); - - // if we are coming from the left, the cursor should - // end up on the second widget. - if (extended.contains(cursorPosition) && - right->size().width() >= - conf::sideBarCollapsePoint + ui::sidebar::NormalSize) { - left->setMinimumWidth(ui::sidebar::NormalSize); - left->setMaximumWidth(2 * ui::sidebar::NormalSize); - - leftMoveCount_ = 0; - } - } - } -} - -void -Splitter::hideSidebar() -{ - auto left = widget(0); - if (left) - left->hide(); -} - -void -Splitter::showChatView() -{ - auto left = widget(0); - auto right = widget(1); - - if (right->isHidden()) { - left->hide(); - right->show(); - - // Restore previous size. - if (left->minimumWidth() == ui::sidebar::SmallSize) { - left->setMinimumWidth(ui::sidebar::SmallSize); - left->setMaximumWidth(ui::sidebar::SmallSize); - } else { - left->setMinimumWidth(ui::sidebar::NormalSize); - left->setMaximumWidth(2 * ui::sidebar::NormalSize); - } - } -} - -void -Splitter::showFullRoomList() -{ - auto left = widget(0); - auto right = widget(1); - - right->hide(); - - left->show(); - left->setMaximumWidth(MaxWidth); -} diff --git a/src/Splitter.cpp b/src/Splitter.cpp new file mode 100644 index 00000000..f5bbf367 --- /dev/null +++ b/src/Splitter.cpp @@ -0,0 +1,168 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include + +#include "Config.h" +#include "Splitter.h" +#include "ui/Theme.h" + +constexpr auto MaxWidth = (1 << 24) - 1; + +Splitter::Splitter(QWidget *parent) + : QSplitter(parent) +{ + connect(this, &QSplitter::splitterMoved, this, &Splitter::onSplitterMoved); + setChildrenCollapsible(false); + setStyleSheet("QSplitter::handle { image: none; }"); +} + +void +Splitter::restoreSizes(int fallback) +{ + QSettings settings; + int savedWidth = settings.value("sidebar/width").toInt(); + + auto left = widget(0); + if (savedWidth == 0) { + hideSidebar(); + return; + } else if (savedWidth == ui::sidebar::SmallSize) { + if (left) { + left->setMinimumWidth(ui::sidebar::SmallSize); + left->setMaximumWidth(ui::sidebar::SmallSize); + return; + } + } + + left->setMinimumWidth(ui::sidebar::NormalSize); + left->setMaximumWidth(2 * ui::sidebar::NormalSize); + setSizes({ui::sidebar::NormalSize, fallback - ui::sidebar::NormalSize}); + + setStretchFactor(0, 0); + setStretchFactor(1, 1); +} + +Splitter::~Splitter() +{ + auto left = widget(0); + + if (left) { + QSettings settings; + settings.setValue("sidebar/width", left->width()); + } +} + +void +Splitter::onSplitterMoved(int pos, int index) +{ + Q_UNUSED(pos); + Q_UNUSED(index); + + auto s = sizes(); + + if (s.count() < 2) { + qWarning() << "Splitter needs at least two children"; + return; + } + + if (s[0] == ui::sidebar::NormalSize) { + rightMoveCount_ += 1; + + if (rightMoveCount_ > moveEventLimit_) { + auto left = widget(0); + auto cursorPosition = left->mapFromGlobal(QCursor::pos()); + + // if we are coming from the right, the cursor should + // end up on the first widget. + if (left->rect().contains(cursorPosition)) { + left->setMinimumWidth(ui::sidebar::SmallSize); + left->setMaximumWidth(ui::sidebar::SmallSize); + + rightMoveCount_ = 0; + } + } + } else if (s[0] == ui::sidebar::SmallSize) { + leftMoveCount_ += 1; + + if (leftMoveCount_ > moveEventLimit_) { + auto left = widget(0); + auto right = widget(1); + auto cursorPosition = right->mapFromGlobal(QCursor::pos()); + + // We move the start a little further so the transition isn't so abrupt. + auto extended = right->rect(); + extended.translate(100, 0); + + // if we are coming from the left, the cursor should + // end up on the second widget. + if (extended.contains(cursorPosition) && + right->size().width() >= + conf::sideBarCollapsePoint + ui::sidebar::NormalSize) { + left->setMinimumWidth(ui::sidebar::NormalSize); + left->setMaximumWidth(2 * ui::sidebar::NormalSize); + + leftMoveCount_ = 0; + } + } + } +} + +void +Splitter::hideSidebar() +{ + auto left = widget(0); + if (left) + left->hide(); +} + +void +Splitter::showChatView() +{ + auto left = widget(0); + auto right = widget(1); + + if (right->isHidden()) { + left->hide(); + right->show(); + + // Restore previous size. + if (left->minimumWidth() == ui::sidebar::SmallSize) { + left->setMinimumWidth(ui::sidebar::SmallSize); + left->setMaximumWidth(ui::sidebar::SmallSize); + } else { + left->setMinimumWidth(ui::sidebar::NormalSize); + left->setMaximumWidth(2 * ui::sidebar::NormalSize); + } + } +} + +void +Splitter::showFullRoomList() +{ + auto left = widget(0); + auto right = widget(1); + + right->hide(); + + left->show(); + left->setMaximumWidth(MaxWidth); +} diff --git a/src/Splitter.h b/src/Splitter.h new file mode 100644 index 00000000..99e02eed --- /dev/null +++ b/src/Splitter.h @@ -0,0 +1,46 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +class Splitter : public QSplitter +{ + Q_OBJECT +public: + explicit Splitter(QWidget *parent = nullptr); + ~Splitter(); + + void restoreSizes(int fallback); + +public slots: + void hideSidebar(); + void showFullRoomList(); + void showChatView(); + +signals: + void hiddenSidebar(); + +private: + void onSplitterMoved(int pos, int index); + + int moveEventLimit_ = 50; + + int leftMoveCount_ = 0; + int rightMoveCount_ = 0; +}; diff --git a/src/SuggestionsPopup.cpp b/src/SuggestionsPopup.cpp index bcfcb233..5ea78460 100644 --- a/src/SuggestionsPopup.cpp +++ b/src/SuggestionsPopup.cpp @@ -1,14 +1,13 @@ -#include "Avatar.h" -#include "AvatarProvider.h" -#include "Config.h" -#include "DropShadow.h" -#include "SuggestionsPopup.hpp" -#include "Utils.h" - #include #include #include +#include "Config.h" +#include "SuggestionsPopup.h" +#include "Utils.h" +#include "ui/Avatar.h" +#include "ui/DropShadow.h" + constexpr int PopupHMargin = 4; constexpr int PopupItemMargin = 3; diff --git a/src/SuggestionsPopup.h b/src/SuggestionsPopup.h new file mode 100644 index 00000000..72d6c7eb --- /dev/null +++ b/src/SuggestionsPopup.h @@ -0,0 +1,147 @@ +#pragma once + +#include +#include +#include +#include + +#include "AvatarProvider.h" +#include "Cache.h" +#include "ChatPage.h" + +class Avatar; +struct SearchResult; + +class PopupItem : public QWidget +{ + Q_OBJECT + + Q_PROPERTY(QColor hoverColor READ hoverColor WRITE setHoverColor) + Q_PROPERTY(bool hovering READ hovering WRITE setHovering) + +public: + PopupItem(QWidget *parent); + + QString selectedText() const { return QString(); } + QColor hoverColor() const { return hoverColor_; } + void setHoverColor(QColor &color) { hoverColor_ = color; } + + bool hovering() const { return hovering_; } + void setHovering(const bool hover) { hovering_ = hover; }; + +protected: + void paintEvent(QPaintEvent *event) override; + +signals: + void clicked(const QString &text); + +protected: + QHBoxLayout *topLayout_; + Avatar *avatar_; + QColor hoverColor_; + + //! Set if the item is currently being + //! hovered during tab completion (cycling). + bool hovering_; +}; + +class UserItem : public PopupItem +{ + Q_OBJECT + +public: + UserItem(QWidget *parent, const QString &user_id); + QString selectedText() const { return userId_; } + void updateItem(const QString &user_id); + +protected: + void mousePressEvent(QMouseEvent *event) override; + +private: + void resolveAvatar(const QString &user_id); + + QLabel *userName_; + QString userId_; +}; + +class RoomItem : public PopupItem +{ + Q_OBJECT + +public: + RoomItem(QWidget *parent, const RoomSearchResult &res); + QString selectedText() const { return roomId_; } + void updateItem(const RoomSearchResult &res); + +protected: + void mousePressEvent(QMouseEvent *event) override; + +private: + QLabel *roomName_; + QString roomId_; + RoomSearchResult info_; +}; + +class SuggestionsPopup : public QWidget +{ + Q_OBJECT + +public: + explicit SuggestionsPopup(QWidget *parent = nullptr); + + template + void selectHoveredSuggestion() + { + const auto item = layout_->itemAt(selectedItem_); + if (!item) + return; + + const auto &widget = qobject_cast(item->widget()); + emit itemSelected( + Cache::displayName(ChatPage::instance()->currentRoom(), widget->selectedText())); + + resetSelection(); + } + +public slots: + void addUsers(const QVector &users); + void addRooms(const std::vector &rooms); + + //! Move to the next available suggestion item. + void selectNextSuggestion(); + //! Move to the previous available suggestion item. + void selectPreviousSuggestion(); + //! Remove hovering from all items. + void resetHovering(); + //! Set hovering to the item in the given layout position. + void setHovering(int pos); + +protected: + void paintEvent(QPaintEvent *event) override; + +signals: + void itemSelected(const QString &user); + +private: + void hoverSelection(); + void resetSelection() { selectedItem_ = -1; } + void selectFirstItem() { selectedItem_ = 0; } + void selectLastItem() { selectedItem_ = layout_->count() - 1; } + void removeLayoutItemsAfter(size_t startingPos) + { + size_t posToRemove = layout_->count() - 1; + + QLayoutItem *item; + while (startingPos <= posToRemove && (item = layout_->takeAt(posToRemove)) != 0) { + delete item->widget(); + delete item; + + posToRemove = layout_->count() - 1; + } + } + + QVBoxLayout *layout_; + + //! Counter for tab completion (cycling). + int selectedItem_ = -1; +}; diff --git a/src/TextInputWidget.cc b/src/TextInputWidget.cc deleted file mode 100644 index bb72c533..00000000 --- a/src/TextInputWidget.cc +++ /dev/null @@ -1,629 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "Cache.h" -#include "ChatPage.h" -#include "Config.h" -#include "TextInputWidget.h" -#include "Utils.h" - -static constexpr size_t INPUT_HISTORY_SIZE = 127; -static constexpr int MAX_TEXTINPUT_HEIGHT = 120; -static constexpr int InputHeight = 26; -static constexpr int ButtonHeight = 24; - -FilteredTextEdit::FilteredTextEdit(QWidget *parent) - : QTextEdit{parent} - , history_index_{0} - , popup_{parent} - , previewDialog_{parent} -{ - setFrameStyle(QFrame::NoFrame); - connect(document()->documentLayout(), - &QAbstractTextDocumentLayout::documentSizeChanged, - this, - &FilteredTextEdit::updateGeometry); - connect(document()->documentLayout(), - &QAbstractTextDocumentLayout::documentSizeChanged, - this, - [this]() { emit heightChanged(document()->size().toSize().height()); }); - working_history_.push_back(""); - connect(this, &QTextEdit::textChanged, this, &FilteredTextEdit::textChanged); - setAcceptRichText(false); - - typingTimer_ = new QTimer(this); - typingTimer_->setInterval(1000); - typingTimer_->setSingleShot(true); - - connect(typingTimer_, &QTimer::timeout, this, &FilteredTextEdit::stopTyping); - connect(&previewDialog_, - &dialogs::PreviewUploadOverlay::confirmUpload, - this, - &FilteredTextEdit::uploadData); - - connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults); - connect(&popup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) { - popup_.hide(); - - auto cursor = textCursor(); - const int end = cursor.position(); - - cursor.setPosition(atTriggerPosition_, QTextCursor::MoveAnchor); - cursor.setPosition(end, QTextCursor::KeepAnchor); - cursor.removeSelectedText(); - cursor.insertText(text); - }); - - // For cycling through the suggestions by hitting tab. - connect(this, - &FilteredTextEdit::selectNextSuggestion, - &popup_, - &SuggestionsPopup::selectNextSuggestion); - connect(this, - &FilteredTextEdit::selectPreviousSuggestion, - &popup_, - &SuggestionsPopup::selectPreviousSuggestion); - connect(this, &FilteredTextEdit::selectHoveredSuggestion, this, [this]() { - popup_.selectHoveredSuggestion(); - }); - - previewDialog_.hide(); -} - -void -FilteredTextEdit::showResults(const QVector &results) -{ - QPoint pos; - - if (atTriggerPosition_ != -1) { - auto cursor = textCursor(); - cursor.setPosition(atTriggerPosition_); - pos = viewport()->mapToGlobal(cursorRect(cursor).topLeft()); - } else { - auto rect = cursorRect(); - pos = viewport()->mapToGlobal(rect.topLeft()); - } - - popup_.addUsers(results); - popup_.move(pos.x(), pos.y() - popup_.height() - 10); - popup_.show(); -} - -void -FilteredTextEdit::keyPressEvent(QKeyEvent *event) -{ - const bool isModifier = (event->modifiers() != Qt::NoModifier); - - if (!isModifier) { - if (!typingTimer_->isActive()) - emit startedTyping(); - - typingTimer_->start(); - } - - // calculate the new query - if (textCursor().position() < atTriggerPosition_ || atTriggerPosition_ == -1) { - resetAnchor(); - closeSuggestions(); - } - - if (popup_.isVisible()) { - switch (event->key()) { - case Qt::Key_Down: - case Qt::Key_Tab: - emit selectNextSuggestion(); - return; - case Qt::Key_Enter: - case Qt::Key_Return: - emit selectHoveredSuggestion(); - return; - case Qt::Key_Escape: - closeSuggestions(); - return; - case Qt::Key_Up: - case Qt::Key_Backtab: { - emit selectPreviousSuggestion(); - return; - } - default: - break; - } - } - - switch (event->key()) { - case Qt::Key_At: - atTriggerPosition_ = textCursor().position(); - - QTextEdit::keyPressEvent(event); - break; - case Qt::Key_Return: - case Qt::Key_Enter: - if (!(event->modifiers() & Qt::ShiftModifier)) { - stopTyping(); - submit(); - } else { - QTextEdit::keyPressEvent(event); - } - break; - case Qt::Key_Up: { - auto initial_cursor = textCursor(); - QTextEdit::keyPressEvent(event); - - if (textCursor() == initial_cursor && textCursor().atStart() && - history_index_ + 1 < working_history_.size()) { - ++history_index_; - setPlainText(working_history_[history_index_]); - moveCursor(QTextCursor::End); - } else if (textCursor() == initial_cursor) { - // Move to the start of the text if there aren't any lines to move up to. - initial_cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor, 1); - setTextCursor(initial_cursor); - } - - break; - } - case Qt::Key_Down: { - auto initial_cursor = textCursor(); - QTextEdit::keyPressEvent(event); - - if (textCursor() == initial_cursor && textCursor().atEnd() && history_index_ > 0) { - --history_index_; - setPlainText(working_history_[history_index_]); - moveCursor(QTextCursor::End); - } else if (textCursor() == initial_cursor) { - // Move to the end of the text if there aren't any lines to move down to. - initial_cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor, 1); - setTextCursor(initial_cursor); - } - - break; - } - default: - QTextEdit::keyPressEvent(event); - - // Check if the current word should be autocompleted. - auto cursor = textCursor(); - cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor); - auto word = cursor.selectedText(); - - if (cursor.position() == 0) { - resetAnchor(); - closeSuggestions(); - return; - } - - if (cursor.position() == atTriggerPosition_ + 1) { - const auto q = query(); - - if (q.isEmpty()) { - closeSuggestions(); - return; - } - - emit showSuggestions(query()); - } else { - resetAnchor(); - closeSuggestions(); - } - - break; - } -} - -bool -FilteredTextEdit::canInsertFromMimeData(const QMimeData *source) const -{ - return (source->hasImage() || QTextEdit::canInsertFromMimeData(source)); -} - -void -FilteredTextEdit::insertFromMimeData(const QMimeData *source) -{ - const auto formats = source->formats().filter("/"); - const auto image = formats.filter("image/", Qt::CaseInsensitive); - const auto audio = formats.filter("audio/", Qt::CaseInsensitive); - const auto video = formats.filter("video/", Qt::CaseInsensitive); - - if (!image.empty()) { - showPreview(source, image); - } else if (!audio.empty()) { - showPreview(source, audio); - } else if (!video.empty()) { - showPreview(source, video); - } else if (source->hasUrls()) { - // Generic file path for any platform. - QString path; - for (auto &&u : source->urls()) { - if (u.isLocalFile()) { - path = u.toLocalFile(); - break; - } - } - - if (!path.isEmpty() && QFileInfo{path}.exists()) { - previewDialog_.setPreview(path); - } else { - qWarning() - << "Clipboard does not contain any valid file paths:" << source->urls(); - } - } else if (source->hasFormat("x-special/gnome-copied-files")) { - // Special case for X11 users. See "Notes for X11 Users" in source. - // Source: http://doc.qt.io/qt-5/qclipboard.html - - // This MIME type returns a string with multiple lines separated by '\n'. The first - // line is the command to perform with the clipboard (not useful to us). The - // following lines are the file URIs. - // - // Source: the nautilus source code in file 'src/nautilus-clipboard.c' in function - // nautilus_clipboard_get_uri_list_from_selection_data() - // https://github.com/GNOME/nautilus/blob/master/src/nautilus-clipboard.c - - auto data = source->data("x-special/gnome-copied-files").split('\n'); - if (data.size() < 2) { - qWarning() << "MIME format is malformed, cannot perform paste."; - return; - } - - QString path; - for (int i = 1; i < data.size(); ++i) { - QUrl url{data[i]}; - if (url.isLocalFile()) { - path = url.toLocalFile(); - break; - } - } - - if (!path.isEmpty()) { - previewDialog_.setPreview(path); - } else { - qWarning() << "Clipboard does not contain any valid file paths:" << data; - } - } else { - QTextEdit::insertFromMimeData(source); - } -} - -void -FilteredTextEdit::stopTyping() -{ - typingTimer_->stop(); - emit stoppedTyping(); -} - -QSize -FilteredTextEdit::sizeHint() const -{ - ensurePolished(); - auto margins = viewportMargins(); - margins += document()->documentMargin(); - QSize size = document()->size().toSize(); - size.rwidth() += margins.left() + margins.right(); - size.rheight() += margins.top() + margins.bottom(); - return size; -} - -QSize -FilteredTextEdit::minimumSizeHint() const -{ - ensurePolished(); - auto margins = viewportMargins(); - margins += document()->documentMargin(); - margins += contentsMargins(); - QSize size(fontMetrics().averageCharWidth() * 10, - fontMetrics().lineSpacing() + margins.top() + margins.bottom()); - return size; -} - -void -FilteredTextEdit::submit() -{ - if (toPlainText().trimmed().isEmpty()) - return; - - if (true_history_.size() == INPUT_HISTORY_SIZE) - true_history_.pop_back(); - true_history_.push_front(toPlainText()); - working_history_ = true_history_; - working_history_.push_front(""); - history_index_ = 0; - - QString text = toPlainText(); - - if (text.startsWith('/')) { - int command_end = text.indexOf(' '); - if (command_end == -1) - command_end = text.size(); - auto name = text.mid(1, command_end - 1); - auto args = text.mid(command_end + 1); - if (name.isEmpty() || name == "/") { - message(args); - } else { - command(name, args); - } - } else { - message(std::move(text)); - } - - clear(); -} - -void -FilteredTextEdit::textChanged() -{ - working_history_[history_index_] = toPlainText(); -} - -void -FilteredTextEdit::uploadData(const QByteArray data, const QString &media, const QString &filename) -{ - QSharedPointer buffer{new QBuffer{this}}; - buffer->setData(data); - - emit startedUpload(); - - if (media == "image") - emit image(buffer, filename); - else if (media == "audio") - emit audio(buffer, filename); - else if (media == "video") - emit video(buffer, filename); - else - emit file(buffer, filename); -} - -void -FilteredTextEdit::showPreview(const QMimeData *source, const QStringList &formats) -{ - // Retrieve data as MIME type. - auto const &mime = formats.first(); - QByteArray data = source->data(mime); - previewDialog_.setPreview(data, mime); -} - -TextInputWidget::TextInputWidget(QWidget *parent) - : QWidget(parent) -{ - setFont(QFont("Emoji One")); - - setFixedHeight(conf::textInput::height); - setCursor(Qt::ArrowCursor); - - topLayout_ = new QHBoxLayout(); - topLayout_->setSpacing(0); - topLayout_->setContentsMargins(15, 0, 15, 0); - - QIcon send_file_icon; - send_file_icon.addFile(":/icons/icons/ui/paper-clip-outline.png"); - - sendFileBtn_ = new FlatButton(this); - sendFileBtn_->setIcon(send_file_icon); - sendFileBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight)); - - spinner_ = new LoadingIndicator(this); - spinner_->setFixedHeight(InputHeight); - spinner_->setFixedWidth(InputHeight); - spinner_->setObjectName("FileUploadSpinner"); - spinner_->hide(); - - QFont font; - font.setPixelSize(conf::textInputFontSize); - - input_ = new FilteredTextEdit(this); - input_->setFixedHeight(InputHeight); - input_->setFont(font); - input_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - input_->setPlaceholderText(tr("Write a message...")); - - connect(input_, &FilteredTextEdit::heightChanged, this, [this](int height) { - int textInputHeight = std::min(MAX_TEXTINPUT_HEIGHT, std::max(height, InputHeight)); - int widgetHeight = - std::min(MAX_TEXTINPUT_HEIGHT, std::max(height, conf::textInput::height)); - - setFixedHeight(widgetHeight); - input_->setFixedHeight(textInputHeight); - }); - connect(input_, &FilteredTextEdit::showSuggestions, this, [this](const QString &q) { - if (q.isEmpty() || !cache::client()) - return; - - QtConcurrent::run([this, q = q.toLower().toStdString()]() { - try { - emit input_->resultsRetrieved(cache::client()->searchUsers( - ChatPage::instance()->currentRoom().toStdString(), q)); - } catch (const lmdb::error &e) { - std::cout << e.what() << '\n'; - } - }); - }); - - sendMessageBtn_ = new FlatButton(this); - - QIcon send_message_icon; - send_message_icon.addFile(":/icons/icons/ui/cursor.png"); - sendMessageBtn_->setIcon(send_message_icon); - sendMessageBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight)); - - emojiBtn_ = new emoji::PickButton(this); - - QIcon emoji_icon; - emoji_icon.addFile(":/icons/icons/ui/smile.png"); - emojiBtn_->setIcon(emoji_icon); - emojiBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight)); - - topLayout_->addWidget(sendFileBtn_); - topLayout_->addWidget(input_); - topLayout_->addWidget(emojiBtn_); - topLayout_->addWidget(sendMessageBtn_); - - setLayout(topLayout_); - - connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit); - connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); - connect(input_, &FilteredTextEdit::message, this, &TextInputWidget::sendTextMessage); - connect(input_, &FilteredTextEdit::command, this, &TextInputWidget::command); - connect(input_, &FilteredTextEdit::image, this, &TextInputWidget::uploadImage); - connect(input_, &FilteredTextEdit::audio, this, &TextInputWidget::uploadAudio); - connect(input_, &FilteredTextEdit::video, this, &TextInputWidget::uploadVideo); - connect(input_, &FilteredTextEdit::file, this, &TextInputWidget::uploadFile); - connect(emojiBtn_, - SIGNAL(emojiSelected(const QString &)), - this, - SLOT(addSelectedEmoji(const QString &))); - - connect(input_, &FilteredTextEdit::startedTyping, this, &TextInputWidget::startedTyping); - - connect(input_, &FilteredTextEdit::stoppedTyping, this, &TextInputWidget::stoppedTyping); - - connect( - input_, &FilteredTextEdit::startedUpload, this, &TextInputWidget::showUploadSpinner); -} - -void -TextInputWidget::addSelectedEmoji(const QString &emoji) -{ - QTextCursor cursor = input_->textCursor(); - - QFont emoji_font("Emoji One"); - emoji_font.setPixelSize(conf::emojiSize); - - QFont text_font("Open Sans"); - text_font.setPixelSize(conf::fontSize); - - QTextCharFormat charfmt; - charfmt.setFont(emoji_font); - input_->setCurrentCharFormat(charfmt); - - input_->insertPlainText(emoji); - cursor.movePosition(QTextCursor::End); - - charfmt.setFont(text_font); - input_->setCurrentCharFormat(charfmt); - - input_->show(); -} - -void -TextInputWidget::command(QString command, QString args) -{ - if (command == "me") { - sendEmoteMessage(args); - } else if (command == "join") { - sendJoinRoomRequest(args); - } else if (command == "shrug") { - sendTextMessage("¯\\_(ツ)_/¯"); - } else if (command == "fliptable") { - sendTextMessage("(╯°□°)╯︵ ┻━┻"); - } -} - -void -TextInputWidget::openFileSelection() -{ - const auto fileName = - QFileDialog::getOpenFileName(this, tr("Select a file"), "", tr("All Files (*)")); - - if (fileName.isEmpty()) - return; - - QMimeDatabase db; - QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent); - - const auto format = mime.name().split("/")[0]; - - QSharedPointer file{new QFile{fileName, this}}; - if (format == "image") - emit uploadImage(file, fileName); - else if (format == "audio") - emit uploadAudio(file, fileName); - else if (format == "video") - emit uploadVideo(file, fileName); - else - emit uploadFile(file, fileName); - - showUploadSpinner(); -} - -void -TextInputWidget::showUploadSpinner() -{ - topLayout_->removeWidget(sendFileBtn_); - sendFileBtn_->hide(); - - topLayout_->insertWidget(0, spinner_); - spinner_->start(); -} - -void -TextInputWidget::hideUploadSpinner() -{ - topLayout_->removeWidget(spinner_); - topLayout_->insertWidget(0, sendFileBtn_); - sendFileBtn_->show(); - spinner_->stop(); -} - -void -TextInputWidget::stopTyping() -{ - input_->stopTyping(); -} - -void -TextInputWidget::focusInEvent(QFocusEvent *event) -{ - input_->setFocus(event->reason()); -} - -void -TextInputWidget::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); - - p.setPen(QPen(borderColor())); - p.drawLine(QPointF(0, 0), QPointF(width(), 0)); -} - -void -TextInputWidget::addReply(const QString &username, const QString &msg) -{ - input_->setText(QString("> %1: %2\n\n").arg(username).arg(msg)); - input_->setFocus(); - - auto cursor = input_->textCursor(); - cursor.movePosition(QTextCursor::End); - input_->setTextCursor(cursor); -} diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp new file mode 100644 index 00000000..a419ed84 --- /dev/null +++ b/src/TextInputWidget.cpp @@ -0,0 +1,631 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "Cache.h" +#include "ChatPage.h" +#include "Config.h" +#include "TextInputWidget.h" +#include "Utils.h" +#include "ui/FlatButton.h" +#include "ui/LoadingIndicator.h" + +static constexpr size_t INPUT_HISTORY_SIZE = 127; +static constexpr int MAX_TEXTINPUT_HEIGHT = 120; +static constexpr int InputHeight = 26; +static constexpr int ButtonHeight = 24; + +FilteredTextEdit::FilteredTextEdit(QWidget *parent) + : QTextEdit{parent} + , history_index_{0} + , popup_{parent} + , previewDialog_{parent} +{ + setFrameStyle(QFrame::NoFrame); + connect(document()->documentLayout(), + &QAbstractTextDocumentLayout::documentSizeChanged, + this, + &FilteredTextEdit::updateGeometry); + connect(document()->documentLayout(), + &QAbstractTextDocumentLayout::documentSizeChanged, + this, + [this]() { emit heightChanged(document()->size().toSize().height()); }); + working_history_.push_back(""); + connect(this, &QTextEdit::textChanged, this, &FilteredTextEdit::textChanged); + setAcceptRichText(false); + + typingTimer_ = new QTimer(this); + typingTimer_->setInterval(1000); + typingTimer_->setSingleShot(true); + + connect(typingTimer_, &QTimer::timeout, this, &FilteredTextEdit::stopTyping); + connect(&previewDialog_, + &dialogs::PreviewUploadOverlay::confirmUpload, + this, + &FilteredTextEdit::uploadData); + + connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults); + connect(&popup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) { + popup_.hide(); + + auto cursor = textCursor(); + const int end = cursor.position(); + + cursor.setPosition(atTriggerPosition_, QTextCursor::MoveAnchor); + cursor.setPosition(end, QTextCursor::KeepAnchor); + cursor.removeSelectedText(); + cursor.insertText(text); + }); + + // For cycling through the suggestions by hitting tab. + connect(this, + &FilteredTextEdit::selectNextSuggestion, + &popup_, + &SuggestionsPopup::selectNextSuggestion); + connect(this, + &FilteredTextEdit::selectPreviousSuggestion, + &popup_, + &SuggestionsPopup::selectPreviousSuggestion); + connect(this, &FilteredTextEdit::selectHoveredSuggestion, this, [this]() { + popup_.selectHoveredSuggestion(); + }); + + previewDialog_.hide(); +} + +void +FilteredTextEdit::showResults(const QVector &results) +{ + QPoint pos; + + if (atTriggerPosition_ != -1) { + auto cursor = textCursor(); + cursor.setPosition(atTriggerPosition_); + pos = viewport()->mapToGlobal(cursorRect(cursor).topLeft()); + } else { + auto rect = cursorRect(); + pos = viewport()->mapToGlobal(rect.topLeft()); + } + + popup_.addUsers(results); + popup_.move(pos.x(), pos.y() - popup_.height() - 10); + popup_.show(); +} + +void +FilteredTextEdit::keyPressEvent(QKeyEvent *event) +{ + const bool isModifier = (event->modifiers() != Qt::NoModifier); + + if (!isModifier) { + if (!typingTimer_->isActive()) + emit startedTyping(); + + typingTimer_->start(); + } + + // calculate the new query + if (textCursor().position() < atTriggerPosition_ || atTriggerPosition_ == -1) { + resetAnchor(); + closeSuggestions(); + } + + if (popup_.isVisible()) { + switch (event->key()) { + case Qt::Key_Down: + case Qt::Key_Tab: + emit selectNextSuggestion(); + return; + case Qt::Key_Enter: + case Qt::Key_Return: + emit selectHoveredSuggestion(); + return; + case Qt::Key_Escape: + closeSuggestions(); + return; + case Qt::Key_Up: + case Qt::Key_Backtab: { + emit selectPreviousSuggestion(); + return; + } + default: + break; + } + } + + switch (event->key()) { + case Qt::Key_At: + atTriggerPosition_ = textCursor().position(); + + QTextEdit::keyPressEvent(event); + break; + case Qt::Key_Return: + case Qt::Key_Enter: + if (!(event->modifiers() & Qt::ShiftModifier)) { + stopTyping(); + submit(); + } else { + QTextEdit::keyPressEvent(event); + } + break; + case Qt::Key_Up: { + auto initial_cursor = textCursor(); + QTextEdit::keyPressEvent(event); + + if (textCursor() == initial_cursor && textCursor().atStart() && + history_index_ + 1 < working_history_.size()) { + ++history_index_; + setPlainText(working_history_[history_index_]); + moveCursor(QTextCursor::End); + } else if (textCursor() == initial_cursor) { + // Move to the start of the text if there aren't any lines to move up to. + initial_cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor, 1); + setTextCursor(initial_cursor); + } + + break; + } + case Qt::Key_Down: { + auto initial_cursor = textCursor(); + QTextEdit::keyPressEvent(event); + + if (textCursor() == initial_cursor && textCursor().atEnd() && history_index_ > 0) { + --history_index_; + setPlainText(working_history_[history_index_]); + moveCursor(QTextCursor::End); + } else if (textCursor() == initial_cursor) { + // Move to the end of the text if there aren't any lines to move down to. + initial_cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor, 1); + setTextCursor(initial_cursor); + } + + break; + } + default: + QTextEdit::keyPressEvent(event); + + // Check if the current word should be autocompleted. + auto cursor = textCursor(); + cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor); + auto word = cursor.selectedText(); + + if (cursor.position() == 0) { + resetAnchor(); + closeSuggestions(); + return; + } + + if (cursor.position() == atTriggerPosition_ + 1) { + const auto q = query(); + + if (q.isEmpty()) { + closeSuggestions(); + return; + } + + emit showSuggestions(query()); + } else { + resetAnchor(); + closeSuggestions(); + } + + break; + } +} + +bool +FilteredTextEdit::canInsertFromMimeData(const QMimeData *source) const +{ + return (source->hasImage() || QTextEdit::canInsertFromMimeData(source)); +} + +void +FilteredTextEdit::insertFromMimeData(const QMimeData *source) +{ + const auto formats = source->formats().filter("/"); + const auto image = formats.filter("image/", Qt::CaseInsensitive); + const auto audio = formats.filter("audio/", Qt::CaseInsensitive); + const auto video = formats.filter("video/", Qt::CaseInsensitive); + + if (!image.empty()) { + showPreview(source, image); + } else if (!audio.empty()) { + showPreview(source, audio); + } else if (!video.empty()) { + showPreview(source, video); + } else if (source->hasUrls()) { + // Generic file path for any platform. + QString path; + for (auto &&u : source->urls()) { + if (u.isLocalFile()) { + path = u.toLocalFile(); + break; + } + } + + if (!path.isEmpty() && QFileInfo{path}.exists()) { + previewDialog_.setPreview(path); + } else { + qWarning() + << "Clipboard does not contain any valid file paths:" << source->urls(); + } + } else if (source->hasFormat("x-special/gnome-copied-files")) { + // Special case for X11 users. See "Notes for X11 Users" in source. + // Source: http://doc.qt.io/qt-5/qclipboard.html + + // This MIME type returns a string with multiple lines separated by '\n'. The first + // line is the command to perform with the clipboard (not useful to us). The + // following lines are the file URIs. + // + // Source: the nautilus source code in file 'src/nautilus-clipboard.c' in function + // nautilus_clipboard_get_uri_list_from_selection_data() + // https://github.com/GNOME/nautilus/blob/master/src/nautilus-clipboard.c + + auto data = source->data("x-special/gnome-copied-files").split('\n'); + if (data.size() < 2) { + qWarning() << "MIME format is malformed, cannot perform paste."; + return; + } + + QString path; + for (int i = 1; i < data.size(); ++i) { + QUrl url{data[i]}; + if (url.isLocalFile()) { + path = url.toLocalFile(); + break; + } + } + + if (!path.isEmpty()) { + previewDialog_.setPreview(path); + } else { + qWarning() << "Clipboard does not contain any valid file paths:" << data; + } + } else { + QTextEdit::insertFromMimeData(source); + } +} + +void +FilteredTextEdit::stopTyping() +{ + typingTimer_->stop(); + emit stoppedTyping(); +} + +QSize +FilteredTextEdit::sizeHint() const +{ + ensurePolished(); + auto margins = viewportMargins(); + margins += document()->documentMargin(); + QSize size = document()->size().toSize(); + size.rwidth() += margins.left() + margins.right(); + size.rheight() += margins.top() + margins.bottom(); + return size; +} + +QSize +FilteredTextEdit::minimumSizeHint() const +{ + ensurePolished(); + auto margins = viewportMargins(); + margins += document()->documentMargin(); + margins += contentsMargins(); + QSize size(fontMetrics().averageCharWidth() * 10, + fontMetrics().lineSpacing() + margins.top() + margins.bottom()); + return size; +} + +void +FilteredTextEdit::submit() +{ + if (toPlainText().trimmed().isEmpty()) + return; + + if (true_history_.size() == INPUT_HISTORY_SIZE) + true_history_.pop_back(); + true_history_.push_front(toPlainText()); + working_history_ = true_history_; + working_history_.push_front(""); + history_index_ = 0; + + QString text = toPlainText(); + + if (text.startsWith('/')) { + int command_end = text.indexOf(' '); + if (command_end == -1) + command_end = text.size(); + auto name = text.mid(1, command_end - 1); + auto args = text.mid(command_end + 1); + if (name.isEmpty() || name == "/") { + message(args); + } else { + command(name, args); + } + } else { + message(std::move(text)); + } + + clear(); +} + +void +FilteredTextEdit::textChanged() +{ + working_history_[history_index_] = toPlainText(); +} + +void +FilteredTextEdit::uploadData(const QByteArray data, const QString &media, const QString &filename) +{ + QSharedPointer buffer{new QBuffer{this}}; + buffer->setData(data); + + emit startedUpload(); + + if (media == "image") + emit image(buffer, filename); + else if (media == "audio") + emit audio(buffer, filename); + else if (media == "video") + emit video(buffer, filename); + else + emit file(buffer, filename); +} + +void +FilteredTextEdit::showPreview(const QMimeData *source, const QStringList &formats) +{ + // Retrieve data as MIME type. + auto const &mime = formats.first(); + QByteArray data = source->data(mime); + previewDialog_.setPreview(data, mime); +} + +TextInputWidget::TextInputWidget(QWidget *parent) + : QWidget(parent) +{ + setFont(QFont("Emoji One")); + + setFixedHeight(conf::textInput::height); + setCursor(Qt::ArrowCursor); + + topLayout_ = new QHBoxLayout(); + topLayout_->setSpacing(0); + topLayout_->setContentsMargins(15, 0, 15, 0); + + QIcon send_file_icon; + send_file_icon.addFile(":/icons/icons/ui/paper-clip-outline.png"); + + sendFileBtn_ = new FlatButton(this); + sendFileBtn_->setIcon(send_file_icon); + sendFileBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight)); + + spinner_ = new LoadingIndicator(this); + spinner_->setFixedHeight(InputHeight); + spinner_->setFixedWidth(InputHeight); + spinner_->setObjectName("FileUploadSpinner"); + spinner_->hide(); + + QFont font; + font.setPixelSize(conf::textInputFontSize); + + input_ = new FilteredTextEdit(this); + input_->setFixedHeight(InputHeight); + input_->setFont(font); + input_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + input_->setPlaceholderText(tr("Write a message...")); + + connect(input_, &FilteredTextEdit::heightChanged, this, [this](int height) { + int textInputHeight = std::min(MAX_TEXTINPUT_HEIGHT, std::max(height, InputHeight)); + int widgetHeight = + std::min(MAX_TEXTINPUT_HEIGHT, std::max(height, conf::textInput::height)); + + setFixedHeight(widgetHeight); + input_->setFixedHeight(textInputHeight); + }); + connect(input_, &FilteredTextEdit::showSuggestions, this, [this](const QString &q) { + if (q.isEmpty() || !cache::client()) + return; + + QtConcurrent::run([this, q = q.toLower().toStdString()]() { + try { + emit input_->resultsRetrieved(cache::client()->searchUsers( + ChatPage::instance()->currentRoom().toStdString(), q)); + } catch (const lmdb::error &e) { + std::cout << e.what() << '\n'; + } + }); + }); + + sendMessageBtn_ = new FlatButton(this); + + QIcon send_message_icon; + send_message_icon.addFile(":/icons/icons/ui/cursor.png"); + sendMessageBtn_->setIcon(send_message_icon); + sendMessageBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight)); + + emojiBtn_ = new emoji::PickButton(this); + + QIcon emoji_icon; + emoji_icon.addFile(":/icons/icons/ui/smile.png"); + emojiBtn_->setIcon(emoji_icon); + emojiBtn_->setIconSize(QSize(ButtonHeight, ButtonHeight)); + + topLayout_->addWidget(sendFileBtn_); + topLayout_->addWidget(input_); + topLayout_->addWidget(emojiBtn_); + topLayout_->addWidget(sendMessageBtn_); + + setLayout(topLayout_); + + connect(sendMessageBtn_, &FlatButton::clicked, input_, &FilteredTextEdit::submit); + connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); + connect(input_, &FilteredTextEdit::message, this, &TextInputWidget::sendTextMessage); + connect(input_, &FilteredTextEdit::command, this, &TextInputWidget::command); + connect(input_, &FilteredTextEdit::image, this, &TextInputWidget::uploadImage); + connect(input_, &FilteredTextEdit::audio, this, &TextInputWidget::uploadAudio); + connect(input_, &FilteredTextEdit::video, this, &TextInputWidget::uploadVideo); + connect(input_, &FilteredTextEdit::file, this, &TextInputWidget::uploadFile); + connect(emojiBtn_, + SIGNAL(emojiSelected(const QString &)), + this, + SLOT(addSelectedEmoji(const QString &))); + + connect(input_, &FilteredTextEdit::startedTyping, this, &TextInputWidget::startedTyping); + + connect(input_, &FilteredTextEdit::stoppedTyping, this, &TextInputWidget::stoppedTyping); + + connect( + input_, &FilteredTextEdit::startedUpload, this, &TextInputWidget::showUploadSpinner); +} + +void +TextInputWidget::addSelectedEmoji(const QString &emoji) +{ + QTextCursor cursor = input_->textCursor(); + + QFont emoji_font("Emoji One"); + emoji_font.setPixelSize(conf::emojiSize); + + QFont text_font("Open Sans"); + text_font.setPixelSize(conf::fontSize); + + QTextCharFormat charfmt; + charfmt.setFont(emoji_font); + input_->setCurrentCharFormat(charfmt); + + input_->insertPlainText(emoji); + cursor.movePosition(QTextCursor::End); + + charfmt.setFont(text_font); + input_->setCurrentCharFormat(charfmt); + + input_->show(); +} + +void +TextInputWidget::command(QString command, QString args) +{ + if (command == "me") { + sendEmoteMessage(args); + } else if (command == "join") { + sendJoinRoomRequest(args); + } else if (command == "shrug") { + sendTextMessage("¯\\_(ツ)_/¯"); + } else if (command == "fliptable") { + sendTextMessage("(╯°□°)╯︵ ┻━┻"); + } +} + +void +TextInputWidget::openFileSelection() +{ + const auto fileName = + QFileDialog::getOpenFileName(this, tr("Select a file"), "", tr("All Files (*)")); + + if (fileName.isEmpty()) + return; + + QMimeDatabase db; + QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent); + + const auto format = mime.name().split("/")[0]; + + QSharedPointer file{new QFile{fileName, this}}; + if (format == "image") + emit uploadImage(file, fileName); + else if (format == "audio") + emit uploadAudio(file, fileName); + else if (format == "video") + emit uploadVideo(file, fileName); + else + emit uploadFile(file, fileName); + + showUploadSpinner(); +} + +void +TextInputWidget::showUploadSpinner() +{ + topLayout_->removeWidget(sendFileBtn_); + sendFileBtn_->hide(); + + topLayout_->insertWidget(0, spinner_); + spinner_->start(); +} + +void +TextInputWidget::hideUploadSpinner() +{ + topLayout_->removeWidget(spinner_); + topLayout_->insertWidget(0, sendFileBtn_); + sendFileBtn_->show(); + spinner_->stop(); +} + +void +TextInputWidget::stopTyping() +{ + input_->stopTyping(); +} + +void +TextInputWidget::focusInEvent(QFocusEvent *event) +{ + input_->setFocus(event->reason()); +} + +void +TextInputWidget::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + + p.setPen(QPen(borderColor())); + p.drawLine(QPointF(0, 0), QPointF(width(), 0)); +} + +void +TextInputWidget::addReply(const QString &username, const QString &msg) +{ + input_->setText(QString("> %1: %2\n\n").arg(username).arg(msg)); + input_->setFocus(); + + auto cursor = input_->textCursor(); + cursor.movePosition(QTextCursor::End); + input_->setTextCursor(cursor); +} diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h new file mode 100644 index 00000000..e7d5f948 --- /dev/null +++ b/src/TextInputWidget.h @@ -0,0 +1,183 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "SuggestionsPopup.h" +#include "dialogs/PreviewUploadOverlay.h" +#include "emoji/PickButton.h" + +namespace dialogs { +class PreviewUploadOverlay; +} + +struct SearchResult; + +class FlatButton; +class LoadingIndicator; + +class FilteredTextEdit : public QTextEdit +{ + Q_OBJECT + +public: + explicit FilteredTextEdit(QWidget *parent = nullptr); + + void stopTyping(); + + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + + void submit(); + +signals: + void heightChanged(int height); + void startedTyping(); + void stoppedTyping(); + void startedUpload(); + void message(QString); + void command(QString name, QString args); + void image(QSharedPointer data, const QString &filename); + void audio(QSharedPointer data, const QString &filename); + void video(QSharedPointer data, const QString &filename); + void file(QSharedPointer data, const QString &filename); + + //! Trigger the suggestion popup. + void showSuggestions(const QString &query); + void resultsRetrieved(const QVector &results); + void selectNextSuggestion(); + void selectPreviousSuggestion(); + void selectHoveredSuggestion(); + +public slots: + void showResults(const QVector &results); + +protected: + void keyPressEvent(QKeyEvent *event) override; + bool canInsertFromMimeData(const QMimeData *source) const override; + void insertFromMimeData(const QMimeData *source) override; + void focusOutEvent(QFocusEvent *event) override + { + popup_.hide(); + QTextEdit::focusOutEvent(event); + } + +private: + std::deque true_history_, working_history_; + size_t history_index_; + QTimer *typingTimer_; + + SuggestionsPopup popup_; + + void closeSuggestions() { popup_.hide(); } + void resetAnchor() { atTriggerPosition_ = -1; } + + QString query() + { + auto cursor = textCursor(); + cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor); + return cursor.selectedText(); + } + + dialogs::PreviewUploadOverlay previewDialog_; + + //! Latest position of the '@' character that triggers the username completer. + int atTriggerPosition_ = -1; + + void textChanged(); + void uploadData(const QByteArray data, const QString &media, const QString &filename); + void afterCompletion(int); + void showPreview(const QMimeData *source, const QStringList &formats); +}; + +class TextInputWidget : public QWidget +{ + Q_OBJECT + + Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor) + +public: + TextInputWidget(QWidget *parent = 0); + + void stopTyping(); + + QColor borderColor() const { return borderColor_; } + void setBorderColor(QColor &color) { borderColor_ = color; } + void disableInput() + { + input_->setEnabled(false); + input_->setPlaceholderText(tr("Connection lost. Nheko is trying to re-connect...")); + } + void enableInput() + { + input_->setEnabled(true); + input_->setPlaceholderText(tr("Write a message...")); + } + +public slots: + void openFileSelection(); + void hideUploadSpinner(); + void focusLineEdit() { input_->setFocus(); } + void addReply(const QString &username, const QString &msg); + +private slots: + void addSelectedEmoji(const QString &emoji); + +signals: + void sendTextMessage(QString msg); + void sendEmoteMessage(QString msg); + + void uploadImage(const QSharedPointer data, const QString &filename); + void uploadFile(const QSharedPointer data, const QString &filename); + void uploadAudio(const QSharedPointer data, const QString &filename); + void uploadVideo(const QSharedPointer data, const QString &filename); + + void sendJoinRoomRequest(const QString &room); + + void startedTyping(); + void stoppedTyping(); + +protected: + void focusInEvent(QFocusEvent *event) override; + void paintEvent(QPaintEvent *) override; + +private: + void showUploadSpinner(); + void command(QString name, QString args); + + QHBoxLayout *topLayout_; + FilteredTextEdit *input_; + + LoadingIndicator *spinner_; + + FlatButton *sendFileBtn_; + FlatButton *sendMessageBtn_; + emoji::PickButton *emojiBtn_; + + QColor borderColor_; +}; diff --git a/src/TopRoomBar.cc b/src/TopRoomBar.cc deleted file mode 100644 index 7b2814b9..00000000 --- a/src/TopRoomBar.cc +++ /dev/null @@ -1,184 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include - -#include "Avatar.h" -#include "Config.h" -#include "FlatButton.h" -#include "MainWindow.h" -#include "Menu.h" -#include "OverlayModal.h" -#include "TopRoomBar.h" -#include "Utils.h" - -TopRoomBar::TopRoomBar(QWidget *parent) - : QWidget(parent) - , buttonSize_{32} -{ - setFixedHeight(60); - - topLayout_ = new QHBoxLayout(this); - topLayout_->setSpacing(8); - topLayout_->setMargin(8); - - avatar_ = new Avatar(this); - avatar_->setLetter(""); - avatar_->setSize(35); - - textLayout_ = new QVBoxLayout(); - textLayout_->setSpacing(0); - textLayout_->setContentsMargins(0, 0, 0, 0); - - QFont roomFont("Open Sans SemiBold"); - roomFont.setPixelSize(conf::topRoomBar::fonts::roomName); - - nameLabel_ = new QLabel(this); - nameLabel_->setFont(roomFont); - nameLabel_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); - - QFont descriptionFont("Open Sans"); - descriptionFont.setPixelSize(conf::topRoomBar::fonts::roomDescription); - - topicLabel_ = new QLabel(this); - topicLabel_->setFont(descriptionFont); - topicLabel_->setTextInteractionFlags(Qt::TextBrowserInteraction); - topicLabel_->setOpenExternalLinks(true); - topicLabel_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); - - textLayout_->addWidget(nameLabel_); - textLayout_->addWidget(topicLabel_); - - settingsBtn_ = new FlatButton(this); - settingsBtn_->setFixedSize(buttonSize_, buttonSize_); - settingsBtn_->setCornerRadius(buttonSize_ / 2); - - QIcon settings_icon; - settings_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png"); - settingsBtn_->setIcon(settings_icon); - settingsBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2)); - - backBtn_ = new FlatButton(this); - backBtn_->setFixedSize(buttonSize_, buttonSize_); - backBtn_->setCornerRadius(buttonSize_ / 2); - - QIcon backIcon; - backIcon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); - backBtn_->setIcon(backIcon); - backBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2)); - backBtn_->hide(); - - connect(backBtn_, &QPushButton::clicked, this, &TopRoomBar::showRoomList); - - topLayout_->addWidget(avatar_); - topLayout_->addWidget(backBtn_); - topLayout_->addLayout(textLayout_, 1); - topLayout_->addWidget(settingsBtn_, 0, Qt::AlignRight); - - menu_ = new Menu(this); - - inviteUsers_ = new QAction(tr("Invite users"), this); - connect(inviteUsers_, &QAction::triggered, this, [this]() { - MainWindow::instance()->openInviteUsersDialog( - [this](const QStringList &invitees) { emit inviteUsers(invitees); }); - }); - - roomMembers_ = new QAction(tr("Members"), this); - connect(roomMembers_, &QAction::triggered, this, []() { - MainWindow::instance()->openMemberListDialog(); - }); - - leaveRoom_ = new QAction(tr("Leave room"), this); - connect(leaveRoom_, &QAction::triggered, this, []() { - MainWindow::instance()->openLeaveRoomDialog(); - }); - - roomSettings_ = new QAction(tr("Settings"), this); - connect(roomSettings_, &QAction::triggered, this, []() { - MainWindow::instance()->openRoomSettings(); - }); - - menu_->addAction(inviteUsers_); - menu_->addAction(roomMembers_); - menu_->addAction(leaveRoom_); - menu_->addAction(roomSettings_); - - connect(settingsBtn_, &QPushButton::clicked, this, [this]() { - auto pos = mapToGlobal(settingsBtn_->pos()); - menu_->popup( - QPoint(pos.x() + buttonSize_ - menu_->sizeHint().width(), pos.y() + buttonSize_)); - }); -} - -void -TopRoomBar::enableBackButton() -{ - avatar_->hide(); - backBtn_->show(); -} - -void -TopRoomBar::disableBackButton() -{ - avatar_->show(); - backBtn_->hide(); -} - -void -TopRoomBar::updateRoomAvatarFromName(const QString &name) -{ - avatar_->setLetter(utils::firstChar(name)); - update(); -} - -void -TopRoomBar::reset() -{ - nameLabel_->setText(""); - topicLabel_->setText(""); - avatar_->setLetter(""); -} - -void -TopRoomBar::updateRoomAvatar(const QImage &avatar_image) -{ - avatar_->setImage(avatar_image); - update(); -} - -void -TopRoomBar::updateRoomAvatar(const QIcon &icon) -{ - avatar_->setIcon(icon); - update(); -} - -void -TopRoomBar::updateRoomName(const QString &name) -{ - nameLabel_->setText(name); - update(); -} - -void -TopRoomBar::updateRoomTopic(QString topic) -{ - topic.replace(conf::strings::url_regex, conf::strings::url_html); - topicLabel_->setText(topic); - update(); -} diff --git a/src/TopRoomBar.cpp b/src/TopRoomBar.cpp new file mode 100644 index 00000000..c9609788 --- /dev/null +++ b/src/TopRoomBar.cpp @@ -0,0 +1,184 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "Config.h" +#include "MainWindow.h" +#include "TopRoomBar.h" +#include "Utils.h" +#include "ui/Avatar.h" +#include "ui/FlatButton.h" +#include "ui/Menu.h" +#include "ui/OverlayModal.h" + +TopRoomBar::TopRoomBar(QWidget *parent) + : QWidget(parent) + , buttonSize_{32} +{ + setFixedHeight(60); + + topLayout_ = new QHBoxLayout(this); + topLayout_->setSpacing(8); + topLayout_->setMargin(8); + + avatar_ = new Avatar(this); + avatar_->setLetter(""); + avatar_->setSize(35); + + textLayout_ = new QVBoxLayout(); + textLayout_->setSpacing(0); + textLayout_->setContentsMargins(0, 0, 0, 0); + + QFont roomFont("Open Sans SemiBold"); + roomFont.setPixelSize(conf::topRoomBar::fonts::roomName); + + nameLabel_ = new QLabel(this); + nameLabel_->setFont(roomFont); + nameLabel_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); + + QFont descriptionFont("Open Sans"); + descriptionFont.setPixelSize(conf::topRoomBar::fonts::roomDescription); + + topicLabel_ = new QLabel(this); + topicLabel_->setFont(descriptionFont); + topicLabel_->setTextInteractionFlags(Qt::TextBrowserInteraction); + topicLabel_->setOpenExternalLinks(true); + topicLabel_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); + + textLayout_->addWidget(nameLabel_); + textLayout_->addWidget(topicLabel_); + + settingsBtn_ = new FlatButton(this); + settingsBtn_->setFixedSize(buttonSize_, buttonSize_); + settingsBtn_->setCornerRadius(buttonSize_ / 2); + + QIcon settings_icon; + settings_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png"); + settingsBtn_->setIcon(settings_icon); + settingsBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2)); + + backBtn_ = new FlatButton(this); + backBtn_->setFixedSize(buttonSize_, buttonSize_); + backBtn_->setCornerRadius(buttonSize_ / 2); + + QIcon backIcon; + backIcon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); + backBtn_->setIcon(backIcon); + backBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2)); + backBtn_->hide(); + + connect(backBtn_, &QPushButton::clicked, this, &TopRoomBar::showRoomList); + + topLayout_->addWidget(avatar_); + topLayout_->addWidget(backBtn_); + topLayout_->addLayout(textLayout_, 1); + topLayout_->addWidget(settingsBtn_, 0, Qt::AlignRight); + + menu_ = new Menu(this); + + inviteUsers_ = new QAction(tr("Invite users"), this); + connect(inviteUsers_, &QAction::triggered, this, [this]() { + MainWindow::instance()->openInviteUsersDialog( + [this](const QStringList &invitees) { emit inviteUsers(invitees); }); + }); + + roomMembers_ = new QAction(tr("Members"), this); + connect(roomMembers_, &QAction::triggered, this, []() { + MainWindow::instance()->openMemberListDialog(); + }); + + leaveRoom_ = new QAction(tr("Leave room"), this); + connect(leaveRoom_, &QAction::triggered, this, []() { + MainWindow::instance()->openLeaveRoomDialog(); + }); + + roomSettings_ = new QAction(tr("Settings"), this); + connect(roomSettings_, &QAction::triggered, this, []() { + MainWindow::instance()->openRoomSettings(); + }); + + menu_->addAction(inviteUsers_); + menu_->addAction(roomMembers_); + menu_->addAction(leaveRoom_); + menu_->addAction(roomSettings_); + + connect(settingsBtn_, &QPushButton::clicked, this, [this]() { + auto pos = mapToGlobal(settingsBtn_->pos()); + menu_->popup( + QPoint(pos.x() + buttonSize_ - menu_->sizeHint().width(), pos.y() + buttonSize_)); + }); +} + +void +TopRoomBar::enableBackButton() +{ + avatar_->hide(); + backBtn_->show(); +} + +void +TopRoomBar::disableBackButton() +{ + avatar_->show(); + backBtn_->hide(); +} + +void +TopRoomBar::updateRoomAvatarFromName(const QString &name) +{ + avatar_->setLetter(utils::firstChar(name)); + update(); +} + +void +TopRoomBar::reset() +{ + nameLabel_->setText(""); + topicLabel_->setText(""); + avatar_->setLetter(""); +} + +void +TopRoomBar::updateRoomAvatar(const QImage &avatar_image) +{ + avatar_->setImage(avatar_image); + update(); +} + +void +TopRoomBar::updateRoomAvatar(const QIcon &icon) +{ + avatar_->setIcon(icon); + update(); +} + +void +TopRoomBar::updateRoomName(const QString &name) +{ + nameLabel_->setText(name); + update(); +} + +void +TopRoomBar::updateRoomTopic(QString topic) +{ + topic.replace(conf::strings::url_regex, conf::strings::url_html); + topicLabel_->setText(topic); + update(); +} diff --git a/src/TopRoomBar.h b/src/TopRoomBar.h new file mode 100644 index 00000000..1c42e25f --- /dev/null +++ b/src/TopRoomBar.h @@ -0,0 +1,107 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class Avatar; +class FlatButton; +class Menu; +class OverlayModal; + +class TopRoomBar : public QWidget +{ + Q_OBJECT + + Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor) + +public: + TopRoomBar(QWidget *parent = 0); + + void updateRoomAvatar(const QImage &avatar_image); + void updateRoomAvatar(const QIcon &icon); + void updateRoomName(const QString &name); + void updateRoomTopic(QString topic); + void updateRoomAvatarFromName(const QString &name); + + void reset(); + + QColor borderColor() const { return borderColor_; } + void setBorderColor(QColor &color) { borderColor_ = color; } + +public slots: + //! Add a "back-arrow" button that can switch to roomlist only view. + void enableBackButton(); + //! Replace the "back-arrow" button with the avatar of the room. + void disableBackButton(); + +signals: + void inviteUsers(QStringList users); + void showRoomList(); + +protected: + void mousePressEvent(QMouseEvent *) override + { + if (roomSettings_ != nullptr) + roomSettings_->trigger(); + } + + void paintEvent(QPaintEvent *) override + { + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + + p.setPen(QPen(borderColor())); + p.drawLine(QPointF(0, height() - p.pen().widthF()), + QPointF(width(), height() - p.pen().widthF())); + } + +private: + QHBoxLayout *topLayout_ = nullptr; + QVBoxLayout *textLayout_ = nullptr; + + QLabel *nameLabel_ = nullptr; + QLabel *topicLabel_ = nullptr; + + Menu *menu_; + QAction *leaveRoom_ = nullptr; + QAction *roomMembers_ = nullptr; + QAction *roomSettings_ = nullptr; + QAction *inviteUsers_ = nullptr; + + FlatButton *settingsBtn_; + FlatButton *backBtn_; + + Avatar *avatar_; + + int buttonSize_; + + QColor borderColor_; +}; diff --git a/src/TrayIcon.cc b/src/TrayIcon.cc deleted file mode 100644 index ac84aaca..00000000 --- a/src/TrayIcon.cc +++ /dev/null @@ -1,153 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include - -#include "TrayIcon.h" - -#if defined(Q_OS_MAC) -#include -#endif - -MsgCountComposedIcon::MsgCountComposedIcon(const QString &filename) - : QIconEngine() -{ - icon_ = QIcon(filename); -} - -void -MsgCountComposedIcon::paint(QPainter *painter, - const QRect &rect, - QIcon::Mode mode, - QIcon::State state) -{ - painter->setRenderHint(QPainter::TextAntialiasing); - painter->setRenderHint(QPainter::SmoothPixmapTransform); - painter->setRenderHint(QPainter::Antialiasing); - - icon_.paint(painter, rect, Qt::AlignCenter, mode, state); - - if (msgCount <= 0) - return; - - QColor backgroundColor("red"); - QColor textColor("white"); - - QBrush brush; - brush.setStyle(Qt::SolidPattern); - brush.setColor(backgroundColor); - - painter->setBrush(brush); - painter->setPen(Qt::NoPen); - painter->setFont(QFont("Open Sans", 8, QFont::Black)); - - QRectF bubble(rect.width() - BubbleDiameter, - rect.height() - BubbleDiameter, - BubbleDiameter, - BubbleDiameter); - painter->drawEllipse(bubble); - painter->setPen(QPen(textColor)); - painter->setBrush(Qt::NoBrush); - painter->drawText(bubble, Qt::AlignCenter, QString::number(msgCount)); -} - -QIconEngine * -MsgCountComposedIcon::clone() const -{ - return new MsgCountComposedIcon(*this); -} - -QList -MsgCountComposedIcon::availableSizes(QIcon::Mode mode, QIcon::State state) const -{ - Q_UNUSED(mode); - Q_UNUSED(state); - QList sizes; - sizes.append(QSize(24, 24)); - sizes.append(QSize(32, 32)); - sizes.append(QSize(48, 48)); - sizes.append(QSize(64, 64)); - sizes.append(QSize(128, 128)); - sizes.append(QSize(256, 256)); - return sizes; -} - -QPixmap -MsgCountComposedIcon::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) -{ - QImage img(size, QImage::Format_ARGB32); - img.fill(qRgba(0, 0, 0, 0)); - QPixmap result = QPixmap::fromImage(img, Qt::NoFormatConversion); - { - QPainter painter(&result); - paint(&painter, QRect(QPoint(0, 0), size), mode, state); - } - return result; -} - -TrayIcon::TrayIcon(const QString &filename, QWidget *parent) - : QSystemTrayIcon(parent) -{ -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) - setIcon(QIcon(filename)); -#else - icon_ = new MsgCountComposedIcon(filename); - setIcon(QIcon(icon_)); -#endif - - QMenu *menu = new QMenu(parent); - viewAction_ = new QAction(tr("Show"), parent); - quitAction_ = new QAction(tr("Quit"), parent); - - connect(viewAction_, SIGNAL(triggered()), parent, SLOT(show())); - connect(quitAction_, &QAction::triggered, this, QApplication::quit); - - menu->addAction(viewAction_); - menu->addAction(quitAction_); - - setContextMenu(menu); -} - -void -TrayIcon::setUnreadCount(int count) -{ -// Use the native badge counter in MacOS. -#if defined(Q_OS_MAC) - auto labelText = count == 0 ? "" : QString::number(count); - - if (labelText == QtMac::badgeLabelText()) - return; - - QtMac::setBadgeLabelText(labelText); -#elif defined(Q_OS_WIN) -// FIXME: Find a way to use Windows apis for the badge counter (if any). -#else - if (count == icon_->msgCount) - return; - - // Custom drawing on Linux. - MsgCountComposedIcon *tmp = static_cast(icon_->clone()); - tmp->msgCount = count; - - setIcon(QIcon(tmp)); - - icon_ = tmp; -#endif -} diff --git a/src/TrayIcon.cpp b/src/TrayIcon.cpp new file mode 100644 index 00000000..ac84aaca --- /dev/null +++ b/src/TrayIcon.cpp @@ -0,0 +1,153 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include + +#include "TrayIcon.h" + +#if defined(Q_OS_MAC) +#include +#endif + +MsgCountComposedIcon::MsgCountComposedIcon(const QString &filename) + : QIconEngine() +{ + icon_ = QIcon(filename); +} + +void +MsgCountComposedIcon::paint(QPainter *painter, + const QRect &rect, + QIcon::Mode mode, + QIcon::State state) +{ + painter->setRenderHint(QPainter::TextAntialiasing); + painter->setRenderHint(QPainter::SmoothPixmapTransform); + painter->setRenderHint(QPainter::Antialiasing); + + icon_.paint(painter, rect, Qt::AlignCenter, mode, state); + + if (msgCount <= 0) + return; + + QColor backgroundColor("red"); + QColor textColor("white"); + + QBrush brush; + brush.setStyle(Qt::SolidPattern); + brush.setColor(backgroundColor); + + painter->setBrush(brush); + painter->setPen(Qt::NoPen); + painter->setFont(QFont("Open Sans", 8, QFont::Black)); + + QRectF bubble(rect.width() - BubbleDiameter, + rect.height() - BubbleDiameter, + BubbleDiameter, + BubbleDiameter); + painter->drawEllipse(bubble); + painter->setPen(QPen(textColor)); + painter->setBrush(Qt::NoBrush); + painter->drawText(bubble, Qt::AlignCenter, QString::number(msgCount)); +} + +QIconEngine * +MsgCountComposedIcon::clone() const +{ + return new MsgCountComposedIcon(*this); +} + +QList +MsgCountComposedIcon::availableSizes(QIcon::Mode mode, QIcon::State state) const +{ + Q_UNUSED(mode); + Q_UNUSED(state); + QList sizes; + sizes.append(QSize(24, 24)); + sizes.append(QSize(32, 32)); + sizes.append(QSize(48, 48)); + sizes.append(QSize(64, 64)); + sizes.append(QSize(128, 128)); + sizes.append(QSize(256, 256)); + return sizes; +} + +QPixmap +MsgCountComposedIcon::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) +{ + QImage img(size, QImage::Format_ARGB32); + img.fill(qRgba(0, 0, 0, 0)); + QPixmap result = QPixmap::fromImage(img, Qt::NoFormatConversion); + { + QPainter painter(&result); + paint(&painter, QRect(QPoint(0, 0), size), mode, state); + } + return result; +} + +TrayIcon::TrayIcon(const QString &filename, QWidget *parent) + : QSystemTrayIcon(parent) +{ +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) + setIcon(QIcon(filename)); +#else + icon_ = new MsgCountComposedIcon(filename); + setIcon(QIcon(icon_)); +#endif + + QMenu *menu = new QMenu(parent); + viewAction_ = new QAction(tr("Show"), parent); + quitAction_ = new QAction(tr("Quit"), parent); + + connect(viewAction_, SIGNAL(triggered()), parent, SLOT(show())); + connect(quitAction_, &QAction::triggered, this, QApplication::quit); + + menu->addAction(viewAction_); + menu->addAction(quitAction_); + + setContextMenu(menu); +} + +void +TrayIcon::setUnreadCount(int count) +{ +// Use the native badge counter in MacOS. +#if defined(Q_OS_MAC) + auto labelText = count == 0 ? "" : QString::number(count); + + if (labelText == QtMac::badgeLabelText()) + return; + + QtMac::setBadgeLabelText(labelText); +#elif defined(Q_OS_WIN) +// FIXME: Find a way to use Windows apis for the badge counter (if any). +#else + if (count == icon_->msgCount) + return; + + // Custom drawing on Linux. + MsgCountComposedIcon *tmp = static_cast(icon_->clone()); + tmp->msgCount = count; + + setIcon(QIcon(tmp)); + + icon_ = tmp; +#endif +} diff --git a/src/TrayIcon.h b/src/TrayIcon.h new file mode 100644 index 00000000..a3536cc3 --- /dev/null +++ b/src/TrayIcon.h @@ -0,0 +1,59 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +class MsgCountComposedIcon : public QIconEngine +{ +public: + MsgCountComposedIcon(const QString &filename); + + virtual void paint(QPainter *p, const QRect &rect, QIcon::Mode mode, QIcon::State state); + virtual QIconEngine *clone() const; + virtual QList availableSizes(QIcon::Mode mode, QIcon::State state) const; + virtual QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state); + + int msgCount = 0; + +private: + const int BubbleDiameter = 17; + + QIcon icon_; +}; + +class TrayIcon : public QSystemTrayIcon +{ + Q_OBJECT +public: + TrayIcon(const QString &filename, QWidget *parent); + +public slots: + void setUnreadCount(int count); + +private: + QAction *viewAction_; + QAction *quitAction_; + + MsgCountComposedIcon *icon_; +}; diff --git a/src/TypingDisplay.cc b/src/TypingDisplay.cc deleted file mode 100644 index da9c1679..00000000 --- a/src/TypingDisplay.cc +++ /dev/null @@ -1,54 +0,0 @@ -#include -#include - -#include "Config.h" -#include "TypingDisplay.h" - -TypingDisplay::TypingDisplay(QWidget *parent) - : QWidget(parent) - , leftPadding_{24} -{ - QFont font; - font.setPixelSize(conf::typingNotificationFontSize); - - setFixedHeight(QFontMetrics(font).height() + 2); -} - -void -TypingDisplay::setUsers(const QStringList &uid) -{ - if (uid.isEmpty()) - text_.clear(); - else - text_ = uid.join(", "); - - if (uid.size() == 1) - text_ += tr(" is typing"); - else if (uid.size() > 1) - text_ += tr(" are typing"); - - update(); -} - -void -TypingDisplay::paintEvent(QPaintEvent *) -{ - QPen pen(QColor("#898989")); - - QFont font("Open Sans Bold"); - font.setPixelSize(conf::typingNotificationFontSize); - font.setItalic(true); - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - p.setFont(font); - p.setPen(pen); - - QRect region = rect(); - region.translate(leftPadding_, 0); - - QFontMetrics fm(font); - text_ = fm.elidedText(text_, Qt::ElideRight, width() - 3 * leftPadding_); - - p.drawText(region, Qt::AlignVCenter, text_); -} diff --git a/src/TypingDisplay.cpp b/src/TypingDisplay.cpp new file mode 100644 index 00000000..da9c1679 --- /dev/null +++ b/src/TypingDisplay.cpp @@ -0,0 +1,54 @@ +#include +#include + +#include "Config.h" +#include "TypingDisplay.h" + +TypingDisplay::TypingDisplay(QWidget *parent) + : QWidget(parent) + , leftPadding_{24} +{ + QFont font; + font.setPixelSize(conf::typingNotificationFontSize); + + setFixedHeight(QFontMetrics(font).height() + 2); +} + +void +TypingDisplay::setUsers(const QStringList &uid) +{ + if (uid.isEmpty()) + text_.clear(); + else + text_ = uid.join(", "); + + if (uid.size() == 1) + text_ += tr(" is typing"); + else if (uid.size() > 1) + text_ += tr(" are typing"); + + update(); +} + +void +TypingDisplay::paintEvent(QPaintEvent *) +{ + QPen pen(QColor("#898989")); + + QFont font("Open Sans Bold"); + font.setPixelSize(conf::typingNotificationFontSize); + font.setItalic(true); + + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + p.setFont(font); + p.setPen(pen); + + QRect region = rect(); + region.translate(leftPadding_, 0); + + QFontMetrics fm(font); + text_ = fm.elidedText(text_, Qt::ElideRight, width() - 3 * leftPadding_); + + p.drawText(region, Qt::AlignVCenter, text_); +} diff --git a/src/TypingDisplay.h b/src/TypingDisplay.h new file mode 100644 index 00000000..db8a9519 --- /dev/null +++ b/src/TypingDisplay.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +class TypingDisplay : public QWidget +{ + Q_OBJECT + +public: + TypingDisplay(QWidget *parent = nullptr); + + void setUsers(const QStringList &user_ids); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + QString text_; + int leftPadding_; +}; diff --git a/src/UserInfoWidget.cc b/src/UserInfoWidget.cc deleted file mode 100644 index 092184f7..00000000 --- a/src/UserInfoWidget.cc +++ /dev/null @@ -1,165 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include - -#include "Avatar.h" -#include "Config.h" -#include "FlatButton.h" -#include "MainWindow.h" -#include "OverlayModal.h" -#include "UserInfoWidget.h" - -UserInfoWidget::UserInfoWidget(QWidget *parent) - : QWidget(parent) - , display_name_("User") - , user_id_("@user:homeserver.org") - , logoutButtonSize_{20} -{ - setFixedHeight(60); - - topLayout_ = new QHBoxLayout(this); - topLayout_->setSpacing(0); - topLayout_->setMargin(5); - - avatarLayout_ = new QHBoxLayout(); - textLayout_ = new QVBoxLayout(); - - userAvatar_ = new Avatar(this); - userAvatar_->setObjectName("userAvatar"); - userAvatar_->setLetter(QChar('?')); - userAvatar_->setSize(45); - - QFont nameFont("Open Sans SemiBold"); - nameFont.setPixelSize(conf::userInfoWidget::fonts::displayName); - - displayNameLabel_ = new QLabel(this); - displayNameLabel_->setFont(nameFont); - displayNameLabel_->setObjectName("displayNameLabel"); - displayNameLabel_->setStyleSheet("padding: 0 9px; margin-bottom: -10px;"); - displayNameLabel_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignTop); - - QFont useridFont("Open Sans"); - useridFont.setPixelSize(conf::userInfoWidget::fonts::userid); - - userIdLabel_ = new QLabel(this); - userIdLabel_->setFont(useridFont); - userIdLabel_->setObjectName("userIdLabel"); - userIdLabel_->setStyleSheet("padding: 0 8px 8px 8px;"); - userIdLabel_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignVCenter); - - avatarLayout_->addWidget(userAvatar_); - textLayout_->addWidget(displayNameLabel_); - textLayout_->addWidget(userIdLabel_); - - topLayout_->addLayout(avatarLayout_); - topLayout_->addLayout(textLayout_); - topLayout_->addStretch(1); - - buttonLayout_ = new QHBoxLayout(); - buttonLayout_->setSpacing(0); - buttonLayout_->setMargin(0); - - logoutButton_ = new FlatButton(this); - logoutButton_->setCornerRadius(logoutButtonSize_ / 2); - - QIcon icon; - icon.addFile(":/icons/icons/ui/power-button-off.png"); - - logoutButton_->setIcon(icon); - logoutButton_->setIconSize(QSize(logoutButtonSize_, logoutButtonSize_)); - - buttonLayout_->addWidget(logoutButton_); - - topLayout_->addLayout(buttonLayout_); - - // Show the confirmation dialog. - connect(logoutButton_, &QPushButton::clicked, this, [this]() { - MainWindow::instance()->openLogoutDialog([this]() { emit logout(); }); - }); -} - -void -UserInfoWidget::resizeEvent(QResizeEvent *event) -{ - Q_UNUSED(event); - - if (width() <= ui::sidebar::SmallSize) { - topLayout_->setContentsMargins(0, 0, logoutButtonSize_ / 2 - 5 / 2, 0); - - userAvatar_->hide(); - displayNameLabel_->hide(); - userIdLabel_->hide(); - } else { - topLayout_->setMargin(5); - userAvatar_->show(); - displayNameLabel_->show(); - userIdLabel_->show(); - } - - QWidget::resizeEvent(event); -} - -void -UserInfoWidget::reset() -{ - displayNameLabel_->setText(""); - userIdLabel_->setText(""); - userAvatar_->setLetter(QChar('?')); -} - -void -UserInfoWidget::setAvatar(const QImage &img) -{ - avatar_image_ = img; - userAvatar_->setImage(img); - update(); -} - -void -UserInfoWidget::setDisplayName(const QString &name) -{ - if (name.isEmpty()) - display_name_ = user_id_.split(':')[0].split('@')[1]; - else - display_name_ = name; - - displayNameLabel_->setText(display_name_); - userAvatar_->setLetter(QChar(display_name_[0])); - update(); -} - -void -UserInfoWidget::setUserId(const QString &userid) -{ - user_id_ = userid; - userIdLabel_->setText(userid); -} - -void -UserInfoWidget::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); - - p.setPen(QPen(borderColor())); - p.drawLine(QPointF(0, height()), QPointF(width(), height())); -} diff --git a/src/UserInfoWidget.cpp b/src/UserInfoWidget.cpp new file mode 100644 index 00000000..1470fc25 --- /dev/null +++ b/src/UserInfoWidget.cpp @@ -0,0 +1,165 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include "Config.h" +#include "MainWindow.h" +#include "UserInfoWidget.h" +#include "ui/Avatar.h" +#include "ui/FlatButton.h" +#include "ui/OverlayModal.h" + +UserInfoWidget::UserInfoWidget(QWidget *parent) + : QWidget(parent) + , display_name_("User") + , user_id_("@user:homeserver.org") + , logoutButtonSize_{20} +{ + setFixedHeight(60); + + topLayout_ = new QHBoxLayout(this); + topLayout_->setSpacing(0); + topLayout_->setMargin(5); + + avatarLayout_ = new QHBoxLayout(); + textLayout_ = new QVBoxLayout(); + + userAvatar_ = new Avatar(this); + userAvatar_->setObjectName("userAvatar"); + userAvatar_->setLetter(QChar('?')); + userAvatar_->setSize(45); + + QFont nameFont("Open Sans SemiBold"); + nameFont.setPixelSize(conf::userInfoWidget::fonts::displayName); + + displayNameLabel_ = new QLabel(this); + displayNameLabel_->setFont(nameFont); + displayNameLabel_->setObjectName("displayNameLabel"); + displayNameLabel_->setStyleSheet("padding: 0 9px; margin-bottom: -10px;"); + displayNameLabel_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignTop); + + QFont useridFont("Open Sans"); + useridFont.setPixelSize(conf::userInfoWidget::fonts::userid); + + userIdLabel_ = new QLabel(this); + userIdLabel_->setFont(useridFont); + userIdLabel_->setObjectName("userIdLabel"); + userIdLabel_->setStyleSheet("padding: 0 8px 8px 8px;"); + userIdLabel_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignVCenter); + + avatarLayout_->addWidget(userAvatar_); + textLayout_->addWidget(displayNameLabel_); + textLayout_->addWidget(userIdLabel_); + + topLayout_->addLayout(avatarLayout_); + topLayout_->addLayout(textLayout_); + topLayout_->addStretch(1); + + buttonLayout_ = new QHBoxLayout(); + buttonLayout_->setSpacing(0); + buttonLayout_->setMargin(0); + + logoutButton_ = new FlatButton(this); + logoutButton_->setCornerRadius(logoutButtonSize_ / 2); + + QIcon icon; + icon.addFile(":/icons/icons/ui/power-button-off.png"); + + logoutButton_->setIcon(icon); + logoutButton_->setIconSize(QSize(logoutButtonSize_, logoutButtonSize_)); + + buttonLayout_->addWidget(logoutButton_); + + topLayout_->addLayout(buttonLayout_); + + // Show the confirmation dialog. + connect(logoutButton_, &QPushButton::clicked, this, [this]() { + MainWindow::instance()->openLogoutDialog([this]() { emit logout(); }); + }); +} + +void +UserInfoWidget::resizeEvent(QResizeEvent *event) +{ + Q_UNUSED(event); + + if (width() <= ui::sidebar::SmallSize) { + topLayout_->setContentsMargins(0, 0, logoutButtonSize_ / 2 - 5 / 2, 0); + + userAvatar_->hide(); + displayNameLabel_->hide(); + userIdLabel_->hide(); + } else { + topLayout_->setMargin(5); + userAvatar_->show(); + displayNameLabel_->show(); + userIdLabel_->show(); + } + + QWidget::resizeEvent(event); +} + +void +UserInfoWidget::reset() +{ + displayNameLabel_->setText(""); + userIdLabel_->setText(""); + userAvatar_->setLetter(QChar('?')); +} + +void +UserInfoWidget::setAvatar(const QImage &img) +{ + avatar_image_ = img; + userAvatar_->setImage(img); + update(); +} + +void +UserInfoWidget::setDisplayName(const QString &name) +{ + if (name.isEmpty()) + display_name_ = user_id_.split(':')[0].split('@')[1]; + else + display_name_ = name; + + displayNameLabel_->setText(display_name_); + userAvatar_->setLetter(QChar(display_name_[0])); + update(); +} + +void +UserInfoWidget::setUserId(const QString &userid) +{ + user_id_ = userid; + userIdLabel_->setText(userid); +} + +void +UserInfoWidget::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + + p.setPen(QPen(borderColor())); + p.drawLine(QPointF(0, height()), QPointF(width(), height())); +} diff --git a/src/UserInfoWidget.h b/src/UserInfoWidget.h new file mode 100644 index 00000000..ea2d5400 --- /dev/null +++ b/src/UserInfoWidget.h @@ -0,0 +1,73 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +class Avatar; +class FlatButton; +class OverlayModal; + +class UserInfoWidget : public QWidget +{ + Q_OBJECT + + Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor) + +public: + UserInfoWidget(QWidget *parent = 0); + + void setAvatar(const QImage &img); + void setDisplayName(const QString &name); + void setUserId(const QString &userid); + + void reset(); + + QColor borderColor() const { return borderColor_; } + void setBorderColor(QColor &color) { borderColor_ = color; } + +signals: + void logout(); + +protected: + void resizeEvent(QResizeEvent *event) override; + void paintEvent(QPaintEvent *event) override; + +private: + Avatar *userAvatar_; + + QHBoxLayout *topLayout_; + QHBoxLayout *avatarLayout_; + QVBoxLayout *textLayout_; + QHBoxLayout *buttonLayout_; + + FlatButton *logoutButton_; + + QLabel *displayNameLabel_; + QLabel *userIdLabel_; + + QString display_name_; + QString user_id_; + + QImage avatar_image_; + + int logoutButtonSize_; + + QColor borderColor_; +}; diff --git a/src/UserSettingsPage.cc b/src/UserSettingsPage.cc deleted file mode 100644 index 7354e413..00000000 --- a/src/UserSettingsPage.cc +++ /dev/null @@ -1,323 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include - -#include "Config.h" -#include "FlatButton.h" -#include "UserSettingsPage.h" -#include - -#include "version.hpp" - -UserSettings::UserSettings() { load(); } - -void -UserSettings::load() -{ - QSettings settings; - isTrayEnabled_ = settings.value("user/window/tray", true).toBool(); - isStartInTrayEnabled_ = settings.value("user/window/start_in_tray", false).toBool(); - isOrderingEnabled_ = settings.value("user/room_ordering", true).toBool(); - isGroupViewEnabled_ = settings.value("user/group_view", true).toBool(); - isTypingNotificationsEnabled_ = settings.value("user/typing_notifications", true).toBool(); - isReadReceiptsEnabled_ = settings.value("user/read_receipts", true).toBool(); - theme_ = settings.value("user/theme", "light").toString(); - - applyTheme(); -} - -void -UserSettings::setTheme(QString theme) -{ - theme_ = theme; - save(); - applyTheme(); -} - -void -UserSettings::applyTheme() -{ - QFile stylefile; - QPalette pal; - - if (theme() == "light") { - stylefile.setFileName(":/styles/styles/nheko.qss"); - pal.setColor(QPalette::Link, QColor("#333")); - } else if (theme() == "dark") { - stylefile.setFileName(":/styles/styles/nheko-dark.qss"); - pal.setColor(QPalette::Link, QColor("#d7d9dc")); - } else { - stylefile.setFileName(":/styles/styles/system.qss"); - } - - stylefile.open(QFile::ReadOnly); - QString stylesheet = QString(stylefile.readAll()); - - QApplication::setPalette(pal); - qobject_cast(QApplication::instance())->setStyleSheet(stylesheet); -} - -void -UserSettings::save() -{ - QSettings settings; - settings.beginGroup("user"); - - settings.beginGroup("window"); - settings.setValue("tray", isTrayEnabled_); - settings.setValue("start_in_tray", isStartInTrayEnabled_); - settings.endGroup(); - - settings.setValue("room_ordering", isOrderingEnabled_); - settings.setValue("typing_notifications", isTypingNotificationsEnabled_); - settings.setValue("read_receipts", isReadReceiptsEnabled_); - settings.setValue("group_view", isGroupViewEnabled_); - settings.setValue("theme", theme()); - settings.endGroup(); -} - -HorizontalLine::HorizontalLine(QWidget *parent) - : QFrame{parent} -{ - setFrameShape(QFrame::HLine); - setFrameShadow(QFrame::Sunken); -} - -UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidget *parent) - : QWidget{parent} - , settings_{settings} -{ - topLayout_ = new QVBoxLayout(this); - - QIcon icon; - icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); - - auto backBtn_ = new FlatButton(this); - backBtn_->setMinimumSize(QSize(24, 24)); - backBtn_->setIcon(icon); - backBtn_->setIconSize(QSize(24, 24)); - - auto heading_ = new QLabel(tr("User Settings")); - heading_->setStyleSheet("font-weight: bold; font-size: 22px;"); - - auto versionInfo = new QLabel(QString("%1 | %2").arg(nheko::version).arg(nheko::build_os)); - versionInfo->setTextInteractionFlags(Qt::TextBrowserInteraction); - - topBarLayout_ = new QHBoxLayout; - topBarLayout_->setSpacing(0); - topBarLayout_->setMargin(0); - topBarLayout_->addWidget(backBtn_, 1, Qt::AlignLeft | Qt::AlignVCenter); - topBarLayout_->addWidget(heading_, 0, Qt::AlignBottom); - topBarLayout_->addStretch(1); - - auto trayOptionLayout_ = new QHBoxLayout; - trayOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto trayLabel = new QLabel(tr("Minimize to tray"), this); - trayToggle_ = new Toggle(this); - trayLabel->setStyleSheet("font-size: 15px;"); - - trayOptionLayout_->addWidget(trayLabel); - trayOptionLayout_->addWidget(trayToggle_, 0, Qt::AlignBottom | Qt::AlignRight); - - auto startInTrayOptionLayout_ = new QHBoxLayout; - startInTrayOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto startInTrayLabel = new QLabel(tr("Start in tray"), this); - startInTrayToggle_ = new Toggle(this); - if (!settings_->isTrayEnabled()) - startInTrayToggle_->setDisabled(true); - startInTrayLabel->setStyleSheet("font-size: 15px;"); - - startInTrayOptionLayout_->addWidget(startInTrayLabel); - startInTrayOptionLayout_->addWidget( - startInTrayToggle_, 0, Qt::AlignBottom | Qt::AlignRight); - - auto orderRoomLayout = new QHBoxLayout; - orderRoomLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto orderLabel = new QLabel(tr("Re-order rooms based on activity"), this); - roomOrderToggle_ = new Toggle(this); - orderLabel->setStyleSheet("font-size: 15px;"); - - orderRoomLayout->addWidget(orderLabel); - orderRoomLayout->addWidget(roomOrderToggle_, 0, Qt::AlignBottom | Qt::AlignRight); - - auto groupViewLayout = new QHBoxLayout; - groupViewLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto groupViewLabel = new QLabel(tr("Group's sidebar"), this); - groupViewToggle_ = new Toggle(this); - groupViewLabel->setStyleSheet("font-size: 15px;"); - - groupViewLayout->addWidget(groupViewLabel); - groupViewLayout->addWidget(groupViewToggle_, 0, Qt::AlignBottom | Qt::AlignRight); - - auto typingLayout = new QHBoxLayout; - typingLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto typingLabel = new QLabel(tr("Typing notifications"), this); - typingNotifications_ = new Toggle(this); - typingLabel->setStyleSheet("font-size: 15px;"); - - typingLayout->addWidget(typingLabel); - typingLayout->addWidget(typingNotifications_, 0, Qt::AlignBottom | Qt::AlignRight); - - auto receiptsLayout = new QHBoxLayout; - receiptsLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto receiptsLabel = new QLabel(tr("Read receipts"), this); - readReceipts_ = new Toggle(this); - receiptsLabel->setStyleSheet("font-size: 15px;"); - - receiptsLayout->addWidget(receiptsLabel); - receiptsLayout->addWidget(readReceipts_, 0, Qt::AlignBottom | Qt::AlignRight); - - auto themeOptionLayout_ = new QHBoxLayout; - themeOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto themeLabel_ = new QLabel(tr("Theme"), this); - themeCombo_ = new QComboBox(this); - themeCombo_->addItem("Light"); - themeCombo_->addItem("Dark"); - themeCombo_->addItem("System"); - themeLabel_->setStyleSheet("font-size: 15px;"); - - themeOptionLayout_->addWidget(themeLabel_); - themeOptionLayout_->addWidget(themeCombo_, 0, Qt::AlignBottom | Qt::AlignRight); - - auto general_ = new QLabel(tr("GENERAL"), this); - general_->setStyleSheet("font-size: 17px"); - - mainLayout_ = new QVBoxLayout; - mainLayout_->setSpacing(7); - mainLayout_->setContentsMargins( - sideMargin_, LayoutTopMargin, sideMargin_, LayoutBottomMargin); - mainLayout_->addWidget(general_, 1, Qt::AlignLeft | Qt::AlignVCenter); - mainLayout_->addWidget(new HorizontalLine(this)); - mainLayout_->addLayout(trayOptionLayout_); - mainLayout_->addLayout(startInTrayOptionLayout_); - mainLayout_->addWidget(new HorizontalLine(this)); - mainLayout_->addLayout(orderRoomLayout); - mainLayout_->addWidget(new HorizontalLine(this)); - mainLayout_->addLayout(groupViewLayout); - mainLayout_->addWidget(new HorizontalLine(this)); - mainLayout_->addLayout(typingLayout); - mainLayout_->addLayout(receiptsLayout); - mainLayout_->addWidget(new HorizontalLine(this)); - mainLayout_->addLayout(themeOptionLayout_); - mainLayout_->addWidget(new HorizontalLine(this)); - - auto scrollArea_ = new QScrollArea(this); - scrollArea_->setFrameShape(QFrame::NoFrame); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); - scrollArea_->setWidgetResizable(true); - scrollArea_->setAlignment(Qt::AlignTop | Qt::AlignVCenter); - - auto scrollAreaContents_ = new QWidget(this); - scrollAreaContents_->setObjectName("UserSettingScrollWidget"); - scrollAreaContents_->setLayout(mainLayout_); - - scrollArea_->setWidget(scrollAreaContents_); - topLayout_->addLayout(topBarLayout_); - topLayout_->addWidget(scrollArea_); - topLayout_->addWidget(versionInfo); - - connect(themeCombo_, - static_cast(&QComboBox::activated), - [this](const QString &text) { settings_->setTheme(text.toLower()); }); - - connect(trayToggle_, &Toggle::toggled, this, [this](bool isDisabled) { - settings_->setTray(!isDisabled); - if (isDisabled) { - startInTrayToggle_->setDisabled(true); - } else { - startInTrayToggle_->setEnabled(true); - } - emit trayOptionChanged(!isDisabled); - }); - - connect(startInTrayToggle_, &Toggle::toggled, this, [this](bool isDisabled) { - settings_->setStartInTray(!isDisabled); - }); - - connect(roomOrderToggle_, &Toggle::toggled, this, [this](bool isDisabled) { - settings_->setRoomOrdering(!isDisabled); - }); - - connect(groupViewToggle_, &Toggle::toggled, this, [this](bool isDisabled) { - settings_->setGroupView(!isDisabled); - }); - - connect(typingNotifications_, &Toggle::toggled, this, [this](bool isDisabled) { - settings_->setTypingNotifications(!isDisabled); - }); - - connect(readReceipts_, &Toggle::toggled, this, [this](bool isDisabled) { - settings_->setReadReceipts(!isDisabled); - }); - - connect(backBtn_, &QPushButton::clicked, this, [this]() { - settings_->save(); - emit moveBack(); - }); -} - -void -UserSettingsPage::showEvent(QShowEvent *) -{ - restoreThemeCombo(); - - // FIXME: Toggle treats true as "off" - trayToggle_->setState(!settings_->isTrayEnabled()); - startInTrayToggle_->setState(!settings_->isStartInTrayEnabled()); - roomOrderToggle_->setState(!settings_->isOrderingEnabled()); - groupViewToggle_->setState(!settings_->isGroupViewEnabled()); - typingNotifications_->setState(!settings_->isTypingNotificationsEnabled()); - readReceipts_->setState(!settings_->isReadReceiptsEnabled()); -} - -void -UserSettingsPage::resizeEvent(QResizeEvent *event) -{ - sideMargin_ = width() * 0.2; - mainLayout_->setContentsMargins( - sideMargin_, LayoutTopMargin, sideMargin_, LayoutBottomMargin); - - QWidget::resizeEvent(event); -} - -void -UserSettingsPage::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -UserSettingsPage::restoreThemeCombo() const -{ - if (settings_->theme() == "light") - themeCombo_->setCurrentIndex(0); - else if (settings_->theme() == "dark") - themeCombo_->setCurrentIndex(1); - else - themeCombo_->setCurrentIndex(2); -} diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp new file mode 100644 index 00000000..4c249369 --- /dev/null +++ b/src/UserSettingsPage.cpp @@ -0,0 +1,323 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "Config.h" +#include "UserSettingsPage.h" +#include "ui/FlatButton.h" +#include "ui/ToggleButton.h" + +#include "version.h" + +UserSettings::UserSettings() { load(); } + +void +UserSettings::load() +{ + QSettings settings; + isTrayEnabled_ = settings.value("user/window/tray", true).toBool(); + isStartInTrayEnabled_ = settings.value("user/window/start_in_tray", false).toBool(); + isOrderingEnabled_ = settings.value("user/room_ordering", true).toBool(); + isGroupViewEnabled_ = settings.value("user/group_view", true).toBool(); + isTypingNotificationsEnabled_ = settings.value("user/typing_notifications", true).toBool(); + isReadReceiptsEnabled_ = settings.value("user/read_receipts", true).toBool(); + theme_ = settings.value("user/theme", "light").toString(); + + applyTheme(); +} + +void +UserSettings::setTheme(QString theme) +{ + theme_ = theme; + save(); + applyTheme(); +} + +void +UserSettings::applyTheme() +{ + QFile stylefile; + QPalette pal; + + if (theme() == "light") { + stylefile.setFileName(":/styles/styles/nheko.qss"); + pal.setColor(QPalette::Link, QColor("#333")); + } else if (theme() == "dark") { + stylefile.setFileName(":/styles/styles/nheko-dark.qss"); + pal.setColor(QPalette::Link, QColor("#d7d9dc")); + } else { + stylefile.setFileName(":/styles/styles/system.qss"); + } + + stylefile.open(QFile::ReadOnly); + QString stylesheet = QString(stylefile.readAll()); + + QApplication::setPalette(pal); + qobject_cast(QApplication::instance())->setStyleSheet(stylesheet); +} + +void +UserSettings::save() +{ + QSettings settings; + settings.beginGroup("user"); + + settings.beginGroup("window"); + settings.setValue("tray", isTrayEnabled_); + settings.setValue("start_in_tray", isStartInTrayEnabled_); + settings.endGroup(); + + settings.setValue("room_ordering", isOrderingEnabled_); + settings.setValue("typing_notifications", isTypingNotificationsEnabled_); + settings.setValue("read_receipts", isReadReceiptsEnabled_); + settings.setValue("group_view", isGroupViewEnabled_); + settings.setValue("theme", theme()); + settings.endGroup(); +} + +HorizontalLine::HorizontalLine(QWidget *parent) + : QFrame{parent} +{ + setFrameShape(QFrame::HLine); + setFrameShadow(QFrame::Sunken); +} + +UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidget *parent) + : QWidget{parent} + , settings_{settings} +{ + topLayout_ = new QVBoxLayout(this); + + QIcon icon; + icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); + + auto backBtn_ = new FlatButton(this); + backBtn_->setMinimumSize(QSize(24, 24)); + backBtn_->setIcon(icon); + backBtn_->setIconSize(QSize(24, 24)); + + auto heading_ = new QLabel(tr("User Settings")); + heading_->setStyleSheet("font-weight: bold; font-size: 22px;"); + + auto versionInfo = new QLabel(QString("%1 | %2").arg(nheko::version).arg(nheko::build_os)); + versionInfo->setTextInteractionFlags(Qt::TextBrowserInteraction); + + topBarLayout_ = new QHBoxLayout; + topBarLayout_->setSpacing(0); + topBarLayout_->setMargin(0); + topBarLayout_->addWidget(backBtn_, 1, Qt::AlignLeft | Qt::AlignVCenter); + topBarLayout_->addWidget(heading_, 0, Qt::AlignBottom); + topBarLayout_->addStretch(1); + + auto trayOptionLayout_ = new QHBoxLayout; + trayOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); + auto trayLabel = new QLabel(tr("Minimize to tray"), this); + trayToggle_ = new Toggle(this); + trayLabel->setStyleSheet("font-size: 15px;"); + + trayOptionLayout_->addWidget(trayLabel); + trayOptionLayout_->addWidget(trayToggle_, 0, Qt::AlignBottom | Qt::AlignRight); + + auto startInTrayOptionLayout_ = new QHBoxLayout; + startInTrayOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); + auto startInTrayLabel = new QLabel(tr("Start in tray"), this); + startInTrayToggle_ = new Toggle(this); + if (!settings_->isTrayEnabled()) + startInTrayToggle_->setDisabled(true); + startInTrayLabel->setStyleSheet("font-size: 15px;"); + + startInTrayOptionLayout_->addWidget(startInTrayLabel); + startInTrayOptionLayout_->addWidget( + startInTrayToggle_, 0, Qt::AlignBottom | Qt::AlignRight); + + auto orderRoomLayout = new QHBoxLayout; + orderRoomLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); + auto orderLabel = new QLabel(tr("Re-order rooms based on activity"), this); + roomOrderToggle_ = new Toggle(this); + orderLabel->setStyleSheet("font-size: 15px;"); + + orderRoomLayout->addWidget(orderLabel); + orderRoomLayout->addWidget(roomOrderToggle_, 0, Qt::AlignBottom | Qt::AlignRight); + + auto groupViewLayout = new QHBoxLayout; + groupViewLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); + auto groupViewLabel = new QLabel(tr("Group's sidebar"), this); + groupViewToggle_ = new Toggle(this); + groupViewLabel->setStyleSheet("font-size: 15px;"); + + groupViewLayout->addWidget(groupViewLabel); + groupViewLayout->addWidget(groupViewToggle_, 0, Qt::AlignBottom | Qt::AlignRight); + + auto typingLayout = new QHBoxLayout; + typingLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); + auto typingLabel = new QLabel(tr("Typing notifications"), this); + typingNotifications_ = new Toggle(this); + typingLabel->setStyleSheet("font-size: 15px;"); + + typingLayout->addWidget(typingLabel); + typingLayout->addWidget(typingNotifications_, 0, Qt::AlignBottom | Qt::AlignRight); + + auto receiptsLayout = new QHBoxLayout; + receiptsLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); + auto receiptsLabel = new QLabel(tr("Read receipts"), this); + readReceipts_ = new Toggle(this); + receiptsLabel->setStyleSheet("font-size: 15px;"); + + receiptsLayout->addWidget(receiptsLabel); + receiptsLayout->addWidget(readReceipts_, 0, Qt::AlignBottom | Qt::AlignRight); + + auto themeOptionLayout_ = new QHBoxLayout; + themeOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); + auto themeLabel_ = new QLabel(tr("Theme"), this); + themeCombo_ = new QComboBox(this); + themeCombo_->addItem("Light"); + themeCombo_->addItem("Dark"); + themeCombo_->addItem("System"); + themeLabel_->setStyleSheet("font-size: 15px;"); + + themeOptionLayout_->addWidget(themeLabel_); + themeOptionLayout_->addWidget(themeCombo_, 0, Qt::AlignBottom | Qt::AlignRight); + + auto general_ = new QLabel(tr("GENERAL"), this); + general_->setStyleSheet("font-size: 17px"); + + mainLayout_ = new QVBoxLayout; + mainLayout_->setSpacing(7); + mainLayout_->setContentsMargins( + sideMargin_, LayoutTopMargin, sideMargin_, LayoutBottomMargin); + mainLayout_->addWidget(general_, 1, Qt::AlignLeft | Qt::AlignVCenter); + mainLayout_->addWidget(new HorizontalLine(this)); + mainLayout_->addLayout(trayOptionLayout_); + mainLayout_->addLayout(startInTrayOptionLayout_); + mainLayout_->addWidget(new HorizontalLine(this)); + mainLayout_->addLayout(orderRoomLayout); + mainLayout_->addWidget(new HorizontalLine(this)); + mainLayout_->addLayout(groupViewLayout); + mainLayout_->addWidget(new HorizontalLine(this)); + mainLayout_->addLayout(typingLayout); + mainLayout_->addLayout(receiptsLayout); + mainLayout_->addWidget(new HorizontalLine(this)); + mainLayout_->addLayout(themeOptionLayout_); + mainLayout_->addWidget(new HorizontalLine(this)); + + auto scrollArea_ = new QScrollArea(this); + scrollArea_->setFrameShape(QFrame::NoFrame); + scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + scrollArea_->setWidgetResizable(true); + scrollArea_->setAlignment(Qt::AlignTop | Qt::AlignVCenter); + + auto scrollAreaContents_ = new QWidget(this); + scrollAreaContents_->setObjectName("UserSettingScrollWidget"); + scrollAreaContents_->setLayout(mainLayout_); + + scrollArea_->setWidget(scrollAreaContents_); + topLayout_->addLayout(topBarLayout_); + topLayout_->addWidget(scrollArea_); + topLayout_->addWidget(versionInfo); + + connect(themeCombo_, + static_cast(&QComboBox::activated), + [this](const QString &text) { settings_->setTheme(text.toLower()); }); + + connect(trayToggle_, &Toggle::toggled, this, [this](bool isDisabled) { + settings_->setTray(!isDisabled); + if (isDisabled) { + startInTrayToggle_->setDisabled(true); + } else { + startInTrayToggle_->setEnabled(true); + } + emit trayOptionChanged(!isDisabled); + }); + + connect(startInTrayToggle_, &Toggle::toggled, this, [this](bool isDisabled) { + settings_->setStartInTray(!isDisabled); + }); + + connect(roomOrderToggle_, &Toggle::toggled, this, [this](bool isDisabled) { + settings_->setRoomOrdering(!isDisabled); + }); + + connect(groupViewToggle_, &Toggle::toggled, this, [this](bool isDisabled) { + settings_->setGroupView(!isDisabled); + }); + + connect(typingNotifications_, &Toggle::toggled, this, [this](bool isDisabled) { + settings_->setTypingNotifications(!isDisabled); + }); + + connect(readReceipts_, &Toggle::toggled, this, [this](bool isDisabled) { + settings_->setReadReceipts(!isDisabled); + }); + + connect(backBtn_, &QPushButton::clicked, this, [this]() { + settings_->save(); + emit moveBack(); + }); +} + +void +UserSettingsPage::showEvent(QShowEvent *) +{ + restoreThemeCombo(); + + // FIXME: Toggle treats true as "off" + trayToggle_->setState(!settings_->isTrayEnabled()); + startInTrayToggle_->setState(!settings_->isStartInTrayEnabled()); + roomOrderToggle_->setState(!settings_->isOrderingEnabled()); + groupViewToggle_->setState(!settings_->isGroupViewEnabled()); + typingNotifications_->setState(!settings_->isTypingNotificationsEnabled()); + readReceipts_->setState(!settings_->isReadReceiptsEnabled()); +} + +void +UserSettingsPage::resizeEvent(QResizeEvent *event) +{ + sideMargin_ = width() * 0.2; + mainLayout_->setContentsMargins( + sideMargin_, LayoutTopMargin, sideMargin_, LayoutBottomMargin); + + QWidget::resizeEvent(event); +} + +void +UserSettingsPage::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} + +void +UserSettingsPage::restoreThemeCombo() const +{ + if (settings_->theme() == "light") + themeCombo_->setCurrentIndex(0); + else if (settings_->theme() == "dark") + themeCombo_->setCurrentIndex(1); + else + themeCombo_->setCurrentIndex(2); +} diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h new file mode 100644 index 00000000..177f1921 --- /dev/null +++ b/src/UserSettingsPage.h @@ -0,0 +1,148 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include + +class Toggle; + +constexpr int OptionMargin = 6; +constexpr int LayoutTopMargin = 50; +constexpr int LayoutBottomMargin = LayoutTopMargin; + +class UserSettings : public QObject +{ + Q_OBJECT + +public: + UserSettings(); + + void save(); + void load(); + void applyTheme(); + void setTheme(QString theme); + void setTray(bool state) + { + isTrayEnabled_ = state; + save(); + }; + + void setStartInTray(bool state) + { + isStartInTrayEnabled_ = state; + save(); + }; + + void setRoomOrdering(bool state) + { + isOrderingEnabled_ = state; + save(); + }; + + void setGroupView(bool state) + { + if (isGroupViewEnabled_ != state) + emit groupViewStateChanged(state); + + isGroupViewEnabled_ = state; + save(); + }; + + void setReadReceipts(bool state) + { + isReadReceiptsEnabled_ = state; + save(); + } + + void setTypingNotifications(bool state) + { + isTypingNotificationsEnabled_ = state; + save(); + }; + + QString theme() const { return !theme_.isEmpty() ? theme_ : "light"; } + bool isTrayEnabled() const { return isTrayEnabled_; } + bool isStartInTrayEnabled() const { return isStartInTrayEnabled_; } + bool isOrderingEnabled() const { return isOrderingEnabled_; } + bool isGroupViewEnabled() const { return isGroupViewEnabled_; } + bool isTypingNotificationsEnabled() const { return isTypingNotificationsEnabled_; } + bool isReadReceiptsEnabled() const { return isReadReceiptsEnabled_; } + +signals: + void groupViewStateChanged(bool state); + +private: + QString theme_; + bool isTrayEnabled_; + bool isStartInTrayEnabled_; + bool isOrderingEnabled_; + bool isGroupViewEnabled_; + bool isTypingNotificationsEnabled_; + bool isReadReceiptsEnabled_; +}; + +class HorizontalLine : public QFrame +{ + Q_OBJECT + +public: + HorizontalLine(QWidget *parent = nullptr); +}; + +class UserSettingsPage : public QWidget +{ + Q_OBJECT + +public: + UserSettingsPage(QSharedPointer settings, QWidget *parent = 0); + +protected: + void showEvent(QShowEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void paintEvent(QPaintEvent *event) override; + +signals: + void moveBack(); + void trayOptionChanged(bool value); + +private: + void restoreThemeCombo() const; + + // Layouts + QVBoxLayout *topLayout_; + QVBoxLayout *mainLayout_; + QHBoxLayout *topBarLayout_; + + // Shared settings object. + QSharedPointer settings_; + + Toggle *trayToggle_; + Toggle *startInTrayToggle_; + Toggle *roomOrderToggle_; + Toggle *groupViewToggle_; + Toggle *typingNotifications_; + Toggle *readReceipts_; + + QComboBox *themeCombo_; + + int sideMargin_ = 0; +}; diff --git a/src/Utils.cc b/src/Utils.cc deleted file mode 100644 index 2247c2b7..00000000 --- a/src/Utils.cc +++ /dev/null @@ -1,188 +0,0 @@ -#include "Utils.h" - -#include -#include - -#include - -using TimelineEvent = mtx::events::collections::TimelineEvents; - -QString -utils::descriptiveTime(const QDateTime &then) -{ - const auto now = QDateTime::currentDateTime(); - const auto days = then.daysTo(now); - - if (days == 0) - return then.toString("HH:mm"); - else if (days < 2) - return QString("Yesterday"); - else if (days < 365) - return then.toString("dd/MM"); - - return then.toString("dd/MM/yy"); -} - -DescInfo -utils::getMessageDescription(const TimelineEvent &event, - const QString &localUser, - const QString &room_id) -{ - using Audio = mtx::events::RoomEvent; - using Emote = mtx::events::RoomEvent; - using File = mtx::events::RoomEvent; - using Image = mtx::events::RoomEvent; - using Notice = mtx::events::RoomEvent; - using Text = mtx::events::RoomEvent; - using Video = mtx::events::RoomEvent; - using Encrypted = mtx::events::EncryptedEvent; - - if (mpark::holds_alternative