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/ --- .ci/format.sh | 2 +- CMakeLists.txt | 259 +++-- cmake/version.h | 5 + cmake/version.hpp | 5 - include/AvatarProvider.h | 36 - include/Cache.h | 661 ------------ include/ChatPage.h | 268 ----- include/CommunitiesList.h | 50 - include/CommunitiesListItem.h | 88 -- include/Config.h | 106 -- include/InviteeItem.h | 27 - include/Logging.hpp | 21 - include/LoginPage.h | 124 --- include/MainWindow.h | 174 ---- include/MatrixClient.h | 30 - include/Olm.hpp | 86 -- include/QuickSwitcher.h | 79 -- include/RegisterPage.h | 84 -- include/RoomInfoListItem.h | 204 ---- include/RoomList.h | 108 -- include/RunGuard.h | 31 - include/SideBarActions.h | 50 - include/Splitter.h | 46 - include/SuggestionsPopup.hpp | 148 --- include/TextInputWidget.h | 184 ---- include/TopRoomBar.h | 107 -- include/TrayIcon.h | 59 -- include/TypingDisplay.h | 21 - include/UserInfoWidget.h | 73 -- include/UserSettingsPage.h | 148 --- include/Utils.h | 194 ---- include/WelcomePage.h | 44 - include/dialogs/CreateRoom.h | 45 - include/dialogs/ImageOverlay.h | 47 - include/dialogs/InviteUsers.h | 42 - include/dialogs/JoinRoom.h | 30 - include/dialogs/LeaveRoom.h | 25 - include/dialogs/Logout.h | 42 - include/dialogs/MemberList.hpp | 61 -- include/dialogs/PreviewUploadOverlay.h | 61 -- include/dialogs/ReCaptcha.hpp | 28 - include/dialogs/ReadReceipts.h | 58 -- include/dialogs/RoomSettings.hpp | 126 --- include/emoji/Category.h | 59 -- include/emoji/ItemDelegate.h | 43 - include/emoji/Panel.h | 66 -- include/emoji/PickButton.h | 53 - include/emoji/Provider.h | 45 - include/notifications/Manager.h | 55 - include/timeline/TimelineItem.h | 380 ------- include/timeline/TimelineView.h | 426 -------- include/timeline/TimelineViewManager.h | 94 -- include/timeline/widgets/AudioItem.h | 107 -- include/timeline/widgets/FileItem.h | 82 -- include/timeline/widgets/ImageItem.h | 108 -- include/timeline/widgets/VideoItem.h | 51 - include/ui/Avatar.h | 47 - include/ui/Badge.h | 62 -- include/ui/DropShadow.h | 111 -- include/ui/FlatButton.h | 185 ---- include/ui/FloatingButton.h | 26 - include/ui/InfoMessage.hpp | 47 - include/ui/Label.h | 25 - include/ui/LoadingIndicator.h | 38 - include/ui/Menu.h | 32 - include/ui/OverlayModal.h | 45 - include/ui/OverlayWidget.h | 21 - include/ui/Painter.h | 161 --- include/ui/RaisedButton.h | 28 - include/ui/Ripple.h | 145 --- include/ui/RippleOverlay.h | 57 - include/ui/ScrollBar.h | 54 - include/ui/SnackBar.h | 79 -- include/ui/TextField.h | 174 ---- include/ui/Theme.h | 97 -- include/ui/ThemeManager.h | 31 - include/ui/ToggleButton.h | 110 -- 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 ++ 281 files changed, 23762 insertions(+), 23760 deletions(-) create mode 100644 cmake/version.h delete mode 100644 cmake/version.hpp delete mode 100644 include/AvatarProvider.h delete mode 100644 include/Cache.h delete mode 100644 include/ChatPage.h delete mode 100644 include/CommunitiesList.h delete mode 100644 include/CommunitiesListItem.h delete mode 100644 include/Config.h delete mode 100644 include/InviteeItem.h delete mode 100644 include/Logging.hpp delete mode 100644 include/LoginPage.h delete mode 100644 include/MainWindow.h delete mode 100644 include/MatrixClient.h delete mode 100644 include/Olm.hpp delete mode 100644 include/QuickSwitcher.h delete mode 100644 include/RegisterPage.h delete mode 100644 include/RoomInfoListItem.h delete mode 100644 include/RoomList.h delete mode 100644 include/RunGuard.h delete mode 100644 include/SideBarActions.h delete mode 100644 include/Splitter.h delete mode 100644 include/SuggestionsPopup.hpp delete mode 100644 include/TextInputWidget.h delete mode 100644 include/TopRoomBar.h delete mode 100644 include/TrayIcon.h delete mode 100644 include/TypingDisplay.h delete mode 100644 include/UserInfoWidget.h delete mode 100644 include/UserSettingsPage.h delete mode 100644 include/Utils.h delete mode 100644 include/WelcomePage.h delete mode 100644 include/dialogs/CreateRoom.h delete mode 100644 include/dialogs/ImageOverlay.h delete mode 100644 include/dialogs/InviteUsers.h delete mode 100644 include/dialogs/JoinRoom.h delete mode 100644 include/dialogs/LeaveRoom.h delete mode 100644 include/dialogs/Logout.h delete mode 100644 include/dialogs/MemberList.hpp delete mode 100644 include/dialogs/PreviewUploadOverlay.h delete mode 100644 include/dialogs/ReCaptcha.hpp delete mode 100644 include/dialogs/ReadReceipts.h delete mode 100644 include/dialogs/RoomSettings.hpp delete mode 100644 include/emoji/Category.h delete mode 100644 include/emoji/ItemDelegate.h delete mode 100644 include/emoji/Panel.h delete mode 100644 include/emoji/PickButton.h delete mode 100644 include/emoji/Provider.h delete mode 100644 include/notifications/Manager.h delete mode 100644 include/timeline/TimelineItem.h delete mode 100644 include/timeline/TimelineView.h delete mode 100644 include/timeline/TimelineViewManager.h delete mode 100644 include/timeline/widgets/AudioItem.h delete mode 100644 include/timeline/widgets/FileItem.h delete mode 100644 include/timeline/widgets/ImageItem.h delete mode 100644 include/timeline/widgets/VideoItem.h delete mode 100644 include/ui/Avatar.h delete mode 100644 include/ui/Badge.h delete mode 100644 include/ui/DropShadow.h delete mode 100644 include/ui/FlatButton.h delete mode 100644 include/ui/FloatingButton.h delete mode 100644 include/ui/InfoMessage.hpp delete mode 100644 include/ui/Label.h delete mode 100644 include/ui/LoadingIndicator.h delete mode 100644 include/ui/Menu.h delete mode 100644 include/ui/OverlayModal.h delete mode 100644 include/ui/OverlayWidget.h delete mode 100644 include/ui/Painter.h delete mode 100644 include/ui/RaisedButton.h delete mode 100644 include/ui/Ripple.h delete mode 100644 include/ui/RippleOverlay.h delete mode 100644 include/ui/ScrollBar.h delete mode 100644 include/ui/SnackBar.h delete mode 100644 include/ui/TextField.h delete mode 100644 include/ui/Theme.h delete mode 100644 include/ui/ThemeManager.h delete mode 100644 include/ui/ToggleButton.h 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 diff --git a/.ci/format.sh b/.ci/format.sh index 89fa4e88..d87bfb66 100755 --- a/.ci/format.sh +++ b/.ci/format.sh @@ -9,6 +9,6 @@ set -o errexit set -o pipefail set -o nounset -FILES=`find include src -type f -type f \( -iname "*.cc" -o -iname "*.h" \)` +FILES=`find src -type f -type f \( -iname "*.cpp" -o -iname "*.h" \)` clang-format -i $FILES && git diff --exit-code diff --git a/CMakeLists.txt b/CMakeLists.txt index 8204df9c..59c2ce32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -136,7 +136,7 @@ message(STATUS "Version: ${PROJECT_VERSION}") cmake_host_system_information(RESULT BUILD_HOST QUERY HOSTNAME) set(BUILD_USER $ENV{USER}) -configure_file(cmake/version.hpp config/version.hpp) +configure_file(cmake/version.h config/version.h) # @@ -144,82 +144,82 @@ configure_file(cmake/version.hpp config/version.hpp) # set(SRC_FILES # Dialogs - src/dialogs/CreateRoom.cc - src/dialogs/ImageOverlay.cc - src/dialogs/PreviewUploadOverlay.cc - src/dialogs/InviteUsers.cc - src/dialogs/JoinRoom.cc + src/dialogs/CreateRoom.cpp + src/dialogs/ImageOverlay.cpp + src/dialogs/PreviewUploadOverlay.cpp + src/dialogs/InviteUsers.cpp + src/dialogs/JoinRoom.cpp src/dialogs/MemberList.cpp - src/dialogs/LeaveRoom.cc - src/dialogs/Logout.cc - src/dialogs/ReadReceipts.cc + src/dialogs/LeaveRoom.cpp + src/dialogs/Logout.cpp + src/dialogs/ReadReceipts.cpp src/dialogs/ReCaptcha.cpp src/dialogs/RoomSettings.cpp # Emoji - src/emoji/Category.cc - src/emoji/ItemDelegate.cc - src/emoji/Panel.cc - src/emoji/PickButton.cc - src/emoji/Provider.cc + src/emoji/Category.cpp + src/emoji/ItemDelegate.cpp + src/emoji/Panel.cpp + src/emoji/PickButton.cpp + src/emoji/Provider.cpp # Timeline - src/timeline/TimelineViewManager.cc - src/timeline/TimelineItem.cc - src/timeline/TimelineView.cc - src/timeline/widgets/AudioItem.cc - src/timeline/widgets/FileItem.cc - src/timeline/widgets/ImageItem.cc - src/timeline/widgets/VideoItem.cc + src/timeline/TimelineViewManager.cpp + src/timeline/TimelineItem.cpp + src/timeline/TimelineView.cpp + src/timeline/widgets/AudioItem.cpp + src/timeline/widgets/FileItem.cpp + src/timeline/widgets/ImageItem.cpp + src/timeline/widgets/VideoItem.cpp # UI components - src/ui/Avatar.cc - src/ui/Badge.cc - src/ui/LoadingIndicator.cc + src/ui/Avatar.cpp + src/ui/Badge.cpp + src/ui/LoadingIndicator.cpp src/ui/InfoMessage.cpp - src/ui/FlatButton.cc - src/ui/FloatingButton.cc - src/ui/Label.cc - src/ui/OverlayModal.cc - src/ui/ScrollBar.cc - src/ui/SnackBar.cc - src/ui/RaisedButton.cc - src/ui/Ripple.cc - src/ui/RippleOverlay.cc - src/ui/OverlayWidget.cc - src/ui/TextField.cc - src/ui/ToggleButton.cc - src/ui/Theme.cc - src/ui/ThemeManager.cc - - src/AvatarProvider.cc - src/Cache.cc - src/ChatPage.cc - src/CommunitiesListItem.cc - src/CommunitiesList.cc - src/InviteeItem.cc - src/LoginPage.cc + src/ui/FlatButton.cpp + src/ui/FloatingButton.cpp + src/ui/Label.cpp + src/ui/OverlayModal.cpp + src/ui/ScrollBar.cpp + src/ui/SnackBar.cpp + src/ui/RaisedButton.cpp + src/ui/Ripple.cpp + src/ui/RippleOverlay.cpp + src/ui/OverlayWidget.cpp + src/ui/TextField.cpp + src/ui/ToggleButton.cpp + src/ui/Theme.cpp + src/ui/ThemeManager.cpp + + src/AvatarProvider.cpp + src/Cache.cpp + src/ChatPage.cpp + src/CommunitiesListItem.cpp + src/CommunitiesList.cpp + src/InviteeItem.cpp + src/LoginPage.cpp src/Logging.cpp - src/MainWindow.cc - src/MatrixClient.cc - src/QuickSwitcher.cc + src/MainWindow.cpp + src/MatrixClient.cpp + src/QuickSwitcher.cpp src/Olm.cpp - src/RegisterPage.cc - src/RoomInfoListItem.cc - src/RoomList.cc - src/RunGuard.cc - src/SideBarActions.cc - src/Splitter.cc + src/RegisterPage.cpp + src/RoomInfoListItem.cpp + src/RoomList.cpp + src/RunGuard.cpp + src/SideBarActions.cpp + src/Splitter.cpp src/SuggestionsPopup.cpp - src/TextInputWidget.cc - src/TopRoomBar.cc - src/TrayIcon.cc - src/TypingDisplay.cc - src/Utils.cc - src/UserInfoWidget.cc - src/UserSettingsPage.cc - src/WelcomePage.cc - src/main.cc + src/TextInputWidget.cpp + src/TopRoomBar.cpp + src/TrayIcon.cpp + src/TypingDisplay.cpp + src/Utils.cpp + src/UserInfoWidget.cpp + src/UserSettingsPage.cpp + src/WelcomePage.cpp + src/main.cpp ) # ExternalProject dependencies @@ -252,82 +252,81 @@ if(NOT TWEENY_INCLUDE_DIR) endif() include_directories(SYSTEM ${TWEENY_INCLUDE_DIR}) -include_directories(include) -include_directories(include/ui) +include_directories(${CMAKE_SOURCE_DIR}/src) include_directories(${CMAKE_CURRENT_BINARY_DIR}/config) qt5_wrap_cpp(MOC_HEADERS # Dialogs - include/dialogs/CreateRoom.h - include/dialogs/ImageOverlay.h - include/dialogs/PreviewUploadOverlay.h - include/dialogs/InviteUsers.h - include/dialogs/JoinRoom.h - include/dialogs/MemberList.hpp - include/dialogs/LeaveRoom.h - include/dialogs/Logout.h - include/dialogs/ReadReceipts.h - include/dialogs/ReCaptcha.hpp - include/dialogs/RoomSettings.hpp + src/dialogs/CreateRoom.h + src/dialogs/ImageOverlay.h + src/dialogs/PreviewUploadOverlay.h + src/dialogs/InviteUsers.h + src/dialogs/JoinRoom.h + src/dialogs/MemberList.h + src/dialogs/LeaveRoom.h + src/dialogs/Logout.h + src/dialogs/ReadReceipts.h + src/dialogs/ReCaptcha.h + src/dialogs/RoomSettings.h # Emoji - include/emoji/Category.h - include/emoji/ItemDelegate.h - include/emoji/Panel.h - include/emoji/PickButton.h + src/emoji/Category.h + src/emoji/ItemDelegate.h + src/emoji/Panel.h + src/emoji/PickButton.h # Timeline - include/timeline/TimelineItem.h - include/timeline/TimelineView.h - include/timeline/TimelineViewManager.h - include/timeline/widgets/AudioItem.h - include/timeline/widgets/FileItem.h - include/timeline/widgets/ImageItem.h - include/timeline/widgets/VideoItem.h + src/timeline/TimelineItem.h + src/timeline/TimelineView.h + src/timeline/TimelineViewManager.h + src/timeline/widgets/AudioItem.h + src/timeline/widgets/FileItem.h + src/timeline/widgets/ImageItem.h + src/timeline/widgets/VideoItem.h # UI components - include/ui/Avatar.h - include/ui/Badge.h - include/ui/LoadingIndicator.h - include/ui/InfoMessage.hpp - include/ui/FlatButton.h - include/ui/Label.h - include/ui/FloatingButton.h - include/ui/OverlayWidget.h - include/ui/ScrollBar.h - include/ui/SnackBar.h - include/ui/RaisedButton.h - include/ui/Ripple.h - include/ui/RippleOverlay.h - include/ui/TextField.h - include/ui/ToggleButton.h - include/ui/Theme.h - include/ui/ThemeManager.h - - include/notifications/Manager.h - - include/AvatarProvider.h - include/Cache.h - include/ChatPage.h - include/CommunitiesListItem.h - include/CommunitiesList.h - include/LoginPage.h - include/MainWindow.h - include/InviteeItem.h - include/QuickSwitcher.h - include/RegisterPage.h - include/RoomInfoListItem.h - include/RoomList.h - include/SideBarActions.h - include/Splitter.h - include/SuggestionsPopup.hpp - include/TextInputWidget.h - include/TopRoomBar.h - include/TrayIcon.h - include/TypingDisplay.h - include/UserInfoWidget.h - include/UserSettingsPage.h - include/WelcomePage.h + src/ui/Avatar.h + src/ui/Badge.h + src/ui/LoadingIndicator.h + src/ui/InfoMessage.h + src/ui/FlatButton.h + src/ui/Label.h + src/ui/FloatingButton.h + src/ui/OverlayWidget.h + src/ui/ScrollBar.h + src/ui/SnackBar.h + src/ui/RaisedButton.h + src/ui/Ripple.h + src/ui/RippleOverlay.h + src/ui/TextField.h + src/ui/ToggleButton.h + src/ui/Theme.h + src/ui/ThemeManager.h + + src/notifications/Manager.h + + src/AvatarProvider.h + src/Cache.h + src/ChatPage.h + src/CommunitiesListItem.h + src/CommunitiesList.h + src/LoginPage.h + src/MainWindow.h + src/InviteeItem.h + src/QuickSwitcher.h + src/RegisterPage.h + src/RoomInfoListItem.h + src/RoomList.h + src/SideBarActions.h + src/Splitter.h + src/SuggestionsPopup.h + src/TextInputWidget.h + src/TopRoomBar.h + src/TrayIcon.h + src/TypingDisplay.h + src/UserInfoWidget.h + src/UserSettingsPage.h + src/WelcomePage.h ) # diff --git a/cmake/version.h b/cmake/version.h new file mode 100644 index 00000000..204bc895 --- /dev/null +++ b/cmake/version.h @@ -0,0 +1,5 @@ +namespace nheko { +static constexpr const char *version = "${PROJECT_VERSION}"; +static constexpr const char *build_user = "${BUILD_USER}@${BUILD_HOST}"; +static constexpr const char *build_os = "${CMAKE_HOST_SYSTEM_NAME}"; +} diff --git a/cmake/version.hpp b/cmake/version.hpp deleted file mode 100644 index 204bc895..00000000 --- a/cmake/version.hpp +++ /dev/null @@ -1,5 +0,0 @@ -namespace nheko { -static constexpr const char *version = "${PROJECT_VERSION}"; -static constexpr const char *build_user = "${BUILD_USER}@${BUILD_HOST}"; -static constexpr const char *build_os = "${CMAKE_HOST_SYSTEM_NAME}"; -} diff --git a/include/AvatarProvider.h b/include/AvatarProvider.h deleted file mode 100644 index 4b4e15e9..00000000 --- a/include/AvatarProvider.h +++ /dev/null @@ -1,36 +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 . - */ - -#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/include/Cache.h b/include/Cache.h deleted file mode 100644 index 14f991e8..00000000 --- a/include/Cache.h +++ /dev/null @@ -1,661 +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 . - */ - -#pragma once - -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "Logging.hpp" - -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/include/ChatPage.h b/include/ChatPage.h deleted file mode 100644 index 6a70acf4..00000000 --- a/include/ChatPage.h +++ /dev/null @@ -1,268 +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 . - */ - -#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/include/CommunitiesList.h b/include/CommunitiesList.h deleted file mode 100644 index 32a64bf2..00000000 --- a/include/CommunitiesList.h +++ /dev/null @@ -1,50 +0,0 @@ -#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/include/CommunitiesListItem.h b/include/CommunitiesListItem.h deleted file mode 100644 index a9b6e333..00000000 --- a/include/CommunitiesListItem.h +++ /dev/null @@ -1,88 +0,0 @@ -#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/include/Config.h b/include/Config.h deleted file mode 100644 index 3a3296d6..00000000 --- a/include/Config.h +++ /dev/null @@ -1,106 +0,0 @@ -#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/include/InviteeItem.h b/include/InviteeItem.h deleted file mode 100644 index f0bdbdf0..00000000 --- a/include/InviteeItem.h +++ /dev/null @@ -1,27 +0,0 @@ -#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/include/Logging.hpp b/include/Logging.hpp deleted file mode 100644 index 2feae60d..00000000 --- a/include/Logging.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#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/include/LoginPage.h b/include/LoginPage.h deleted file mode 100644 index c52ccaa4..00000000 --- a/include/LoginPage.h +++ /dev/null @@ -1,124 +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 . - */ - -#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/include/MainWindow.h b/include/MainWindow.h deleted file mode 100644 index 92040191..00000000 --- a/include/MainWindow.h +++ /dev/null @@ -1,174 +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 . - */ - -#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/include/MatrixClient.h b/include/MatrixClient.h deleted file mode 100644 index 12bba889..00000000 --- a/include/MatrixClient.h +++ /dev/null @@ -1,30 +0,0 @@ -#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/include/Olm.hpp b/include/Olm.hpp deleted file mode 100644 index ae4e0659..00000000 --- a/include/Olm.hpp +++ /dev/null @@ -1,86 +0,0 @@ -#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/include/QuickSwitcher.h b/include/QuickSwitcher.h deleted file mode 100644 index 254d7a16..00000000 --- a/include/QuickSwitcher.h +++ /dev/null @@ -1,79 +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 . - */ - -#pragma once - -#include -#include -#include -#include - -#include "SuggestionsPopup.hpp" -#include "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/include/RegisterPage.h b/include/RegisterPage.h deleted file mode 100644 index d02de7c4..00000000 --- a/include/RegisterPage.h +++ /dev/null @@ -1,84 +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 . - */ - -#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/include/RoomInfoListItem.h b/include/RoomInfoListItem.h deleted file mode 100644 index 95db1d75..00000000 --- a/include/RoomInfoListItem.h +++ /dev/null @@ -1,204 +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 . - */ - -#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/include/RoomList.h b/include/RoomList.h deleted file mode 100644 index 59b0e865..00000000 --- a/include/RoomList.h +++ /dev/null @@ -1,108 +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 . - */ - -#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/include/RunGuard.h b/include/RunGuard.h deleted file mode 100644 index f9a9641a..00000000 --- a/include/RunGuard.h +++ /dev/null @@ -1,31 +0,0 @@ -#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/include/SideBarActions.h b/include/SideBarActions.h deleted file mode 100644 index a29757dd..00000000 --- a/include/SideBarActions.h +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "FlatButton.h" -#include "Menu.h" - -namespace mtx { -namespace requests { -struct CreateRoom; -} -} - -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/include/Splitter.h b/include/Splitter.h deleted file mode 100644 index 99e02eed..00000000 --- a/include/Splitter.h +++ /dev/null @@ -1,46 +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 . - */ - -#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/include/SuggestionsPopup.hpp b/include/SuggestionsPopup.hpp deleted file mode 100644 index 4a8dd00c..00000000 --- a/include/SuggestionsPopup.hpp +++ /dev/null @@ -1,148 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "Avatar.h" -#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/include/TextInputWidget.h b/include/TextInputWidget.h deleted file mode 100644 index 2dcb178f..00000000 --- a/include/TextInputWidget.h +++ /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 . - */ - -#pragma once - -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "FlatButton.h" -#include "LoadingIndicator.h" -#include "SuggestionsPopup.hpp" - -#include "dialogs/PreviewUploadOverlay.h" - -#include "emoji/PickButton.h" - -namespace dialogs { -class PreviewUploadOverlay; -} - -struct SearchResult; - -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/include/TopRoomBar.h b/include/TopRoomBar.h deleted file mode 100644 index 1c42e25f..00000000 --- a/include/TopRoomBar.h +++ /dev/null @@ -1,107 +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 . - */ - -#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/include/TrayIcon.h b/include/TrayIcon.h deleted file mode 100644 index a3536cc3..00000000 --- a/include/TrayIcon.h +++ /dev/null @@ -1,59 +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 . - */ - -#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/include/TypingDisplay.h b/include/TypingDisplay.h deleted file mode 100644 index db8a9519..00000000 --- a/include/TypingDisplay.h +++ /dev/null @@ -1,21 +0,0 @@ -#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/include/UserInfoWidget.h b/include/UserInfoWidget.h deleted file mode 100644 index ea2d5400..00000000 --- a/include/UserInfoWidget.h +++ /dev/null @@ -1,73 +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 . - */ - -#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/include/UserSettingsPage.h b/include/UserSettingsPage.h deleted file mode 100644 index 177f1921..00000000 --- a/include/UserSettingsPage.h +++ /dev/null @@ -1,148 +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 . - */ - -#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/include/Utils.h b/include/Utils.h deleted file mode 100644 index 8f9b7cff..00000000 --- a/include/Utils.h +++ /dev/null @@ -1,194 +0,0 @@ -#pragma once - -#include "Cache.h" -#include "RoomInfoListItem.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" - -#include -#include -#include - -namespace utils { - -using TimelineEvent = mtx::events::collections::TimelineEvents; - -//! Human friendly timestamp representation. -QString -descriptiveTime(const QDateTime &then); - -//! Generate a message description from the event to be displayed -//! in the RoomList. -DescInfo -getMessageDescription(const TimelineEvent &event, const QString &localUser, const QString &room_id); - -//! Get the first character of a string, taking into account that -//! surrogate pairs might be in use. -QString -firstChar(const QString &input); - -//! Get a human readable file size with the appropriate units attached. -QString -humanReadableFileSize(uint64_t bytes); - -QString -event_body(const mtx::events::collections::TimelineEvents &event); - -//! Match widgets/events with a description message. -template -QString -messageDescription(const QString &username = "", const QString &body = "") -{ - 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 Sticker = mtx::events::Sticker; - using Text = mtx::events::RoomEvent; - using Video = mtx::events::RoomEvent; - using Encrypted = mtx::events::EncryptedEvent; - - if (std::is_same::value || std::is_same::value) - return QString("sent an audio clip"); - else if (std::is_same::value || std::is_same::value) - return QString("sent an image"); - else if (std::is_same::value || std::is_same::value) - return QString("sent a file"); - else if (std::is_same::value || std::is_same::value) - return QString("sent a video clip"); - else if (std::is_same::value || std::is_same::value) - return QString("sent a sticker"); - else if (std::is_same::value) - return QString("sent a notification"); - else if (std::is_same::value) - return QString(": %1").arg(body); - else if (std::is_same::value) - return QString("* %1 %2").arg(username).arg(body); - else if (std::is_same::value) - return QString("sent an encrypted message"); -} - -template -DescInfo -createDescriptionInfo(const Event &event, const QString &localUser, const QString &room_id) -{ - using Text = mtx::events::RoomEvent; - using Emote = mtx::events::RoomEvent; - - const auto msg = mpark::get(event); - const auto sender = QString::fromStdString(msg.sender); - - const auto username = Cache::displayName(room_id, sender); - const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); - - bool isText = std::is_same::value; - bool isEmote = std::is_same::value; - - return DescInfo{ - isEmote ? "" : (sender == localUser ? "You" : username), - sender, - (isText || isEmote) - ? messageDescription(username, QString::fromStdString(msg.content.body).trimmed()) - : QString(" %1").arg(messageDescription()), - utils::descriptiveTime(ts), - ts}; -} - -//! Scale down an image to fit to the given width & height limitations. -template -ImageType -scaleDown(uint64_t max_width, uint64_t max_height, const ImageType &source) -{ - if (source.isNull()) - return QPixmap(); - - auto width_ratio = (double)max_width / (double)source.width(); - auto height_ratio = (double)max_height / (double)source.height(); - - auto min_aspect_ratio = std::min(width_ratio, height_ratio); - - int final_width = 0; - int final_height = 0; - - if (min_aspect_ratio > 1) { - final_width = source.width(); - final_height = source.height(); - } else { - final_width = source.width() * min_aspect_ratio; - final_height = source.height() * min_aspect_ratio; - } - - return source.scaled( - final_width, final_height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); -} - -//! Delete items in a container based on a predicate. -template -void -erase_if(ContainerT &items, const PredicateT &predicate) -{ - for (auto it = items.begin(); it != items.end();) { - if (predicate(*it)) - it = items.erase(it); - else - ++it; - } -} - -inline uint64_t -event_timestamp(const mtx::events::collections::TimelineEvents &event) -{ - return mpark::visit([](auto msg) { return msg.origin_server_ts; }, event); -} - -inline nlohmann::json -serialize_event(const mtx::events::collections::TimelineEvents &event) -{ - return mpark::visit([](auto msg) { return json(msg); }, event); -} - -inline mtx::events::EventType -event_type(const mtx::events::collections::TimelineEvents &event) -{ - return mpark::visit([](auto msg) { return msg.type; }, event); -} - -inline std::string -event_id(const mtx::events::collections::TimelineEvents &event) -{ - return mpark::visit([](auto msg) { return msg.event_id; }, event); -} - -inline QString -eventId(const mtx::events::collections::TimelineEvents &event) -{ - return QString::fromStdString(event_id(event)); -} - -inline QString -event_sender(const mtx::events::collections::TimelineEvents &event) -{ - return mpark::visit([](auto msg) { return QString::fromStdString(msg.sender); }, event); -} - -template -QString -message_body(const mtx::events::collections::TimelineEvents &event) -{ - return QString::fromStdString(mpark::get(event).content.body); -} - -//! Calculate the Levenshtein distance between two strings with character skipping. -int -levenshtein_distance(const std::string &s1, const std::string &s2); - -QPixmap -scaleImageToPixmap(const QImage &img, int size); - -//! Convert a Content Matrix URI to an HTTP link. -QString -mxcToHttp(const QUrl &url, const QString &server, int port); -} diff --git a/include/WelcomePage.h b/include/WelcomePage.h deleted file mode 100644 index b33ca669..00000000 --- a/include/WelcomePage.h +++ /dev/null @@ -1,44 +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 . - */ - -#pragma once - -#include - -class RaisedButton; - -class WelcomePage : public QWidget -{ - Q_OBJECT - -public: - explicit WelcomePage(QWidget *parent = 0); - -protected: - void paintEvent(QPaintEvent *event) override; - -signals: - // Notify that the user wants to login in. - void userLogin(); - - // Notify that the user wants to register. - void userRegister(); - -private: - RaisedButton *registerBtn_; - RaisedButton *loginBtn_; -}; diff --git a/include/dialogs/CreateRoom.h b/include/dialogs/CreateRoom.h deleted file mode 100644 index 46edebdc..00000000 --- a/include/dialogs/CreateRoom.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include - -#include - -class FlatButton; -class TextField; -class QComboBox; -class Toggle; - -namespace dialogs { - -class CreateRoom : public QFrame -{ - Q_OBJECT -public: - CreateRoom(QWidget *parent = nullptr); - -signals: - void closing(bool isCreating, const mtx::requests::CreateRoom &request); - -protected: - void paintEvent(QPaintEvent *event) override; - void showEvent(QShowEvent *event) override; - -private: - void clearFields(); - - QComboBox *visibilityCombo_; - QComboBox *presetCombo_; - - Toggle *directToggle_; - - FlatButton *confirmBtn_; - FlatButton *cancelBtn_; - - TextField *nameInput_; - TextField *topicInput_; - TextField *aliasInput_; - - mtx::requests::CreateRoom request_; -}; - -} // dialogs diff --git a/include/dialogs/ImageOverlay.h b/include/dialogs/ImageOverlay.h deleted file mode 100644 index b4d42acb..00000000 --- a/include/dialogs/ImageOverlay.h +++ /dev/null @@ -1,47 +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 . - */ - -#pragma once - -#include -#include -#include - -namespace dialogs { - -class ImageOverlay : public QWidget -{ - Q_OBJECT -public: - ImageOverlay(QPixmap image, QWidget *parent = nullptr); - -protected: - void mousePressEvent(QMouseEvent *event) override; - void paintEvent(QPaintEvent *event) override; - -signals: - void closing(); - -private: - QPixmap originalImage_; - QPixmap image_; - - QRect content_; - QRect close_button_; - QRect screen_; -}; -} // dialogs diff --git a/include/dialogs/InviteUsers.h b/include/dialogs/InviteUsers.h deleted file mode 100644 index 41e6236a..00000000 --- a/include/dialogs/InviteUsers.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -class FlatButton; -class TextField; -class QListWidget; - -namespace dialogs { - -class InviteUsers : public QFrame -{ - Q_OBJECT -public: - explicit InviteUsers(QWidget *parent = nullptr); - -protected: - void paintEvent(QPaintEvent *event) override; - void showEvent(QShowEvent *event) override; - -signals: - void closing(bool isLeaving, QStringList invitees); - -private slots: - void removeInvitee(QListWidgetItem *item); - -private: - void addUser(); - QStringList invitedUsers() const; - - FlatButton *confirmBtn_; - FlatButton *cancelBtn_; - - TextField *inviteeInput_; - QLabel *errorLabel_; - - QListWidget *inviteeList_; -}; -} // dialogs diff --git a/include/dialogs/JoinRoom.h b/include/dialogs/JoinRoom.h deleted file mode 100644 index 5919f08f..00000000 --- a/include/dialogs/JoinRoom.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include - -class FlatButton; -class TextField; - -namespace dialogs { - -class JoinRoom : public QFrame -{ - Q_OBJECT -public: - JoinRoom(QWidget *parent = nullptr); - -signals: - void closing(bool isJoining, const QString &room); - -protected: - void paintEvent(QPaintEvent *event) override; - void showEvent(QShowEvent *event) override; - -private: - FlatButton *confirmBtn_; - FlatButton *cancelBtn_; - - TextField *roomInput_; -}; - -} // dialogs diff --git a/include/dialogs/LeaveRoom.h b/include/dialogs/LeaveRoom.h deleted file mode 100644 index 98e4938d..00000000 --- a/include/dialogs/LeaveRoom.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include - -class FlatButton; - -namespace dialogs { - -class LeaveRoom : public QFrame -{ - Q_OBJECT -public: - explicit LeaveRoom(QWidget *parent = nullptr); - -protected: - void paintEvent(QPaintEvent *event) override; - -signals: - void closing(bool isLeaving); - -private: - FlatButton *confirmBtn_; - FlatButton *cancelBtn_; -}; -} // dialogs diff --git a/include/dialogs/Logout.h b/include/dialogs/Logout.h deleted file mode 100644 index cfefb970..00000000 --- a/include/dialogs/Logout.h +++ /dev/null @@ -1,42 +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 . - */ - -#pragma once - -#include - -class FlatButton; - -namespace dialogs { - -class Logout : public QFrame -{ - Q_OBJECT -public: - explicit Logout(QWidget *parent = nullptr); - -protected: - void paintEvent(QPaintEvent *event) override; - -signals: - void closing(bool isLoggingOut); - -private: - FlatButton *confirmBtn_; - FlatButton *cancelBtn_; -}; -} // dialogs diff --git a/include/dialogs/MemberList.hpp b/include/dialogs/MemberList.hpp deleted file mode 100644 index 9c3dc5dc..00000000 --- a/include/dialogs/MemberList.hpp +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once - -#include -#include - -class Avatar; -class FlatButton; -class QHBoxLayout; -class QLabel; -class QVBoxLayout; - -struct RoomMember; - -template -class QSharedPointer; - -namespace dialogs { - -class MemberItem : public QWidget -{ - Q_OBJECT - -public: - MemberItem(const RoomMember &member, QWidget *parent); - -private: - QHBoxLayout *topLayout_; - QVBoxLayout *textLayout_; - - Avatar *avatar_; - - QLabel *userName_; - QLabel *userId_; -}; - -class MemberList : public QFrame -{ - Q_OBJECT -public: - MemberList(const QString &room_id, QWidget *parent = nullptr); - -public slots: - void addUsers(const std::vector &users); - -protected: - void paintEvent(QPaintEvent *event) override; - void hideEvent(QHideEvent *event) override - { - list_->clear(); - QFrame::hideEvent(event); - } - -private: - void moveButtonToBottom(); - - QString room_id_; - QLabel *topLabel_; - QListWidget *list_; - FlatButton *moreBtn_; -}; -} // dialogs diff --git a/include/dialogs/PreviewUploadOverlay.h b/include/dialogs/PreviewUploadOverlay.h deleted file mode 100644 index 48795ca0..00000000 --- a/include/dialogs/PreviewUploadOverlay.h +++ /dev/null @@ -1,61 +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 . - */ - -#pragma once - -#include -#include -#include -#include - -#include "FlatButton.h" - -class QMimeData; - -namespace dialogs { - -class PreviewUploadOverlay : public QWidget -{ - Q_OBJECT -public: - PreviewUploadOverlay(QWidget *parent = nullptr); - - void setPreview(const QByteArray data, const QString &mime); - void setPreview(const QString &path); - -signals: - void confirmUpload(const QByteArray data, const QString &media, const QString &filename); - -private: - void init(); - void setLabels(const QString &type, const QString &mime, uint64_t upload_size); - - bool isImage_; - QPixmap image_; - - QByteArray data_; - QString filePath_; - QString mediaType_; - - QLabel titleLabel_; - QLabel infoLabel_; - QLineEdit fileName_; - - FlatButton upload_; - FlatButton cancel_; -}; -} // dialogs diff --git a/include/dialogs/ReCaptcha.hpp b/include/dialogs/ReCaptcha.hpp deleted file mode 100644 index 5f47b0eb..00000000 --- a/include/dialogs/ReCaptcha.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include - -class FlatButton; -class RaisedButton; - -namespace dialogs { - -class ReCaptcha : public QWidget -{ - Q_OBJECT - -public: - ReCaptcha(const QString &session, QWidget *parent = nullptr); - -protected: - void paintEvent(QPaintEvent *event) override; - -signals: - void closing(); - -private: - FlatButton *openCaptchaBtn_; - RaisedButton *confirmBtn_; - RaisedButton *cancelBtn_; -}; -} // dialogs diff --git a/include/dialogs/ReadReceipts.h b/include/dialogs/ReadReceipts.h deleted file mode 100644 index 5e5615df..00000000 --- a/include/dialogs/ReadReceipts.h +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -class Avatar; - -namespace dialogs { - -class ReceiptItem : public QWidget -{ - Q_OBJECT - -public: - ReceiptItem(QWidget *parent, - const QString &user_id, - uint64_t timestamp, - const QString &room_id); - -private: - QString dateFormat(const QDateTime &then) const; - - QHBoxLayout *topLayout_; - QVBoxLayout *textLayout_; - - Avatar *avatar_; - - QLabel *userName_; - QLabel *timestamp_; -}; - -class ReadReceipts : public QFrame -{ - Q_OBJECT -public: - explicit ReadReceipts(QWidget *parent = nullptr); - -public slots: - void addUsers(const std::multimap> &users); - -protected: - void paintEvent(QPaintEvent *event) override; - void hideEvent(QHideEvent *event) override - { - userList_->clear(); - QFrame::hideEvent(event); - } - -private: - QLabel *topLabel_; - - QListWidget *userList_; -}; -} // dialogs diff --git a/include/dialogs/RoomSettings.hpp b/include/dialogs/RoomSettings.hpp deleted file mode 100644 index 6cab03b7..00000000 --- a/include/dialogs/RoomSettings.hpp +++ /dev/null @@ -1,126 +0,0 @@ -#pragma once - -#include -#include - -#include "Cache.h" - -class Avatar; -class FlatButton; -class QComboBox; -class QHBoxLayout; -class QLabel; -class QLabel; -class QLayout; -class QPixmap; -class TextField; -class TextField; -class Toggle; - -template -class QSharedPointer; - -class EditModal : public QWidget -{ - Q_OBJECT - -public: - EditModal(const QString &roomId, QWidget *parent = nullptr); - - void setFields(const QString &roomName, const QString &roomTopic); - -signals: - void nameChanged(const QString &roomName); - void nameEventSentCb(const QString &newName); - void topicEventSentCb(); - void stateEventErrorCb(const QString &msg); - -private: - QString roomId_; - QString initialName_; - QString initialTopic_; - - QLabel *errorField_; - - TextField *nameInput_; - TextField *topicInput_; - - FlatButton *applyBtn_; - FlatButton *cancelBtn_; -}; - -class TopSection : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - -public: - TopSection(const RoomInfo &info, const QImage &img, QWidget *parent = nullptr); - QSize sizeHint() const override; - void setRoomName(const QString &name); - - QColor textColor() const { return textColor_; } - void setTextColor(QColor &color) { textColor_ = color; } - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - static constexpr int AvatarSize = 72; - static constexpr int Padding = 5; - - RoomInfo info_; - QPixmap avatar_; - QColor textColor_; -}; - -namespace dialogs { - -class RoomSettings : public QFrame -{ - Q_OBJECT -public: - RoomSettings(const QString &room_id, QWidget *parent = nullptr); - -signals: - void closing(); - void enableEncryptionError(const QString &msg); - -protected: - void paintEvent(QPaintEvent *event) override; - -private slots: - void saveSettings(); - -private: - static constexpr int AvatarSize = 64; - - void setAvatar(const QImage &img) { avatarImg_ = img; } - void setupEditButton(); - //! Retrieve the current room information from cache. - void retrieveRoomInfo(); - void enableEncryption(); - - //! Whether the user would be able to change the name or the topic of the room. - bool hasEditRights_ = true; - bool usesEncryption_ = false; - QHBoxLayout *editLayout_; - - // Button section - FlatButton *okBtn_; - FlatButton *cancelBtn_; - - FlatButton *editFieldsBtn_; - - RoomInfo info_; - QString room_id_; - QImage avatarImg_; - - TopSection *topSection_; - - QComboBox *accessCombo; - Toggle *encryptionToggle_; -}; - -} // dialogs diff --git a/include/emoji/Category.h b/include/emoji/Category.h deleted file mode 100644 index a14029c8..00000000 --- a/include/emoji/Category.h +++ /dev/null @@ -1,59 +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 . - */ - -#pragma once - -#include -#include -#include -#include - -#include "ItemDelegate.h" - -namespace emoji { - -class Category : public QWidget -{ - Q_OBJECT - -public: - Category(QString category, std::vector emoji, QWidget *parent = nullptr); - -signals: - void emojiSelected(const QString &emoji); - -protected: - void paintEvent(QPaintEvent *event) override; - -private slots: - void clickIndex(const QModelIndex &index) - { - emit emojiSelected(index.data(Qt::UserRole).toString()); - }; - -private: - QVBoxLayout *mainLayout_; - - QStandardItemModel *itemModel_; - QListView *emojiListView_; - - emoji::Emoji *data_; - emoji::ItemDelegate *delegate_; - - QLabel *category_; -}; -} // namespace emoji diff --git a/include/emoji/ItemDelegate.h b/include/emoji/ItemDelegate.h deleted file mode 100644 index e0456308..00000000 --- a/include/emoji/ItemDelegate.h +++ /dev/null @@ -1,43 +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 . - */ - -#pragma once - -#include -#include -#include - -#include "Provider.h" - -namespace emoji { - -class ItemDelegate : public QStyledItemDelegate -{ - Q_OBJECT - -public: - explicit ItemDelegate(QObject *parent = nullptr); - ~ItemDelegate(); - - void paint(QPainter *painter, - const QStyleOptionViewItem &option, - const QModelIndex &index) const override; - -private: - Emoji *data_; -}; -} // namespace emoji diff --git a/include/emoji/Panel.h b/include/emoji/Panel.h deleted file mode 100644 index ad233c27..00000000 --- a/include/emoji/Panel.h +++ /dev/null @@ -1,66 +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 . - */ - -#pragma once - -#include - -#include "Provider.h" - -namespace emoji { - -class Category; - -class Panel : public QWidget -{ - Q_OBJECT - -public: - Panel(QWidget *parent = nullptr); - -signals: - void mouseLeft(); - void emojiSelected(const QString &emoji); - -protected: - void leaveEvent(QEvent *event) override - { - emit leaving(); - QWidget::leaveEvent(event); - } - - void paintEvent(QPaintEvent *event) override; - -signals: - void leaving(); - -private: - void showCategory(const Category *category); - - Provider emoji_provider_; - - QScrollArea *scrollArea_; - - int shadowMargin_; - - // Panel dimensions. - int width_; - int height_; - - int categoryIconSize_; -}; -} // namespace emoji diff --git a/include/emoji/PickButton.h b/include/emoji/PickButton.h deleted file mode 100644 index 9117e61f..00000000 --- a/include/emoji/PickButton.h +++ /dev/null @@ -1,53 +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 . - */ - -#pragma once - -#include -#include -#include - -#include "FlatButton.h" - -namespace emoji { - -class Panel; - -class PickButton : public FlatButton -{ - Q_OBJECT -public: - explicit PickButton(QWidget *parent = nullptr); - -signals: - void emojiSelected(const QString &emoji); - -protected: - void enterEvent(QEvent *e) override; - void leaveEvent(QEvent *e) override; - -private: - // Vertical distance from panel's bottom. - int vertical_distance_ = 10; - - // Horizontal distance from panel's bottom right corner. - int horizontal_distance_ = 70; - - QSharedPointer panel_; - QTimer hideTimer_; -}; -} // namespace emoji diff --git a/include/emoji/Provider.h b/include/emoji/Provider.h deleted file mode 100644 index 5cc3ced4..00000000 --- a/include/emoji/Provider.h +++ /dev/null @@ -1,45 +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 . - */ - -#pragma once - -#include -#include - -namespace emoji { - -struct Emoji -{ - // Unicode code. - QString unicode; - // Keyboard shortcut e.g :emoji: - QString shortname; -}; - -class Provider -{ -public: - static const std::vector people; - static const std::vector nature; - static const std::vector food; - static const std::vector activity; - static const std::vector travel; - static const std::vector objects; - static const std::vector symbols; - static const std::vector flags; -}; -} // namespace emoji diff --git a/include/notifications/Manager.h b/include/notifications/Manager.h deleted file mode 100644 index 4ac60097..00000000 --- a/include/notifications/Manager.h +++ /dev/null @@ -1,55 +0,0 @@ -#pragma once - -#include -#include -#include - -#if defined(Q_OS_LINUX) -#include -#include -#endif - -struct roomEventId -{ - QString roomId; - QString eventId; -}; - -class NotificationsManager : public QObject -{ - Q_OBJECT -public: - NotificationsManager(QObject *parent = nullptr); - - void postNotification(const QString &roomId, - const QString &eventId, - const QString &roomName, - const QString &senderName, - const QString &text, - const QImage &icon); - -signals: - void notificationClicked(const QString roomId, const QString eventId); - -#if defined(Q_OS_LINUX) -private: - QDBusInterface dbus; - uint showNotification(const QString summary, const QString text, const QImage image); - - // notification ID to (room ID, event ID) - QMap notificationIds; -#endif - - // these slots are platform specific (D-Bus only) - // but Qt slot declarations can not be inside an ifdef! -private slots: - void actionInvoked(uint id, QString action); - void notificationClosed(uint id, uint reason); -}; - -#if defined(Q_OS_LINUX) -QDBusArgument & -operator<<(QDBusArgument &arg, const QImage &image); -const QDBusArgument & -operator>>(const QDBusArgument &arg, QImage &); -#endif diff --git a/include/timeline/TimelineItem.h b/include/timeline/TimelineItem.h deleted file mode 100644 index d3cab0a0..00000000 --- a/include/timeline/TimelineItem.h +++ /dev/null @@ -1,380 +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 . - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "AvatarProvider.h" -#include "RoomInfoListItem.h" -#include "Utils.h" - -#include "Cache.h" -#include "MatrixClient.h" - -class ImageItem; -class StickerItem; -class AudioItem; -class VideoItem; -class FileItem; -class Avatar; - -enum class StatusIndicatorState -{ - //! The encrypted message was received by the server. - Encrypted, - //! The plaintext message was received by the server. - Received, - //! The client sent the message. Not yet received. - Sent, - //! When the message is loaded from cache or backfill. - Empty, -}; - -//! -//! Used to notify the user about the status of a message. -//! -class StatusIndicator : public QWidget -{ - Q_OBJECT - -public: - explicit StatusIndicator(QWidget *parent); - void setState(StatusIndicatorState state); - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - void paintIcon(QPainter &p, QIcon &icon); - - QIcon lockIcon_; - QIcon clockIcon_; - QIcon checkmarkIcon_; - - QColor iconColor_ = QColor("#999"); - - StatusIndicatorState state_ = StatusIndicatorState::Empty; - - static constexpr int MaxWidth = 24; -}; - -class TextLabel : public QTextBrowser -{ - Q_OBJECT - -public: - TextLabel(const QString &text, QWidget *parent = 0) - : QTextBrowser(parent) - { - setText(text); - setOpenExternalLinks(true); - - // Make it look and feel like an ordinary label. - setReadOnly(true); - setFrameStyle(QFrame::NoFrame); - QPalette pal = palette(); - pal.setColor(QPalette::Base, Qt::transparent); - setPalette(pal); - - // Wrap anywhere but prefer words, adjust minimum height on the fly. - setLineWrapMode(QTextEdit::WidgetWidth); - setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); - connect(document()->documentLayout(), - &QAbstractTextDocumentLayout::documentSizeChanged, - this, - &TextLabel::adjustHeight); - document()->setDocumentMargin(0); - - setFixedHeight(20); - } - - void wheelEvent(QWheelEvent *event) override { event->ignore(); } - -private slots: - void adjustHeight(const QSizeF &size) { setFixedHeight(size.height()); } -}; - -class UserProfileFilter : public QObject -{ - Q_OBJECT - -public: - explicit UserProfileFilter(const QString &user_id, QLabel *parent) - : QObject(parent) - , user_id_{user_id} - {} - -signals: - void hoverOff(); - void hoverOn(); - -protected: - bool eventFilter(QObject *obj, QEvent *event) - { - if (event->type() == QEvent::MouseButtonRelease) { - // QMouseEvent *mouseEvent = static_cast(event); - // TODO: Open user profile - return true; - } else if (event->type() == QEvent::HoverLeave) { - emit hoverOff(); - return true; - } else if (event->type() == QEvent::HoverEnter) { - emit hoverOn(); - return true; - } - - return QObject::eventFilter(obj, event); - } - -private: - QString user_id_; -}; - -class TimelineItem : public QWidget -{ - Q_OBJECT -public: - TimelineItem(const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - - // For local messages. - // m.text & m.emote - TimelineItem(mtx::events::MessageType ty, - const QString &userid, - QString body, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - // m.image - TimelineItem(ImageItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(FileItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(AudioItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(VideoItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - - TimelineItem(ImageItem *img, - const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(StickerItem *img, - const mtx::events::Sticker &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(FileItem *file, - const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(AudioItem *audio, - const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(VideoItem *video, - const mtx::events::RoomEvent &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - - void setUserAvatar(const QImage &pixmap); - DescInfo descriptionMessage() const { return descriptionMsg_; } - QString eventId() const { return event_id_; } - void setEventId(const QString &event_id) { event_id_ = event_id; } - void markReceived(bool isEncrypted); - void markSent(); - bool isReceived() { return isReceived_; }; - void setRoomId(QString room_id) { room_id_ = room_id; } - void sendReadReceipt() const; - - //! Add a user avatar for this event. - void addAvatar(); - void addKeyRequestAction(); - -signals: - void eventRedacted(const QString &event_id); - void redactionFailed(const QString &msg); - -protected: - void paintEvent(QPaintEvent *event) override; - void contextMenuEvent(QContextMenuEvent *event) override; - -private: - void init(); - //! Add a context menu option to save the image of the timeline item. - void addSaveImageAction(ImageItem *image); - //! Add the reply action in the context menu for widgets that support it. - void addReplyAction(); - - template - void setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender); - - template - void setupWidgetLayout(Widget *widget, const Event &event, bool withSender); - - void generateBody(const QString &body); - void generateBody(const QString &user_id, const QString &displayname, const QString &body); - void generateTimestamp(const QDateTime &time); - - void setupAvatarLayout(const QString &userName); - void setupSimpleLayout(); - - void adjustMessageLayout(); - void adjustMessageLayoutForWidget(); - - //! Whether or not the event associated with the widget - //! has been acknowledged by the server. - bool isReceived_ = false; - - QString replaceEmoji(const QString &body); - QString event_id_; - QString room_id_; - - DescInfo descriptionMsg_; - - QMenu *contextMenu_; - QAction *showReadReceipts_; - QAction *markAsRead_; - QAction *redactMsg_; - QAction *replyMsg_; - - QHBoxLayout *topLayout_ = nullptr; - QHBoxLayout *messageLayout_ = nullptr; - QVBoxLayout *mainLayout_ = nullptr; - QHBoxLayout *widgetLayout_ = nullptr; - - Avatar *userAvatar_; - - QFont font_; - QFont usernameFont_; - - StatusIndicator *statusIndicator_; - - QLabel *timestamp_; - QLabel *userName_; - TextLabel *body_; -}; - -template -void -TimelineItem::setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender) -{ - auto displayName = Cache::displayName(room_id_, userid); - auto timestamp = QDateTime::currentDateTime(); - - descriptionMsg_ = {"You", - userid, - QString(" %1").arg(utils::messageDescription()), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - widgetLayout_ = new QHBoxLayout; - widgetLayout_->setContentsMargins(0, 2, 0, 2); - widgetLayout_->addWidget(widget); - widgetLayout_->addStretch(1); - - if (withSender) { - generateBody(userid, displayName, ""); - setupAvatarLayout(displayName); - - AvatarProvider::resolve( - room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); - } else { - setupSimpleLayout(); - } - - adjustMessageLayoutForWidget(); -} - -template -void -TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSender) -{ - init(); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - - QSettings settings; - descriptionMsg_ = {sender == settings.value("auth/user_id") ? "You" : displayName, - sender, - QString(" %1").arg(utils::messageDescription()), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - widgetLayout_ = new QHBoxLayout(); - widgetLayout_->setContentsMargins(0, 2, 0, 2); - widgetLayout_->addWidget(widget); - widgetLayout_->addStretch(1); - - if (withSender) { - generateBody(sender, displayName, ""); - setupAvatarLayout(displayName); - - AvatarProvider::resolve( - room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); - } else { - setupSimpleLayout(); - } - - adjustMessageLayoutForWidget(); -} diff --git a/include/timeline/TimelineView.h b/include/timeline/TimelineView.h deleted file mode 100644 index e6b80637..00000000 --- a/include/timeline/TimelineView.h +++ /dev/null @@ -1,426 +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 . - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "MatrixClient.h" -#include "ScrollBar.h" -#include "TimelineItem.h" - -class StateKeeper -{ -public: - StateKeeper(std::function &&fn) - : fn_(std::move(fn)) - {} - - ~StateKeeper() { fn_(); } - -private: - std::function fn_; -}; - -struct DecryptionResult -{ - //! The decrypted content as a normal plaintext event. - utils::TimelineEvent event; - //! Whether or not the decryption was successful. - bool isDecrypted = false; -}; - -class FloatingButton; -struct DescInfo; - -// Contains info about a message shown in the history view -// but not yet confirmed by the homeserver through sync. -struct PendingMessage -{ - mtx::events::MessageType ty; - std::string txn_id; - QString body; - QString filename; - QString mime; - uint64_t media_size; - QString event_id; - TimelineItem *widget; - QSize dimensions; - bool is_encrypted = false; -}; - -template -MessageT -toRoomMessage(const PendingMessage &) = delete; - -template<> -mtx::events::msg::Audio -toRoomMessage(const PendingMessage &m); - -template<> -mtx::events::msg::Emote -toRoomMessage(const PendingMessage &m); - -template<> -mtx::events::msg::File -toRoomMessage(const PendingMessage &); - -template<> -mtx::events::msg::Image -toRoomMessage(const PendingMessage &m); - -template<> -mtx::events::msg::Text -toRoomMessage(const PendingMessage &); - -template<> -mtx::events::msg::Video -toRoomMessage(const PendingMessage &m); - -// In which place new TimelineItems should be inserted. -enum class TimelineDirection -{ - Top, - Bottom, -}; - -class TimelineView : public QWidget -{ - Q_OBJECT - -public: - TimelineView(const mtx::responses::Timeline &timeline, - const QString &room_id, - QWidget *parent = 0); - TimelineView(const QString &room_id, QWidget *parent = 0); - - // Add new events at the end of the timeline. - void addEvents(const mtx::responses::Timeline &timeline); - void addUserMessage(mtx::events::MessageType ty, const QString &msg); - - template - void addUserMessage(const QString &url, - const QString &filename, - const QString &mime, - uint64_t size, - const QSize &dimensions = QSize()); - void updatePendingMessage(const std::string &txn_id, const QString &event_id); - void scrollDown(); - - //! Remove an item from the timeline with the given Event ID. - void removeEvent(const QString &event_id); - void setPrevBatchToken(const QString &token) { prev_batch_token_ = token; } - -public slots: - void sliderRangeChanged(int min, int max); - void sliderMoved(int position); - void fetchHistory(); - - // Add old events at the top of the timeline. - void addBackwardsEvents(const mtx::responses::Messages &msgs); - - // Whether or not the initial batch has been loaded. - bool hasLoaded() { return scroll_layout_->count() > 1 || isTimelineFinished; } - - void handleFailedMessage(const std::string &txn_id); - -private slots: - void sendNextPendingMessage(); - -signals: - void updateLastTimelineMessage(const QString &user, const DescInfo &info); - void messagesRetrieved(const mtx::responses::Messages &res); - void messageFailed(const std::string &txn_id); - void messageSent(const std::string &txn_id, const QString &event_id); - -protected: - void paintEvent(QPaintEvent *event) override; - void showEvent(QShowEvent *event) override; - bool event(QEvent *event) override; - -private: - using TimelineEvent = mtx::events::collections::TimelineEvents; - - QWidget *relativeWidget(QWidget *item, int dt) const; - - DecryptionResult parseEncryptedEvent( - const mtx::events::EncryptedEvent &e); - - void handleClaimedKeys(std::shared_ptr keeper, - const std::map &room_key, - const std::map &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err); - - //! Callback for all message sending. - void sendRoomMessageHandler(const std::string &txn_id, - const mtx::responses::EventId &res, - mtx::http::RequestErr err); - void prepareEncryptedMessage(const PendingMessage &msg); - - //! Call the /messages endpoint to fill the timeline. - void getMessages(); - //! HACK: Fixing layout flickering when adding to the bottom - //! of the timeline. - void pushTimelineItem(QWidget *item) - { - item->hide(); - scroll_layout_->addWidget(item); - QTimer::singleShot(0, this, [item]() { item->show(); }); - }; - - //! Decides whether or not to show or hide the scroll down button. - void toggleScrollDownButton(); - void init(); - void addTimelineItem(QWidget *item, - TimelineDirection direction = TimelineDirection::Bottom); - void updateLastSender(const QString &user_id, TimelineDirection direction); - void notifyForLastEvent(); - void notifyForLastEvent(const TimelineEvent &event); - //! Keep track of the sender and the timestamp of the current message. - void saveLastMessageInfo(const QString &sender, const QDateTime &datetime) - { - lastSender_ = sender; - lastMsgTimestamp_ = datetime; - } - void saveFirstMessageInfo(const QString &sender, const QDateTime &datetime) - { - firstSender_ = sender; - firstMsgTimestamp_ = datetime; - } - //! Keep track of the sender and the timestamp of the current message. - void saveMessageInfo(const QString &sender, - uint64_t origin_server_ts, - TimelineDirection direction); - - TimelineEvent findFirstViewableEvent(const std::vector &events); - TimelineEvent findLastViewableEvent(const std::vector &events); - - //! Mark the last event as read. - void readLastEvent() const; - //! Whether or not the scrollbar is visible (non-zero height). - bool isScrollbarActivated() { return scroll_area_->verticalScrollBar()->value() != 0; } - //! Retrieve the event id of the last item. - QString getLastEventId() const; - - template - TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); - - // TODO: Remove this eventually. - template - TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); - - // For events with custom display widgets. - template - TimelineItem *createTimelineItem(const Event &event, bool withSender); - - // For events without custom display widgets. - // TODO: All events should have custom widgets. - template - TimelineItem *createTimelineItem(const Event &event, bool withSender); - - // Used to determine whether or not we should prefix a message with the - // sender's name. - bool isSenderRendered(const QString &user_id, - uint64_t origin_server_ts, - TimelineDirection direction); - - bool isPendingMessage(const std::string &txn_id, - const QString &sender, - const QString &userid); - void removePendingMessage(const std::string &txn_id); - - bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); } - - void handleNewUserMessage(PendingMessage msg); - bool isDateDifference(const QDateTime &first, - const QDateTime &second = QDateTime::currentDateTime()) const; - - // Return nullptr if the event couldn't be parsed. - QWidget *parseMessageEvent(const mtx::events::collections::TimelineEvents &event, - TimelineDirection direction); - - //! Store the event id associated with the given widget. - void saveEventId(QWidget *widget); - - QVBoxLayout *top_layout_; - QVBoxLayout *scroll_layout_; - - QScrollArea *scroll_area_; - ScrollBar *scrollbar_; - QWidget *scroll_widget_; - - QString firstSender_; - QDateTime firstMsgTimestamp_; - QString lastSender_; - QDateTime lastMsgTimestamp_; - - QString room_id_; - QString prev_batch_token_; - QString local_user_; - - bool isPaginationInProgress_ = false; - - // Keeps track whether or not the user has visited the view. - bool isInitialized = false; - bool isTimelineFinished = false; - bool isInitialSync = true; - - const int SCROLL_BAR_GAP = 200; - - QTimer *paginationTimer_; - - int scroll_height_ = 0; - int previous_max_height_ = 0; - - int oldPosition_; - int oldHeight_; - - FloatingButton *scrollDownBtn_; - - TimelineDirection lastMessageDirection_; - - //! Messages received by sync not added to the timeline. - std::vector bottomMessages_; - //! Messages received by /messages not added to the timeline. - std::vector topMessages_; - - //! Render the given timeline events to the bottom of the timeline. - void renderBottomEvents(const std::vector &events); - //! Render the given timeline events to the top of the timeline. - void renderTopEvents(const std::vector &events); - - // The events currently rendered. Used for duplicate detection. - QMap eventIds_; - QQueue pending_msgs_; - QList pending_sent_msgs_; -}; - -template -void -TimelineView::addUserMessage(const QString &url, - const QString &filename, - const QString &mime, - uint64_t size, - const QSize &dimensions) -{ - auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_); - auto trimmed = QFileInfo{filename}.fileName(); // Trim file path. - - auto widget = new Widget(url, trimmed, size, this); - - TimelineItem *view_item = - new TimelineItem(widget, local_user_, with_sender, room_id_, scroll_widget_); - - addTimelineItem(view_item); - - lastMessageDirection_ = TimelineDirection::Bottom; - - // Keep track of the sender and the timestamp of the current message. - saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - - PendingMessage message; - message.ty = MsgType; - message.txn_id = http::client()->generate_txn_id(); - message.body = url; - message.filename = trimmed; - message.mime = mime; - message.media_size = size; - message.widget = view_item; - message.dimensions = dimensions; - - handleNewUserMessage(message); -} - -template -TimelineItem * -TimelineView::createTimelineItem(const Event &event, bool withSender) -{ - TimelineItem *item = new TimelineItem(event, withSender, room_id_, scroll_widget_); - return item; -} - -template -TimelineItem * -TimelineView::createTimelineItem(const Event &event, bool withSender) -{ - auto eventWidget = new Widget(event); - auto item = new TimelineItem(eventWidget, event, withSender, room_id_, scroll_widget_); - - return item; -} - -template -TimelineItem * -TimelineView::processMessageEvent(const Event &event, TimelineDirection direction) -{ - const auto event_id = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - const auto txn_id = event.unsigned_data.transaction_id; - if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || - isDuplicate(event_id)) { - removePendingMessage(txn_id); - return nullptr; - } - - auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction); - - saveMessageInfo(sender, event.origin_server_ts, direction); - - auto item = createTimelineItem(event, with_sender); - - eventIds_[event_id] = item; - - return item; -} - -template -TimelineItem * -TimelineView::processMessageEvent(const Event &event, TimelineDirection direction) -{ - const auto event_id = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - const auto txn_id = event.unsigned_data.transaction_id; - if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || - isDuplicate(event_id)) { - removePendingMessage(txn_id); - return nullptr; - } - - auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction); - - saveMessageInfo(sender, event.origin_server_ts, direction); - - auto item = createTimelineItem(event, with_sender); - - eventIds_[event_id] = item; - - return item; -} diff --git a/include/timeline/TimelineViewManager.h b/include/timeline/TimelineViewManager.h deleted file mode 100644 index f3c099c1..00000000 --- a/include/timeline/TimelineViewManager.h +++ /dev/null @@ -1,94 +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 . - */ - -#pragma once - -#include -#include - -#include - -class QFile; - -class RoomInfoListItem; -class TimelineView; -struct DescInfo; -struct SavedMessages; - -class TimelineViewManager : public QStackedWidget -{ - Q_OBJECT - -public: - TimelineViewManager(QWidget *parent); - - // Initialize with timeline events. - void initialize(const mtx::responses::Rooms &rooms); - // Empty initialization. - void initialize(const std::vector &rooms); - - void addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id); - void addRoom(const QString &room_id); - - void sync(const mtx::responses::Rooms &rooms); - void clearAll() { views_.clear(); } - - // Check if all the timelines have been loaded. - bool hasLoaded() const; - - static QString chooseRandomColor(); - -signals: - void clearRoomMessageCount(QString roomid); - void updateRoomsLastMessage(const QString &user, const DescInfo &info); - -public slots: - void removeTimelineEvent(const QString &room_id, const QString &event_id); - void initWithMessages(const std::map &msgs); - - void setHistoryView(const QString &room_id); - void queueTextMessage(const QString &msg); - void queueEmoteMessage(const QString &msg); - void queueImageMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize, - const QSize &dimensions); - void queueFileMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize); - void queueAudioMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize); - void queueVideoMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t dsize); - -private: - //! Check if the given room id is managed by a TimelineView. - bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); } - - QString active_room_; - std::map> views_; -}; diff --git a/include/timeline/widgets/AudioItem.h b/include/timeline/widgets/AudioItem.h deleted file mode 100644 index 7b0781a2..00000000 --- a/include/timeline/widgets/AudioItem.h +++ /dev/null @@ -1,107 +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 . - */ - -#pragma once - -#include -#include -#include -#include -#include -#include - -#include - -class AudioItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - - Q_PROPERTY(QColor durationBackgroundColor WRITE setDurationBackgroundColor READ - durationBackgroundColor) - Q_PROPERTY(QColor durationForegroundColor WRITE setDurationForegroundColor READ - durationForegroundColor) - -public: - AudioItem(const mtx::events::RoomEvent &event, - QWidget *parent = nullptr); - - AudioItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - - void setTextColor(const QColor &color) { textColor_ = color; } - void setIconColor(const QColor &color) { iconColor_ = color; } - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - - void setDurationBackgroundColor(const QColor &color) { durationBgColor_ = color; } - void setDurationForegroundColor(const QColor &color) { durationFgColor_ = color; } - - QColor textColor() const { return textColor_; } - QColor iconColor() const { return iconColor_; } - QColor backgroundColor() const { return backgroundColor_; } - - QColor durationBackgroundColor() const { return durationBgColor_; } - QColor durationForegroundColor() const { return durationFgColor_; } - -protected: - void paintEvent(QPaintEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - -signals: - void fileDownloadedCb(const QByteArray &data); - -private slots: - void fileDownloaded(const QByteArray &data); - -private: - void init(); - - enum class AudioState - { - Play, - Pause, - }; - - AudioState state_ = AudioState::Play; - - QUrl url_; - QString text_; - QString readableFileSize_; - QString filenameToSave_; - - mtx::events::RoomEvent event_; - - QMediaPlayer *player_; - - QIcon playIcon_; - QIcon pauseIcon_; - - QColor textColor_ = QColor("white"); - QColor iconColor_ = QColor("#38A3D8"); - QColor backgroundColor_ = QColor("#333"); - - QColor durationBgColor_ = QColor("black"); - QColor durationFgColor_ = QColor("blue"); -}; diff --git a/include/timeline/widgets/FileItem.h b/include/timeline/widgets/FileItem.h deleted file mode 100644 index 66543e79..00000000 --- a/include/timeline/widgets/FileItem.h +++ /dev/null @@ -1,82 +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 . - */ - -#pragma once - -#include -#include -#include -#include -#include - -#include - -class FileItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - -public: - FileItem(const mtx::events::RoomEvent &event, - QWidget *parent = nullptr); - - FileItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - - void setTextColor(const QColor &color) { textColor_ = color; } - void setIconColor(const QColor &color) { iconColor_ = color; } - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - - QColor textColor() const { return textColor_; } - QColor iconColor() const { return iconColor_; } - QColor backgroundColor() const { return backgroundColor_; } - -signals: - void fileDownloadedCb(const QByteArray &data); - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - -private slots: - void fileDownloaded(const QByteArray &data); - -private: - void openUrl(); - void init(); - - QUrl url_; - QString text_; - QString readableFileSize_; - QString filenameToSave_; - - mtx::events::RoomEvent event_; - - QIcon icon_; - - QColor textColor_ = QColor("white"); - QColor iconColor_ = QColor("#38A3D8"); - QColor backgroundColor_ = QColor("#333"); -}; diff --git a/include/timeline/widgets/ImageItem.h b/include/timeline/widgets/ImageItem.h deleted file mode 100644 index e9d823f4..00000000 --- a/include/timeline/widgets/ImageItem.h +++ /dev/null @@ -1,108 +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 . - */ - -#pragma once - -#include -#include -#include -#include - -#include - -namespace dialogs { -class ImageOverlay; -} - -class ImageItem : public QWidget -{ - Q_OBJECT -public: - ImageItem(const mtx::events::RoomEvent &event, - QWidget *parent = nullptr); - - ImageItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - -public slots: - //! Show a save as dialog for the image. - void saveAs(); - void setImage(const QPixmap &image); - void saveImage(const QString &filename, const QByteArray &data); - -signals: - void imageDownloaded(const QPixmap &img); - void imageSaved(const QString &filename, const QByteArray &data); - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - - //! Whether the user can interact with the displayed image. - bool isInteractive_ = true; - -private: - void init(); - void openUrl(); - void downloadMedia(const QUrl &url); - - int max_width_ = 500; - int max_height_ = 300; - - int width_; - int height_; - - QPixmap scaled_image_; - QPixmap image_; - - QUrl url_; - QString text_; - - int bottom_height_ = 30; - - QRectF textRegion_; - QRectF imageRegion_; - - mtx::events::RoomEvent event_; -}; - -class StickerItem : public ImageItem -{ - Q_OBJECT - -public: - StickerItem(const mtx::events::Sticker &event, QWidget *parent = nullptr) - : ImageItem{QString::fromStdString(event.content.url), - QString::fromStdString(event.content.body), - event.content.info.size, - parent} - , event_{event} - { - isInteractive_ = false; - setCursor(Qt::ArrowCursor); - setMouseTracking(false); - setAttribute(Qt::WA_Hover, false); - } - -private: - mtx::events::Sticker event_; -}; diff --git a/include/timeline/widgets/VideoItem.h b/include/timeline/widgets/VideoItem.h deleted file mode 100644 index 26fa1c35..00000000 --- a/include/timeline/widgets/VideoItem.h +++ /dev/null @@ -1,51 +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 . - */ - -#pragma once - -#include -#include -#include -#include -#include - -#include - -class VideoItem : public QWidget -{ - Q_OBJECT - -public: - VideoItem(const mtx::events::RoomEvent &event, - QWidget *parent = nullptr); - - VideoItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - -private: - void init(); - - QUrl url_; - QString text_; - QString readableFileSize_; - - QLabel *label_; - - mtx::events::RoomEvent event_; -}; diff --git a/include/ui/Avatar.h b/include/ui/Avatar.h deleted file mode 100644 index 41967af5..00000000 --- a/include/ui/Avatar.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "Theme.h" - -class Avatar : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - -public: - explicit Avatar(QWidget *parent = 0); - - void setBackgroundColor(const QColor &color); - void setIcon(const QIcon &icon); - void setImage(const QImage &image); - void setLetter(const QString &letter); - void setSize(int size); - void setTextColor(const QColor &color); - - QColor backgroundColor() const; - QColor textColor() const; - int size() const; - - QSize sizeHint() const override; - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - void init(); - - ui::AvatarType type_; - QString letter_; - QColor background_color_; - QColor text_color_; - QIcon icon_; - QImage image_; - QPixmap pixmap_; - int size_; -}; diff --git a/include/ui/Badge.h b/include/ui/Badge.h deleted file mode 100644 index fd73ad30..00000000 --- a/include/ui/Badge.h +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "OverlayWidget.h" - -class Badge : public OverlayWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - Q_PROPERTY(QPointF relativePosition WRITE setRelativePosition READ relativePosition) - -public: - explicit Badge(QWidget *parent = 0); - explicit Badge(const QIcon &icon, QWidget *parent = 0); - explicit Badge(const QString &text, QWidget *parent = 0); - - void setBackgroundColor(const QColor &color); - void setTextColor(const QColor &color); - void setIcon(const QIcon &icon); - void setRelativePosition(const QPointF &pos); - void setRelativePosition(qreal x, qreal y); - void setRelativeXPosition(qreal x); - void setRelativeYPosition(qreal y); - void setText(const QString &text); - void setDiameter(int diameter); - - QIcon icon() const; - QString text() const; - QColor backgroundColor() const; - QColor textColor() const; - QPointF relativePosition() const; - QSize sizeHint() const override; - qreal relativeXPosition() const; - qreal relativeYPosition() const; - - int diameter() const; - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - void init(); - - QColor background_color_; - QColor text_color_; - - QIcon icon_; - QSize size_; - QString text_; - - int padding_; - int diameter_; - - qreal x_; - qreal y_; -}; diff --git a/include/ui/DropShadow.h b/include/ui/DropShadow.h deleted file mode 100644 index b7ba1985..00000000 --- a/include/ui/DropShadow.h +++ /dev/null @@ -1,111 +0,0 @@ -#pragma once - -#include -#include -#include - -class DropShadow -{ -public: - static void draw(QPainter &painter, - qint16 margin, - qreal radius, - QColor start, - QColor end, - qreal startPosition, - qreal endPosition0, - qreal endPosition1, - qreal width, - qreal height) - { - painter.setPen(Qt::NoPen); - - QLinearGradient gradient; - gradient.setColorAt(startPosition, start); - gradient.setColorAt(endPosition0, end); - - // Right - QPointF right0(width - margin, height / 2); - QPointF right1(width, height / 2); - gradient.setStart(right0); - gradient.setFinalStop(right1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect( - QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)), - 0.0, - 0.0); - - // Left - QPointF left0(margin, height / 2); - QPointF left1(0, height / 2); - gradient.setStart(left0); - gradient.setFinalStop(left1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect( - QRectF(QPointF(margin * radius, margin), QPointF(0, height - margin)), 0.0, 0.0); - - // Top - QPointF top0(width / 2, margin); - QPointF top1(width / 2, 0); - gradient.setStart(top0); - gradient.setFinalStop(top1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect( - QRectF(QPointF(width - margin, 0), QPointF(margin, margin)), 0.0, 0.0); - - // Bottom - QPointF bottom0(width / 2, height - margin); - QPointF bottom1(width / 2, height); - gradient.setStart(bottom0); - gradient.setFinalStop(bottom1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect( - QRectF(QPointF(margin, height - margin), QPointF(width - margin, height)), - 0.0, - 0.0); - - // BottomRight - QPointF bottomright0(width - margin, height - margin); - QPointF bottomright1(width, height); - gradient.setStart(bottomright0); - gradient.setFinalStop(bottomright1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect(QRectF(bottomright0, bottomright1), 0.0, 0.0); - - // BottomLeft - QPointF bottomleft0(margin, height - margin); - QPointF bottomleft1(0, height); - gradient.setStart(bottomleft0); - gradient.setFinalStop(bottomleft1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0); - - // TopLeft - QPointF topleft0(margin, margin); - QPointF topleft1(0, 0); - gradient.setStart(topleft0); - gradient.setFinalStop(topleft1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect(QRectF(topleft0, topleft1), 0.0, 0.0); - - // TopRight - QPointF topright0(width - margin, margin); - QPointF topright1(width, 0); - gradient.setStart(topright0); - gradient.setFinalStop(topright1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect(QRectF(topright0, topright1), 0.0, 0.0); - - // Widget - painter.setBrush(QBrush("#FFFFFF")); - painter.setRenderHint(QPainter::Antialiasing); - painter.drawRoundRect( - QRectF(QPointF(margin, margin), QPointF(width - margin, height - margin)), - radius, - radius); - } -}; diff --git a/include/ui/FlatButton.h b/include/ui/FlatButton.h deleted file mode 100644 index 9c2bf425..00000000 --- a/include/ui/FlatButton.h +++ /dev/null @@ -1,185 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "Theme.h" - -class RippleOverlay; -class FlatButton; - -class FlatButtonStateMachine : public QStateMachine -{ - Q_OBJECT - - Q_PROPERTY(qreal overlayOpacity WRITE setOverlayOpacity READ overlayOpacity) - Q_PROPERTY( - qreal checkedOverlayProgress WRITE setCheckedOverlayProgress READ checkedOverlayProgress) - -public: - explicit FlatButtonStateMachine(FlatButton *parent); - ~FlatButtonStateMachine(); - - void setOverlayOpacity(qreal opacity); - void setCheckedOverlayProgress(qreal opacity); - - inline qreal overlayOpacity() const; - inline qreal checkedOverlayProgress() const; - - void startAnimations(); - void setupProperties(); - void updateCheckedStatus(); - -signals: - void buttonPressed(); - void buttonChecked(); - void buttonUnchecked(); - -protected: - bool eventFilter(QObject *watched, QEvent *event) override; - -private: - void addTransition(QObject *object, const char *signal, QState *fromState, QState *toState); - void addTransition(QObject *object, - QEvent::Type eventType, - QState *fromState, - QState *toState); - void addTransition(QAbstractTransition *transition, QState *fromState, QState *toState); - - FlatButton *const button_; - - QState *const top_level_state_; - QState *const config_state_; - QState *const checkable_state_; - QState *const checked_state_; - QState *const unchecked_state_; - QState *const neutral_state_; - QState *const neutral_focused_state_; - QState *const hovered_state_; - QState *const hovered_focused_state_; - QState *const pressed_state_; - - qreal overlay_opacity_; - qreal checked_overlay_progress_; - - bool was_checked_; -}; - -inline qreal -FlatButtonStateMachine::overlayOpacity() const -{ - return overlay_opacity_; -} - -inline qreal -FlatButtonStateMachine::checkedOverlayProgress() const -{ - return checked_overlay_progress_; -} - -class FlatButton : public QPushButton -{ - Q_OBJECT - - Q_PROPERTY(QColor foregroundColor WRITE setForegroundColor READ foregroundColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - Q_PROPERTY(QColor overlayColor WRITE setOverlayColor READ overlayColor) - Q_PROPERTY(QColor disabledForegroundColor WRITE setDisabledForegroundColor READ - disabledForegroundColor) - Q_PROPERTY(QColor disabledBackgroundColor WRITE setDisabledBackgroundColor READ - disabledBackgroundColor) - Q_PROPERTY(qreal fontSize WRITE setFontSize READ fontSize) - -public: - explicit FlatButton(QWidget *parent = 0, - ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset); - explicit FlatButton(const QString &text, - QWidget *parent = 0, - ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset); - FlatButton(const QString &text, - ui::Role role, - QWidget *parent = 0, - ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset); - ~FlatButton(); - - void applyPreset(ui::ButtonPreset preset); - - void setBackgroundColor(const QColor &color); - void setBackgroundMode(Qt::BGMode mode); - void setBaseOpacity(qreal opacity); - void setCheckable(bool value); - void setCornerRadius(qreal radius); - void setDisabledBackgroundColor(const QColor &color); - void setDisabledForegroundColor(const QColor &color); - void setFixedRippleRadius(qreal radius); - void setFontSize(qreal size); - void setForegroundColor(const QColor &color); - void setHasFixedRippleRadius(bool value); - void setIconPlacement(ui::ButtonIconPlacement placement); - void setOverlayColor(const QColor &color); - void setOverlayStyle(ui::OverlayStyle style); - void setRippleStyle(ui::RippleStyle style); - void setRole(ui::Role role); - - QColor foregroundColor() const; - QColor backgroundColor() const; - QColor overlayColor() const; - QColor disabledForegroundColor() const; - QColor disabledBackgroundColor() const; - - qreal fontSize() const; - qreal cornerRadius() const; - qreal baseOpacity() const; - - bool hasFixedRippleRadius() const; - - ui::Role role() const; - ui::OverlayStyle overlayStyle() const; - ui::RippleStyle rippleStyle() const; - ui::ButtonIconPlacement iconPlacement() const; - - Qt::BGMode backgroundMode() const; - - QSize sizeHint() const override; - -protected: - int IconPadding = 0; - - void checkStateSet() override; - void mousePressEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - void paintEvent(QPaintEvent *event) override; - - virtual void paintBackground(QPainter *painter); - virtual void paintForeground(QPainter *painter); - virtual void updateClipPath(); - - void init(); - -private: - RippleOverlay *ripple_overlay_; - FlatButtonStateMachine *state_machine_; - - ui::Role role_; - ui::RippleStyle ripple_style_; - ui::ButtonIconPlacement icon_placement_; - ui::OverlayStyle overlay_style_; - - Qt::BGMode bg_mode_; - - QColor background_color_; - QColor foreground_color_; - QColor overlay_color_; - QColor disabled_color_; - QColor disabled_background_color_; - - qreal fixed_ripple_radius_; - qreal corner_radius_; - qreal base_opacity_; - qreal font_size_; - - bool use_fixed_ripple_radius_; -}; diff --git a/include/ui/FloatingButton.h b/include/ui/FloatingButton.h deleted file mode 100644 index 91e99ebb..00000000 --- a/include/ui/FloatingButton.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include "RaisedButton.h" - -constexpr int DIAMETER = 40; -constexpr int ICON_SIZE = 18; - -constexpr int OFFSET_X = 30; -constexpr int OFFSET_Y = 20; - -class FloatingButton : public RaisedButton -{ - Q_OBJECT - -public: - FloatingButton(const QIcon &icon, QWidget *parent = nullptr); - - QSize sizeHint() const override { return QSize(DIAMETER, DIAMETER); }; - QRect buttonGeometry() const; - -protected: - bool event(QEvent *event) override; - bool eventFilter(QObject *obj, QEvent *event) override; - - void paintEvent(QPaintEvent *event) override; -}; diff --git a/include/ui/InfoMessage.hpp b/include/ui/InfoMessage.hpp deleted file mode 100644 index 58f98b0c..00000000 --- a/include/ui/InfoMessage.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include -#include -#include - -class InfoMessage : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor boxColor WRITE setBoxColor READ boxColor) - -public: - explicit InfoMessage(QWidget *parent = nullptr); - InfoMessage(QString msg, QWidget *parent = nullptr); - - void setTextColor(QColor color) { textColor_ = color; } - void setBoxColor(QColor color) { boxColor_ = color; } - void saveDatetime(QDateTime datetime) { datetime_ = datetime; } - - QColor textColor() const { return textColor_; } - QColor boxColor() const { return boxColor_; } - QDateTime datetime() const { return datetime_; } - -protected: - void paintEvent(QPaintEvent *event) override; - - int width_; - int height_; - - QString msg_; - QFont font_; - - QDateTime datetime_; - - QColor textColor_ = QColor("black"); - QColor boxColor_ = QColor("white"); -}; - -class DateSeparator : public InfoMessage -{ - Q_OBJECT - -public: - DateSeparator(QDateTime datetime, QWidget *parent = nullptr); -}; diff --git a/include/ui/Label.h b/include/ui/Label.h deleted file mode 100644 index 09cf27d7..00000000 --- a/include/ui/Label.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include - -class Label : public QLabel -{ - Q_OBJECT - -public: - explicit Label(QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags()); - explicit Label(const QString &text, - QWidget *parent = Q_NULLPTR, - Qt::WindowFlags f = Qt::WindowFlags()); - -signals: - void clicked(QMouseEvent *e); - void pressed(QMouseEvent *e); - void released(QMouseEvent *e); - -protected: - void mousePressEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - - QPoint pressPosition_; -}; diff --git a/include/ui/LoadingIndicator.h b/include/ui/LoadingIndicator.h deleted file mode 100644 index bb33fe6c..00000000 --- a/include/ui/LoadingIndicator.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -class LoadingIndicator : public QWidget -{ - Q_OBJECT - Q_PROPERTY(QColor color READ color WRITE setColor) - -public: - LoadingIndicator(QWidget *parent = 0); - virtual ~LoadingIndicator(); - - void paintEvent(QPaintEvent *e); - - void start(); - void stop(); - - QColor color() { return color_; } - void setColor(QColor color) { color_ = color; } - - int interval() { return interval_; } - void setInterval(int interval) { interval_ = interval; } - -private slots: - void onTimeout(); - -private: - int interval_; - int angle_; - - QColor color_; - QTimer *timer_; -}; diff --git a/include/ui/Menu.h b/include/ui/Menu.h deleted file mode 100644 index 4c2a3c68..00000000 --- a/include/ui/Menu.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include - -#include "Config.h" - -class Menu : public QMenu -{ -public: - Menu(QWidget *parent = nullptr) - : QMenu(parent) - { - QFont font; - font.setPixelSize(conf::fontSize); - - setFont(font); - setStyleSheet( - "QMenu { color: black; background-color: white; margin: 0px;}" - "QMenu::item {" - "color: black; padding: 7px 20px; border: 1px solid transparent;" - "margin: 2px 0px; }" - "QMenu::item:selected { color: black; background: rgba(180, 180, 180, 100); }"); - }; - -protected: - void leaveEvent(QEvent *e) - { - Q_UNUSED(e); - - hide(); - } -}; diff --git a/include/ui/OverlayModal.h b/include/ui/OverlayModal.h deleted file mode 100644 index a761e3ed..00000000 --- a/include/ui/OverlayModal.h +++ /dev/null @@ -1,45 +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 . - */ - -#pragma once - -#include -#include -#include - -#include "OverlayWidget.h" - -class OverlayModal : public OverlayWidget -{ -public: - OverlayModal(QWidget *parent, QWidget *content); - - void setColor(QColor color) { color_ = color; } - void setDismissible(bool state) { isDismissible_ = state; } - -protected: - void paintEvent(QPaintEvent *event) override; - void keyPressEvent(QKeyEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - -private: - QWidget *content_; - QColor color_; - - //! Decides whether or not the modal can be removed by clicking into it. - bool isDismissible_ = true; -}; diff --git a/include/ui/OverlayWidget.h b/include/ui/OverlayWidget.h deleted file mode 100644 index 6662479d..00000000 --- a/include/ui/OverlayWidget.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -class OverlayWidget : public QWidget -{ - Q_OBJECT - -public: - explicit OverlayWidget(QWidget *parent = nullptr); - -protected: - bool event(QEvent *event) override; - bool eventFilter(QObject *obj, QEvent *event) override; - - QRect overlayGeometry() const; - void paintEvent(QPaintEvent *event) override; -}; diff --git a/include/ui/Painter.h b/include/ui/Painter.h deleted file mode 100644 index 8de39651..00000000 --- a/include/ui/Painter.h +++ /dev/null @@ -1,161 +0,0 @@ -#pragma once - -#include -#include -#include - -class Painter : public QPainter -{ -public: - explicit Painter(QPaintDevice *device) - : QPainter(device) - {} - - void drawTextLeft(int x, int y, const QString &text) - { - QFontMetrics m(fontMetrics()); - drawText(x, y + m.ascent(), text); - } - - void drawTextRight(int x, int y, int outerw, const QString &text, int textWidth = -1) - { - QFontMetrics m(fontMetrics()); - if (textWidth < 0) - textWidth = m.width(text); - drawText((outerw - x - textWidth), y + m.ascent(), text); - } - - void drawPixmapLeft(int x, int y, const QPixmap &pix, const QRect &from) - { - drawPixmap(QPoint(x, y), pix, from); - } - - void drawPixmapLeft(const QPoint &p, const QPixmap &pix, const QRect &from) - { - return drawPixmapLeft(p.x(), p.y(), pix, from); - } - - void drawPixmapLeft(int x, int y, int w, int h, const QPixmap &pix, const QRect &from) - { - drawPixmap(QRect(x, y, w, h), pix, from); - } - - void drawPixmapLeft(const QRect &r, const QPixmap &pix, const QRect &from) - { - return drawPixmapLeft(r.x(), r.y(), r.width(), r.height(), pix, from); - } - - void drawPixmapLeft(int x, int y, int outerw, const QPixmap &pix) - { - Q_UNUSED(outerw); - drawPixmap(QPoint(x, y), pix); - } - - void drawPixmapLeft(const QPoint &p, int outerw, const QPixmap &pix) - { - return drawPixmapLeft(p.x(), p.y(), outerw, pix); - } - - void drawPixmapRight(int x, int y, int outerw, const QPixmap &pix, const QRect &from) - { - drawPixmap( - QPoint((outerw - x - (from.width() / pix.devicePixelRatio())), y), pix, from); - } - - void drawPixmapRight(const QPoint &p, int outerw, const QPixmap &pix, const QRect &from) - { - return drawPixmapRight(p.x(), p.y(), outerw, pix, from); - } - void drawPixmapRight(int x, - int y, - int w, - int h, - int outerw, - const QPixmap &pix, - const QRect &from) - { - drawPixmap(QRect((outerw - x - w), y, w, h), pix, from); - } - - void drawPixmapRight(const QRect &r, int outerw, const QPixmap &pix, const QRect &from) - { - return drawPixmapRight(r.x(), r.y(), r.width(), r.height(), outerw, pix, from); - } - - void drawPixmapRight(int x, int y, int outerw, const QPixmap &pix) - { - drawPixmap(QPoint((outerw - x - (pix.width() / pix.devicePixelRatio())), y), pix); - } - - void drawPixmapRight(const QPoint &p, int outerw, const QPixmap &pix) - { - return drawPixmapRight(p.x(), p.y(), outerw, pix); - } - - void drawAvatar(const QPixmap &pix, int w, int h, int d) - { - QPainterPath pp; - pp.addEllipse((w - d) / 2, (h - d) / 2, d, d); - - QRect region((w - d) / 2, (h - d) / 2, d, d); - - setClipPath(pp); - drawPixmap(region, pix); - } - - void drawLetterAvatar(const QString &c, - const QColor &penColor, - const QColor &brushColor, - int w, - int h, - int d) - { - QRect region((w - d) / 2, (h - d) / 2, d, d); - - setPen(Qt::NoPen); - setBrush(brushColor); - - drawEllipse(region.center(), d / 2, d / 2); - - setBrush(Qt::NoBrush); - drawEllipse(region.center(), d / 2, d / 2); - - setPen(penColor); - drawText(region.translated(0, -1), Qt::AlignCenter, c); - } -}; - -class PainterHighQualityEnabler -{ -public: - PainterHighQualityEnabler(Painter &p) - : _painter(p) - { - static constexpr QPainter::RenderHint Hints[] = {QPainter::Antialiasing, - QPainter::SmoothPixmapTransform, - QPainter::TextAntialiasing, - QPainter::HighQualityAntialiasing}; - - auto hints = _painter.renderHints(); - for (const auto &hint : Hints) { - if (!(hints & hint)) - hints_ |= hint; - } - - if (hints_) - _painter.setRenderHints(hints_); - } - - ~PainterHighQualityEnabler() - { - if (hints_) - _painter.setRenderHints(hints_, false); - } - - PainterHighQualityEnabler(const PainterHighQualityEnabler &other) = delete; - PainterHighQualityEnabler &operator=(const PainterHighQualityEnabler &other) = delete; - -private: - Painter &_painter; - QPainter::RenderHints hints_ = 0; -}; diff --git a/include/ui/RaisedButton.h b/include/ui/RaisedButton.h deleted file mode 100644 index edd5ee4a..00000000 --- a/include/ui/RaisedButton.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "FlatButton.h" - -class RaisedButton : public FlatButton -{ - Q_OBJECT - -public: - explicit RaisedButton(QWidget *parent = 0); - explicit RaisedButton(const QString &text, QWidget *parent = 0); - ~RaisedButton(); - -protected: - bool event(QEvent *event) override; - -private: - void init(); - - QStateMachine *shadow_state_machine_; - QState *normal_state_; - QState *pressed_state_; - QGraphicsDropShadowEffect *effect_; -}; diff --git a/include/ui/Ripple.h b/include/ui/Ripple.h deleted file mode 100644 index 9184f061..00000000 --- a/include/ui/Ripple.h +++ /dev/null @@ -1,145 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -class RippleOverlay; - -class Ripple : public QParallelAnimationGroup -{ - Q_OBJECT - - Q_PROPERTY(qreal radius WRITE setRadius READ radius) - Q_PROPERTY(qreal opacity WRITE setOpacity READ opacity) - -public: - explicit Ripple(const QPoint ¢er, QObject *parent = 0); - Ripple(const QPoint ¢er, RippleOverlay *overlay, QObject *parent = 0); - - inline void setOverlay(RippleOverlay *overlay); - - void setRadius(qreal radius); - void setOpacity(qreal opacity); - void setColor(const QColor &color); - void setBrush(const QBrush &brush); - - inline qreal radius() const; - inline qreal opacity() const; - inline QColor color() const; - inline QBrush brush() const; - inline QPoint center() const; - - inline QPropertyAnimation *radiusAnimation() const; - inline QPropertyAnimation *opacityAnimation() const; - - inline void setOpacityStartValue(qreal value); - inline void setOpacityEndValue(qreal value); - inline void setRadiusStartValue(qreal value); - inline void setRadiusEndValue(qreal value); - inline void setDuration(int msecs); - -protected slots: - void destroy(); - -private: - Q_DISABLE_COPY(Ripple) - - QPropertyAnimation *animate(const QByteArray &property, - const QEasingCurve &easing = QEasingCurve::OutQuad, - int duration = 800); - - void init(); - - RippleOverlay *overlay_; - - QPropertyAnimation *const radius_anim_; - QPropertyAnimation *const opacity_anim_; - - qreal radius_; - qreal opacity_; - - QPoint center_; - QBrush brush_; -}; - -inline void -Ripple::setOverlay(RippleOverlay *overlay) -{ - overlay_ = overlay; -} - -inline qreal -Ripple::radius() const -{ - return radius_; -} - -inline qreal -Ripple::opacity() const -{ - return opacity_; -} - -inline QColor -Ripple::color() const -{ - return brush_.color(); -} - -inline QBrush -Ripple::brush() const -{ - return brush_; -} - -inline QPoint -Ripple::center() const -{ - return center_; -} - -inline QPropertyAnimation * -Ripple::radiusAnimation() const -{ - return radius_anim_; -} - -inline QPropertyAnimation * -Ripple::opacityAnimation() const -{ - return opacity_anim_; -} - -inline void -Ripple::setOpacityStartValue(qreal value) -{ - opacity_anim_->setStartValue(value); -} - -inline void -Ripple::setOpacityEndValue(qreal value) -{ - opacity_anim_->setEndValue(value); -} - -inline void -Ripple::setRadiusStartValue(qreal value) -{ - radius_anim_->setStartValue(value); -} - -inline void -Ripple::setRadiusEndValue(qreal value) -{ - radius_anim_->setEndValue(value); -} - -inline void -Ripple::setDuration(int msecs) -{ - radius_anim_->setDuration(msecs); - opacity_anim_->setDuration(msecs); -} diff --git a/include/ui/RippleOverlay.h b/include/ui/RippleOverlay.h deleted file mode 100644 index 9ef91fbf..00000000 --- a/include/ui/RippleOverlay.h +++ /dev/null @@ -1,57 +0,0 @@ -#pragma once - -#include - -#include "OverlayWidget.h" - -class Ripple; - -class RippleOverlay : public OverlayWidget -{ - Q_OBJECT - -public: - explicit RippleOverlay(QWidget *parent = 0); - - void addRipple(Ripple *ripple); - void addRipple(const QPoint &position, qreal radius = 300); - - void removeRipple(Ripple *ripple); - - inline void setClipping(bool enable); - inline bool hasClipping() const; - - inline void setClipPath(const QPainterPath &path); - -protected: - void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE; - -private: - Q_DISABLE_COPY(RippleOverlay) - - void paintRipple(QPainter *painter, Ripple *ripple); - - QList ripples_; - QPainterPath clip_path_; - bool use_clip_; -}; - -inline void -RippleOverlay::setClipping(bool enable) -{ - use_clip_ = enable; - update(); -} - -inline bool -RippleOverlay::hasClipping() const -{ - return use_clip_; -} - -inline void -RippleOverlay::setClipPath(const QPainterPath &path) -{ - clip_path_ = path; - update(); -} diff --git a/include/ui/ScrollBar.h b/include/ui/ScrollBar.h deleted file mode 100644 index 2b5382aa..00000000 --- a/include/ui/ScrollBar.h +++ /dev/null @@ -1,54 +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 . - */ - -#pragma once - -#include -#include -#include - -class ScrollBar : public QScrollBar -{ - Q_OBJECT - Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) - Q_PROPERTY(QColor handleColor READ handleColor WRITE setHandleColor) - -public: - ScrollBar(QScrollArea *area, QWidget *parent = nullptr); - - QColor backgroundColor() const { return bgColor_; } - void setBackgroundColor(QColor &color) { bgColor_ = color; } - - QColor handleColor() const { return handleColor_; } - void setHandleColor(QColor &color) { handleColor_ = color; } - -protected: - void paintEvent(QPaintEvent *e) override; - -private: - int roundRadius_ = 4; - int handleWidth_ = 7; - int minHandleHeight_ = 20; - - const int Padding = 4; - - QScrollArea *area_; - QRect handle_; - - QColor bgColor_ = QColor(33, 33, 33, 30); - QColor handleColor_ = QColor(0, 0, 0, 80); -}; diff --git a/include/ui/SnackBar.h b/include/ui/SnackBar.h deleted file mode 100644 index eed59c87..00000000 --- a/include/ui/SnackBar.h +++ /dev/null @@ -1,79 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "OverlayWidget.h" - -enum class SnackBarPosition -{ - Bottom, - Top, -}; - -class SnackBar : public OverlayWidget -{ - Q_OBJECT - -public: - explicit SnackBar(QWidget *parent); - - inline void setBackgroundColor(const QColor &color); - inline void setTextColor(const QColor &color); - inline void setPosition(SnackBarPosition pos); - -public slots: - void showMessage(const QString &msg); - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - -private slots: - void hideMessage(); - -private: - void stopTimers(); - void start(); - - QColor bgColor_; - QColor textColor_; - - qreal bgOpacity_; - qreal offset_; - - std::deque messages_; - - QTimer showTimer_; - QTimer hideTimer_; - - int duration_; - int boxWidth_; - int boxHeight_; - int boxPadding_; - - SnackBarPosition position_; -}; - -inline void -SnackBar::setPosition(SnackBarPosition pos) -{ - position_ = pos; - update(); -} - -inline void -SnackBar::setBackgroundColor(const QColor &color) -{ - bgColor_ = color; - update(); -} - -inline void -SnackBar::setTextColor(const QColor &color) -{ - textColor_ = color; - update(); -} diff --git a/include/ui/TextField.h b/include/ui/TextField.h deleted file mode 100644 index 1675a2e0..00000000 --- a/include/ui/TextField.h +++ /dev/null @@ -1,174 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -class TextField; -class TextFieldLabel; -class TextFieldStateMachine; - -class TextField : public QLineEdit -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor inkColor WRITE setInkColor READ inkColor) - Q_PROPERTY(QColor labelColor WRITE setLabelColor READ labelColor) - Q_PROPERTY(QColor underlineColor WRITE setUnderlineColor READ underlineColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - -public: - explicit TextField(QWidget *parent = 0); - - void setInkColor(const QColor &color); - void setBackgroundColor(const QColor &color); - void setLabel(const QString &label); - void setLabelColor(const QColor &color); - void setLabelFontSize(qreal size); - void setShowLabel(bool value); - void setTextColor(const QColor &color); - void setUnderlineColor(const QColor &color); - - QColor inkColor() const; - QColor labelColor() const; - QColor textColor() const; - QColor underlineColor() const; - QColor backgroundColor() const; - QString label() const; - bool hasLabel() const; - qreal labelFontSize() const; - -protected: - bool event(QEvent *event) override; - void paintEvent(QPaintEvent *event) override; - -private: - void init(); - - QColor ink_color_; - QColor background_color_; - QColor label_color_; - QColor text_color_; - QColor underline_color_; - QString label_text_; - TextFieldLabel *label_; - TextFieldStateMachine *state_machine_; - bool show_label_; - qreal label_font_size_; -}; - -class TextFieldLabel : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(qreal scale WRITE setScale READ scale) - Q_PROPERTY(QPointF offset WRITE setOffset READ offset) - Q_PROPERTY(QColor color WRITE setColor READ color) - -public: - TextFieldLabel(TextField *parent); - - inline void setColor(const QColor &color); - inline void setOffset(const QPointF &pos); - inline void setScale(qreal scale); - - inline QColor color() const; - inline QPointF offset() const; - inline qreal scale() const; - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - TextField *const text_field_; - - QColor color_; - qreal scale_; - qreal x_; - qreal y_; -}; - -inline void -TextFieldLabel::setColor(const QColor &color) -{ - color_ = color; - update(); -} - -inline void -TextFieldLabel::setOffset(const QPointF &pos) -{ - x_ = pos.x(); - y_ = pos.y(); - update(); -} - -inline void -TextFieldLabel::setScale(qreal scale) -{ - scale_ = scale; - update(); -} - -inline QPointF -TextFieldLabel::offset() const -{ - return QPointF(x_, y_); -} -inline qreal -TextFieldLabel::scale() const -{ - return scale_; -} -inline QColor -TextFieldLabel::color() const -{ - return color_; -} - -class TextFieldStateMachine : public QStateMachine -{ - Q_OBJECT - - Q_PROPERTY(qreal progress WRITE setProgress READ progress) - -public: - TextFieldStateMachine(TextField *parent); - - inline void setProgress(qreal progress); - void setLabel(TextFieldLabel *label); - - inline qreal progress() const; - -public slots: - void setupProperties(); - -private: - QPropertyAnimation *color_anim_; - QPropertyAnimation *offset_anim_; - - QState *focused_state_; - QState *normal_state_; - - TextField *text_field_; - TextFieldLabel *label_; - - qreal progress_; -}; - -inline void -TextFieldStateMachine::setProgress(qreal progress) -{ - progress_ = progress; - text_field_->update(); -} - -inline qreal -TextFieldStateMachine::progress() const -{ - return progress_; -} diff --git a/include/ui/Theme.h b/include/ui/Theme.h deleted file mode 100644 index 7a0bdcb7..00000000 --- a/include/ui/Theme.h +++ /dev/null @@ -1,97 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace ui { -enum class AvatarType -{ - Icon, - Image, - Letter -}; - -namespace sidebar { -static const int SmallSize = 60; -static const int NormalSize = 260; -static const int CommunitiesSidebarSize = 48; -} -// Default font size. -const int FontSize = 16; - -// Default avatar size. Width and height. -const int AvatarSize = 40; - -enum class ButtonPreset -{ - FlatPreset, - CheckablePreset -}; - -enum class RippleStyle -{ - CenteredRipple, - PositionedRipple, - NoRipple -}; - -enum class OverlayStyle -{ - NoOverlay, - TintedOverlay, - GrayOverlay -}; - -enum class Role -{ - Default, - Primary, - Secondary -}; - -enum class ButtonIconPlacement -{ - LeftIcon, - RightIcon -}; - -enum class ProgressType -{ - DeterminateProgress, - IndeterminateProgress -}; - -enum class Color -{ - Black, - BrightWhite, - FadedWhite, - MediumWhite, - DarkGreen, - LightGreen, - BrightGreen, - Gray, - Red, - Blue, - Transparent -}; - -} // namespace ui - -class Theme : public QObject -{ - Q_OBJECT -public: - explicit Theme(QObject *parent = 0); - - QColor getColor(const QString &key) const; - - void setColor(const QString &key, const QColor &color); - void setColor(const QString &key, ui::Color color); - -private: - QColor rgba(int r, int g, int b, qreal a) const; - - QHash colors_; -}; diff --git a/include/ui/ThemeManager.h b/include/ui/ThemeManager.h deleted file mode 100644 index d35ff754..00000000 --- a/include/ui/ThemeManager.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include - -#include "Theme.h" - -class ThemeManager : public QCommonStyle -{ - Q_OBJECT - -public: - inline static ThemeManager &instance(); - - void setTheme(Theme *theme); - QColor themeColor(const QString &key) const; - -private: - ThemeManager(); - - ThemeManager(ThemeManager const &); - void operator=(ThemeManager const &); - - Theme *theme_; -}; - -inline ThemeManager & -ThemeManager::instance() -{ - static ThemeManager instance; - return instance; -} diff --git a/include/ui/ToggleButton.h b/include/ui/ToggleButton.h deleted file mode 100644 index 14c3450b..00000000 --- a/include/ui/ToggleButton.h +++ /dev/null @@ -1,110 +0,0 @@ -#pragma once - -#include -#include - -class ToggleTrack; -class ToggleThumb; - -enum class Position -{ - Left, - Right -}; - -class Toggle : public QAbstractButton -{ - Q_OBJECT - - Q_PROPERTY(QColor activeColor WRITE setActiveColor READ activeColor) - Q_PROPERTY(QColor disabledColor WRITE setDisabledColor READ disabledColor) - Q_PROPERTY(QColor inactiveColor WRITE setInactiveColor READ inactiveColor) - Q_PROPERTY(QColor trackColor WRITE setTrackColor READ trackColor) - -public: - Toggle(QWidget *parent = nullptr); - - void setState(bool isEnabled); - - void setActiveColor(const QColor &color); - void setDisabledColor(const QColor &color); - void setInactiveColor(const QColor &color); - void setTrackColor(const QColor &color); - - QColor activeColor() const { return activeColor_; }; - QColor disabledColor() const { return disabledColor_; }; - QColor inactiveColor() const { return inactiveColor_; }; - QColor trackColor() const { return trackColor_.isValid() ? trackColor_ : QColor("#eee"); }; - - QSize sizeHint() const override { return QSize(64, 48); }; - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - void init(); - void setupProperties(); - - ToggleTrack *track_; - ToggleThumb *thumb_; - - QColor disabledColor_; - QColor activeColor_; - QColor inactiveColor_; - QColor trackColor_; -}; - -class ToggleThumb : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor thumbColor WRITE setThumbColor READ thumbColor) - -public: - ToggleThumb(Toggle *parent); - - Position shift() const { return position_; }; - qreal offset() const { return offset_; }; - QColor thumbColor() const { return thumbColor_; }; - - void setShift(Position position); - void setThumbColor(const QColor &color) - { - thumbColor_ = color; - update(); - }; - -protected: - bool eventFilter(QObject *obj, QEvent *event) override; - void paintEvent(QPaintEvent *event) override; - -private: - void updateOffset(); - - Toggle *const toggle_; - QColor thumbColor_; - - Position position_; - qreal offset_; -}; - -class ToggleTrack : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor trackColor WRITE setTrackColor READ trackColor) - -public: - ToggleTrack(Toggle *parent); - - void setTrackColor(const QColor &color); - QColor trackColor() const { return trackColor_; }; - -protected: - bool eventFilter(QObject *obj, QEvent *event) override; - void paintEvent(QPaintEvent *event) override; - -private: - Toggle *const toggle_; - QColor trackColor_; -}; 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