summary refs log tree commit diff
diff options
context:
space:
mode:
authorDeepBlueV7.X <nicolas.werner@hotmail.de>2021-06-13 01:44:25 +0000
committerGitHub <noreply@github.com>2021-06-13 01:44:25 +0000
commit5b4566d3f959e13b34d34a4156a0646b26c7fd08 (patch)
tree11d1b42e1d9f92977c48670605bb8a2e26ddd43c
parentTranslated using Weblate (Esperanto) (diff)
parentFix button spacing (diff)
downloadnheko-5b4566d3f959e13b34d34a4156a0646b26c7fd08.tar.xz
Merge pull request #605 from Nheko-Reborn/qml-roomlist
Qml roomlist and stuff
-rw-r--r--CMakeLists.txt42
-rw-r--r--resources/icons/ui/user-friends-solid.svg1
-rw-r--r--resources/qml/Avatar.qml9
-rw-r--r--resources/qml/ChatPage.qml94
-rw-r--r--resources/qml/CommunitiesList.qml160
-rw-r--r--resources/qml/Completer.qml31
-rw-r--r--resources/qml/ElidedLabel.qml28
-rw-r--r--resources/qml/EncryptionIndicator.qml2
-rw-r--r--resources/qml/ForwardCompleter.qml16
-rw-r--r--resources/qml/ImageButton.qml5
-rw-r--r--resources/qml/MatrixText.qml4
-rw-r--r--resources/qml/MatrixTextField.qml10
-rw-r--r--resources/qml/MessageInput.qml72
-rw-r--r--resources/qml/MessageView.qml164
-rw-r--r--resources/qml/NhekoBusyIndicator.qml3
-rw-r--r--resources/qml/QuickSwitcher.qml7
-rw-r--r--resources/qml/Reactions.qml18
-rw-r--r--resources/qml/ReplyPopup.qml6
-rw-r--r--resources/qml/RoomList.qml604
-rw-r--r--resources/qml/RoomSettings.qml12
-rw-r--r--resources/qml/Root.qml123
-rw-r--r--resources/qml/StatusIndicator.qml2
-rw-r--r--resources/qml/TimelineRow.qml10
-rw-r--r--resources/qml/TimelineView.qml409
-rw-r--r--resources/qml/TopBar.qml34
-rw-r--r--resources/qml/TypingIndicator.qml8
-rw-r--r--resources/qml/UserProfile.qml14
-rw-r--r--resources/qml/components/AdaptiveLayout.qml137
-rw-r--r--resources/qml/components/AdaptiveLayoutElement.qml26
-rw-r--r--resources/qml/delegates/FileMessage.qml10
-rw-r--r--resources/qml/delegates/ImageMessage.qml12
-rw-r--r--resources/qml/delegates/MessageDelegate.qml12
-rw-r--r--resources/qml/delegates/NoticeMessage.qml4
-rw-r--r--resources/qml/delegates/Pill.qml5
-rw-r--r--resources/qml/delegates/Placeholder.qml3
-rw-r--r--resources/qml/delegates/PlayableMediaMessage.qml24
-rw-r--r--resources/qml/delegates/Reply.qml2
-rw-r--r--resources/qml/delegates/TextMessage.qml21
-rw-r--r--resources/qml/device-verification/AwaitingVerificationConfirmation.qml2
-rw-r--r--resources/qml/device-verification/DeviceVerification.qml4
-rw-r--r--resources/qml/device-verification/DigitVerification.qml8
-rw-r--r--resources/qml/device-verification/EmojiVerification.qml6
-rw-r--r--resources/qml/device-verification/Failed.qml2
-rw-r--r--resources/qml/device-verification/NewVerificationRequest.qml2
-rw-r--r--resources/qml/device-verification/Success.qml3
-rw-r--r--resources/qml/device-verification/Waiting.qml4
-rw-r--r--resources/qml/dialogs/InputDialog.qml53
-rw-r--r--resources/qml/emoji/EmojiButton.qml2
-rw-r--r--resources/qml/emoji/EmojiPicker.qml16
-rw-r--r--resources/qml/voip/ActiveCallBar.qml6
-rw-r--r--resources/qml/voip/CallDevices.qml10
-rw-r--r--resources/qml/voip/CallInvite.qml16
-rw-r--r--resources/qml/voip/CallInviteBar.qml10
-rw-r--r--resources/qml/voip/DeviceError.qml8
-rw-r--r--resources/qml/voip/PlaceCall.qml28
-rw-r--r--resources/qml/voip/ScreenShare.qml16
-rw-r--r--resources/res.qrc8
-rw-r--r--src/Cache.cpp90
-rw-r--r--src/Cache.h2
-rw-r--r--src/CacheStructs.h13
-rw-r--r--src/Cache_p.h6
-rw-r--r--src/ChatPage.cpp337
-rw-r--r--src/ChatPage.h60
-rw-r--r--src/CommunitiesList.cpp345
-rw-r--r--src/CommunitiesList.h66
-rw-r--r--src/CommunitiesListItem.cpp201
-rw-r--r--src/CommunitiesListItem.h108
-rw-r--r--src/CompletionProxyModel.cpp6
-rw-r--r--src/MainWindow.cpp36
-rw-r--r--src/MainWindow.h5
-rw-r--r--src/Olm.cpp9
-rw-r--r--src/RoomInfoListItem.cpp522
-rw-r--r--src/RoomInfoListItem.h210
-rw-r--r--src/RoomList.cpp540
-rw-r--r--src/RoomList.h101
-rw-r--r--src/SideBarActions.cpp120
-rw-r--r--src/SideBarActions.h54
-rw-r--r--src/Splitter.cpp166
-rw-r--r--src/Splitter.h49
-rw-r--r--src/UserInfoWidget.cpp219
-rw-r--r--src/UserInfoWidget.h68
-rw-r--r--src/UserSettingsPage.cpp68
-rw-r--r--src/UserSettingsPage.h12
-rw-r--r--src/popups/PopupItem.cpp89
-rw-r--r--src/popups/PopupItem.h66
-rw-r--r--src/popups/SuggestionsPopup.cpp164
-rw-r--r--src/popups/SuggestionsPopup.h53
-rw-r--r--src/popups/UserMentions.cpp178
-rw-r--r--src/popups/UserMentions.h49
-rw-r--r--src/timeline/CommunitiesModel.cpp188
-rw-r--r--src/timeline/CommunitiesModel.h64
-rw-r--r--src/timeline/EventStore.cpp14
-rw-r--r--src/timeline/EventStore.h2
-rw-r--r--src/timeline/InputBar.cpp36
-rw-r--r--src/timeline/InputBar.h1
-rw-r--r--src/timeline/RoomlistModel.cpp590
-rw-r--r--src/timeline/RoomlistModel.h164
-rw-r--r--src/timeline/TimelineModel.cpp45
-rw-r--r--src/timeline/TimelineModel.h15
-rw-r--r--src/timeline/TimelineViewManager.cpp326
-rw-r--r--src/timeline/TimelineViewManager.h68
-rw-r--r--src/ui/NhekoDropArea.cpp2
-rw-r--r--src/ui/NhekoGlobalObject.cpp142
-rw-r--r--src/ui/NhekoGlobalObject.h57
-rw-r--r--src/ui/Theme.cpp125
-rw-r--r--src/ui/Theme.h42
-rw-r--r--src/ui/ThemeManager.cpp39
-rw-r--r--src/ui/ThemeManager.h5
-rw-r--r--src/ui/UserProfile.cpp36
-rw-r--r--src/ui/UserProfile.h3
110 files changed, 3473 insertions, 4851 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt

index 96948827..5e4b0f3e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt
@@ -137,7 +137,7 @@ endif() # # Discover Qt dependencies. # -find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia Qml QuickControls2 QuickWidgets REQUIRED) +find_package(Qt5 5.12 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia Qml QuickControls2 QuickWidgets REQUIRED) find_package(Qt5QuickCompiler) find_package(Qt5DBus) @@ -265,6 +265,7 @@ set(SRC_FILES # Timeline + src/timeline/CommunitiesModel.cpp src/timeline/EventStore.cpp src/timeline/InputBar.cpp src/timeline/Reaction.cpp @@ -272,6 +273,7 @@ set(SRC_FILES src/timeline/TimelineModel.cpp src/timeline/DelegateChooser.cpp src/timeline/Permissions.cpp + src/timeline/RoomlistModel.cpp # UI components src/ui/Avatar.cpp @@ -284,11 +286,13 @@ set(SRC_FILES src/ui/LoadingIndicator.cpp src/ui/NhekoCursorShape.cpp src/ui/NhekoDropArea.cpp + src/ui/NhekoGlobalObject.cpp src/ui/OverlayModal.cpp src/ui/OverlayWidget.cpp src/ui/RaisedButton.cpp src/ui/Ripple.cpp src/ui/RippleOverlay.cpp + src/ui/RoomSettings.cpp src/ui/SnackBar.cpp src/ui/TextField.cpp src/ui/TextLabel.cpp @@ -296,7 +300,6 @@ set(SRC_FILES src/ui/ThemeManager.cpp src/ui/ToggleButton.cpp src/ui/UserProfile.cpp - src/ui/RoomSettings.cpp # Generic notification stuff src/notifications/Manager.cpp @@ -309,8 +312,6 @@ set(SRC_FILES src/ChatPage.cpp src/Clipboard.cpp src/ColorImageProvider.cpp - src/CommunitiesList.cpp - src/CommunitiesListItem.cpp src/CompletionProxyModel.cpp src/DeviceVerificationFlow.cpp src/EventAccessors.cpp @@ -322,22 +323,14 @@ set(SRC_FILES src/MxcImageProvider.cpp src/Olm.cpp src/RegisterPage.cpp - src/RoomInfoListItem.cpp - src/RoomList.cpp src/SSOHandler.cpp - src/SideBarActions.cpp - src/Splitter.cpp src/TrayIcon.cpp - src/UserInfoWidget.cpp src/UserSettingsPage.cpp src/UsersModel.cpp src/RoomsModel.cpp src/Utils.cpp src/WebRTCSession.cpp src/WelcomePage.cpp - src/popups/PopupItem.cpp - src/popups/SuggestionsPopup.cpp - src/popups/UserMentions.cpp src/main.cpp third_party/blurhash/blurhash.cpp @@ -489,6 +482,7 @@ qt5_wrap_cpp(MOC_HEADERS src/emoji/Provider.h # Timeline + src/timeline/CommunitiesModel.h src/timeline/EventStore.h src/timeline/InputBar.h src/timeline/Reaction.h @@ -496,30 +490,32 @@ qt5_wrap_cpp(MOC_HEADERS src/timeline/TimelineModel.h src/timeline/DelegateChooser.h src/timeline/Permissions.h + src/timeline/RoomlistModel.h # UI components 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/InfoMessage.h + src/ui/Label.h + src/ui/LoadingIndicator.h src/ui/Menu.h src/ui/NhekoCursorShape.h src/ui/NhekoDropArea.h + src/ui/NhekoGlobalObject.h src/ui/OverlayWidget.h - src/ui/SnackBar.h src/ui/RaisedButton.h src/ui/Ripple.h src/ui/RippleOverlay.h + src/ui/RoomSettings.h + src/ui/SnackBar.h src/ui/TextField.h src/ui/TextLabel.h - src/ui/ToggleButton.h src/ui/Theme.h src/ui/ThemeManager.h + src/ui/ToggleButton.h src/ui/UserProfile.h - src/ui/RoomSettings.h src/notifications/Manager.h @@ -531,8 +527,6 @@ qt5_wrap_cpp(MOC_HEADERS src/CallManager.h src/ChatPage.h src/Clipboard.h - src/CommunitiesList.h - src/CommunitiesListItem.h src/CompletionProxyModel.h src/DeviceVerificationFlow.h src/InviteeItem.h @@ -540,21 +534,13 @@ qt5_wrap_cpp(MOC_HEADERS src/MainWindow.h src/MxcImageProvider.h src/RegisterPage.h - src/RoomInfoListItem.h - src/RoomList.h src/SSOHandler.h - src/SideBarActions.h - src/Splitter.h src/TrayIcon.h - src/UserInfoWidget.h src/UserSettingsPage.h src/UsersModel.h src/RoomsModel.h src/WebRTCSession.h src/WelcomePage.h - src/popups/PopupItem.h - src/popups/SuggestionsPopup.h - src/popups/UserMentions.h ) # diff --git a/resources/icons/ui/user-friends-solid.svg b/resources/icons/ui/user-friends-solid.svg new file mode 100644
index 00000000..1add45ec --- /dev/null +++ b/resources/icons/ui/user-friends-solid.svg
@@ -0,0 +1 @@ +<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="user-friends" class="svg-inline--fa fa-user-friends fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M192 256c61.9 0 112-50.1 112-112S253.9 32 192 32 80 82.1 80 144s50.1 112 112 112zm76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C51.6 288 0 339.6 0 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2zM480 256c53 0 96-43 96-96s-43-96-96-96-96 43-96 96 43 96 96 96zm48 32h-3.8c-13.9 4.8-28.6 8-44.2 8s-30.3-3.2-44.2-8H432c-20.4 0-39.2 5.9-55.7 15.4 24.4 26.3 39.7 61.2 39.7 99.8v38.4c0 2.2-.5 4.3-.6 6.4H592c26.5 0 48-21.5 48-48 0-61.9-50.1-112-112-112z"></path></svg> \ No newline at end of file diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml
index 108bb768..6c12952a 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml
@@ -14,18 +14,21 @@ Rectangle { property alias url: img.source property string userid property string displayName + property alias textColor: label.color signal clicked(var mouse) width: 48 height: 48 radius: Settings.avatarCircles ? height / 2 : 3 - color: colors.alternateBase + color: Nheko.colors.alternateBase Component.onCompleted: { mouseArea.clicked.connect(clicked); } Label { + id: label + anchors.fill: parent text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "") textFormat: Text.RichText @@ -33,7 +36,7 @@ Rectangle { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter visible: img.status != Image.Ready - color: colors.text + color: Nheko.colors.text } Image { @@ -55,7 +58,7 @@ Rectangle { Ripple { rippleTarget: mouseArea - color: Qt.rgba(colors.alternateBase.r, colors.alternateBase.g, colors.alternateBase.b, 0.5) + color: Qt.rgba(Nheko.colors.alternateBase.r, Nheko.colors.alternateBase.g, Nheko.colors.alternateBase.b, 0.5) } } diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml new file mode 100644
index 00000000..7a428019 --- /dev/null +++ b/resources/qml/ChatPage.qml
@@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.9 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.3 +import "components" +import im.nheko 1.0 + +Rectangle { + id: chatPage + + color: Nheko.colors.window + + AdaptiveLayout { + id: adaptiveView + + anchors.fill: parent + singlePageMode: width < communityListC.maximumWidth + roomListC.maximumWidth + timlineViewC.minimumWidth + pageIndex: Rooms.currentRoom ? 2 : 1 + + AdaptiveLayoutElement { + id: communityListC + + minimumWidth: communitiesList.avatarSize * 4 + Nheko.paddingMedium * 2 + collapsedWidth: communitiesList.avatarSize + 2 * Nheko.paddingMedium + preferredWidth: Settings.communityListWidth >= minimumWidth ? Settings.communityListWidth : collapsedWidth + maximumWidth: communitiesList.avatarSize * 10 + 2 * Nheko.paddingMedium + + CommunitiesList { + id: communitiesList + + collapsed: parent.collapsed + } + + Binding { + target: Settings + property: 'communityListWidth' + value: communityListC.preferredWidth + when: !adaptiveView.singlePageMode + delayed: true + } + + } + + AdaptiveLayoutElement { + id: roomListC + + minimumWidth: roomlist.avatarSize * 4 + Nheko.paddingSmall * 2 + preferredWidth: Settings.roomListWidth >= minimumWidth ? Settings.roomListWidth : roomlist.avatarSize * 5 + Nheko.paddingSmall * 2 + maximumWidth: roomlist.avatarSize * 10 + Nheko.paddingSmall * 2 + collapsedWidth: roomlist.avatarSize + 2 * Nheko.paddingMedium + + RoomList { + id: roomlist + + collapsed: parent.collapsed + } + + Binding { + target: Settings + property: 'roomListWidth' + value: roomListC.preferredWidth + when: !adaptiveView.singlePageMode + delayed: true + } + + } + + AdaptiveLayoutElement { + id: timlineViewC + + minimumWidth: 400 + + TimelineView { + id: timeline + + showBackButton: adaptiveView.singlePageMode + room: Rooms.currentRoom + } + + } + + } + + PrivacyScreen { + anchors.fill: parent + visible: Settings.privacyScreen + screenTimeout: Settings.privacyScreenTimeout + timelineRoot: adaptiveView + } + +} diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml new file mode 100644
index 00000000..491913be --- /dev/null +++ b/resources/qml/CommunitiesList.qml
@@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import "./dialogs" +import Qt.labs.platform 1.1 as Platform +import QtQml 2.12 +import QtQuick 2.12 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.3 +import im.nheko 1.0 + +Page { + //leftPadding: Nheko.paddingSmall + //rightPadding: Nheko.paddingSmall + property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 1.6) + property bool collapsed: false + + ListView { + id: communitiesList + + anchors.left: parent.left + anchors.right: parent.right + height: parent.height + model: Communities + + ScrollHelper { + flickable: parent + anchors.fill: parent + enabled: !Settings.mobileMode + } + + Platform.Menu { + id: communityContextMenu + + property string tagId + + function show(id_, tags_) { + tagId = id_; + open(); + } + + Platform.MenuItem { + text: qsTr("Hide rooms with this tag or from this space by default.") + onTriggered: Communities.toggleTagId(communityContextMenu.tagId) + } + + } + + delegate: Rectangle { + id: communityItem + + property color background: Nheko.colors.window + property color importantText: Nheko.colors.text + property color unimportantText: Nheko.colors.buttonText + property color bubbleBackground: Nheko.colors.highlight + property color bubbleText: Nheko.colors.highlightedText + + color: background + height: avatarSize + 2 * Nheko.paddingMedium + width: ListView.view.width + state: "normal" + ToolTip.visible: hovered.hovered && collapsed + ToolTip.text: model.tooltip + states: [ + State { + name: "highlight" + when: (hovered.hovered || model.hidden) && !(Communities.currentTagId == model.id) + + PropertyChanges { + target: communityItem + background: Nheko.colors.dark + importantText: Nheko.colors.brightText + unimportantText: Nheko.colors.brightText + bubbleBackground: Nheko.colors.highlight + bubbleText: Nheko.colors.highlightedText + } + + }, + State { + name: "selected" + when: Communities.currentTagId == model.id + + PropertyChanges { + target: communityItem + background: Nheko.colors.highlight + importantText: Nheko.colors.highlightedText + unimportantText: Nheko.colors.highlightedText + bubbleBackground: Nheko.colors.highlightedText + bubbleText: Nheko.colors.highlight + } + + } + ] + + TapHandler { + margin: -Nheko.paddingSmall + acceptedButtons: Qt.RightButton + onSingleTapped: communityContextMenu.show(model.id) + gesturePolicy: TapHandler.ReleaseWithinBounds + } + + TapHandler { + margin: -Nheko.paddingSmall + onSingleTapped: Communities.setCurrentTagId(model.id) + onLongPressed: communityContextMenu.show(model.id) + } + + HoverHandler { + id: hovered + + margin: -Nheko.paddingSmall + } + + RowLayout { + spacing: Nheko.paddingMedium + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + + Avatar { + id: avatar + + enabled: false + Layout.alignment: Qt.AlignVCenter + height: avatarSize + width: avatarSize + url: { + if (model.avatarUrl.startsWith("mxc://")) + return model.avatarUrl.replace("mxc://", "image://MxcImage/"); + else + return "image://colorimage/" + model.avatarUrl + "?" + communityItem.unimportantText; + } + displayName: model.displayName + color: communityItem.background + } + + ElidedLabel { + visible: !collapsed + Layout.alignment: Qt.AlignVCenter + color: communityItem.importantText + elideWidth: parent.width - avatar.width - Nheko.paddingMedium + fullText: model.displayName + textFormat: Text.PlainText + } + + Item { + Layout.fillWidth: true + } + + } + + } + + } + + background: Rectangle { + color: Nheko.theme.sidebarBackground + } + +} diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml
index d648553f..bbfa7b94 100644 --- a/resources/qml/Completer.qml +++ b/resources/qml/Completer.qml
@@ -70,7 +70,7 @@ Popup { onCompleterNameChanged: { if (completerName) { if (completerName == "user") - completer = TimelineManager.completerFor(completerName, TimelineManager.timeline.roomId()); + completer = TimelineManager.completerFor(completerName, room.roomId()); else completer = TimelineManager.completerFor(completerName); completer.setSearchString(""); @@ -83,8 +83,8 @@ Popup { height: listView.contentHeight + 2 // + 2 for the padding on top and bottom Connections { - onTimelineChanged: completer = null - target: TimelineManager + onRoomChanged: completer = null + target: timelineView } ListView { @@ -100,7 +100,7 @@ Popup { delegate: Rectangle { property variant modelData: model - color: model.index == popup.currentIndex ? colors.highlight : colors.base + color: model.index == popup.currentIndex ? Nheko.colors.highlight : Nheko.colors.base height: chooser.childrenRect.height + 2 * popup.rowMargin implicitWidth: fullWidth ? popup.width : chooser.childrenRect.width + 4 @@ -119,7 +119,7 @@ Popup { Ripple { rippleTarget: mouseArea - color: Qt.rgba(colors.base.r, colors.base.g, colors.base.b, 0.5) + color: Qt.rgba(Nheko.colors.base.r, Nheko.colors.base.g, Nheko.colors.base.b, 0.5) } } @@ -150,12 +150,12 @@ Popup { Label { text: model.displayName - color: model.index == popup.currentIndex ? colors.highlightedText : colors.text + color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text } Label { text: "(" + model.userid + ")" - color: model.index == popup.currentIndex ? colors.highlightedText : colors.buttonText + color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.buttonText } } @@ -173,13 +173,13 @@ Popup { Label { text: model.unicode - color: model.index == popup.currentIndex ? colors.highlightedText : colors.text + color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text font: Settings.emojiFont } Label { text: model.shortName - color: model.index == popup.currentIndex ? colors.highlightedText : colors.text + color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text } } @@ -209,7 +209,8 @@ Popup { Label { text: model.roomName font.pixelSize: popup.avatarHeight * 0.5 - color: model.index == popup.currentIndex ? colors.highlightedText : colors.text + color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text + textFormat: Text.RichText } } @@ -235,12 +236,14 @@ Popup { Label { text: model.roomName - color: model.index == popup.currentIndex ? colors.highlightedText : colors.text + color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text + textFormat: Text.RichText } Label { text: "(" + model.roomAlias + ")" - color: model.index == popup.currentIndex ? colors.highlightedText : colors.buttonText + color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.buttonText + textFormat: Text.RichText } } @@ -274,10 +277,10 @@ Popup { } background: Rectangle { - color: colors.base + color: Nheko.colors.base implicitHeight: popup.contentHeight implicitWidth: popup.contentWidth - border.color: colors.mid + border.color: Nheko.colors.mid } } diff --git a/resources/qml/ElidedLabel.qml b/resources/qml/ElidedLabel.qml new file mode 100644
index 00000000..bc90e479 --- /dev/null +++ b/resources/qml/ElidedLabel.qml
@@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.9 +import QtQuick.Controls 2.5 +import im.nheko 1.0 + +Label { + id: root + + property alias fullText: metrics.text + property alias elideWidth: metrics.elideWidth + + color: Nheko.colors.text + text: (textFormat == Text.PlainText) ? metrics.elidedText : TimelineManager.escapeEmoji(TimelineManager.htmlEscape(metrics.elidedText)) + maximumLineCount: 1 + elide: Text.ElideRight + textFormat: Text.PlainText + + TextMetrics { + id: metrics + + font.pointSize: root.font.pointSize + elide: Text.ElideRight + } + +} diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml
index 1e5d4d38..52d2eeed 100644 --- a/resources/qml/EncryptionIndicator.qml +++ b/resources/qml/EncryptionIndicator.qml
@@ -20,7 +20,7 @@ Image { case Crypto.Verified: return "image://colorimage/:/icons/icons/ui/lock.png?green"; case Crypto.TOFU: - return "image://colorimage/:/icons/icons/ui/lock.png?" + colors.buttonText; + return "image://colorimage/:/icons/icons/ui/lock.png?" + Nheko.colors.buttonText; default: return "image://colorimage/:/icons/icons/ui/lock.png?#dd3d3d"; } diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml
index 408cab12..eee3879c 100644 --- a/resources/qml/ForwardCompleter.qml +++ b/resources/qml/ForwardCompleter.qml
@@ -19,7 +19,7 @@ Popup { x: Math.round(parent.width / 2 - width / 2) y: Math.round(parent.height / 2 - height / 2) modal: true - palette: colors + palette: Nheko.colors parent: Overlay.overlay width: implicitWidth >= (timelineRoot.width * 0.8) ? implicitWidth : (timelineRoot.width * 0.8) height: implicitHeight + completerPopup.height + padding * 2 @@ -44,22 +44,22 @@ Popup { text: qsTr("Forward Message") font.bold: true bottomPadding: 10 - color: colors.text + color: Nheko.colors.text } Reply { id: replyPreview - modelData: TimelineManager.timeline ? TimelineManager.timeline.getDump(mid, "") : { + modelData: room ? room.getDump(mid, "") : { } - userColor: TimelineManager.userColor(modelData.userId, colors.window) + userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.window) } MatrixTextField { id: roomTextInput width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2 - color: colors.text + color: Nheko.colors.text onTextEdited: { completerPopup.completer.searchString = text; } @@ -95,7 +95,7 @@ Popup { Connections { onCompletionSelected: { - TimelineManager.timeline.forwardMessage(messageContextMenu.eventId, id); + room.forwardMessage(messageContextMenu.eventId, id); forwardMessagePopup.close(); } onCountChanged: { @@ -107,11 +107,11 @@ Popup { } background: Rectangle { - color: colors.window + color: Nheko.colors.window } Overlay.modal: Rectangle { - color: Qt.rgba(colors.window.r, colors.window.g, colors.window.b, 0.7) + color: Qt.rgba(Nheko.colors.window.r, Nheko.colors.window.g, Nheko.colors.window.b, 0.7) } } diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml
index 76cc0b42..b5d9c152 100644 --- a/resources/qml/ImageButton.qml +++ b/resources/qml/ImageButton.qml
@@ -12,8 +12,8 @@ AbstractButton { property alias cursor: mouseArea.cursorShape property string image: undefined - property color highlightColor: colors.highlight - property color buttonTextColor: colors.buttonText + property color highlightColor: Nheko.colors.highlight + property color buttonTextColor: Nheko.colors.buttonText property bool changeColorOnHover: true focusPolicy: Qt.NoFocus @@ -26,6 +26,7 @@ AbstractButton { // Workaround, can't get icon.source working for now... anchors.fill: parent source: image != "" ? ("image://colorimage/" + image + "?" + ((button.hovered && changeColorOnHover) ? highlightColor : buttonTextColor)) : "" + fillMode: Image.PreserveAspectFit } CursorShape { diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml
index 7cfa6735..167899a5 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml
@@ -13,8 +13,8 @@ TextEdit { wrapMode: Text.Wrap selectByMouse: !Settings.mobileMode enabled: selectByMouse - color: colors.text - onLinkActivated: TimelineManager.openLink(link) + color: Nheko.colors.text + onLinkActivated: Nheko.openLink(link) ToolTip.visible: hoveredLink ToolTip.text: hoveredLink diff --git a/resources/qml/MatrixTextField.qml b/resources/qml/MatrixTextField.qml
index 3bcc9675..3c660bac 100644 --- a/resources/qml/MatrixTextField.qml +++ b/resources/qml/MatrixTextField.qml
@@ -5,18 +5,20 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 +import im.nheko 1.0 TextField { id: input - palette: colors + palette: Nheko.colors + color: Nheko.colors.text Rectangle { id: blueBar anchors.top: parent.bottom anchors.horizontalCenter: parent.horizontalCenter - color: colors.highlight + color: Nheko.colors.highlight height: 1 width: parent.width @@ -27,7 +29,7 @@ TextField { anchors.horizontalCenter: parent.horizontalCenter height: parent.height + 1 width: 0 - color: colors.text + color: Nheko.colors.text states: State { name: "focused" @@ -60,7 +62,7 @@ TextField { } background: Rectangle { - color: colors.base + color: Nheko.colors.base } } diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index c5dfbfa3..24f9b0e8 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml
@@ -12,7 +12,7 @@ import im.nheko 1.0 Rectangle { id: inputBar - color: colors.window + color: Nheko.colors.window Layout.fillWidth: true Layout.preferredHeight: row.implicitHeight Layout.minimumHeight: 40 @@ -28,7 +28,7 @@ Rectangle { RowLayout { id: row - visible: (TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false) || messageContextMenu.isSender + visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false anchors.fill: parent ImageButton { @@ -43,7 +43,7 @@ Rectangle { ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call") Layout.margins: 8 onClicked: { - if (TimelineManager.timeline) { + if (room) { if (CallManager.haveCallInvite) { return ; } else if (CallManager.isOnCall) { @@ -63,14 +63,14 @@ Rectangle { height: 22 image: ":/icons/icons/ui/paper-clip-outline.png" Layout.margins: 8 - onClicked: TimelineManager.timeline.input.openFileSelection() + onClicked: room.input.openFileSelection() ToolTip.visible: hovered ToolTip.text: qsTr("Send a file") Rectangle { anchors.fill: parent - color: colors.window - visible: TimelineManager.timeline && TimelineManager.timeline.input.uploading + color: Nheko.colors.window + visible: room && room.input.uploading NhekoBusyIndicator { anchors.fill: parent @@ -116,23 +116,23 @@ Rectangle { selectByMouse: true placeholderText: qsTr("Write a message...") - placeholderTextColor: colors.buttonText - color: colors.text + placeholderTextColor: Nheko.colors.buttonText + color: Nheko.colors.text width: textInput.width wrapMode: TextEdit.Wrap padding: 8 focus: true onTextChanged: { - if (TimelineManager.timeline) - TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text); + if (room) + room.input.updateState(selectionStart, selectionEnd, cursorPosition, text); forceActiveFocus(); } onCursorPositionChanged: { - if (!TimelineManager.timeline) + if (!room) return ; - TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text); + room.input.updateState(selectionStart, selectionEnd, cursorPosition, text); if (cursorPosition <= completerTriggeredAt) { completerTriggeredAt = -1; popup.close(); @@ -141,13 +141,13 @@ Rectangle { popup.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)); } - onSelectionStartChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) - onSelectionEndChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) + onSelectionStartChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text) + onSelectionEndChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text) // Ensure that we get escape key press events first. Keys.onShortcutOverride: event.accepted = (completerTriggeredAt != -1 && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter)) Keys.onPressed: { if (event.matches(StandardKey.Paste)) { - TimelineManager.timeline.input.paste(false); + room.input.paste(false); event.accepted = true; } else if (event.key == Qt.Key_Space) { // close popup if user enters space after colon @@ -160,9 +160,9 @@ Rectangle { } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) { messageInput.clear(); } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) { - messageInput.text = TimelineManager.timeline.input.previousText(); + messageInput.text = room.input.previousText(); } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) { - messageInput.text = TimelineManager.timeline.input.nextText(); + messageInput.text = room.input.nextText(); } else if (event.key == Qt.Key_At) { messageInput.openCompleter(cursorPosition, "user"); popup.open(); @@ -188,7 +188,7 @@ Rectangle { return ; } } - TimelineManager.timeline.input.send(); + room.input.send(); event.accepted = true; } else if (event.key == Qt.Key_Tab) { event.accepted = true; @@ -223,11 +223,11 @@ Rectangle { } else if (event.key == Qt.Key_Up && event.modifiers == Qt.NoModifier) { if (cursorPosition == 0) { event.accepted = true; - var idx = TimelineManager.timeline.edit ? TimelineManager.timeline.idToIndex(TimelineManager.timeline.edit) + 1 : 0; + var idx = room.edit ? room.idToIndex(room.edit) + 1 : 0; while (true) { - var id = TimelineManager.timeline.indexToId(idx); - if (!id || TimelineManager.timeline.getDump(id, "").isEditable) { - TimelineManager.timeline.edit = id; + var id = room.indexToId(idx); + if (!id || room.getDump(id, "").isEditable) { + room.edit = id; cursorPosition = 0; Qt.callLater(positionCursorAtEnd); break; @@ -239,13 +239,13 @@ Rectangle { positionCursorAtStart(); } } else if (event.key == Qt.Key_Down && event.modifiers == Qt.NoModifier) { - if (cursorPosition == messageInput.length && TimelineManager.timeline.edit) { + if (cursorPosition == messageInput.length && room.edit) { event.accepted = true; - var idx = TimelineManager.timeline.idToIndex(TimelineManager.timeline.edit) - 1; + var idx = room.idToIndex(room.edit) - 1; while (true) { - var id = TimelineManager.timeline.indexToId(idx); - if (!id || TimelineManager.timeline.getDump(id, "").isEditable) { - TimelineManager.timeline.edit = id; + var id = room.indexToId(idx); + if (!id || room.getDump(id, "").isEditable) { + room.edit = id; Qt.callLater(positionCursorAtStart); break; } @@ -260,14 +260,14 @@ Rectangle { background: null Connections { - onActiveTimelineChanged: { + onRoomChanged: { messageInput.clear(); - messageInput.append(TimelineManager.timeline.input.text()); + messageInput.append(room.input.text()); messageInput.completerTriggeredAt = -1; popup.completerName = ""; messageInput.forceActiveFocus(); } - target: TimelineManager + target: timelineView } Connections { @@ -292,14 +292,14 @@ Rectangle { messageInput.text = newText; messageInput.cursorPosition = newText.length; } - target: TimelineManager.timeline ? TimelineManager.timeline.input : null + target: room ? room.input : null } Connections { ignoreUnknownSignals: true onReplyChanged: messageInput.forceActiveFocus() onEditChanged: messageInput.forceActiveFocus() - target: TimelineManager.timeline + target: room } Connections { @@ -312,7 +312,7 @@ Rectangle { anchors.fill: parent acceptedButtons: Qt.MiddleButton cursorShape: Qt.IBeamCursor - onClicked: TimelineManager.timeline.input.paste(true) + onClicked: room.input.paste(true) } } @@ -347,7 +347,7 @@ Rectangle { ToolTip.visible: hovered ToolTip.text: qsTr("Send") onClicked: { - TimelineManager.timeline.input.send(); + room.input.send(); } } @@ -355,9 +355,9 @@ Rectangle { Text { anchors.centerIn: parent - visible: TimelineManager.timeline ? (!TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage)) : false + visible: room ? (!room.permissions.canSend(MtxEvent.TextMessage)) : false text: qsTr("You don't have permission to send messages in this room") - color: colors.text + color: Nheko.colors.text } } diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 7dbe7e12..c936c638 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml
@@ -4,6 +4,7 @@ import "./delegates" import "./emoji" +import Qt.labs.platform 1.1 as Platform import QtGraphicalEffects 1.0 import QtQuick 2.12 import QtQuick.Controls 2.3 @@ -13,7 +14,7 @@ import im.nheko 1.0 ScrollView { clip: false - palette: colors + palette: Nheko.colors padding: 8 ScrollBar.horizontal.visible: false @@ -22,7 +23,7 @@ ScrollView { property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2 - model: TimelineManager.timeline + model: room boundsBehavior: Flickable.StopAtBounds pixelAligned: true spacing: 4 @@ -51,8 +52,8 @@ ScrollView { z: 10 height: row.implicitHeight + padding * 2 width: row.implicitWidth + padding * 2 - color: colors.window - border.color: colors.buttonText + color: Nheko.colors.window + border.color: Nheko.colors.buttonText border.width: 1 radius: padding @@ -74,7 +75,7 @@ ScrollView { id: editButton visible: !!row.model && row.model.isEditable - buttonTextColor: colors.buttonText + buttonTextColor: Nheko.colors.buttonText width: 16 hoverEnabled: true image: ":/icons/icons/ui/edit.png" @@ -220,7 +221,7 @@ ScrollView { anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined visible: modelData && modelData.previousMessageDay !== modelData.day text: modelData ? chat.model.formatDateSeparator(modelData.timestamp) : "" - color: colors.text + color: Nheko.colors.text height: Math.round(fontMetrics.height * 1.4) width: contentWidth * 1.2 horizontalAlignment: Text.AlignHCenter @@ -228,7 +229,7 @@ ScrollView { background: Rectangle { radius: parent.height / 2 - color: colors.window + color: Nheko.colors.window } } @@ -240,8 +241,8 @@ ScrollView { Avatar { id: messageUserAvatar - width: avatarSize - height: avatarSize + width: Nheko.avatarSize + height: Nheko.avatarSize url: modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : "" displayName: modelData ? modelData.userName : "" userid: modelData ? modelData.userId : "" @@ -267,7 +268,7 @@ ScrollView { id: userName text: modelData ? TimelineManager.escapeEmoji(modelData.userName) : "" - color: TimelineManager.userColor(modelData ? modelData.userId : "", colors.window) + color: TimelineManager.userColor(modelData ? modelData.userId : "", Nheko.colors.window) textFormat: Text.RichText ToolTip.visible: displayNameHover.hovered ToolTip.text: modelData ? modelData.userId : "" @@ -288,11 +289,11 @@ ScrollView { } Label { - color: colors.buttonText + color: Nheko.colors.buttonText text: modelData ? TimelineManager.userStatus(modelData.userId) : "" textFormat: Text.PlainText elide: Text.ElideRight - width: chat.delegateMaxWidth - parent.spacing * 2 - userName.implicitWidth - avatarSize + width: chat.delegateMaxWidth - parent.spacing * 2 - userName.implicitWidth - Nheko.avatarSize font.italic: true } @@ -317,7 +318,7 @@ ScrollView { opacity: 0 visible: true anchors.fill: timelinerow - color: colors.highlight + color: Nheko.colors.highlight states: State { name: "revealed" @@ -413,4 +414,141 @@ ScrollView { } + Platform.Menu { + id: messageContextMenu + + property string eventId + property string link + property string text + property int eventType + property bool isEncrypted + property bool isEditable + property bool isSender + + function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) { + eventId = eventId_; + eventType = eventType_; + isEncrypted = isEncrypted_; + isEditable = isEditable_; + isSender = isSender_; + if (text_) + text = text_; + else + text = ""; + if (link_) + link = link_; + else + link = ""; + if (showAt_) + open(showAt_); + else + open(); + } + + Platform.MenuItem { + visible: messageContextMenu.text + enabled: visible + text: qsTr("&Copy") + onTriggered: Clipboard.text = messageContextMenu.text + } + + Platform.MenuItem { + visible: messageContextMenu.link + enabled: visible + text: qsTr("Copy &link location") + onTriggered: Clipboard.text = messageContextMenu.link + } + + Platform.MenuItem { + id: reactionOption + + visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false + text: qsTr("Re&act") + onTriggered: emojiPopup.show(null, function(emoji) { + room.input.reaction(messageContextMenu.eventId, emoji); + }) + } + + Platform.MenuItem { + visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false + text: qsTr("Repl&y") + onTriggered: room.replyAction(messageContextMenu.eventId) + } + + Platform.MenuItem { + visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) + enabled: visible + text: qsTr("&Edit") + onTriggered: room.editAction(messageContextMenu.eventId) + } + + Platform.MenuItem { + text: qsTr("Read receip&ts") + onTriggered: room.readReceiptsAction(messageContextMenu.eventId) + } + + Platform.MenuItem { + visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage + text: qsTr("&Forward") + onTriggered: { + var forwardMess = forwardCompleterComponent.createObject(timelineRoot); + forwardMess.setMessageEventId(messageContextMenu.eventId); + forwardMess.open(); + } + } + + Platform.MenuItem { + text: qsTr("&Mark as read") + } + + Platform.MenuItem { + text: qsTr("View raw message") + onTriggered: room.viewRawMessage(messageContextMenu.eventId) + } + + Platform.MenuItem { + // TODO(Nico): Fix this still being iterated over, when using keyboard to select options + visible: messageContextMenu.isEncrypted + enabled: visible + text: qsTr("View decrypted raw message") + onTriggered: room.viewDecryptedRawMessage(messageContextMenu.eventId) + } + + Platform.MenuItem { + visible: (room ? room.permissions.canRedact() : false) || messageContextMenu.isSender + text: qsTr("Remo&ve message") + onTriggered: room.redactEvent(messageContextMenu.eventId) + } + + Platform.MenuItem { + visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker + enabled: visible + text: qsTr("&Save as") + onTriggered: room.saveMedia(messageContextMenu.eventId) + } + + Platform.MenuItem { + visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker + enabled: visible + text: qsTr("&Open in external program") + onTriggered: room.openMedia(messageContextMenu.eventId) + } + + Platform.MenuItem { + visible: messageContextMenu.eventId + enabled: visible + text: qsTr("Copy link to eve&nt") + onTriggered: room.copyLinkToEvent(messageContextMenu.eventId) + } + + } + + Component { + id: forwardCompleterComponent + + ForwardCompleter { + } + + } + } diff --git a/resources/qml/NhekoBusyIndicator.qml b/resources/qml/NhekoBusyIndicator.qml
index 917e11dc..82dd26dd 100644 --- a/resources/qml/NhekoBusyIndicator.qml +++ b/resources/qml/NhekoBusyIndicator.qml
@@ -5,6 +5,7 @@ import QtQuick 2.9 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 +import im.nheko 1.0 BusyIndicator { id: control @@ -38,7 +39,7 @@ BusyIndicator { implicitWidth: radius * 2 implicitHeight: radius * 2 radius: item.height / 6 - color: colors.text + color: Nheko.colors.text opacity: (index + 2) / (repeater.count + 2) transform: [ Translate { diff --git a/resources/qml/QuickSwitcher.qml b/resources/qml/QuickSwitcher.qml
index 166c788d..8c4f47ca 100644 --- a/resources/qml/QuickSwitcher.qml +++ b/resources/qml/QuickSwitcher.qml
@@ -19,7 +19,7 @@ Popup { modal: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside parent: Overlay.overlay - palette: colors + palette: Nheko.colors onOpened: { completerPopup.open(); roomTextInput.forceActiveFocus(); @@ -34,7 +34,7 @@ Popup { anchors.fill: parent font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6) padding: textMargin - color: colors.text + color: Nheko.colors.text onTextEdited: { completerPopup.completer.searchString = text; } @@ -72,8 +72,7 @@ Popup { Connections { onCompletionSelected: { - TimelineManager.setHistoryView(id); - TimelineManager.highlightRoom(id); + Rooms.setCurrentRoom(id); quickSwitcher.close(); } onCountChanged: { diff --git a/resources/qml/Reactions.qml b/resources/qml/Reactions.qml
index f53c89ad..def87f75 100644 --- a/resources/qml/Reactions.qml +++ b/resources/qml/Reactions.qml
@@ -12,9 +12,9 @@ Flow { id: reactionFlow // highlight colors for selfReactedEvent background - property real highlightHue: colors.highlight.hslHue - property real highlightSat: colors.highlight.hslSaturation - property real highlightLight: colors.highlight.hslLightness + property real highlightHue: Nheko.colors.highlight.hslHue + property real highlightSat: Nheko.colors.highlight.hslSaturation + property real highlightLight: Nheko.colors.highlight.hslLightness property string eventId property alias reactions: repeater.model @@ -35,7 +35,7 @@ Flow { ToolTip.text: modelData.users onClicked: { console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent); - TimelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key); + room.input.reaction(reactionFlow.eventId, modelData.key); } contentItem: Row { @@ -59,7 +59,7 @@ Flow { anchors.baseline: reactionCounter.baseline text: textMetrics.elidedText + (textMetrics.elidedText == modelData.key ? "" : "…") font.family: Settings.emojiFont - color: reaction.hovered ? colors.highlight : colors.text + color: reaction.hovered ? Nheko.colors.highlight : Nheko.colors.text maximumLineCount: 1 } @@ -68,7 +68,7 @@ Flow { height: Math.floor(reactionCounter.implicitHeight * 1.4) width: 1 - color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text + color: (reaction.hovered || modelData.selfReactedEvent !== '') ? Nheko.colors.highlight : Nheko.colors.text } Text { @@ -77,7 +77,7 @@ Flow { anchors.verticalCenter: divider.verticalCenter text: modelData.count font: reaction.font - color: reaction.hovered ? colors.highlight : colors.text + color: reaction.hovered ? Nheko.colors.highlight : Nheko.colors.text } } @@ -86,8 +86,8 @@ Flow { anchors.centerIn: parent implicitWidth: reaction.implicitWidth implicitHeight: reaction.implicitHeight - border.color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text - color: modelData.selfReactedEvent !== '' ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : colors.window + border.color: (reaction.hovered || modelData.selfReactedEvent !== '') ? Nheko.colors.highlight : Nheko.colors.text + color: modelData.selfReactedEvent !== '' ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : Nheko.colors.window border.width: 1 radius: reaction.height / 2 } diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml
index 37b6f6cc..0de68fe8 100644 --- a/resources/qml/ReplyPopup.qml +++ b/resources/qml/ReplyPopup.qml
@@ -11,13 +11,11 @@ import im.nheko 1.0 Rectangle { id: replyPopup - property var room: TimelineManager.timeline - Layout.fillWidth: true visible: room && (room.reply || room.edit) // Height of child, plus margins, plus border implicitHeight: (room && room.reply ? replyPreview.height : closeEditButton.height) + 10 - color: colors.window + color: Nheko.colors.window z: 3 Reply { @@ -31,7 +29,7 @@ Rectangle { anchors.bottom: parent.bottom modelData: room ? room.getDump(room.reply, room.id) : { } - userColor: TimelineManager.userColor(modelData.userId, colors.window) + userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.window) } ImageButton { diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml new file mode 100644
index 00000000..76680b37 --- /dev/null +++ b/resources/qml/RoomList.qml
@@ -0,0 +1,604 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import "./dialogs" +import Qt.labs.platform 1.1 as Platform +import QtQml 2.12 +import QtQuick 2.12 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.3 +import im.nheko 1.0 + +Page { + //leftPadding: Nheko.paddingSmall + //rightPadding: Nheko.paddingSmall + property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3) + property bool collapsed: false + + ListView { + id: roomlist + + anchors.left: parent.left + anchors.right: parent.right + height: parent.height + model: Rooms + + ScrollHelper { + flickable: parent + anchors.fill: parent + enabled: !Settings.mobileMode + } + + Connections { + onActiveTimelineChanged: { + roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId()), ListView.Contain); + console.log("Test" + Rooms.currentRoom.roomId() + " " + Rooms.roomidToIndex(Rooms.currentRoom.roomId())); + } + target: TimelineManager + } + + Platform.Menu { + id: roomContextMenu + + property string roomid + property var tags + + function show(roomid_, tags_) { + roomid = roomid_; + tags = tags_; + open(); + } + + InputDialog { + id: newTag + + title: qsTr("New tag") + prompt: qsTr("Enter the tag you want to use:") + onAccepted: function(text) { + Rooms.toggleTag(roomContextMenu.roomid, "u." + text, true); + } + } + + Platform.MenuItem { + text: qsTr("Leave room") + onTriggered: Rooms.leave(roomContextMenu.roomid) + } + + Platform.MenuSeparator { + text: qsTr("Tag room as:") + } + + Instantiator { + model: Communities.tags + onObjectAdded: roomContextMenu.insertItem(index + 2, object) + onObjectRemoved: roomContextMenu.removeItem(object) + + delegate: Platform.MenuItem { + property string t: modelData + + text: { + switch (t) { + case "m.favourite": + return qsTr("Favourite"); + case "m.lowpriority": + return qsTr("Low priority"); + case "m.server_notice": + return qsTr("Server notice"); + default: + return t.substring(2); + } + } + checkable: true + checked: roomContextMenu.tags !== undefined && roomContextMenu.tags.includes(t) + onTriggered: Rooms.toggleTag(roomContextMenu.roomid, t, checked) + } + + } + + Platform.MenuItem { + text: qsTr("Create new tag...") + onTriggered: newTag.show() + } + + } + + delegate: Rectangle { + id: roomItem + + property color background: Nheko.colors.window + property color importantText: Nheko.colors.text + property color unimportantText: Nheko.colors.buttonText + property color bubbleBackground: Nheko.colors.highlight + property color bubbleText: Nheko.colors.highlightedText + + color: background + height: avatarSize + 2 * Nheko.paddingMedium + width: ListView.view.width + state: "normal" + ToolTip.visible: hovered.hovered && collapsed + ToolTip.text: model.roomName + states: [ + State { + name: "highlight" + when: hovered.hovered && !(Rooms.currentRoom && model.roomId == Rooms.currentRoom.roomId()) + + PropertyChanges { + target: roomItem + background: Nheko.colors.dark + importantText: Nheko.colors.brightText + unimportantText: Nheko.colors.brightText + bubbleBackground: Nheko.colors.highlight + bubbleText: Nheko.colors.highlightedText + } + + }, + State { + name: "selected" + when: Rooms.currentRoom && model.roomId == Rooms.currentRoom.roomId() + + PropertyChanges { + target: roomItem + background: Nheko.colors.highlight + importantText: Nheko.colors.highlightedText + unimportantText: Nheko.colors.highlightedText + bubbleBackground: Nheko.colors.highlightedText + bubbleText: Nheko.colors.highlight + } + + } + ] + + TapHandler { + margin: -Nheko.paddingSmall + acceptedButtons: Qt.RightButton + onSingleTapped: { + if (!TimelineManager.isInvite) + roomContextMenu.show(model.roomId, model.tags); + + } + gesturePolicy: TapHandler.ReleaseWithinBounds + } + + TapHandler { + margin: -Nheko.paddingSmall + onSingleTapped: Rooms.setCurrentRoom(model.roomId) + onLongPressed: { + if (!TimelineManager.isInvite) + roomContextMenu.show(model.roomId, model.tags); + + } + } + + HoverHandler { + id: hovered + + margin: -Nheko.paddingSmall + } + + RowLayout { + spacing: Nheko.paddingMedium + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + + Avatar { + // In the future we could show an online indicator by setting the userid for the avatar + //userid: Nheko.currentUser.userid + + id: avatar + + enabled: false + Layout.alignment: Qt.AlignVCenter + height: avatarSize + width: avatarSize + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + displayName: model.roomName + + Rectangle { + id: collapsedNotificationBubble + + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: -Nheko.paddingSmall + visible: collapsed && model.notificationCount > 0 + enabled: false + Layout.alignment: Qt.AlignRight + height: fontMetrics.averageCharacterWidth * 3 + width: height + radius: height / 2 + color: model.hasLoudNotification ? Nheko.theme.red : roomItem.bubbleBackground + + Label { + anchors.centerIn: parent + width: parent.width * 0.8 + height: parent.height * 0.8 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + fontSizeMode: Text.Fit + font.bold: true + font.pixelSize: fontMetrics.font.pixelSize * 0.8 + color: model.hasLoudNotification ? "white" : roomItem.bubbleText + text: model.notificationCount > 99 ? "99+" : model.notificationCount + } + + } + + } + + ColumnLayout { + id: textContent + + visible: !collapsed + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + Layout.minimumWidth: 100 + width: parent.width - avatar.width + Layout.preferredWidth: parent.width - avatar.width + spacing: Nheko.paddingSmall + + RowLayout { + Layout.fillWidth: true + spacing: 0 + + ElidedLabel { + Layout.alignment: Qt.AlignBottom + color: roomItem.importantText + elideWidth: textContent.width - timestamp.width - Nheko.paddingMedium + fullText: model.roomName + textFormat: Text.RichText + } + + Item { + Layout.fillWidth: true + } + + Label { + id: timestamp + + Layout.alignment: Qt.AlignRight | Qt.AlignBottom + font.pixelSize: fontMetrics.font.pixelSize * 0.9 + color: roomItem.unimportantText + text: model.time + } + + } + + RowLayout { + Layout.fillWidth: true + spacing: 0 + visible: !model.isInvite + height: visible ? 0 : undefined + + ElidedLabel { + color: roomItem.unimportantText + font.weight: Font.Thin + font.pixelSize: fontMetrics.font.pixelSize * 0.9 + elideWidth: textContent.width - (notificationBubble.visible ? notificationBubble.width : 0) - Nheko.paddingSmall + fullText: model.lastMessage + textFormat: Text.RichText + } + + Item { + Layout.fillWidth: true + } + + Rectangle { + id: notificationBubble + + visible: model.notificationCount > 0 + Layout.alignment: Qt.AlignRight + height: fontMetrics.averageCharacterWidth * 3 + width: height + radius: height / 2 + color: model.hasLoudNotification ? Nheko.theme.red : roomItem.bubbleBackground + + Label { + anchors.centerIn: parent + width: parent.width * 0.8 + height: parent.height * 0.8 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + fontSizeMode: Text.Fit + font.bold: true + font.pixelSize: fontMetrics.font.pixelSize * 0.8 + color: model.hasLoudNotification ? "white" : roomItem.bubbleText + text: model.notificationCount > 99 ? "99+" : model.notificationCount + } + + } + + } + + RowLayout { + Layout.fillWidth: true + spacing: Nheko.paddingMedium + visible: model.isInvite + enabled: visible + height: visible ? 0 : undefined + + ElidedLabel { + elideWidth: textContent.width / 2 - 2 * Nheko.paddingMedium + fullText: qsTr("Accept") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + leftPadding: Nheko.paddingMedium + rightPadding: Nheko.paddingMedium + color: Nheko.colors.brightText + + TapHandler { + onSingleTapped: Rooms.acceptInvite(model.roomId) + } + + background: Rectangle { + color: Nheko.theme.alternateButton + radius: height / 2 + } + + } + + ElidedLabel { + Layout.alignment: Qt.AlignRight + elideWidth: textContent.width / 2 - 2 * Nheko.paddingMedium + fullText: qsTr("Decline") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + leftPadding: Nheko.paddingMedium + rightPadding: Nheko.paddingMedium + color: Nheko.colors.brightText + + TapHandler { + onSingleTapped: Rooms.declineInvite(model.roomId) + } + + background: Rectangle { + color: Nheko.theme.alternateButton + radius: height / 2 + } + + } + + Item { + Layout.fillWidth: true + } + + } + + } + + } + + Rectangle { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + height: parent.height - Nheko.paddingSmall * 2 + width: 3 + color: Nheko.colors.highlight + visible: model.hasUnreadMessages + } + + } + + } + + background: Rectangle { + color: Nheko.theme.sidebarBackground + } + + header: ColumnLayout { + spacing: 0 + + Rectangle { + id: userInfoPanel + + function openUserProfile() { + Nheko.updateUserProfile(); + var userProfile = userProfileComponent.createObject(timelineRoot, { + "profile": Nheko.currentUser + }); + userProfile.show(); + } + + color: Nheko.colors.window + Layout.fillWidth: true + Layout.alignment: Qt.AlignBottom + Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium + Layout.minimumHeight: 40 + + InputDialog { + id: statusDialog + + title: qsTr("Status Message") + prompt: qsTr("Enter your status message:") + onAccepted: function(text) { + Nheko.setStatusMessage(text); + } + } + + Platform.Menu { + id: userInfoMenu + + Platform.MenuItem { + text: qsTr("Profile settings") + onTriggered: userInfoPanel.openUserProfile() + } + + Platform.MenuItem { + text: qsTr("Set status message") + onTriggered: statusDialog.show() + } + + } + + TapHandler { + margin: -Nheko.paddingSmall + acceptedButtons: Qt.LeftButton + onSingleTapped: userInfoPanel.openUserProfile() + onLongPressed: userInfoMenu.open() + gesturePolicy: TapHandler.ReleaseWithinBounds + } + + TapHandler { + margin: -Nheko.paddingSmall + acceptedButtons: Qt.RightButton + onSingleTapped: userInfoMenu.open() + gesturePolicy: TapHandler.ReleaseWithinBounds + } + + RowLayout { + id: userInfoGrid + + property var profile: Nheko.currentUser + + spacing: Nheko.paddingMedium + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + + Avatar { + id: avatar + + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: fontMetrics.lineSpacing * 2 + Layout.preferredHeight: fontMetrics.lineSpacing * 2 + url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/") + displayName: userInfoGrid.profile ? userInfoGrid.profile.displayName : "" + userid: userInfoGrid.profile ? userInfoGrid.profile.userid : "" + } + + ColumnLayout { + id: col + + visible: !collapsed + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + width: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2 + Layout.preferredWidth: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2 + spacing: 0 + + ElidedLabel { + Layout.alignment: Qt.AlignBottom + font.pointSize: fontMetrics.font.pointSize * 1.1 + font.weight: Font.DemiBold + fullText: userInfoGrid.profile ? userInfoGrid.profile.displayName : "" + elideWidth: col.width + } + + ElidedLabel { + Layout.alignment: Qt.AlignTop + color: Nheko.colors.buttonText + font.weight: Font.Thin + font.pointSize: fontMetrics.font.pointSize * 0.9 + elideWidth: col.width + fullText: userInfoGrid.profile ? userInfoGrid.profile.userid : "" + } + + } + + Item { + } + + ImageButton { + id: logoutButton + + visible: !collapsed + Layout.alignment: Qt.AlignVCenter + image: ":/icons/icons/ui/power-button-off.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("Logout") + onClicked: Nheko.openLogoutDialog() + } + + } + + } + + Rectangle { + color: Nheko.theme.separator + height: 2 + Layout.fillWidth: true + } + + } + + footer: ColumnLayout { + spacing: 0 + + Rectangle { + color: Nheko.theme.separator + height: 1 + Layout.fillWidth: true + } + + Rectangle { + color: Nheko.colors.window + Layout.fillWidth: true + Layout.alignment: Qt.AlignBottom + Layout.preferredHeight: buttonRow.implicitHeight + Layout.minimumHeight: 40 + + RowLayout { + id: buttonRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Nheko.paddingMedium + + ImageButton { + Layout.fillWidth: true + hoverEnabled: true + width: 22 + height: 22 + image: ":/icons/icons/ui/plus-black-symbol.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("Start a new chat") + Layout.margins: Nheko.paddingMedium + onClicked: roomJoinCreateMenu.open(parent) + + Platform.Menu { + id: roomJoinCreateMenu + + Platform.MenuItem { + text: qsTr("Join a room") + onTriggered: Nheko.openJoinRoomDialog() + } + + Platform.MenuItem { + text: qsTr("Create a new room") + onTriggered: Nheko.openCreateRoomDialog() + } + + } + + } + + ImageButton { + visible: !collapsed + Layout.fillWidth: true + hoverEnabled: true + width: 22 + height: 22 + image: ":/icons/icons/ui/speech-bubbles-comment-option.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("Room directory") + Layout.margins: Nheko.paddingMedium + } + + ImageButton { + visible: !collapsed + Layout.fillWidth: true + hoverEnabled: true + width: 22 + height: 22 + image: ":/icons/icons/ui/settings.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("User settings") + Layout.margins: Nheko.paddingMedium + onClicked: Nheko.showUserSettingsPage() + } + + } + + } + + } + +} diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml
index 58567916..1f7fe5de 100644 --- a/resources/qml/RoomSettings.qml +++ b/resources/qml/RoomSettings.qml
@@ -18,9 +18,9 @@ ApplicationWindow { y: MainWindow.y + (MainWindow.height / 2) - (height / 2) minimumWidth: 420 minimumHeight: 650 - palette: colors - color: colors.window - modality: Qt.WindowModal + palette: Nheko.colors + color: Nheko.colors.window + modality: Qt.NonModal flags: Qt.Dialog title: qsTr("Room Settings") @@ -126,9 +126,9 @@ ApplicationWindow { readOnly: true background: null selectByMouse: true - color: colors.text + color: Nheko.colors.text horizontalAlignment: TextEdit.AlignHCenter - onLinkActivated: TimelineManager.openLink(link) + onLinkActivated: Nheko.openLink(link) CursorShape { anchors.fill: parent @@ -205,7 +205,7 @@ ApplicationWindow { title: qsTr("End-to-End Encryption") text: qsTr("Encryption is currently experimental and things might break unexpectedly. <br> Please take note that it can't be disabled afterwards.") - modality: Qt.WindowModal + modality: Qt.NonModal onAccepted: { if (roomSettings.isEncryptionEnabled) return ; diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml new file mode 100644
index 00000000..5316e20d --- /dev/null +++ b/resources/qml/Root.qml
@@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import "./delegates" +import "./device-verification" +import "./emoji" +import "./voip" +import Qt.labs.platform 1.1 as Platform +import QtGraphicalEffects 1.0 +import QtQuick 2.9 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.3 +import QtQuick.Window 2.2 +import im.nheko 1.0 +import im.nheko.EmojiModel 1.0 + +Page { + id: timelineRoot + + palette: Nheko.colors + + FontMetrics { + id: fontMetrics + } + + EmojiPicker { + id: emojiPopup + + colors: palette + model: TimelineManager.completerFor("allemoji", "") + } + + Component { + id: userProfileComponent + + UserProfile { + } + + } + + Component { + id: roomSettingsComponent + + RoomSettings { + } + + } + + Component { + id: mobileCallInviteDialog + + CallInvite { + } + + } + + Component { + id: quickSwitcherComponent + + QuickSwitcher { + } + + } + + Shortcut { + sequence: "Ctrl+K" + onActivated: { + var quickSwitch = quickSwitcherComponent.createObject(timelineRoot); + TimelineManager.focusTimeline(); + quickSwitch.open(); + } + } + + Shortcut { + sequence: "Ctrl+Down" + onActivated: Rooms.nextRoom() + } + + Shortcut { + sequence: "Ctrl+Up" + onActivated: Rooms.previousRoom() + } + + Component { + id: deviceVerificationDialog + + DeviceVerification { + } + + } + + Connections { + target: TimelineManager + onNewDeviceVerificationRequest: { + var dialog = deviceVerificationDialog.createObject(timelineRoot, { + "flow": flow + }); + dialog.show(); + } + onOpenProfile: { + var userProfile = userProfileComponent.createObject(timelineRoot, { + "profile": profile + }); + userProfile.show(); + } + } + + Connections { + target: CallManager + onNewInviteState: { + if (CallManager.haveCallInvite && Settings.mobileMode) { + var dialog = mobileCallInviteDialog.createObject(msgView); + dialog.open(); + } + } + } + + ChatPage { + anchors.fill: parent + } + +} diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml
index 3d2d8278..739cc007 100644 --- a/resources/qml/StatusIndicator.qml +++ b/resources/qml/StatusIndicator.qml
@@ -31,7 +31,7 @@ ImageButton { } onClicked: { if (model.state == MtxEvent.Read) - TimelineManager.timeline.readReceiptsAction(model.id); + room.readReceiptsAction(model.id); } image: { diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 09a55e60..3fa1ad8e 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml
@@ -16,7 +16,7 @@ Item { height: row.height Rectangle { - color: (Settings.messageHoverHighlight && hoverHandler.hovered) ? colors.alternateBase : "transparent" + color: (Settings.messageHoverHighlight && hoverHandler.hovered) ? Nheko.colors.alternateBase : "transparent" anchors.fill: row } @@ -42,7 +42,7 @@ Item { id: row anchors.rightMargin: 1 - anchors.leftMargin: avatarSize + 16 + anchors.leftMargin: Nheko.avatarSize + 16 anchors.left: parent.left anchors.right: parent.right @@ -57,7 +57,7 @@ Item { Reply { visible: model.replyTo modelData: chat.model.getDump(model.replyTo, model.id) - userColor: TimelineManager.userColor(modelData.userId, colors.base) + userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.base) } // actual message content @@ -101,7 +101,7 @@ Item { width: 16 sourceSize.width: 16 sourceSize.height: 16 - source: "image://colorimage/:/icons/icons/ui/edit.png?" + ((model.id == chat.model.edit) ? colors.highlight : colors.buttonText) + source: "image://colorimage/:/icons/icons/ui/edit.png?" + ((model.id == chat.model.edit) ? Nheko.colors.highlight : Nheko.colors.buttonText) ToolTip.visible: editHovered.hovered ToolTip.text: qsTr("Edited") @@ -115,7 +115,7 @@ Item { Layout.alignment: Qt.AlignRight | Qt.AlignTop text: model.timestamp.toLocaleTimeString(Locale.ShortFormat) width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth) - color: inactiveColors.text + color: Nheko.inactiveColors.text ToolTip.visible: ma.hovered ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 86287586..90e28166 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml
@@ -9,382 +9,137 @@ import "./voip" import Qt.labs.platform 1.1 as Platform import QtGraphicalEffects 1.0 import QtQuick 2.9 -import QtQuick.Controls 2.3 +import QtQuick.Controls 2.5 import QtQuick.Layouts 1.3 import QtQuick.Window 2.2 import im.nheko 1.0 import im.nheko.EmojiModel 1.0 -Page { - id: timelineRoot +Item { + id: timelineView - property var colors: currentActivePalette - property var systemInactive - property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive - readonly property int avatarSize: 40 - property real highlightHue: colors.highlight.hslHue - property real highlightSat: colors.highlight.hslSaturation - property real highlightLight: colors.highlight.hslLightness - - palette: colors - - FontMetrics { - id: fontMetrics - } - - EmojiPicker { - id: emojiPopup - - colors: palette - model: TimelineManager.completerFor("allemoji", "") - } - - Component { - id: userProfileComponent - - UserProfile { - } - - } - - Component { - id: roomSettingsComponent - - RoomSettings { - } - - } - - Component { - id: mobileCallInviteDialog - - CallInvite { - } - - } - - Component { - id: quickSwitcherComponent - - QuickSwitcher { - } - - } - - Component { - id: forwardCompleterComponent - - ForwardCompleter { - } + property var room: null + property bool showBackButton: false + Label { + visible: !room && !TimelineManager.isInitialSync + anchors.centerIn: parent + text: qsTr("No room open") + font.pointSize: 24 + color: Nheko.colors.text } - Shortcut { - sequence: "Ctrl+K" - onActivated: { - var quickSwitch = quickSwitcherComponent.createObject(timelineRoot); - TimelineManager.focusTimeline(); - quickSwitch.open(); - } + BusyIndicator { + visible: running + anchors.centerIn: parent + running: TimelineManager.isInitialSync + height: 200 + width: 200 + z: 3 } - Platform.Menu { - id: messageContextMenu - - property string eventId - property string link - property string text - property int eventType - property bool isEncrypted - property bool isEditable - property bool isSender - - function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) { - eventId = eventId_; - eventType = eventType_; - isEncrypted = isEncrypted_; - isEditable = isEditable_; - isSender = isSender_; - if (text_) - text = text_; - else - text = ""; - if (link_) - link = link_; - else - link = ""; - if (showAt_) - open(showAt_); - else - open(); - } - - Platform.MenuItem { - visible: messageContextMenu.text - enabled: visible - text: qsTr("&Copy") - onTriggered: Clipboard.text = messageContextMenu.text - } - - Platform.MenuItem { - visible: messageContextMenu.link - enabled: visible - text: qsTr("Copy &link location") - onTriggered: Clipboard.text = messageContextMenu.link - } - - Platform.MenuItem { - id: reactionOption - - visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.Reaction) : false - text: qsTr("Re&act") - onTriggered: emojiPopup.show(null, function(emoji) { - TimelineManager.queueReactionMessage(messageContextMenu.eventId, emoji); - }) - } - - Platform.MenuItem { - visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false - text: qsTr("Repl&y") - onTriggered: TimelineManager.timeline.replyAction(messageContextMenu.eventId) - } - - Platform.MenuItem { - visible: messageContextMenu.isEditable && (TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false) - enabled: visible - text: qsTr("&Edit") - onTriggered: TimelineManager.timeline.editAction(messageContextMenu.eventId) - } - - Platform.MenuItem { - text: qsTr("Read receip&ts") - onTriggered: TimelineManager.timeline.readReceiptsAction(messageContextMenu.eventId) - } - - Platform.MenuItem { - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage - text: qsTr("&Forward") - onTriggered: { - var forwardMess = forwardCompleterComponent.createObject(timelineRoot); - forwardMess.setMessageEventId(messageContextMenu.eventId); - forwardMess.open(); - } - } - - Platform.MenuItem { - text: qsTr("&Mark as read") - } - - Platform.MenuItem { - text: qsTr("View raw message") - onTriggered: TimelineManager.timeline.viewRawMessage(messageContextMenu.eventId) - } - - Platform.MenuItem { - // TODO(Nico): Fix this still being iterated over, when using keyboard to select options - visible: messageContextMenu.isEncrypted - enabled: visible - text: qsTr("View decrypted raw message") - onTriggered: TimelineManager.timeline.viewDecryptedRawMessage(messageContextMenu.eventId) - } - - Platform.MenuItem { - visible: (TimelineManager.timeline ? TimelineManager.timeline.permissions.canRedact() : false) || messageContextMenu.isSender - text: qsTr("Remo&ve message") - onTriggered: TimelineManager.timeline.redactEvent(messageContextMenu.eventId) - } - - Platform.MenuItem { - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker - enabled: visible - text: qsTr("&Save as") - onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId) - } - - Platform.MenuItem { - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker - enabled: visible - text: qsTr("&Open in external program") - onTriggered: TimelineManager.timeline.openMedia(messageContextMenu.eventId) - } + ColumnLayout { + id: timelineLayout - Platform.MenuItem { - visible: messageContextMenu.eventId - enabled: visible - text: qsTr("Copy link to eve&nt") - onTriggered: TimelineManager.timeline.copyLinkToEvent(messageContextMenu.eventId) - } - - } - - Rectangle { + visible: room != null anchors.fill: parent - color: colors.window - - Component { - id: deviceVerificationDialog - - DeviceVerification { - } - - } - - Connections { - target: TimelineManager - onNewDeviceVerificationRequest: { - var dialog = deviceVerificationDialog.createObject(timelineRoot, { - "flow": flow - }); - dialog.show(); - } - onOpenProfile: { - var userProfile = userProfileComponent.createObject(timelineRoot, { - "profile": profile - }); - userProfile.show(); - } - } - - Connections { - target: TimelineManager.timeline - onOpenRoomSettingsDialog: { - var roomSettings = roomSettingsComponent.createObject(timelineRoot, { - "roomSettings": settings - }); - roomSettings.show(); - } - } - - Connections { - target: CallManager - onNewInviteState: { - if (CallManager.haveCallInvite && Settings.mobileMode) { - var dialog = mobileCallInviteDialog.createObject(msgView); - dialog.open(); - } - } - } + spacing: 0 - Label { - visible: !TimelineManager.timeline && !TimelineManager.isInitialSync - anchors.centerIn: parent - text: qsTr("No room open") - font.pointSize: 24 - color: colors.text + TopBar { + showBackButton: timelineView.showBackButton } - BusyIndicator { - visible: running - anchors.centerIn: parent - running: TimelineManager.isInitialSync - height: 200 - width: 200 + Rectangle { + Layout.fillWidth: true + height: 1 z: 3 + color: Nheko.theme.separator } - ColumnLayout { - id: timelineLayout + Rectangle { + id: msgView - visible: TimelineManager.timeline != null - anchors.fill: parent - spacing: 0 + Layout.fillWidth: true + Layout.fillHeight: true + color: Nheko.colors.base - TopBar { - } - - Rectangle { - Layout.fillWidth: true - height: 1 - z: 3 - color: colors.mid - } - - Rectangle { - id: msgView - - Layout.fillWidth: true - Layout.fillHeight: true - color: colors.base - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - StackLayout { - id: stackLayout + ColumnLayout { + anchors.fill: parent + spacing: 0 - currentIndex: 0 + StackLayout { + id: stackLayout - Connections { - function onActiveTimelineChanged() { - stackLayout.currentIndex = 0; - } + currentIndex: 0 - target: TimelineManager + Connections { + function onRoomChanged() { + stackLayout.currentIndex = 0; } - MessageView { - Layout.fillWidth: true - Layout.fillHeight: true - } - - Loader { - source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : "" - onLoaded: TimelineManager.setVideoCallItem() - } + target: timelineView + } + MessageView { + Layout.fillWidth: true + implicitHeight: msgView.height - typingIndicator.height } - TypingIndicator { + Loader { + source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : "" + onLoaded: TimelineManager.setVideoCallItem() } } - } - - CallInviteBar { - id: callInviteBar + TypingIndicator { + id: typingIndicator + } - Layout.fillWidth: true - z: 3 } - ActiveCallBar { - Layout.fillWidth: true - z: 3 - } + } - Rectangle { - Layout.fillWidth: true - z: 3 - height: 1 - color: colors.mid - } + CallInviteBar { + id: callInviteBar - ReplyPopup { - } + Layout.fillWidth: true + z: 3 + } - MessageInput { - } + ActiveCallBar { + Layout.fillWidth: true + z: 3 + } + + Rectangle { + Layout.fillWidth: true + z: 3 + height: 1 + color: Nheko.theme.separator + } + ReplyPopup { } - NhekoDropArea { - anchors.fill: parent - roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : "" + MessageInput { } } - PrivacyScreen { + NhekoDropArea { anchors.fill: parent - visible: Settings.privacyScreen - screenTimeout: Settings.privacyScreenTimeout - timelineRoot: timelineLayout + roomid: room ? room.roomId() : "" } - systemInactive: SystemPalette { - colorGroup: SystemPalette.Disabled + Connections { + target: room + onOpenRoomSettingsDialog: { + var roomSettings = roomSettingsComponent.createObject(timelineRoot, { + "roomSettings": settings + }); + roomSettings.show(); + } } } diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index 0b943ed1..30ab2e7c 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml
@@ -11,16 +11,16 @@ import im.nheko 1.0 Rectangle { id: topBar - property var room: TimelineManager.timeline + property bool showBackButton: false Layout.fillWidth: true - implicitHeight: topLayout.height + 16 + implicitHeight: topLayout.height + Nheko.paddingMedium * 2 z: 3 - color: colors.window + color: Nheko.colors.window TapHandler { onSingleTapped: { - TimelineManager.timeline.openRoomSettings(); + room.openRoomSettings(); eventPoint.accepted = true; } gesturePolicy: TapHandler.ReleaseWithinBounds @@ -33,7 +33,7 @@ Rectangle { anchors.left: parent.left anchors.right: parent.right - anchors.margins: 8 + anchors.margins: Nheko.paddingMedium anchors.verticalCenter: parent.verticalCenter ImageButton { @@ -43,13 +43,13 @@ Rectangle { Layout.row: 0 Layout.rowSpan: 2 Layout.alignment: Qt.AlignVCenter - width: avatarSize - height: avatarSize - visible: TimelineManager.isNarrowView + width: Nheko.avatarSize + height: Nheko.avatarSize + visible: showBackButton image: ":/icons/icons/ui/angle-pointing-to-left.png" ToolTip.visible: hovered ToolTip.text: qsTr("Back to room list") - onClicked: TimelineManager.backToRooms() + onClicked: Rooms.resetCurrentRoom() } Avatar { @@ -57,18 +57,18 @@ Rectangle { Layout.row: 0 Layout.rowSpan: 2 Layout.alignment: Qt.AlignVCenter - width: avatarSize - height: avatarSize + width: Nheko.avatarSize + height: Nheko.avatarSize url: room ? room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : "" displayName: room ? room.roomName : qsTr("No room selected") - onClicked: TimelineManager.timeline.openRoomSettings() + onClicked: room.openRoomSettings() } Label { Layout.fillWidth: true Layout.column: 2 Layout.row: 0 - color: colors.text + color: Nheko.colors.text font.pointSize: fontMetrics.font.pointSize * 1.1 text: room ? room.roomName : qsTr("No room selected") maximumLineCount: 1 @@ -101,24 +101,24 @@ Rectangle { id: roomOptionsMenu Platform.MenuItem { - visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canInvite() : false + visible: room ? room.permissions.canInvite() : false text: qsTr("Invite users") onTriggered: TimelineManager.openInviteUsersDialog() } Platform.MenuItem { text: qsTr("Members") - onTriggered: TimelineManager.openMemberListDialog() + onTriggered: TimelineManager.openMemberListDialog(room.roomId()) } Platform.MenuItem { text: qsTr("Leave room") - onTriggered: TimelineManager.openLeaveRoomDialog() + onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId()) } Platform.MenuItem { text: qsTr("Settings") - onTriggered: TimelineManager.timeline.openRoomSettings() + onTriggered: room.openRoomSettings() } } diff --git a/resources/qml/TypingIndicator.qml b/resources/qml/TypingIndicator.qml
index ffe88fb6..974d1840 100644 --- a/resources/qml/TypingIndicator.qml +++ b/resources/qml/TypingIndicator.qml
@@ -8,8 +8,6 @@ import QtQuick.Layouts 1.2 import im.nheko 1.0 Item { - property var room: TimelineManager.timeline - implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height) Layout.fillWidth: true @@ -17,7 +15,7 @@ Item { id: typingRect visible: (room && room.typingUsers.length > 0) - color: colors.base + color: Nheko.colors.base anchors.fill: parent z: 3 @@ -29,8 +27,8 @@ Item { anchors.right: parent.right anchors.rightMargin: 10 anchors.bottom: parent.bottom - color: colors.text - text: room ? room.formatTypingUsers(room.typingUsers, colors.base) : "" + color: Nheko.colors.text + text: room ? room.formatTypingUsers(room.typingUsers, Nheko.colors.base) : "" textFormat: Text.RichText } diff --git a/resources/qml/UserProfile.qml b/resources/qml/UserProfile.qml
index 21c44793..21f34f15 100644 --- a/resources/qml/UserProfile.qml +++ b/resources/qml/UserProfile.qml
@@ -19,10 +19,10 @@ ApplicationWindow { height: 650 width: 420 minimumHeight: 420 - palette: colors - color: colors.window + palette: Nheko.colors + color: Nheko.colors.window title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile") - modality: Qt.WindowModal + modality: Qt.NonModal flags: Qt.Dialog Shortcut { @@ -97,7 +97,7 @@ ApplicationWindow { readOnly: !isUsernameEditingAllowed text: profile.displayName font.pixelSize: 20 - color: TimelineManager.userColor(profile.userid, colors.window) + color: TimelineManager.userColor(profile.userid, Nheko.colors.window) font.bold: true Layout.alignment: Qt.AlignHCenter selectByMouse: true @@ -145,7 +145,7 @@ ApplicationWindow { Image { Layout.preferredHeight: 16 Layout.preferredWidth: 16 - source: "image://colorimage/:/icons/icons/ui/lock.png?" + ((profile.userVerified == Crypto.Verified) ? "green" : colors.buttonText) + source: "image://colorimage/:/icons/icons/ui/lock.png?" + ((profile.userVerified == Crypto.Verified) ? "green" : Nheko.colors.buttonText) visible: profile.userVerified != Crypto.Unverified Layout.alignment: Qt.AlignHCenter } @@ -218,7 +218,7 @@ ApplicationWindow { Layout.alignment: Qt.AlignLeft elide: Text.ElideRight font.bold: true - color: colors.text + color: Nheko.colors.text text: model.deviceId } @@ -226,7 +226,7 @@ ApplicationWindow { Layout.fillWidth: true Layout.alignment: Qt.AlignRight elide: Text.ElideRight - color: colors.text + color: Nheko.colors.text text: model.deviceName } diff --git a/resources/qml/components/AdaptiveLayout.qml b/resources/qml/components/AdaptiveLayout.qml new file mode 100644
index 00000000..357a7831 --- /dev/null +++ b/resources/qml/components/AdaptiveLayout.qml
@@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.12 +import im.nheko 1.0 + +Container { + //Component.onCompleted: { + // parent.width = Qt.binding(function() { return calculatedWidth; }) + //} + + id: container + + property bool singlePageMode: width < 800 + property int splitterGrabMargin: Nheko.paddingSmall + property int pageIndex: 0 + property Component handle + property Component handleToucharea + + anchors.fill: parent + Component.onCompleted: { + for (var i = 0; i < count - 1; i++) { + let handle_ = handle.createObject(contentChildren[i]); + let split_ = handleToucharea.createObject(contentChildren[i]); + contentChildren[i].width = Qt.binding(function() { + return split_.calculatedWidth; + }); + contentChildren[i].splitterWidth = Qt.binding(function() { + return handle_.width; + }); + } + contentChildren[count - 1].width = Qt.binding(function() { + if (container.singlePageMode) { + return container.width; + } else { + var w = container.width; + for (var i = 0; i < count - 1; i++) { + if (contentChildren[i].width) + w = w - contentChildren[i].width; + + } + return w; + } + }); + contentChildren[count - 1].splitterWidth = 0; + for (var i = 0; i < count; i++) { + contentChildren[i].height = Qt.binding(function() { + return container.height; + }); + contentChildren[i].children[0].height = Qt.binding(function() { + return container.height; + }); + } + } + + handle: Rectangle { + z: 3 + color: Nheko.theme.separator + height: container.height + width: visible ? 1 : 0 + anchors.right: parent.right + } + + handleToucharea: Item { + id: splitter + + property int minimumWidth: parent.minimumWidth + property int maximumWidth: parent.maximumWidth + property int collapsedWidth: parent.collapsedWidth + property bool collapsible: parent.collapsible + property int calculatedWidth: { + if (!visible) + return 0; + else if (container.singlePageMode) + return container.width; + else + return (collapsible && x < minimumWidth) ? collapsedWidth : x; + } + + //visible: !container.singlePageMode + enabled: !container.singlePageMode + height: container.height + width: 1 + x: parent.preferredWidth + z: 3 + + CursorShape { + height: parent.height + width: container.splitterGrabMargin * 2 + x: -container.splitterGrabMargin + cursorShape: Qt.SizeHorCursor + } + + DragHandler { + id: dragHandler + + enabled: !container.singlePageMode + xAxis.enabled: true + yAxis.enabled: false + xAxis.minimum: splitter.minimumWidth - 1 + xAxis.maximum: splitter.maximumWidth + margin: container.splitterGrabMargin + //dragThreshold: 0 + grabPermissions: PointerHandler.CanTakeOverFromAnything | PointerHandler.ApprovesTakeOverByHandlersOfSameType + //cursorShape: Qt.SizeHorCursor + onActiveChanged: { + if (!active) + splitter.parent.preferredWidth = splitter.x; + + } + } + + HoverHandler { + //cursorShape: Qt.SizeHorCursor + + enabled: !container.singlePageMode + margin: container.splitterGrabMargin + } + + } + + contentItem: ListView { + id: view + + model: container.contentModel + snapMode: ListView.SnapOneItem + orientation: ListView.Horizontal + highlightRangeMode: ListView.StrictlyEnforceRange + interactive: false + highlightMoveDuration: container.singlePageMode ? 200 : 0 + currentIndex: container.singlePageMode ? container.pageIndex : 0 + } + +} diff --git a/resources/qml/components/AdaptiveLayoutElement.qml b/resources/qml/components/AdaptiveLayoutElement.qml new file mode 100644
index 00000000..a4aec72e --- /dev/null +++ b/resources/qml/components/AdaptiveLayoutElement.qml
@@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.12 + +Item { + property int minimumWidth: 100 + property int maximumWidth: 400 + property int collapsedWidth: 40 + property bool collapsible: true + property bool collapsed: width < minimumWidth + property int splitterWidth: 1 + property int preferredWidth: 100 + + Component.onCompleted: { + children[0].width = Qt.binding(() => { + return parent.singlePageMode ? parent.width : width - splitterWidth; + }); + children[0].height = Qt.binding(() => { + return parent.height; + }); + } +} diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml
index e883ddbb..0392c73a 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml
@@ -20,7 +20,7 @@ Item { Rectangle { id: button - color: colors.light + color: Nheko.colors.light radius: 22 height: 44 width: 44 @@ -34,7 +34,7 @@ Item { } TapHandler { - onSingleTapped: TimelineManager.timeline.saveMedia(model.data.id) + onSingleTapped: room.saveMedia(model.data.id) gesturePolicy: TapHandler.ReleaseWithinBounds } @@ -55,7 +55,7 @@ Item { text: model.data.filename textFormat: Text.PlainText elide: Text.ElideRight - color: colors.text + color: Nheko.colors.text } Text { @@ -65,7 +65,7 @@ Item { text: model.data.filesize textFormat: Text.PlainText elide: Text.ElideRight - color: colors.text + color: Nheko.colors.text } } @@ -73,7 +73,7 @@ Item { } Rectangle { - color: colors.alternateBase + color: Nheko.colors.alternateBase z: -1 radius: 10 height: row.height + 24 diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml
index 8fcf3f82..ce8e779c 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml
@@ -9,17 +9,17 @@ Item { property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? parent.width : model.data.width) property double tempHeight: tempWidth * model.data.proportionalHeight property double divisor: model.isReply ? 5 : 3 - property bool tooHigh: tempHeight > timelineRoot.height / divisor + property bool tooHigh: tempHeight > timelineView.height / divisor - height: Math.round(tooHigh ? timelineRoot.height / divisor : tempHeight) - width: Math.round(tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth) + height: Math.round(tooHigh ? timelineView.height / divisor : tempHeight) + width: Math.round(tooHigh ? (timelineView.height / divisor) / model.data.proportionalHeight : tempWidth) Image { id: blurhash anchors.fill: parent visible: img.status != Image.Ready - source: model.data.blurhash ? ("image://blurhash/" + model.data.blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?" + colors.buttonText) + source: model.data.blurhash ? ("image://blurhash/" + model.data.blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?" + Nheko.colors.buttonText) asynchronous: true fillMode: Image.PreserveAspectFit sourceSize.width: parent.width @@ -61,7 +61,7 @@ Item { width: parent.width implicitHeight: imgcaption.implicitHeight anchors.bottom: overlay.bottom - color: colors.window + color: Nheko.colors.window opacity: 0.75 } @@ -74,7 +74,7 @@ Item { verticalAlignment: Text.AlignVCenter // See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530 text: model.data.filename ? model.data.filename : model.data.body - color: colors.text + color: Nheko.colors.text } } diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index d278a586..9e076a7a 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml
@@ -58,7 +58,7 @@ Item { NoticeMessage { formatted: TimelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody - color: TimelineManager.userColor(modelData.userId, colors.window) + color: TimelineManager.userColor(modelData.userId, Nheko.colors.window) } } @@ -207,7 +207,7 @@ Item { roleValue: MtxEvent.PowerLevels NoticeMessage { - text: TimelineManager.timeline.formatPowerLevelEvent(model.data.id) + text: room.formatPowerLevelEvent(model.data.id) } } @@ -216,7 +216,7 @@ Item { roleValue: MtxEvent.RoomJoinRules NoticeMessage { - text: TimelineManager.timeline.formatJoinRuleEvent(model.data.id) + text: room.formatJoinRuleEvent(model.data.id) } } @@ -225,7 +225,7 @@ Item { roleValue: MtxEvent.RoomHistoryVisibility NoticeMessage { - text: TimelineManager.timeline.formatHistoryVisibilityEvent(model.data.id) + text: room.formatHistoryVisibilityEvent(model.data.id) } } @@ -234,7 +234,7 @@ Item { roleValue: MtxEvent.RoomGuestAccess NoticeMessage { - text: TimelineManager.timeline.formatGuestAccessEvent(model.data.id) + text: room.formatGuestAccessEvent(model.data.id) } } @@ -243,7 +243,7 @@ Item { roleValue: MtxEvent.Member NoticeMessage { - text: TimelineManager.timeline.formatMemberEvent(model.data.id) + text: room.formatMemberEvent(model.data.id) } } diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml
index 4f8fbfd2..2af41bb2 100644 --- a/resources/qml/delegates/NoticeMessage.qml +++ b/resources/qml/delegates/NoticeMessage.qml
@@ -2,7 +2,9 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +import im.nheko 1.0 + TextMessage { font.italic: true - color: colors.buttonText + color: Nheko.colors.buttonText } diff --git a/resources/qml/delegates/Pill.qml b/resources/qml/delegates/Pill.qml
index a3fa0d9e..248d91da 100644 --- a/resources/qml/delegates/Pill.qml +++ b/resources/qml/delegates/Pill.qml
@@ -4,16 +4,17 @@ import QtQuick 2.5 import QtQuick.Controls 2.1 +import im.nheko 1.0 Label { - color: colors.brightText + color: Nheko.colors.brightText horizontalAlignment: Text.AlignHCenter height: contentHeight * 1.2 width: contentWidth * 1.2 background: Rectangle { radius: parent.height / 2 - color: colors.alternateBase + color: Nheko.colors.alternateBase } } diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml
index addbc7e7..c4fc6cc3 100644 --- a/resources/qml/delegates/Placeholder.qml +++ b/resources/qml/delegates/Placeholder.qml
@@ -3,9 +3,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later import ".." +import im.nheko 1.0 MatrixText { text: qsTr("unimplemented event: ") + model.data.typeString width: parent ? parent.width : undefined - color: inactiveColors.text + color: Nheko.inactiveColors.text } diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index b62c90df..83864db9 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -13,7 +13,7 @@ Rectangle { id: bg radius: 10 - color: colors.alternateBase + color: Nheko.colors.alternateBase height: Math.round(content.height + 24) width: parent ? parent.width : undefined @@ -29,11 +29,11 @@ Rectangle { property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? 400 : model.data.width) property double tempHeight: tempWidth * model.data.proportionalHeight property double divisor: model.isReply ? 4 : 2 - property bool tooHigh: tempHeight > timelineRoot.height / divisor + property bool tooHigh: tempHeight > timelineView.height / divisor visible: model.data.type == MtxEvent.VideoMessage - height: tooHigh ? timelineRoot.height / divisor : tempHeight - width: tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth + height: tooHigh ? timelineView.height / divisor : tempHeight + width: tooHigh ? (timelineView.height / divisor) / model.data.proportionalHeight : tempWidth Image { anchors.fill: parent @@ -58,7 +58,7 @@ Rectangle { id: positionText text: "--:--:--" - color: colors.text + color: Nheko.colors.text } Slider { @@ -92,14 +92,14 @@ Rectangle { to: media.duration onMoved: media.seek(value) onValueChanged: updatePositionTexts() - palette: colors + palette: Nheko.colors } Text { id: durationText text: "--:--:--" - color: colors.text + color: Nheko.colors.text } } @@ -112,7 +112,7 @@ Rectangle { id: button Layout.alignment: Qt.AlignVCenter - //color: colors.window + //color: Nheko.colors.window //radius: 22 height: 32 width: 32 @@ -121,7 +121,7 @@ Rectangle { onClicked: { switch (button.state) { case "": - TimelineManager.timeline.cacheMedia(model.data.id); + room.cacheMedia(model.data.id); break; case "stopped": media.play(); @@ -174,7 +174,7 @@ Rectangle { } Connections { - target: TimelineManager.timeline + target: room onMediaCached: { if (mxcUrl == model.data.url) { media.source = cacheUrl; @@ -194,7 +194,7 @@ Rectangle { Layout.fillWidth: true text: model.data.body elide: Text.ElideRight - color: colors.text + color: Nheko.colors.text } Text { @@ -202,7 +202,7 @@ Rectangle { text: model.data.filesize textFormat: Text.PlainText elide: Text.ElideRight - color: colors.text + color: Nheko.colors.text } } diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index 603d0cf6..b8c33539 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml
@@ -33,7 +33,7 @@ Item { anchors.top: replyContainer.top anchors.bottom: replyContainer.bottom width: 4 - color: TimelineManager.userColor(reply.modelData.userId, colors.window) + color: TimelineManager.userColor(reply.modelData.userId, Nheko.colors.window) } Column { diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml
index f44165b4..cd46f8ca 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml
@@ -9,9 +9,26 @@ MatrixText { property string formatted: model.data.formattedBody property string copyText: selectedText ? getText(selectionStart, selectionEnd) : model.data.body - text: "<style type=\"text/css\">a { color:" + colors.link + ";}\ncode { background-color: " + colors.alternateBase + ";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap; background-color: " + colors.alternateBase + "'>") + // table border-collapse doesn't seem to work + text: " + <style type=\"text/css\"> + a { color:" + Nheko.colors.link + ";} + code { background-color: " + Nheko.colors.alternateBase + ";} + table { + border-width: 1px; + border-collapse: collapse; + border-style: solid; + } + table th, + table td { + bgcolor: " + Nheko.colors.alternateBase + "; + border-collapse: collapse; + border: 1px solid " + Nheko.colors.text + "; + } + </style> + " + formatted.replace("<pre>", "<pre style='white-space: pre-wrap; background-color: " + Nheko.colors.alternateBase + "'>").replace("<del>", "<s>").replace("</del>", "</s>").replace("<strike>", "<s>").replace("</strike>", "</s>") width: parent ? parent.width : undefined - height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined + height: isReply ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : undefined clip: isReply selectByMouse: !Settings.mobileMode && !isReply font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize diff --git a/resources/qml/device-verification/AwaitingVerificationConfirmation.qml b/resources/qml/device-verification/AwaitingVerificationConfirmation.qml
index ae62c334..a6a7f027 100644 --- a/resources/qml/device-verification/AwaitingVerificationConfirmation.qml +++ b/resources/qml/device-verification/AwaitingVerificationConfirmation.qml
@@ -21,7 +21,7 @@ Pane { Layout.fillWidth: true wrapMode: Text.Wrap text: qsTr("Waiting for other side to complete verification.") - color: colors.text + color: Nheko.colors.text verticalAlignment: Text.AlignVCenter } diff --git a/resources/qml/device-verification/DeviceVerification.qml b/resources/qml/device-verification/DeviceVerification.qml
index 41ed8d57..e2c66c5a 100644 --- a/resources/qml/device-verification/DeviceVerification.qml +++ b/resources/qml/device-verification/DeviceVerification.qml
@@ -15,8 +15,8 @@ ApplicationWindow { onClosing: TimelineManager.removeVerificationFlow(flow) title: stack.currentItem.title flags: Qt.Dialog - modality: Qt.WindowModal - palette: colors + modality: Qt.NonModal + palette: Nheko.colors height: stack.implicitHeight width: stack.implicitWidth x: MainWindow.x + (MainWindow.width / 2) - (width / 2) diff --git a/resources/qml/device-verification/DigitVerification.qml b/resources/qml/device-verification/DigitVerification.qml
index a387756d..aafdc043 100644 --- a/resources/qml/device-verification/DigitVerification.qml +++ b/resources/qml/device-verification/DigitVerification.qml
@@ -19,7 +19,7 @@ Pane { Layout.fillWidth: true wrapMode: Text.Wrap text: qsTr("Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification!") - color: colors.text + color: Nheko.colors.text verticalAlignment: Text.AlignVCenter } @@ -29,19 +29,19 @@ Pane { Label { font.pixelSize: Qt.application.font.pixelSize * 2 text: flow.sasList[0] - color: colors.text + color: Nheko.colors.text } Label { font.pixelSize: Qt.application.font.pixelSize * 2 text: flow.sasList[1] - color: colors.text + color: Nheko.colors.text } Label { font.pixelSize: Qt.application.font.pixelSize * 2 text: flow.sasList[2] - color: colors.text + color: Nheko.colors.text } } diff --git a/resources/qml/device-verification/EmojiVerification.qml b/resources/qml/device-verification/EmojiVerification.qml
index be9e3938..b6e8484f 100644 --- a/resources/qml/device-verification/EmojiVerification.qml +++ b/resources/qml/device-verification/EmojiVerification.qml
@@ -19,7 +19,7 @@ Pane { Layout.fillWidth: true wrapMode: Text.Wrap text: qsTr("Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification!") - color: colors.text + color: Nheko.colors.text verticalAlignment: Text.AlignVCenter } @@ -374,13 +374,13 @@ Pane { text: col.emoji.emoji font.pixelSize: Qt.application.font.pixelSize * 2 font.family: Settings.emojiFont - color: colors.text + color: Nheko.colors.text } Label { Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom text: col.emoji.description - color: colors.text + color: Nheko.colors.text } } diff --git a/resources/qml/device-verification/Failed.qml b/resources/qml/device-verification/Failed.qml
index 5c71b02e..71ef8b9b 100644 --- a/resources/qml/device-verification/Failed.qml +++ b/resources/qml/device-verification/Failed.qml
@@ -38,7 +38,7 @@ Pane { return "Unknown verification error."; } } - color: colors.text + color: Nheko.colors.text verticalAlignment: Text.AlignVCenter } diff --git a/resources/qml/device-verification/NewVerificationRequest.qml b/resources/qml/device-verification/NewVerificationRequest.qml
index e8589cf7..5ae2d25b 100644 --- a/resources/qml/device-verification/NewVerificationRequest.qml +++ b/resources/qml/device-verification/NewVerificationRequest.qml
@@ -35,7 +35,7 @@ Pane { return qsTr("Your device (%1) has requested to be verified.").arg(flow.deviceId); } } - color: colors.text + color: Nheko.colors.text verticalAlignment: Text.AlignVCenter } diff --git a/resources/qml/device-verification/Success.qml b/resources/qml/device-verification/Success.qml
index f2657b12..70cfafaf 100644 --- a/resources/qml/device-verification/Success.qml +++ b/resources/qml/device-verification/Success.qml
@@ -5,6 +5,7 @@ import QtQuick 2.3 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.10 +import im.nheko 1.0 Pane { property string title: qsTr("Successful Verification") @@ -20,7 +21,7 @@ Pane { Layout.fillWidth: true wrapMode: Text.Wrap text: qsTr("Verification successful! Both sides verified their devices!") - color: colors.text + color: Nheko.colors.text verticalAlignment: Text.AlignVCenter } diff --git a/resources/qml/device-verification/Waiting.qml b/resources/qml/device-verification/Waiting.qml
index 3bfa153d..c521503b 100644 --- a/resources/qml/device-verification/Waiting.qml +++ b/resources/qml/device-verification/Waiting.qml
@@ -30,13 +30,13 @@ Pane { return qsTr("Waiting for other side to complete the verification process."); } } - color: colors.text + color: Nheko.colors.text verticalAlignment: Text.AlignVCenter } BusyIndicator { Layout.alignment: Qt.AlignHCenter - palette: colors + palette: Nheko.colors } RowLayout { diff --git a/resources/qml/dialogs/InputDialog.qml b/resources/qml/dialogs/InputDialog.qml new file mode 100644
index 00000000..134b78a3 --- /dev/null +++ b/resources/qml/dialogs/InputDialog.qml
@@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import ".." +import QtQuick 2.12 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.3 +import im.nheko 1.0 + +ApplicationWindow { + id: inputDialog + + property alias prompt: promptLabel.text + property var onAccepted: undefined + + modality: Qt.NonModal + flags: Qt.Dialog + width: 350 + height: fontMetrics.lineSpacing * 7 + + ColumnLayout { + anchors.margins: Nheko.paddingLarge + anchors.fill: parent + + Label { + id: promptLabel + + color: Nheko.colors.text + } + + MatrixTextField { + id: statusInput + + Layout.fillWidth: true + } + + } + + footer: DialogButtonBox { + standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel + onAccepted: { + if (inputDialog.onAccepted) + inputDialog.onAccepted(statusInput.text); + + inputDialog.close(); + } + onRejected: { + inputDialog.close(); + } + } + +} diff --git a/resources/qml/emoji/EmojiButton.qml b/resources/qml/emoji/EmojiButton.qml
index cec51d75..5f4d23d3 100644 --- a/resources/qml/emoji/EmojiButton.qml +++ b/resources/qml/emoji/EmojiButton.qml
@@ -17,7 +17,7 @@ ImageButton { image: ":/icons/icons/ui/smile.png" onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) { - TimelineManager.queueReactionMessage(event_id, emoji); + room.input.reaction(event_id, emoji); TimelineManager.focusMessageInput(); }) } diff --git a/resources/qml/emoji/EmojiPicker.qml b/resources/qml/emoji/EmojiPicker.qml
index 4aad832d..efcdc2cf 100644 --- a/resources/qml/emoji/EmojiPicker.qml +++ b/resources/qml/emoji/EmojiPicker.qml
@@ -18,9 +18,9 @@ Menu { property alias model: gridView.model property var textArea property string emojiCategory: "people" - property real highlightHue: colors.highlight.hslHue - property real highlightSat: colors.highlight.hslSaturation - property real highlightLight: colors.highlight.hslLightness + property real highlightHue: Nheko.colors.highlight.hslHue + property real highlightSat: Nheko.colors.highlight.hslSaturation + property real highlightLight: Nheko.colors.highlight.hslLightness function show(showAt, callback) { console.debug("Showing emojiPicker"); @@ -80,7 +80,7 @@ Menu { id: clearSearch visible: emojiSearch.text !== '' - icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? colors.highlight : colors.buttonText) + icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? Nheko.colors.highlight : Nheko.colors.buttonText) focusPolicy: Qt.NoFocus onClicked: emojiSearch.clear() @@ -146,7 +146,7 @@ Menu { background: Rectangle { anchors.fill: parent - color: hovered ? colors.highlight : 'transparent' + color: hovered ? Nheko.colors.highlight : 'transparent' radius: 5 } @@ -163,7 +163,7 @@ Menu { visible: emojiSearch.text === '' Layout.fillWidth: true Layout.preferredHeight: 1 - color: emojiPopup.colors.alternateBase + color: emojiPopup.Nheko.colors.alternateBase } // Category picker row @@ -265,14 +265,14 @@ Menu { fillMode: Image.Pad sourceSize.width: 32 sourceSize.height: 32 - source: "image://colorimage/" + model.image + "?" + (hovered ? colors.highlight : colors.buttonText) + source: "image://colorimage/" + model.image + "?" + (hovered ? Nheko.colors.highlight : Nheko.colors.buttonText) } background: Rectangle { anchors.fill: parent color: emojiPopup.model.category === model.category ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : 'transparent' radius: 5 - border.color: emojiPopup.model.category === model.category ? colors.highlight : 'transparent' + border.color: emojiPopup.model.category === model.category ? Nheko.colors.highlight : 'transparent' } } diff --git a/resources/qml/voip/ActiveCallBar.qml b/resources/qml/voip/ActiveCallBar.qml
index 68d3bc4a..3106c382 100644 --- a/resources/qml/voip/ActiveCallBar.qml +++ b/resources/qml/voip/ActiveCallBar.qml
@@ -31,11 +31,11 @@ Rectangle { anchors.leftMargin: 8 Avatar { - width: avatarSize - height: avatarSize + width: Nheko.avatarSize + height: Nheko.avatarSize url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") displayName: CallManager.callParty - onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id) + onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id) } Label { diff --git a/resources/qml/voip/CallDevices.qml b/resources/qml/voip/CallDevices.qml
index 11644797..824bb2e0 100644 --- a/resources/qml/voip/CallDevices.qml +++ b/resources/qml/voip/CallDevices.qml
@@ -9,7 +9,7 @@ import im.nheko 1.0 Popup { modal: true - palette: colors + palette: Nheko.colors // only set the anchors on Qt 5.12 or higher // see https://doc.qt.io/qt-5/qml-qtquick-controls2-popup.html#anchors.centerIn-prop Component.onCompleted: { @@ -31,7 +31,7 @@ Popup { Image { Layout.preferredWidth: 22 Layout.preferredHeight: 22 - source: "image://colorimage/:/icons/icons/ui/microphone-unmute.png?" + colors.windowText + source: "image://colorimage/:/icons/icons/ui/microphone-unmute.png?" + Nheko.colors.windowText } ComboBox { @@ -49,7 +49,7 @@ Popup { Image { Layout.preferredWidth: 22 Layout.preferredHeight: 22 - source: "image://colorimage/:/icons/icons/ui/video-call.png?" + colors.windowText + source: "image://colorimage/:/icons/icons/ui/video-call.png?" + Nheko.colors.windowText } ComboBox { @@ -81,8 +81,8 @@ Popup { } background: Rectangle { - color: colors.window - border.color: colors.windowText + color: Nheko.colors.window + border.color: Nheko.colors.windowText } } diff --git a/resources/qml/voip/CallInvite.qml b/resources/qml/voip/CallInvite.qml
index 15d987e7..1b57976d 100644 --- a/resources/qml/voip/CallInvite.qml +++ b/resources/qml/voip/CallInvite.qml
@@ -12,7 +12,7 @@ Popup { closePolicy: Popup.NoAutoClose width: parent.width height: parent.height - palette: colors + palette: Nheko.colors Component { id: deviceError @@ -41,7 +41,7 @@ Popup { Layout.topMargin: msgView.height / 25 text: CallManager.callParty font.pointSize: fontMetrics.font.pointSize * 2 - color: colors.windowText + color: Nheko.colors.windowText } Avatar { @@ -62,14 +62,14 @@ Popup { Layout.alignment: Qt.AlignCenter Layout.preferredWidth: msgView.height / 10 Layout.preferredHeight: msgView.height / 10 - source: "image://colorimage/" + image + "?" + colors.windowText + source: "image://colorimage/" + image + "?" + Nheko.colors.windowText } Label { Layout.alignment: Qt.AlignCenter text: CallManager.callType == CallType.VIDEO ? qsTr("Video Call") : qsTr("Voice Call") font.pointSize: fontMetrics.font.pointSize * 2 - color: colors.windowText + color: Nheko.colors.windowText } } @@ -88,7 +88,7 @@ Popup { Image { Layout.preferredWidth: deviceCombos.imageSize Layout.preferredHeight: deviceCombos.imageSize - source: "image://colorimage/:/icons/icons/ui/microphone-unmute.png?" + colors.windowText + source: "image://colorimage/:/icons/icons/ui/microphone-unmute.png?" + Nheko.colors.windowText } ComboBox { @@ -107,7 +107,7 @@ Popup { Image { Layout.preferredWidth: deviceCombos.imageSize Layout.preferredHeight: deviceCombos.imageSize - source: "image://colorimage/:/icons/icons/ui/video-call.png?" + colors.windowText + source: "image://colorimage/:/icons/icons/ui/video-call.png?" + Nheko.colors.windowText } ComboBox { @@ -194,8 +194,8 @@ Popup { } background: Rectangle { - color: colors.window - border.color: colors.windowText + color: Nheko.colors.window + border.color: Nheko.colors.windowText } } diff --git a/resources/qml/voip/CallInviteBar.qml b/resources/qml/voip/CallInviteBar.qml
index fe3f791f..2d8e3040 100644 --- a/resources/qml/voip/CallInviteBar.qml +++ b/resources/qml/voip/CallInviteBar.qml
@@ -38,11 +38,11 @@ Rectangle { anchors.leftMargin: 8 Avatar { - width: avatarSize - height: avatarSize + width: Nheko.avatarSize + height: Nheko.avatarSize url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") displayName: CallManager.callParty - onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id) + onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id) } Label { @@ -88,7 +88,7 @@ Rectangle { Layout.rightMargin: 4 icon.source: CallManager.callType == CallType.VIDEO ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png" text: qsTr("Accept") - palette: colors + palette: Nheko.colors onClicked: { if (CallManager.mics.length == 0) { var dialog = deviceError.createObject(timelineRoot, { @@ -121,7 +121,7 @@ Rectangle { Layout.rightMargin: 16 icon.source: "qrc:/icons/icons/ui/end-call.png" text: qsTr("Decline") - palette: colors + palette: Nheko.colors onClicked: { CallManager.hangUp(); } diff --git a/resources/qml/voip/DeviceError.qml b/resources/qml/voip/DeviceError.qml
index 05cfd409..47ded50a 100644 --- a/resources/qml/voip/DeviceError.qml +++ b/resources/qml/voip/DeviceError.qml
@@ -24,19 +24,19 @@ Popup { Image { Layout.preferredWidth: 16 Layout.preferredHeight: 16 - source: "image://colorimage/" + image + "?" + colors.windowText + source: "image://colorimage/" + image + "?" + Nheko.colors.windowText } Label { text: errorString - color: colors.windowText + color: Nheko.colors.windowText } } background: Rectangle { - color: colors.window - border.color: colors.windowText + color: Nheko.colors.window + border.color: Nheko.colors.windowText } } diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml
index c9aa8ea1..97e39e02 100644 --- a/resources/qml/voip/PlaceCall.qml +++ b/resources/qml/voip/PlaceCall.qml
@@ -17,7 +17,7 @@ Popup { anchors.centerIn = parent; } - palette: colors + palette: Nheko.colors Component { id: deviceError @@ -45,8 +45,8 @@ Popup { Layout.leftMargin: 8 Label { - text: qsTr("Place a call to %1?").arg(TimelineManager.timeline.roomName) - color: colors.windowText + text: qsTr("Place a call to %1?").arg(room.roomName) + color: Nheko.colors.windowText } Item { @@ -75,11 +75,11 @@ Popup { Avatar { Layout.rightMargin: cameraCombo.visible ? 16 : 64 - width: avatarSize - height: avatarSize - url: TimelineManager.timeline.roomAvatarUrl.replace("mxc://", "image://MxcImage/") - displayName: TimelineManager.timeline.roomName - onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id) + width: Nheko.avatarSize + height: Nheko.avatarSize + url: room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") + displayName: room.roomName + onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id) } Button { @@ -88,7 +88,7 @@ Popup { onClicked: { if (buttonLayout.validateMic()) { Settings.microphone = micCombo.currentText; - CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.VOICE); + CallManager.sendInvite(room.roomId(), CallType.VOICE); close(); } } @@ -102,7 +102,7 @@ Popup { if (buttonLayout.validateMic()) { Settings.microphone = micCombo.currentText; Settings.camera = cameraCombo.currentText; - CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.VIDEO); + CallManager.sendInvite(room.roomId(), CallType.VIDEO); close(); } } @@ -139,7 +139,7 @@ Popup { Image { Layout.preferredWidth: 22 Layout.preferredHeight: 22 - source: "image://colorimage/:/icons/icons/ui/microphone-unmute.png?" + colors.windowText + source: "image://colorimage/:/icons/icons/ui/microphone-unmute.png?" + Nheko.colors.windowText } ComboBox { @@ -160,7 +160,7 @@ Popup { Image { Layout.preferredWidth: 22 Layout.preferredHeight: 22 - source: "image://colorimage/:/icons/icons/ui/video-call.png?" + colors.windowText + source: "image://colorimage/:/icons/icons/ui/video-call.png?" + Nheko.colors.windowText } ComboBox { @@ -177,8 +177,8 @@ Popup { } background: Rectangle { - color: colors.window - border.color: colors.windowText + color: Nheko.colors.window + border.color: Nheko.colors.windowText } } diff --git a/resources/qml/voip/ScreenShare.qml b/resources/qml/voip/ScreenShare.qml
index af473c04..a10057b2 100644 --- a/resources/qml/voip/ScreenShare.qml +++ b/resources/qml/voip/ScreenShare.qml
@@ -18,7 +18,7 @@ Popup { frameRateCombo.currentIndex = frameRateCombo.find(Settings.screenShareFrameRate); } - palette: colors + palette: Nheko.colors ColumnLayout { Label { @@ -27,8 +27,8 @@ Popup { Layout.leftMargin: 8 Layout.rightMargin: 8 Layout.alignment: Qt.AlignLeft - text: qsTr("Share desktop with %1?").arg(TimelineManager.timeline.roomName) - color: colors.windowText + text: qsTr("Share desktop with %1?").arg(room.roomName) + color: Nheko.colors.windowText } RowLayout { @@ -39,7 +39,7 @@ Popup { Label { Layout.alignment: Qt.AlignLeft text: qsTr("Window:") - color: colors.windowText + color: Nheko.colors.windowText } ComboBox { @@ -59,7 +59,7 @@ Popup { Label { Layout.alignment: Qt.AlignLeft text: qsTr("Frame rate:") - color: colors.windowText + color: Nheko.colors.windowText } ComboBox { @@ -136,7 +136,7 @@ Popup { Settings.screenSharePiP = pipCheckBox.checked; Settings.screenShareRemoteVideo = remoteVideoCheckBox.checked; Settings.screenShareHideCursor = hideCursorCheckBox.checked; - CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.SCREEN, windowCombo.currentIndex); + CallManager.sendInvite(room.roomId(), CallType.SCREEN, windowCombo.currentIndex); close(); } } @@ -161,8 +161,8 @@ Popup { } background: Rectangle { - color: colors.window - border.color: colors.windowText + color: Nheko.colors.window + border.color: Nheko.colors.windowText } } diff --git a/resources/res.qrc b/resources/res.qrc
index 304493b6..53c74ae3 100644 --- a/resources/res.qrc +++ b/resources/res.qrc
@@ -123,11 +123,16 @@ <qresource prefix="/"> <file>qtquickcontrols2.conf</file> + <file>qml/Root.qml</file> + <file>qml/ChatPage.qml</file> + <file>qml/CommunitiesList.qml</file> + <file>qml/RoomList.qml</file> <file>qml/TimelineView.qml</file> <file>qml/Avatar.qml</file> <file>qml/Completer.qml</file> <file>qml/EncryptionIndicator.qml</file> <file>qml/ImageButton.qml</file> + <file>qml/ElidedLabel.qml</file> <file>qml/MatrixText.qml</file> <file>qml/MatrixTextField.qml</file> <file>qml/ToggleButton.qml</file> @@ -164,6 +169,7 @@ <file>qml/device-verification/NewVerificationRequest.qml</file> <file>qml/device-verification/Failed.qml</file> <file>qml/device-verification/Success.qml</file> + <file>qml/dialogs/InputDialog.qml</file> <file>qml/ui/Ripple.qml</file> <file>qml/voip/ActiveCallBar.qml</file> <file>qml/voip/CallDevices.qml</file> @@ -173,6 +179,8 @@ <file>qml/voip/PlaceCall.qml</file> <file>qml/voip/ScreenShare.qml</file> <file>qml/voip/VideoCall.qml</file> + <file>qml/components/AdaptiveLayout.qml</file> + <file>qml/components/AdaptiveLayoutElement.qml</file> </qresource> <qresource prefix="/media"> <file>media/ring.ogg</file> diff --git a/src/Cache.cpp b/src/Cache.cpp
index 24b2bc24..0d75ac51 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp
@@ -253,6 +253,8 @@ Cache::setup() outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); txn.commit(); + + databaseReady_ = true; } void @@ -788,6 +790,7 @@ Cache::nextBatchToken() void Cache::deleteData() { + this->databaseReady_ = false; // TODO: We need to remove the env_ while not accepting new requests. lmdb::dbi_close(env_, syncStateDb_); lmdb::dbi_close(env_, roomsDb_); @@ -2042,21 +2045,57 @@ Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id) return fallbackDesc; } -std::map<QString, bool> +QHash<QString, RoomInfo> Cache::invites() { - std::map<QString, bool> result; + QHash<QString, RoomInfo> result; auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); auto cursor = lmdb::cursor::open(txn, invitesDb_); - std::string_view room_id, unused; + std::string_view room_id, room_data; - while (cursor.get(room_id, unused, MDB_NEXT)) - result.emplace(QString::fromStdString(std::string(room_id)), true); + while (cursor.get(room_id, room_data, MDB_NEXT)) { + try { + RoomInfo tmp = json::parse(room_data); + tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn); + result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp)); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse room info for invite: " + "room_id ({}), {}: {}", + room_id, + std::string(room_data), + e.what()); + } + } cursor.close(); - txn.commit(); + + return result; +} + +std::optional<RoomInfo> +Cache::invite(std::string_view roomid) +{ + std::optional<RoomInfo> result; + + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + std::string_view room_data; + + if (invitesDb_.get(txn, roomid, room_data)) { + try { + RoomInfo tmp = json::parse(room_data); + tmp.member_count = getInviteMembersDb(txn, std::string(roomid)).size(txn); + result = std::move(tmp); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse room info for invite: " + "room_id ({}), {}: {}", + roomid, + std::string(room_data), + e.what()); + } + } return result; } @@ -2426,7 +2465,7 @@ Cache::joinedRooms() std::optional<MemberInfo> Cache::getMember(const std::string &room_id, const std::string &user_id) { - if (user_id.empty()) + if (user_id.empty() || !env_.handle()) return std::nullopt; try { @@ -2440,7 +2479,8 @@ Cache::getMember(const std::string &room_id, const std::string &user_id) return m; } } catch (std::exception &e) { - nhlog::db()->warn("Failed to read member ({}): {}", user_id, e.what()); + nhlog::db()->warn( + "Failed to read member ({}) in room ({}): {}", user_id, room_id, e.what()); } return std::nullopt; } @@ -3412,6 +3452,10 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query if (!updateToWrite.master_keys.keys.empty() && update.master_keys.keys != updateToWrite.master_keys.keys) { + nhlog::db()->debug("Master key of {} changed:\nold: {}\nnew: {}", + user, + updateToWrite.master_keys.keys.size(), + update.master_keys.keys.size()); updateToWrite.master_key_changed = true; } @@ -3466,6 +3510,7 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query } } } + for (auto &[user_id, update] : updates) { (void)update; if (user_id == local_user) { @@ -3473,9 +3518,8 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query (void)status; emit verificationStatusChanged(user); } - } else { - emit verificationStatusChanged(user_id); } + emit verificationStatusChanged(user_id); } } @@ -3549,10 +3593,23 @@ Cache::query_keys(const std::string &user_id, last_changed = cache_->last_changed; req.token = last_changed; + // use context object so that we can disconnect again + QObject *context{new QObject(this)}; + QObject::connect(this, + &Cache::verificationStatusChanged, + context, + [cb, user_id, context_ = context](std::string updated_user) mutable { + if (user_id == updated_user) { + context_->deleteLater(); + auto keys = cache::userKeys(user_id); + cb(keys.value_or(UserKeyCache{}), {}); + } + }); + http::client()->query_keys( req, - [cb, user_id, last_changed](const mtx::responses::QueryKeys &res, - mtx::http::RequestErr err) { + [cb, user_id, last_changed, this](const mtx::responses::QueryKeys &res, + mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("failed to query device keys: {},{}", mtx::errors::to_string(err->matrix_error.errcode), @@ -3561,10 +3618,7 @@ Cache::query_keys(const std::string &user_id, return; } - cache::updateUserKeys(last_changed, res); - - auto keys = cache::userKeys(user_id); - cb(keys.value_or(UserKeyCache{}), err); + emit userKeysUpdate(last_changed, res); }); } @@ -3999,6 +4053,8 @@ avatarUrl(const QString &room_id, const QString &user_id) mtx::presence::PresenceState presenceState(const std::string &user_id) { + if (!instance_) + return {}; return instance_->presenceState(user_id); } std::string @@ -4049,7 +4105,7 @@ roomInfo(bool withInvites) { return instance_->roomInfo(withInvites); } -std::map<QString, bool> +QHash<QString, RoomInfo> invites() { return instance_->invites(); diff --git a/src/Cache.h b/src/Cache.h
index 427dbafc..74ec9695 100644 --- a/src/Cache.h +++ b/src/Cache.h
@@ -62,7 +62,7 @@ joinedRooms(); QMap<QString, RoomInfo> roomInfo(bool withInvites = true); -std::map<QString, bool> +QHash<QString, RoomInfo> invites(); //! Calculate & return the name of the room. diff --git a/src/CacheStructs.h b/src/CacheStructs.h
index c449f013..f7d6f0e2 100644 --- a/src/CacheStructs.h +++ b/src/CacheStructs.h
@@ -50,6 +50,19 @@ struct DescInfo QDateTime datetime; }; +inline bool +operator==(const DescInfo &a, const DescInfo &b) +{ + return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) == + std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime); +} +inline bool +operator!=(const DescInfo &a, const DescInfo &b) +{ + return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) != + std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime); +} + //! UI info associated with a room. struct RoomInfo { diff --git a/src/Cache_p.h b/src/Cache_p.h
index 356c6e42..f2911622 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h
@@ -70,7 +70,8 @@ public: QMap<QString, RoomInfo> roomInfo(bool withInvites = true); std::optional<mtx::events::state::CanonicalAlias> getRoomAliases(const std::string &roomid); - std::map<QString, bool> invites(); + QHash<QString, RoomInfo> invites(); + std::optional<RoomInfo> invite(std::string_view roomid); //! Calculate & return the name of the room. QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); @@ -100,6 +101,7 @@ public: void saveState(const mtx::responses::Sync &res); bool isInitialized(); + bool isDatabaseReady() { return databaseReady_ && isInitialized(); } std::string nextBatchToken(); @@ -620,6 +622,8 @@ private: QString cacheDirectory_; VerificationStorage verification_storage; + + bool databaseReady_ = false; }; namespace cache { diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 1a1c1044..0f16f205 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp
@@ -23,10 +23,6 @@ #include "MainWindow.h" #include "MatrixClient.h" #include "Olm.h" -#include "RoomList.h" -#include "SideBarActions.h" -#include "Splitter.h" -#include "UserInfoWidget.h" #include "UserSettingsPage.h" #include "Utils.h" #include "ui/OverlayModal.h" @@ -36,7 +32,6 @@ #include "notifications/Manager.h" #include "dialogs/ReadReceipts.h" -#include "popups/UserMentions.h" #include "timeline/TimelineViewManager.h" #include "blurhash.hpp" @@ -76,62 +71,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) 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(::splitter::calculateSidebarSizes(QFont{}).normal); - 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_); - connect(user_info_widget_, &UserInfoWidget::openGlobalUserProfile, this, [this]() { - UserProfile *userProfile = new UserProfile("", utils::localUser(), view_manager_); - emit view_manager_->openProfile(userProfile); - }); - - user_mentions_popup_ = new popups::UserMentions(); - 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); - view_manager_ = new TimelineViewManager(callManager_, this); - contentLayout_->addWidget(view_manager_->getWidget()); - - // Splitter - splitter->addWidget(sideBar_); - splitter->addWidget(content_); - splitter->restoreSizes(parent->width()); + topLayout_->addWidget(view_manager_->getWidget()); connect(this, &ChatPage::downloadedSecrets, @@ -153,17 +95,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) trySync(); }); - connect( - new QShortcut(QKeySequence("Ctrl+Down"), this), &QShortcut::activated, this, [this]() { - if (isVisible()) - room_list_->nextRoom(); - }); - connect( - new QShortcut(QKeySequence("Ctrl+Up"), this), &QShortcut::activated, this, [this]() { - if (isVisible()) - room_list_->previousRoom(); - }); - connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL); connect(&connectivityTimer_, &QTimer::timeout, this, [=]() { if (http::client()->access_token().empty()) { @@ -185,10 +116,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connect(this, &ChatPage::loggedOut, this, &ChatPage::logout); - connect( - view_manager_, &TimelineViewManager::showRoomList, splitter, &Splitter::showFullRoomList); connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList users) { - const auto room_id = current_room_.toStdString(); + const auto room_id = currentRoom().toStdString(); for (int ii = 0; ii < users.size(); ++ii) { QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { @@ -211,36 +140,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) } }); - connect(room_list_, &RoomList::roomChanged, this, [this](QString room_id) { - this->current_room_ = room_id; - }); - connect(room_list_, &RoomList::roomChanged, splitter, &Splitter::showChatView); - connect( - room_list_, &RoomList::roomChanged, view_manager_, &TimelineViewManager::setHistoryView); - - connect(room_list_, &RoomList::acceptInvite, this, [this](const QString &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(view_manager_, - &TimelineViewManager::updateRoomsLastMessage, - room_list_, - &RoomList::updateRoomDescription); - - connect(room_list_, - SIGNAL(totalUnreadMessageCountUpdated(int)), - this, - SIGNAL(unreadMessages(int))); - - connect( - this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities); - connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom); connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection); connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications); @@ -255,60 +154,31 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) } }); - connect(communitiesList_, - &CommunitiesList::communityChanged, - this, - [this](const QString &groupId) { - current_community_ = groupId; - - if (groupId == "world") { - auto hidden = communitiesList_->hiddenTagsAndCommunities(); - std::set<QString> roomsToHide = communitiesList_->roomList(groupId); - for (const auto &hiddenTag : hidden) { - auto temp = communitiesList_->roomList(hiddenTag); - roomsToHide.insert(temp.begin(), temp.end()); - } - - room_list_->removeFilter(roomsToHide); - } else { - auto hidden = communitiesList_->hiddenTagsAndCommunities(); - hidden.erase(current_community_); - - auto roomsToShow = communitiesList_->roomList(groupId); - for (const auto &hiddenTag : hidden) { - for (const auto &r : communitiesList_->roomList(hiddenTag)) - roomsToShow.erase(r); - } - - room_list_->applyFilter(roomsToShow); - } - }); - connect(&notificationsManager, &NotificationsManager::notificationClicked, this, [this](const QString &roomid, const QString &eventid) { Q_UNUSED(eventid) - room_list_->highlightSelectedRoom(roomid); + view_manager_->rooms()->setCurrentRoom(roomid); activateWindow(); }); connect(&notificationsManager, &NotificationsManager::sendNotificationReply, this, [this](const QString &roomid, const QString &eventid, const QString &body) { + view_manager_->rooms()->setCurrentRoom(roomid); view_manager_->queueReply(roomid, eventid, body); - room_list_->highlightSelectedRoom(roomid); activateWindow(); }); - setGroupViewState(userSettings_->groupView()); - - connect(userSettings_.data(), - &UserSettings::groupViewStateChanged, - this, - &ChatPage::setGroupViewState); + connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() { + // ensure the qml context is shutdown before we destroy all other singletons + // Otherwise Qml will try to access the room list or settings, after they have been + // destroyed + topLayout_->removeWidget(view_manager_->getWidget()); + delete view_manager_->getWidget(); + }); - connect(this, &ChatPage::initializeRoomList, room_list_, &RoomList::initialize); connect( this, &ChatPage::initializeViews, @@ -318,31 +188,14 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connect(this, &ChatPage::initializeEmptyViews, view_manager_, - &TimelineViewManager::initWithMessages); - connect(this, - &ChatPage::initializeMentions, - user_mentions_popup_, - &popups::UserMentions::initializeMentions); + &TimelineViewManager::initializeRoomlist); connect( this, &ChatPage::chatFocusChanged, view_manager_, &TimelineViewManager::chatFocusChanged); connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) { - try { - room_list_->cleanupInvites(cache::invites()); - } catch (const lmdb::error &e) { - nhlog::db()->error("failed to retrieve invites: {}", e.what()); - } - view_manager_->sync(rooms); - removeLeftRooms(rooms.leave); bool hasNotifications = false; for (const auto &room : rooms.join) { - auto room_id = QString::fromStdString(room.first); - updateRoomNotificationCount( - room_id, - room.second.unread_notifications.notification_count, - room.second.unread_notifications.highlight_count); - if (room.second.unread_notifications.notification_count > 0) hasNotifications = true; } @@ -365,16 +218,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) emit notificationsRetrieved(std::move(res)); }); }); - connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync); - connect(this, &ChatPage::syncTags, communitiesList_, &CommunitiesList::syncTags); - - // 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) { - auto userid = utils::localUser(); - user_info_widget_->setUserId(userid); - user_info_widget_->setDisplayName(name); - }); connect( this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync, Qt::QueuedConnection); @@ -427,8 +270,6 @@ ChatPage::dropToLoginPage(const QString &msg) void ChatPage::resetUI() { - room_list_->clear(); - user_info_widget_->reset(); view_manager_->clearAll(); emit unreadMessages(0); @@ -481,9 +322,6 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) view_manager_, &TimelineViewManager::updateReadReceipts); - connect( - cache::client(), &Cache::roomReadStatus, room_list_, &RoomList::updateReadStatus); - connect(cache::client(), &Cache::removeNotification, &notificationsManager, @@ -559,10 +397,8 @@ ChatPage::loadStateFromCache() try { olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY); - emit initializeEmptyViews(cache::client()->roomIds()); - emit initializeRoomList(cache::roomInfo()); + emit initializeEmptyViews(); emit initializeMentions(cache::getTimelineMentions()); - emit syncTags(cache::roomInfo().toStdMap()); cache::calculateRoomReadStatus(); @@ -600,38 +436,6 @@ ChatPage::removeRoom(const QString &room_id) nhlog::db()->critical("failure while removing room: {}", e.what()); // TODO: Notify the user. } - - room_list_->removeRoom(room_id, room_id == current_room_); -} - -void -ChatPage::removeLeftRooms(const std::map<std::string, mtx::responses::LeftRoom> &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::setGroupViewState(bool isEnabled) -{ - if (!isEnabled) { - communitiesList_->communityChanged("world"); - communitiesList_->hide(); - - return; - } - - communitiesList_->show(); -} - -void -ChatPage::updateRoomNotificationCount(const QString &room_id, - uint16_t notification_count, - uint16_t highlight_count) -{ - room_list_->updateUnreadMessageCount(room_id, notification_count, highlight_count); } void @@ -680,18 +484,6 @@ ChatPage::sendNotifications(const mtx::responses::Notifications &res) } void -ChatPage::showNotificationsDialog(const QPoint &widgetPos) -{ - auto notifDialog = user_mentions_popup_; - - notifDialog->setGeometry( - widgetPos.x() - (width() / 10), widgetPos.y() + 25, width() / 5, height() / 2); - - notifDialog->raise(); - notifDialog->showPopup(); -} - -void ChatPage::tryInitialSync() { nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); @@ -789,11 +581,9 @@ ChatPage::startInitialSync() olm::handle_to_device_messages(res.to_device.events); emit initializeViews(std::move(res.rooms)); - emit initializeRoomList(cache::roomInfo()); emit initializeMentions(cache::getTimelineMentions()); cache::calculateRoomReadStatus(); - emit syncTags(cache::roomInfo().toStdMap()); } catch (const lmdb::error &e) { nhlog::db()->error("failed to save state after initial sync: {}", e.what()); @@ -830,12 +620,8 @@ ChatPage::handleSyncResponse(const mtx::responses::Sync &res, const std::string auto updates = cache::getRoomInfo(cache::client()->roomsWithStateUpdates(res)); - emit syncRoomlist(updates); - emit syncUI(res.rooms); - emit syncTags(cache::getRoomInfo(cache::client()->roomsWithTagUpdates(res))); - // if we process a lot of syncs (1 every 200ms), this means we clean the // db every 100s static int syncCounter = 0; @@ -939,7 +725,7 @@ ChatPage::joinRoomVia(const std::string &room_id, emit showNotification(tr("Failed to remove invite: %1").arg(e.what())); } - room_list_->highlightSelectedRoom(QString::fromStdString(room_id)); + view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id)); }); } @@ -987,19 +773,18 @@ ChatPage::leaveRoom(const QString &room_id) void ChatPage::changeRoom(const QString &room_id) { - view_manager_->setHistoryView(room_id); - room_list_->highlightSelectedRoom(room_id); + view_manager_->rooms()->setCurrentRoom(room_id); } void ChatPage::inviteUser(QString userid, QString reason) { - auto room = current_room_; + auto room = currentRoom(); if (QMessageBox::question(this, tr("Confirm invite"), tr("Do you really want to invite %1 (%2)?") - .arg(cache::displayName(current_room_, userid)) + .arg(cache::displayName(room, userid)) .arg(userid)) != QMessageBox::Yes) return; @@ -1021,12 +806,12 @@ ChatPage::inviteUser(QString userid, QString reason) void ChatPage::kickUser(QString userid, QString reason) { - auto room = current_room_; + auto room = currentRoom(); if (QMessageBox::question(this, tr("Confirm kick"), tr("Do you really want to kick %1 (%2)?") - .arg(cache::displayName(current_room_, userid)) + .arg(cache::displayName(room, userid)) .arg(userid)) != QMessageBox::Yes) return; @@ -1048,12 +833,12 @@ ChatPage::kickUser(QString userid, QString reason) void ChatPage::banUser(QString userid, QString reason) { - auto room = current_room_; + auto room = currentRoom(); if (QMessageBox::question(this, tr("Confirm ban"), tr("Do you really want to ban %1 (%2)?") - .arg(cache::displayName(current_room_, userid)) + .arg(cache::displayName(room, userid)) .arg(userid)) != QMessageBox::Yes) return; @@ -1075,12 +860,12 @@ ChatPage::banUser(QString userid, QString reason) void ChatPage::unbanUser(QString userid, QString reason) { - auto room = current_room_; + auto room = currentRoom(); if (QMessageBox::question(this, tr("Confirm unban"), tr("Do you really want to unban %1 (%2)?") - .arg(cache::displayName(current_room_, userid)) + .arg(cache::displayName(room, userid)) .arg(userid)) != QMessageBox::Yes) return; @@ -1182,57 +967,6 @@ ChatPage::getProfileInfo() emit setUserAvatar(QString::fromStdString(res.avatar_url)); }); - - 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<int>(err->status_code), - err->matrix_error.error); - emit updateGroupsInfo({}); - return; - } - - emit updateGroupsInfo(res); - }); -} - -bool -ChatPage::isRoomActive(const QString &room_id) -{ - return isActiveWindow() && content_->isVisible() && currentRoom() == room_id; -} - -void -ChatPage::hideSideBars() -{ - // Don't hide side bar, if we are currently only showing the side bar! - if (view_manager_->getWidget()->isVisible()) { - communitiesList_->hide(); - sideBar_->hide(); - } - view_manager_->enableBackButton(); -} - -void -ChatPage::showSideBars() -{ - if (userSettings_->groupView()) - communitiesList_->show(); - - sideBar_->show(); - view_manager_->disableBackButton(); - content_->show(); -} - -uint64_t -ChatPage::timelineWidth() -{ - int sidebarWidth = sideBar_->minimumSize().width(); - sidebarWidth += communitiesList_->minimumSize().width(); - nhlog::ui()->info("timelineWidth: {}", size().width() - sidebarWidth); - - return size().width() - sidebarWidth; } void @@ -1318,7 +1052,8 @@ ChatPage::startChat(QString userid) if (std::find(room_members.begin(), room_members.end(), (userid).toStdString()) != room_members.end()) { - room_list_->highlightSelectedRoom(QString::fromStdString(room_id)); + view_manager_->rooms()->setCurrentRoom( + QString::fromStdString(room_id)); return; } } @@ -1408,7 +1143,8 @@ ChatPage::handleMatrixUri(const QByteArray &uri) if (sigil1 == "u") { if (action.isEmpty()) { - view_manager_->activeTimeline()->openUserProfile(mxid1); + if (auto t = view_manager_->rooms()->currentRoom()) + t->openUserProfile(mxid1); } else if (action == "chat") { this->startChat(mxid1); } @@ -1418,7 +1154,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri) for (auto roomid : joined_rooms) { if (roomid == targetRoomId) { - room_list_->highlightSelectedRoom(mxid1); + view_manager_->rooms()->setCurrentRoom(mxid1); if (!mxid2.isEmpty()) view_manager_->showEvent(mxid1, mxid2); return; @@ -1436,7 +1172,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri) auto aliases = cache::client()->getRoomAliases(roomid); if (aliases) { if (aliases->alias == targetRoomAlias) { - room_list_->highlightSelectedRoom( + view_manager_->rooms()->setCurrentRoom( QString::fromStdString(roomid)); if (!mxid2.isEmpty()) view_manager_->showEvent( @@ -1458,8 +1194,17 @@ ChatPage::handleMatrixUri(const QUrl &uri) handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8()); } -void -ChatPage::highlightRoom(const QString &room_id) +bool +ChatPage::isRoomActive(const QString &room_id) { - room_list_->highlightSelectedRoom(room_id); + return isActiveWindow() && currentRoom() == room_id; +} + +QString +ChatPage::currentRoom() const +{ + if (view_manager_->rooms()->currentRoom()) + return view_manager_->rooms()->currentRoom()->roomId(); + else + return ""; } diff --git a/src/ChatPage.h b/src/ChatPage.h
index a7489455..751e7074 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h
@@ -27,15 +27,10 @@ #include "CacheCryptoStructs.h" #include "CacheStructs.h" -#include "CommunitiesList.h" #include "notifications/Manager.h" class OverlayModal; -class RoomList; -class SideBarActions; -class Splitter; class TimelineViewManager; -class UserInfoWidget; class UserSettings; class NotificationsManager; class TimelineModel; @@ -53,11 +48,6 @@ struct Notifications; struct Sync; struct Timeline; struct Rooms; -struct LeftRoom; -} - -namespace popups { -class UserMentions; } using SecretsToDecrypt = std::map<std::string, mtx::secret_storage::AesHmacSha2EncryptedData>; @@ -71,7 +61,6 @@ public: // Initialize all the components of the UI. void bootstrap(QString userid, QString homeserver, QString token); - QString currentRoom() const { return current_room_; } static ChatPage *instance() { return instance_; } @@ -80,14 +69,6 @@ public: TimelineViewManager *timelineManager() { return view_manager_; } void deleteConfigs(); - CommunitiesList *communitiesList() { return communitiesList_; } - - //! Calculate the width of the message timeline. - uint64_t timelineWidth(); - //! Hide the room & group list (if it was visible). - void hideSideBars(); - //! Show the room/group list (if it was visible). - void showSideBars(); void initiateLogout(); QString status() const; @@ -95,6 +76,9 @@ public: mtx::presence::PresenceState currentPresence() const; + // TODO(Nico): Get rid of this! + QString currentRoom() const; + public slots: void handleMatrixUri(const QByteArray &uri); void handleMatrixUri(const QUrl &uri); @@ -102,7 +86,6 @@ public slots: void startChat(QString userid); void leaveRoom(const QString &room_id); void createRoom(const mtx::requests::CreateRoom &req); - void highlightRoom(const QString &room_id); void joinRoom(const QString &room); void joinRoomVia(const std::string &room_id, const std::vector<std::string> &via, @@ -145,13 +128,10 @@ signals: void leftRoom(const QString &room_id); void newRoom(const QString &room_id); - void initializeRoomList(QMap<QString, RoomInfo>); void initializeViews(const mtx::responses::Rooms &rooms); - void initializeEmptyViews(const std::vector<QString> &roomIds); + void initializeEmptyViews(); void initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs); void syncUI(const mtx::responses::Rooms &rooms); - void syncRoomlist(const std::map<QString, RoomInfo> &updates); - void syncTags(const std::map<QString, RoomInfo> &updates); void dropToLoginPageCb(const QString &msg); void notifyMessage(const QString &roomid, @@ -161,7 +141,6 @@ signals: const QString &message, const QImage &icon); - void updateGroupsInfo(const mtx::responses::JoinedGroups &groups); void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state); void themeChanged(); void decryptSidebarChanged(); @@ -213,56 +192,25 @@ private: using Membership = mtx::events::StateEvent<mtx::events::state::Member>; using Memberships = std::map<std::string, Membership>; - using LeftRooms = std::map<std::string, mtx::responses::LeftRoom>; - void removeLeftRooms(const LeftRooms &rooms); - void loadStateFromCache(); void resetUI(); - //! Decides whether or not to hide the group's sidebar. - void setGroupViewState(bool isEnabled); template<class Collection> Memberships getMemberships(const std::vector<Collection> &events) const; - //! Update the room with the new notification count. - void updateRoomNotificationCount(const QString &room_id, - uint16_t notification_count, - uint16_t highlight_count); //! Send desktop notification for the received messages. void sendNotifications(const mtx::responses::Notifications &); - void showNotificationsDialog(const QPoint &point); - template<typename T> void connectCallMessage(); 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_; QTimer connectivityTimer_; std::atomic_bool isConnected_; - QString current_room_; - QString current_community_; - - UserInfoWidget *user_info_widget_; - - popups::UserMentions *user_mentions_popup_; - // Global user settings. QSharedPointer<UserSettings> userSettings_; diff --git a/src/CommunitiesList.cpp b/src/CommunitiesList.cpp deleted file mode 100644
index 7cc5d10e..00000000 --- a/src/CommunitiesList.cpp +++ /dev/null
@@ -1,345 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "CommunitiesList.h" -#include "Cache.h" -#include "Logging.h" -#include "MatrixClient.h" -#include "MxcImageProvider.h" -#include "Splitter.h" -#include "UserSettingsPage.h" - -#include <mtx/responses/groups.hpp> -#include <nlohmann/json.hpp> - -#include <QLabel> - -CommunitiesList::CommunitiesList(QWidget *parent) - : QWidget(parent) -{ - QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); - sizePolicy.setHorizontalStretch(0); - sizePolicy.setVerticalStretch(1); - setSizePolicy(sizePolicy); - - topLayout_ = new QVBoxLayout(this); - topLayout_->setSpacing(0); - topLayout_->setMargin(0); - - const auto sideBarSizes = splitter::calculateSidebarSizes(QFont{}); - setFixedWidth(sideBarSizes.groups); - - 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); - - contentsLayout_ = new QVBoxLayout(); - contentsLayout_->setSpacing(0); - contentsLayout_->setMargin(0); - - addGlobalItem(); - contentsLayout_->addStretch(1); - - scrollArea_->setLayout(contentsLayout_); - topLayout_->addWidget(scrollArea_); - - connect( - this, &CommunitiesList::avatarRetrieved, this, &CommunitiesList::updateCommunityAvatar); -} - -void -CommunitiesList::setCommunities(const mtx::responses::JoinedGroups &response) -{ - // remove all non-tag communities - auto it = communities_.begin(); - while (it != communities_.end()) { - if (it->second->is_tag()) { - ++it; - } else { - it = communities_.erase(it); - } - } - - addGlobalItem(); - - for (const auto &group : response.groups) - addCommunity(group); - - communities_["world"]->setPressedState(true); - selectedCommunity_ = "world"; - emit communityChanged("world"); - sortEntries(); -} - -void -CommunitiesList::syncTags(const std::map<QString, RoomInfo> &info) -{ - for (const auto &room : info) - setTagsForRoom(room.first, room.second.tags); - emit communityChanged(selectedCommunity_); - sortEntries(); -} - -void -CommunitiesList::setTagsForRoom(const QString &room_id, const std::vector<std::string> &tags) -{ - // create missing tag if any - for (const auto &tag : tags) { - // filter out tags we should ignore according to the spec - // https://matrix.org/docs/spec/client_server/r0.4.0.html#id154 - // nheko currently does not make use of internal tags - // so we ignore any tag containig a `.` (which would indicate a tag - // in the form `tld.domain.*`) except for `m.*` and `u.*`. - if (tag.find(".") != ::std::string::npos && tag.compare(0, 2, "m.") && - tag.compare(0, 2, "u.")) - continue; - QString name = QString("tag:") + QString::fromStdString(tag); - if (!communityExists(name)) { - addCommunity(std::string("tag:") + tag); - } - } - // update membership of the room for all tags - auto it = communities_.begin(); - while (it != communities_.end()) { - // Skip if the community is not a tag - if (!it->second->is_tag()) { - ++it; - continue; - } - // insert or remove the room from the tag as appropriate - std::string current_tag = - it->first.right(static_cast<int>(it->first.size() - strlen("tag:"))) - .toStdString(); - if (std::find(tags.begin(), tags.end(), current_tag) != tags.end()) { - // the room has this tag - it->second->addRoom(room_id); - } else { - // the room does not have this tag - it->second->delRoom(room_id); - } - // Check if the tag is now empty, if yes delete it - if (it->second->rooms().empty()) { - it = communities_.erase(it); - } else { - ++it; - } - } -} - -void -CommunitiesList::addCommunity(const std::string &group_id) -{ - auto hiddenTags = UserSettings::instance()->hiddenTags(); - - const auto id = QString::fromStdString(group_id); - - CommunitiesListItem *list_item = new CommunitiesListItem(id, scrollArea_); - - if (hiddenTags.contains(id)) - list_item->setDisabled(true); - - communities_.emplace(id, QSharedPointer<CommunitiesListItem>(list_item)); - contentsLayout_->insertWidget(contentsLayout_->count() - 1, list_item); - - connect(list_item, - &CommunitiesListItem::clicked, - this, - &CommunitiesList::highlightSelectedCommunity); - connect(list_item, &CommunitiesListItem::isDisabledChanged, this, [this]() { - for (const auto &community : communities_) { - if (community.second->isPressed()) { - emit highlightSelectedCommunity(community.first); - break; - } - } - - auto hiddenTags = hiddenTagsAndCommunities(); - // Qt < 5.14 compat - QStringList hiddenTags_; - for (auto &&t : hiddenTags) - hiddenTags_.push_back(t); - UserSettings::instance()->setHiddenTags(hiddenTags_); - }); - - if (group_id.empty() || group_id.front() != '+') - return; - - nhlog::ui()->debug("Add community: {}", group_id); - - 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::set<QString> &rooms) { - nhlog::ui()->info( - "Fetched rooms for {}: {}", id.toStdString(), rooms.size()); - if (communities_.find(id) == communities_.end()) - return; - - communities_.at(id)->setRooms(rooms); - }); - - 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::set<QString> room_ids; - for (const auto &room : res.at("chunk")) - room_ids.emplace(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)) { - nhlog::ui()->warn("Avatar update on nonexistent community {}", - community_id.toStdString()); - return; - } - - communities_.at(community_id)->setAvatar(img.toImage()); -} - -void -CommunitiesList::highlightSelectedCommunity(const QString &community_id) -{ - if (!communityExists(community_id)) { - nhlog::ui()->debug("CommunitiesList: clicked unknown community"); - return; - } - - selectedCommunity_ = community_id; - 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) -{ - MxcImageProvider::download( - QString(avatarUrl).remove(QStringLiteral("mxc://")), - QSize(96, 96), - [this, id](QString, QSize, QImage img, QString) { - if (img.isNull()) { - nhlog::net()->warn("failed to download avatar: {})", id.toStdString()); - return; - } - - emit avatarRetrieved(id, QPixmap::fromImage(img)); - }); -} - -std::set<QString> -CommunitiesList::roomList(const QString &id) const -{ - if (communityExists(id)) - return communities_.at(id)->rooms(); - - return {}; -} - -std::vector<std::string> -CommunitiesList::currentTags() const -{ - std::vector<std::string> tags; - for (auto &entry : communities_) { - CommunitiesListItem *item = entry.second.data(); - if (item->is_tag()) - tags.push_back(entry.first.mid(4).toStdString()); - } - return tags; -} - -std::set<QString> -CommunitiesList::hiddenTagsAndCommunities() const -{ - std::set<QString> hiddenTags; - for (auto &entry : communities_) { - if (entry.second->isDisabled()) - hiddenTags.insert(entry.first); - } - - return hiddenTags; -} - -void -CommunitiesList::sortEntries() -{ - std::vector<CommunitiesListItem *> header; - std::vector<CommunitiesListItem *> communities; - std::vector<CommunitiesListItem *> tags; - std::vector<CommunitiesListItem *> footer; - // remove all the contents and sort them in the 4 vectors - for (auto &entry : communities_) { - CommunitiesListItem *item = entry.second.data(); - contentsLayout_->removeWidget(item); - // world is handled separately - if (entry.first == "world") - continue; - // sort the rest - if (item->is_tag()) - if (entry.first == "tag:m.favourite") - header.push_back(item); - else if (entry.first == "tag:m.lowpriority") - footer.push_back(item); - else - tags.push_back(item); - else - communities.push_back(item); - } - - // now there remains only the stretch in the layout, remove it - QLayoutItem *stretch = contentsLayout_->itemAt(0); - contentsLayout_->removeItem(stretch); - - contentsLayout_->addWidget(communities_["world"].data()); - - auto insert_widgets = [this](auto &vec) { - for (auto item : vec) - contentsLayout_->addWidget(item); - }; - insert_widgets(header); - insert_widgets(communities); - insert_widgets(tags); - insert_widgets(footer); - - contentsLayout_->addItem(stretch); -} diff --git a/src/CommunitiesList.h b/src/CommunitiesList.h deleted file mode 100644
index 2586f6f5..00000000 --- a/src/CommunitiesList.h +++ /dev/null
@@ -1,66 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QScrollArea> -#include <QSharedPointer> -#include <QVBoxLayout> - -#include "CacheStructs.h" -#include "CommunitiesListItem.h" -#include "ui/Theme.h" - -namespace mtx::responses { -struct GroupProfile; -struct JoinedGroups; -} - -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::set<QString> roomList(const QString &id) const; - - void syncTags(const std::map<QString, RoomInfo> &info); - void setTagsForRoom(const QString &id, const std::vector<std::string> &tags); - std::vector<std::string> currentTags() const; - std::set<QString> hiddenTagsAndCommunities() 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::set<QString> &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"); } - void sortEntries(); - - //! Check whether or not a community id is currently managed. - bool communityExists(const QString &id) const - { - return communities_.find(id) != communities_.end(); - } - - QString selectedCommunity_; - QVBoxLayout *topLayout_; - QVBoxLayout *contentsLayout_; - QScrollArea *scrollArea_; - - std::map<QString, QSharedPointer<CommunitiesListItem>> communities_; -}; diff --git a/src/CommunitiesListItem.cpp b/src/CommunitiesListItem.cpp deleted file mode 100644
index a2f2777d..00000000 --- a/src/CommunitiesListItem.cpp +++ /dev/null
@@ -1,201 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "CommunitiesListItem.h" - -#include <QMenu> -#include <QMouseEvent> - -#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); - - menu_ = new QMenu(this); - hideRoomsWithTagAction_ = - new QAction(tr("Hide rooms with this tag or from this community"), this); - hideRoomsWithTagAction_->setCheckable(true); - menu_->addAction(hideRoomsWithTagAction_); - connect(menu_, &QMenu::aboutToShow, this, [this]() { - hideRoomsWithTagAction_->setChecked(isDisabled_); - }); - - connect(hideRoomsWithTagAction_, &QAction::triggered, this, [this](bool checked) { - this->setDisabled(checked); - }); - - updateTooltip(); -} - -void -CommunitiesListItem::contextMenuEvent(QContextMenuEvent *event) -{ - menu_->popup(event->globalPos()); -} - -void -CommunitiesListItem::setName(QString name) -{ - name_ = name; - updateTooltip(); -} - -void -CommunitiesListItem::setPressedState(bool state) -{ - if (isPressed_ != state) { - isPressed_ = state; - update(); - } -} - -void -CommunitiesListItem::setDisabled(bool state) -{ - if (isDisabled_ != state) { - isDisabled_ = state; - update(); - emit isDisabledChanged(); - } -} - -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<qreal>(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 (isDisabled_) - p.fillRect(rect(), disabledBackgroundColor_); - else if (underMouse()) - p.fillRect(rect(), hoverBackgroundColor_); - else - p.fillRect(rect(), backgroundColor_); - - if (avatar_.isNull()) { - QPixmap source; - if (groupId_ == "world") - source = QPixmap(":/icons/icons/ui/world.png"); - else if (groupId_ == "tag:m.favourite") - source = QPixmap(":/icons/icons/ui/star.png"); - else if (groupId_ == "tag:m.lowpriority") - source = QPixmap(":/icons/icons/ui/lowprio.png"); - else if (groupId_.startsWith("tag:")) - source = QPixmap(":/icons/icons/ui/tag.png"); - - if (source.isNull()) { - QFont font; - font.setPointSizeF(font.pointSizeF() * 1.3); - p.setFont(font); - - p.drawLetterAvatar(utils::firstChar(resolveName()), - avatarFgColor_, - avatarBgColor_, - width(), - height(), - IconSize); - } else { - QPainter painter(&source); - painter.setCompositionMode(QPainter::CompositionMode_SourceIn); - painter.fillRect(source.rect(), avatarFgColor_); - painter.end(); - - const int imageSz = 32; - p.drawPixmap( - QRect( - (width() - imageSz) / 2, (height() - imageSz) / 2, imageSz, imageSz), - source); - } - } 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("tag:")) - return groupId_.right(static_cast<int>(groupId_.size() - strlen("tag:"))); - 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); -} - -void -CommunitiesListItem::updateTooltip() -{ - if (groupId_ == "world") - setToolTip(tr("All rooms")); - else if (is_tag()) { - QStringRef tag = - groupId_.rightRef(static_cast<int>(groupId_.size() - strlen("tag:"))); - if (tag == "m.favourite") - setToolTip(tr("Favourite rooms")); - else if (tag == "m.lowpriority") - setToolTip(tr("Low priority rooms")); - else if (tag == "m.server_notice") - setToolTip(tr("Server Notices", "Tag translation for m.server_notice")); - else if (tag.startsWith("u.")) - setToolTip(tag.right(tag.size() - 2) + tr(" (tag)")); - else - setToolTip(tag + tr(" (tag)")); - } else { - QString name = resolveName(); - setToolTip(name + tr(" (community)")); - } -} diff --git a/src/CommunitiesListItem.h b/src/CommunitiesListItem.h deleted file mode 100644
index 006511c8..00000000 --- a/src/CommunitiesListItem.h +++ /dev/null
@@ -1,108 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QSharedPointer> -#include <QWidget> - -#include <set> - -#include "Config.h" -#include "ui/Theme.h" - -class RippleOverlay; -class QMouseEvent; -class QMenu; - -class CommunitiesListItem : public QWidget -{ - Q_OBJECT - Q_PROPERTY(QColor highlightedBackgroundColor READ highlightedBackgroundColor WRITE - setHighlightedBackgroundColor) - Q_PROPERTY(QColor disabledBackgroundColor READ disabledBackgroundColor WRITE - setDisabledBackgroundColor) - 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); - bool isPressed() const { return isPressed_; } - bool isDisabled() const { return isDisabled_; } - void setAvatar(const QImage &img); - - void setRooms(std::set<QString> room_ids) { room_ids_ = std::move(room_ids); } - void addRoom(const QString &id) { room_ids_.insert(id); } - void delRoom(const QString &id) { room_ids_.erase(id); } - std::set<QString> rooms() const { return room_ids_; } - - bool is_tag() const { return groupId_.startsWith("tag:"); } - - QColor highlightedBackgroundColor() const { return highlightedBackgroundColor_; } - QColor disabledBackgroundColor() const { return disabledBackgroundColor_; } - 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 setDisabledBackgroundColor(QColor &color) { disabledBackgroundColor_ = 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); - void isDisabledChanged(); - -public slots: - void setPressedState(bool state); - void setDisabled(bool state); - -protected: - void mousePressEvent(QMouseEvent *event) override; - void paintEvent(QPaintEvent *event) override; - void contextMenuEvent(QContextMenuEvent *event) override; - -private: - const int IconSize = 36; - - QString resolveName() const; - void updateTooltip(); - - std::set<QString> room_ids_; - - QString name_; - QString groupId_; - QPixmap avatar_; - - QColor highlightedBackgroundColor_; - QColor disabledBackgroundColor_; - QColor hoverBackgroundColor_; - QColor backgroundColor_; - - QColor avatarFgColor_; - QColor avatarBgColor_; - - bool isPressed_ = false; - bool isDisabled_ = false; - - RippleOverlay *rippleOverlay_; - QMenu *menu_; - QAction *hideRoomsWithTagAction_; -}; diff --git a/src/CompletionProxyModel.cpp b/src/CompletionProxyModel.cpp
index 412708a2..e68944c7 100644 --- a/src/CompletionProxyModel.cpp +++ b/src/CompletionProxyModel.cpp
@@ -19,7 +19,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model, , max_completions_(max_completions) { setSourceModel(model); - QRegularExpression splitPoints("\\s+|-"); + QChar splitPoints(' '); // insert all the full texts for (int i = 0; i < sourceModel()->rowCount(); i++) { @@ -48,7 +48,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model, .toString() .toLower(); - for (const auto &e : string1.split(splitPoints)) { + for (const auto &e : string1.splitRef(splitPoints)) { if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14 trie_.insert(e.toUcs4(), i); } @@ -59,7 +59,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model, .toLower(); if (!string2.isEmpty()) { - for (const auto &e : string2.split(splitPoints)) { + for (const auto &e : string2.splitRef(splitPoints)) { if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14 trie_.insert(e.toUcs4(), i); } diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 92f43e03..ed337ca4 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp
@@ -22,7 +22,6 @@ #include "MainWindow.h" #include "MatrixClient.h" #include "RegisterPage.h" -#include "Splitter.h" #include "TrayIcon.h" #include "UserSettingsPage.h" #include "Utils.h" @@ -109,10 +108,6 @@ MainWindow::MainWindow(QWidget *parent) userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool))); connect( userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged); - connect(userSettingsPage_, - &UserSettingsPage::decryptSidebarChanged, - chat_page_, - &ChatPage::decryptSidebarChanged); connect(trayIcon_, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, @@ -176,20 +171,6 @@ MainWindow::setWindowTitle(int notificationCount) QMainWindow::setWindowTitle(name); } -void -MainWindow::showEvent(QShowEvent *event) -{ - adjustSideBars(); - QMainWindow::showEvent(event); -} - -void -MainWindow::resizeEvent(QResizeEvent *event) -{ - adjustSideBars(); - QMainWindow::resizeEvent(event); -} - bool MainWindow::event(QEvent *event) { @@ -204,22 +185,6 @@ MainWindow::event(QEvent *event) } void -MainWindow::adjustSideBars() -{ - const auto sz = splitter::calculateSidebarSizes(QFont{}); - - const uint64_t timelineWidth = chat_page_->timelineWidth(); - const uint64_t minAvailableWidth = sz.collapsePoint + sz.groups; - - nhlog::ui()->info("timelineWidth: {}, min {}", timelineWidth, minAvailableWidth); - if (timelineWidth < minAvailableWidth) { - chat_page_->hideSideBars(); - } else { - chat_page_->showSideBars(); - } -} - -void MainWindow::restoreWindowSize() { QSettings settings; @@ -295,6 +260,7 @@ MainWindow::showChatPage() &Cache::secretChanged, userSettingsPage_, &UserSettingsPage::updateSecretStatus); + emit reload(); } void diff --git a/src/MainWindow.h b/src/MainWindow.h
index 4122e4c1..3571f079 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h
@@ -77,13 +77,9 @@ public: protected: void closeEvent(QCloseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - void showEvent(QShowEvent *event) override; bool event(QEvent *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); @@ -109,6 +105,7 @@ private slots: signals: void focusChanged(const bool focused); + void reload(); private: bool loadJdenticonPlugin(); diff --git a/src/Olm.cpp b/src/Olm.cpp
index d08c1b3e..ff4c883b 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp
@@ -206,8 +206,11 @@ handle_olm_message(const OlmMessage &msg) for (const auto &cipher : msg.ciphertext) { // We skip messages not meant for the current device. - if (cipher.first != my_key) + if (cipher.first != my_key) { + nhlog::crypto()->debug( + "Skipping message for {} since we are {}.", cipher.first, my_key); continue; + } const auto type = cipher.second.type; nhlog::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE"); @@ -661,8 +664,10 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip for (const auto &id : session_ids) { auto session = cache::getOlmSession(sender_key, id); - if (!session) + if (!session) { + nhlog::crypto()->warn("Unknown olm session: {}:{}", sender_key, id); continue; + } mtx::crypto::BinaryBuf text; diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp deleted file mode 100644
index ea5de674..00000000 --- a/src/RoomInfoListItem.cpp +++ /dev/null
@@ -1,522 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <QDateTime> -#include <QInputDialog> -#include <QMenu> -#include <QMouseEvent> -#include <QPainter> -#include <QtGlobal> - -#include "AvatarProvider.h" -#include "Cache.h" -#include "ChatPage.h" -#include "Config.h" -#include "Logging.h" -#include "MatrixClient.h" -#include "RoomInfoListItem.h" -#include "Splitter.h" -#include "UserSettingsPage.h" -#include "Utils.h" -#include "ui/Ripple.h" -#include "ui/RippleOverlay.h" - -constexpr int MaxUnreadCountDisplayed = 99; - -struct WidgetMetrics -{ - int maxHeight; - int iconSize; - int padding; - int unit; - - int unreadLineWidth; - int unreadLineOffset; - - int inviteBtnX; - int inviteBtnY; -}; - -WidgetMetrics -getMetrics(const QFont &font) -{ - WidgetMetrics m; - - const int height = QFontMetrics(font).lineSpacing(); - - m.unit = height; - m.maxHeight = std::ceil((double)height * 3.8); - m.iconSize = std::ceil((double)height * 2.8); - m.padding = std::ceil((double)height / 2.0); - m.unreadLineWidth = m.padding - m.padding / 3; - m.unreadLineOffset = m.padding - m.padding / 4; - - m.inviteBtnX = m.iconSize + 2 * m.padding; - m.inviteBtnY = m.iconSize / 2.0 + m.padding + m.padding / 3.0; - - return m; -} - -void -RoomInfoListItem::init(QWidget *parent) -{ - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - setMouseTracking(true); - setAttribute(Qt::WA_Hover); - - auto wm = getMetrics(QFont{}); - setFixedHeight(wm.maxHeight); - - QPainterPath path; - path.addRect(0, 0, parent->width(), height()); - - ripple_overlay_ = new RippleOverlay(this); - ripple_overlay_->setClipPath(path); - ripple_overlay_->setClipping(true); - - avatar_ = new Avatar(nullptr, wm.iconSize); - avatar_->setLetter(utils::firstChar(roomName_)); - avatar_->resize(wm.iconSize, wm.iconSize); - - unreadCountFont_.setPointSizeF(unreadCountFont_.pointSizeF() * 0.8); - unreadCountFont_.setBold(true); - - bubbleDiameter_ = QFontMetrics(unreadCountFont_).averageCharWidth() * 3; - - menu_ = new QMenu(this); - leaveRoom_ = new QAction(tr("Leave room"), this); - connect(leaveRoom_, &QAction::triggered, this, [this]() { emit leaveRoom(roomId_); }); - - connect(menu_, &QMenu::aboutToShow, this, [this]() { - menu_->clear(); - menu_->addAction(leaveRoom_); - - menu_->addSection(QIcon(":/icons/icons/ui/tag.png"), tr("Tag room as:")); - - auto roomInfo = cache::singleRoomInfo(roomId_.toStdString()); - - auto tags = ChatPage::instance()->communitiesList()->currentTags(); - - // add default tag, remove server notice tag - if (std::find(tags.begin(), tags.end(), "m.favourite") == tags.end()) - tags.push_back("m.favourite"); - if (std::find(tags.begin(), tags.end(), "m.lowpriority") == tags.end()) - tags.push_back("m.lowpriority"); - if (auto it = std::find(tags.begin(), tags.end(), "m.server_notice"); - it != tags.end()) - tags.erase(it); - - for (const auto &tag : tags) { - QString tagName; - if (tag == "m.favourite") - tagName = tr("Favourite", "Standard matrix tag for favourites"); - else if (tag == "m.lowpriority") - tagName = - tr("Low Priority", "Standard matrix tag for low priority rooms"); - else if (tag == "m.server_notice") - tagName = - tr("Server Notice", "Standard matrix tag for server notices"); - else if ((tag.size() > 2 && tag.substr(0, 2) == "u.") || - tag.find(".") != - std::string::npos) // tag manager creates tags without u., which - // is wrong, but we still want to display them - tagName = QString::fromStdString(tag.substr(2)); - - if (tagName.isEmpty()) - continue; - - auto tagAction = menu_->addAction(tagName); - tagAction->setCheckable(true); - tagAction->setWhatsThis(tr("Adds or removes the specified tag.", - "WhatsThis hint for tag menu actions")); - - for (const auto &riTag : roomInfo.tags) { - if (riTag == tag) { - tagAction->setChecked(true); - break; - } - } - - connect(tagAction, &QAction::triggered, this, [this, tag](bool checked) { - if (checked) - http::client()->put_tag( - roomId_.toStdString(), - tag, - {}, - [tag](mtx::http::RequestErr err) { - if (err) { - nhlog::ui()->error( - "Failed to add tag: {}, {}", - tag, - err->matrix_error.error); - } - }); - else - http::client()->delete_tag( - roomId_.toStdString(), - tag, - [tag](mtx::http::RequestErr err) { - if (err) { - nhlog::ui()->error( - "Failed to delete tag: {}, {}", - tag, - err->matrix_error.error); - } - }); - }); - } - - auto newTagAction = menu_->addAction(tr("New tag...", "Add a new tag to the room")); - connect(newTagAction, &QAction::triggered, this, [this]() { - QString tagName = - QInputDialog::getText(this, - tr("New Tag", "Tag name prompt title"), - tr("Tag:", "Tag name prompt")); - if (tagName.isEmpty()) - return; - - std::string tag = "u." + tagName.toStdString(); - - http::client()->put_tag( - roomId_.toStdString(), tag, {}, [tag](mtx::http::RequestErr err) { - if (err) { - nhlog::ui()->error("Failed to add tag: {}, {}", - tag, - err->matrix_error.error); - } - }); - }); - }); -} - -RoomInfoListItem::RoomInfoListItem(QString room_id, const 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) - , unreadHighlightedMsgCount_(0) -{ - init(parent); -} - -void -RoomInfoListItem::resizeEvent(QResizeEvent *) -{ - // Update ripple's clipping path. - QPainterPath path; - path.addRect(0, 0, width(), height()); - - const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{}); - - if (width() > sidebarSizes.small) - 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(QFont{}); - - QPen titlePen(titleColor_); - QPen subtitlePen(subtitleColor_); - - auto wm = getMetrics(QFont{}); - - QPixmap pixmap(avatar_->size() * p.device()->devicePixelRatioF()); - pixmap.setDevicePixelRatio(p.device()->devicePixelRatioF()); - if (isPressed_) { - p.fillRect(rect(), highlightedBackgroundColor_); - titlePen.setColor(highlightedTitleColor_); - subtitlePen.setColor(highlightedSubtitleColor_); - pixmap.fill(highlightedBackgroundColor_); - } else if (underMouse()) { - p.fillRect(rect(), hoverBackgroundColor_); - titlePen.setColor(hoverTitleColor_); - subtitlePen.setColor(hoverSubtitleColor_); - pixmap.fill(hoverBackgroundColor_); - } else { - p.fillRect(rect(), backgroundColor_); - titlePen.setColor(titleColor_); - subtitlePen.setColor(subtitleColor_); - pixmap.fill(backgroundColor_); - } - - avatar_->render(&pixmap, QPoint(), QRegion(), RenderFlags(DrawChildren)); - p.drawPixmap(QPoint(wm.padding, wm.padding), pixmap); - - // Description line with the default font. - int bottom_y = wm.maxHeight - wm.padding - metrics.ascent() / 2; - - const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{}); - - if (width() > sidebarSizes.small) { - QFont headingFont; - headingFont.setWeight(QFont::Medium); - p.setFont(headingFont); - p.setPen(titlePen); - - QFont tsFont; - tsFont.setPointSizeF(tsFont.pointSizeF() * 0.9); -#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) - const int msgStampWidth = - QFontMetrics(tsFont).width(lastMsgInfo_.descriptiveTime) + 4; -#else - const int msgStampWidth = - QFontMetrics(tsFont).horizontalAdvance(lastMsgInfo_.descriptiveTime) + 4; -#endif - // 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 * wm.padding + fontNameMetrics.ascent() / 2; - - const auto name = metrics.elidedText( - roomName(), - Qt::ElideRight, - (width() - wm.iconSize - 2 * wm.padding - msgStampWidth) * 0.8); - p.drawText(QPoint(2 * wm.padding + wm.iconSize, top_y), name); - - if (roomType_ == RoomType::Joined) { - p.setFont(QFont{}); - p.setPen(subtitlePen); - - int descriptionLimit = std::max( - 0, width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize); - auto description = - metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit); - p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), description); - - // We show the last message timestamp. - p.save(); - if (isPressed_) { - p.setPen(QPen(highlightedTimestampColor_)); - } else if (underMouse()) { - p.setPen(QPen(hoverTimestampColor_)); - } else { - p.setPen(QPen(timestampColor_)); - } - - p.setFont(tsFont); - p.drawText(QPoint(width() - wm.padding - msgStampWidth, top_y), - lastMsgInfo_.descriptiveTime); - p.restore(); - } else { - int btnWidth = (width() - wm.iconSize - 6 * wm.padding) / 2; - - acceptBtnRegion_ = QRectF(wm.inviteBtnX, wm.inviteBtnY, btnWidth, 20); - declineBtnRegion_ = QRectF( - wm.inviteBtnX + btnWidth + 2 * wm.padding, wm.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(QFont{}); - p.drawText(acceptBtnRegion_, - Qt::AlignCenter, - metrics.elidedText(tr("Accept"), Qt::ElideRight, btnWidth)); - p.drawText(declineBtnRegion_, - Qt::AlignCenter, - metrics.elidedText(tr("Decline"), Qt::ElideRight, btnWidth)); - } - } - - p.setPen(Qt::NoPen); - - if (unreadMsgCount_ > 0) { - QBrush brush; - brush.setStyle(Qt::SolidPattern); - if (unreadHighlightedMsgCount_ > 0) { - brush.setColor(mentionedColor()); - } else { - 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_ - wm.padding - x_width, - bottom_y - bubbleDiameter_ / 2 - 5, - bubbleDiameter_ + x_width, - bubbleDiameter_); - - if (width() == sidebarSizes.small) - 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); - } - - if (!isPressed_ && hasUnreadMessages_) { - QPen pen; - pen.setWidth(wm.unreadLineWidth); - pen.setColor(highlightedBackgroundColor_); - - p.setPen(pen); - p.drawLine(0, wm.unreadLineOffset, 0, height() - wm.unreadLineOffset); - } -} - -void -RoomInfoListItem::updateUnreadMessageCount(int count, int highlightedCount) -{ - unreadMsgCount_ = count; - unreadHighlightedMsgCount_ = highlightedCount; - update(); -} - -enum NotificationImportance : short -{ - ImportanceDisabled = -1, - AllEventsRead = 0, - NewMessage = 1, - NewMentions = 2, - Invite = 3 -}; - -short int -RoomInfoListItem::calculateImportance() const -{ - // Returns the degree of importance of the unread messages in the room. - // If sorting by importance is disabled in settings, this only ever - // returns ImportanceDisabled or Invite - if (isInvite()) { - return Invite; - } else if (!ChatPage::instance()->userSettings()->sortByImportance()) { - return ImportanceDisabled; - } else if (unreadHighlightedMsgCount_) { - return NewMentions; - } else if (unreadMsgCount_) { - return NewMessage; - } else { - return AllEventsRead; - } -} - -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; - } else if (event->buttons() == Qt::LeftButton) { - 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<qreal>(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 QString &avatar_url) -{ - if (avatar_url.isEmpty()) - avatar_->setLetter(utils::firstChar(roomName_)); - else - avatar_->setImage(avatar_url); -} - -void -RoomInfoListItem::setDescriptionMessage(const DescInfo &info) -{ - lastMsgInfo_ = info; - update(); -} diff --git a/src/RoomInfoListItem.h b/src/RoomInfoListItem.h deleted file mode 100644
index a5e0009e..00000000 --- a/src/RoomInfoListItem.h +++ /dev/null
@@ -1,210 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QAction> -#include <QDateTime> -#include <QSharedPointer> -#include <QWidget> - -#include <mtx/responses/sync.hpp> - -#include "CacheStructs.h" -#include "UserSettingsPage.h" -#include "ui/Avatar.h" - -class QMenu; -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 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 hoverTimestampColor READ hoverTimestampColor WRITE setHoverTimestampColor) - - Q_PROPERTY( - QColor highlightedTitleColor READ highlightedTitleColor WRITE setHighlightedTitleColor) - Q_PROPERTY(QColor highlightedSubtitleColor READ highlightedSubtitleColor WRITE - setHighlightedSubtitleColor) - - Q_PROPERTY(QColor hoverTitleColor READ hoverTitleColor WRITE setHoverTitleColor) - Q_PROPERTY(QColor hoverSubtitleColor READ hoverSubtitleColor WRITE setHoverSubtitleColor) - - Q_PROPERTY(QColor mentionedColor READ mentionedColor WRITE setMentionedColor) - Q_PROPERTY(QColor btnColor READ btnColor WRITE setBtnColor) - Q_PROPERTY(QColor btnTextColor READ btnTextColor WRITE setBtnTextColor) - -public: - RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent = nullptr); - - void updateUnreadMessageCount(int count, int highlightedCount); - void clearUnreadMessageCount() { updateUnreadMessageCount(0, 0); }; - - short int calculateImportance() const; - - QString roomId() { return roomId_; } - bool isPressed() const { return isPressed_; } - int unreadMessageCount() const { return unreadMsgCount_; } - - void setAvatar(const QString &avatar_url); - void setDescriptionMessage(const DescInfo &info); - DescInfo lastMessageInfo() const { return lastMsgInfo_; } - - QColor highlightedBackgroundColor() const { return highlightedBackgroundColor_; } - QColor hoverBackgroundColor() const { return hoverBackgroundColor_; } - QColor hoverTitleColor() const { return hoverTitleColor_; } - QColor hoverSubtitleColor() const { return hoverSubtitleColor_; } - QColor hoverTimestampColor() const { return hoverTimestampColor_; } - QColor backgroundColor() const { return backgroundColor_; } - - 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_; } - QColor mentionedColor() const { return mentionedFontColor_; } - - void setHighlightedBackgroundColor(QColor &color) { highlightedBackgroundColor_ = color; } - void setHoverBackgroundColor(QColor &color) { hoverBackgroundColor_ = color; } - void setHoverSubtitleColor(QColor &color) { hoverSubtitleColor_ = color; } - void setHoverTitleColor(QColor &color) { hoverTitleColor_ = color; } - void setHoverTimestampColor(QColor &color) { hoverTimestampColor_ = color; } - void setBackgroundColor(QColor &color) { backgroundColor_ = color; } - void setTimestampColor(QColor &color) { timestampColor_ = 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 setMentionedColor(QColor &color) { mentionedFontColor_ = color; } - - void setRoomName(const QString &name) { roomName_ = name; } - void setRoomType(bool isInvite) - { - if (isInvite) - roomType_ = RoomType::Invited; - else - roomType_ = RoomType::Joined; - } - - bool isInvite() const { return roomType_ == RoomType::Invited; } - void setReadState(bool hasUnreadMessages) - { - if (hasUnreadMessages_ != hasUnreadMessages) { - hasUnreadMessages_ = hasUnreadMessages; - update(); - } - } - -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_; - Avatar *avatar_; - - 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_; - - QMenu *menu_; - QAction *leaveRoom_; - - bool isPressed_ = false; - bool hasUnreadMessages_ = true; - - int unreadMsgCount_ = 0; - int unreadHighlightedMsgCount_ = 0; - - QColor highlightedBackgroundColor_; - QColor hoverBackgroundColor_; - QColor backgroundColor_; - - QColor highlightedTitleColor_; - QColor highlightedSubtitleColor_; - - QColor titleColor_; - QColor subtitleColor_; - - QColor hoverTitleColor_; - QColor hoverSubtitleColor_; - - QColor btnColor_; - QColor btnTextColor_; - - QRectF acceptBtnRegion_; - QRectF declineBtnRegion_; - - // Fonts - QColor mentionedFontColor_; - QFont unreadCountFont_; - int bubbleDiameter_; - - QColor timestampColor_; - QColor highlightedTimestampColor_; - QColor hoverTimestampColor_; - - QColor bubbleBgColor_; - QColor bubbleFgColor_; - - friend struct room_sort; -}; diff --git a/src/RoomList.cpp b/src/RoomList.cpp deleted file mode 100644
index 8a807e71..00000000 --- a/src/RoomList.cpp +++ /dev/null
@@ -1,540 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <limits> -#include <set> - -#include <QObject> -#include <QPainter> -#include <QScroller> -#include <QStyle> -#include <QStyleOption> -#include <QTimer> - -#include "Logging.h" -#include "MainWindow.h" -#include "RoomInfoListItem.h" -#include "RoomList.h" -#include "UserSettingsPage.h" -#include "Utils.h" -#include "ui/OverlayModal.h" - -RoomList::RoomList(QSharedPointer<UserSettings> userSettings, QWidget *parent) - : QWidget(parent) -{ - topLayout_ = new QVBoxLayout(this); - topLayout_->setSpacing(0); - topLayout_->setMargin(0); - - scrollArea_ = new QScrollArea(this); - scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); - scrollArea_->setWidgetResizable(true); - scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter); - scrollArea_->setAttribute(Qt::WA_AcceptTouchEvents); - - QScroller::grabGesture(scrollArea_, QScroller::TouchGesture); - QScroller::grabGesture(scrollArea_, QScroller::LeftMouseButtonGesture); - -// The scrollbar on macOS will hide itself when not active so it won't interfere -// with the content. -#if not defined(Q_OS_MAC) - scrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); -#endif - - scrollAreaContents_ = new QWidget(this); - scrollAreaContents_->setObjectName("roomlist_area"); - - contentsLayout_ = new QVBoxLayout(scrollAreaContents_); - contentsLayout_->setAlignment(Qt::AlignTop); - contentsLayout_->setSpacing(0); - contentsLayout_->setMargin(0); - - scrollArea_->setWidget(scrollAreaContents_); - topLayout_->addWidget(scrollArea_); - - connect(this, &RoomList::updateRoomAvatarCb, this, &RoomList::updateRoomAvatar); - connect(userSettings.data(), - &UserSettings::roomSortingChanged, - this, - &RoomList::sortRoomsByLastMessage); -} - -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); - }); - - QSharedPointer<RoomInfoListItem> roomWidget(room_item, &QObject::deleteLater); - rooms_.emplace(room_id, roomWidget); - rooms_sort_cache_.push_back(roomWidget); - - 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) -{ - emit updateRoomAvatarCb(room_id, url); -} - -void -RoomList::removeRoom(const QString &room_id, bool reset) -{ - auto roomIt = rooms_.find(room_id); - if (roomIt == rooms_.end()) { - return; - } - - for (auto roomSortIt = rooms_sort_cache_.begin(); roomSortIt != rooms_sort_cache_.end(); - ++roomSortIt) { - if (roomIt->second == *roomSortIt) { - rooms_sort_cache_.erase(roomSortIt); - break; - } - } - 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, int highlightedCount) -{ - if (!roomExists(roomid)) { - nhlog::ui()->warn("updateUnreadMessageCount: unknown room_id {}", - roomid.toStdString()); - return; - } - - rooms_[roomid]->updateUnreadMessageCount(count, highlightedCount); - - calculateUnreadMessageCount(); - - sortRoomsByLastMessage(); -} - -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<QString, RoomInfo> &info) -{ - nhlog::ui()->info("initialize room list"); - - rooms_.clear(); - - // prevent flickering and save time sorting over and over again - 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; - - sortRoomsByLastMessage(); - - auto room = firstRoom(); - if (room.second.isNull()) - return; - - room.second->setPressedState(true); - emit roomChanged(room.first); -} - -void -RoomList::cleanupInvites(const std::map<QString, bool> &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<QString, RoomInfo> &info) - -{ - for (const auto &room : info) - updateRoom(room.first, room.second); - - if (!info.empty()) - sortRoomsByLastMessage(); -} - -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::nextRoom() -{ - for (int ii = 0; ii < contentsLayout_->count() - 1; ++ii) { - auto room = qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(ii)->widget()); - - if (!room) - continue; - - if (room->roomId() == selectedRoom_) { - auto nextRoom = qobject_cast<RoomInfoListItem *>( - contentsLayout_->itemAt(ii + 1)->widget()); - - // Not a room message. - if (!nextRoom || nextRoom->isInvite()) - return; - - emit roomChanged(nextRoom->roomId()); - if (!roomExists(nextRoom->roomId())) { - nhlog::ui()->warn("roomlist: clicked unknown room_id"); - return; - } - - room->setPressedState(false); - nextRoom->setPressedState(true); - - scrollArea_->ensureWidgetVisible(nextRoom); - selectedRoom_ = nextRoom->roomId(); - return; - } - } -} - -void -RoomList::previousRoom() -{ - for (int ii = 1; ii < contentsLayout_->count(); ++ii) { - auto room = qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(ii)->widget()); - - if (!room) - continue; - - if (room->roomId() == selectedRoom_) { - auto nextRoom = qobject_cast<RoomInfoListItem *>( - contentsLayout_->itemAt(ii - 1)->widget()); - - // Not a room message. - if (!nextRoom || nextRoom->isInvite()) - return; - - emit roomChanged(nextRoom->roomId()); - if (!roomExists(nextRoom->roomId())) { - nhlog::ui()->warn("roomlist: clicked unknown room_id"); - return; - } - - room->setPressedState(false); - nextRoom->setPressedState(true); - - scrollArea_->ensureWidgetVisible(nextRoom); - selectedRoom_ = nextRoom->roomId(); - return; - } - } -} - -void -RoomList::updateRoomAvatar(const QString &roomid, const QString &img) -{ - if (!roomExists(roomid)) { - nhlog::ui()->warn("avatar update on non-existent room_id: {}", - roomid.toStdString()); - return; - } - - rooms_[roomid]->setAvatar(img); - - // 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(); -} - -struct room_sort -{ - bool operator()(const QSharedPointer<RoomInfoListItem> &a, - const QSharedPointer<RoomInfoListItem> &b) const - { - // Sort by "importance" (i.e. invites before mentions before - // notifs before new events before old events), then secondly - // by recency. - - // Checking importance first - const auto a_importance = a->calculateImportance(); - const auto b_importance = b->calculateImportance(); - if (a_importance != b_importance) { - return a_importance > b_importance; - } - - // Now sort by recency - // Zero if empty, otherwise the time that the event occured - const uint64_t a_recency = - a->lastMsgInfo_.userid.isEmpty() ? 0 : a->lastMsgInfo_.timestamp; - const uint64_t b_recency = - b->lastMsgInfo_.userid.isEmpty() ? 0 : b->lastMsgInfo_.timestamp; - return a_recency > b_recency; - } -}; - -void -RoomList::sortRoomsByLastMessage() -{ - isSortPending_ = false; - - std::stable_sort(begin(rooms_sort_cache_), end(rooms_sort_cache_), room_sort{}); - - int newIndex = 0; - for (const auto &roomWidget : rooms_sort_cache_) { - const auto currentIndex = contentsLayout_->indexOf(roomWidget.data()); - - if (currentIndex != newIndex) { - contentsLayout_->removeWidget(roomWidget.data()); - contentsLayout_->insertWidget(newIndex, roomWidget.data()); - } - newIndex++; - } -} - -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::removeFilter(const std::set<QString> &roomsToHide) -{ - setUpdatesEnabled(false); - for (int i = 0; i < contentsLayout_->count(); i++) { - auto widget = - qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(i)->widget()); - if (widget) { - if (roomsToHide.find(widget->roomId()) == roomsToHide.end()) - widget->show(); - else - widget->hide(); - } - } - setUpdatesEnabled(true); -} - -void -RoomList::applyFilter(const std::set<QString> &filter) -{ - // Disabling paint updates will resolve issues with screen flickering on big room lists. - setUpdatesEnabled(false); - - for (int i = 0; i < contentsLayout_->count(); i++) { - // If filter contains the room for the current RoomInfoListItem, - // show the list item, otherwise hide it - auto listitem = - qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(i)->widget()); - - if (!listitem) - continue; - - if (filter.find(listitem->roomId()) != filter.end()) - listitem->show(); - else - listitem->hide(); - } - - setUpdatesEnabled(true); - - // If the already selected room is part of the group, make sure it's visible. - if (!selectedRoom_.isEmpty() && (filter.find(selectedRoom_) != filter.end())) - return; - - selectFirstVisibleRoom(); -} - -void -RoomList::selectFirstVisibleRoom() -{ - for (int i = 0; i < contentsLayout_->count(); i++) { - auto item = qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(i)->widget()); - - if (item && item->isVisible()) { - highlightSelectedRoom(item->roomId()); - break; - } - } -} - -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::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); - - QSharedPointer<RoomInfoListItem> roomWidget(room_item); - rooms_.emplace(room_id, roomWidget); - rooms_sort_cache_.push_back(roomWidget); - - updateAvatar(room_id, QString::fromStdString(info.avatar_url)); - - int pos = contentsLayout_->count() - 1; - contentsLayout_->insertWidget(pos, room_item); -} - -std::pair<QString, QSharedPointer<RoomInfoListItem>> -RoomList::firstRoom() const -{ - for (int i = 0; i < contentsLayout_->count(); i++) { - auto item = qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(i)->widget()); - - if (item) { - auto topRoom = rooms_.find(item->roomId()); - if (topRoom != rooms_.end()) { - return std::pair<QString, QSharedPointer<RoomInfoListItem>>( - item->roomId(), topRoom->second); - } - } - } - - return {}; -} - -void -RoomList::updateReadStatus(const std::map<QString, bool> &status) -{ - for (const auto &room : status) { - if (roomExists(room.first)) { - auto item = rooms_.at(room.first); - - if (item) - item->setReadState(room.second); - } - } -} diff --git a/src/RoomList.h b/src/RoomList.h deleted file mode 100644
index 74152c55..00000000 --- a/src/RoomList.h +++ /dev/null
@@ -1,101 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QPushButton> -#include <QScrollArea> -#include <QSharedPointer> -#include <QVBoxLayout> -#include <QWidget> - -#include <set> - -#include "CacheStructs.h" -#include "UserSettingsPage.h" - -class LeaveRoomDialog; -class OverlayModal; -class RoomInfoListItem; -class Sync; -struct DescInfo; -struct RoomInfo; - -class RoomList : public QWidget -{ - Q_OBJECT - -public: - explicit RoomList(QSharedPointer<UserSettings> userSettings, QWidget *parent = nullptr); - - void initialize(const QMap<QString, RoomInfo> &info); - void sync(const std::map<QString, RoomInfo> &info); - - void clear() - { - rooms_.clear(); - rooms_sort_cache_.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); - //! Hide rooms that are not present in the given filter. - void applyFilter(const std::set<QString> &rooms); - //! Show all the available rooms. - void removeFilter(const std::set<QString> &roomsToHide); - void updateRoom(const QString &room_id, const RoomInfo &info); - void cleanupInvites(const std::map<QString, bool> &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 QString &img); - void joinRoom(const QString &room_id); - void updateRoomAvatarCb(const QString &room_id, const QString &img); - -public slots: - void updateRoomAvatar(const QString &roomid, const QString &img); - void highlightSelectedRoom(const QString &room_id); - void updateUnreadMessageCount(const QString &roomid, int count, int highlightedCount); - void updateRoomDescription(const QString &roomid, const DescInfo &info); - void closeJoinRoomDialog(bool isJoining, QString roomAlias); - void updateReadStatus(const std::map<QString, bool> &status); - void nextRoom(); - void previousRoom(); - -protected: - void paintEvent(QPaintEvent *event) override; - void leaveEvent(QEvent *event) override; - -private slots: - void sortRoomsByLastMessage(); - -private: - //! Return the first non-null room. - std::pair<QString, QSharedPointer<RoomInfoListItem>> firstRoom() const; - void calculateUnreadMessageCount(); - bool roomExists(const QString &room_id) { return rooms_.find(room_id) != rooms_.end(); } - //! Select the first visible room in the room list. - void selectFirstVisibleRoom(); - - QVBoxLayout *topLayout_; - QVBoxLayout *contentsLayout_; - QScrollArea *scrollArea_; - QWidget *scrollAreaContents_; - - QPushButton *joinRoomButton_; - - OverlayModal *joinRoomModal_; - - std::map<QString, QSharedPointer<RoomInfoListItem>> rooms_; - std::vector<QSharedPointer<RoomInfoListItem>> rooms_sort_cache_; - QString selectedRoom_; - - bool isSortPending_ = false; -}; diff --git a/src/SideBarActions.cpp b/src/SideBarActions.cpp deleted file mode 100644
index 0b7756f0..00000000 --- a/src/SideBarActions.cpp +++ /dev/null
@@ -1,120 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <QIcon> -#include <QPainter> -#include <QResizeEvent> -#include <QStyle> -#include <QStyleOption> - -#include <mtx/requests.hpp> - -#include "Config.h" -#include "MainWindow.h" -#include "SideBarActions.h" -#include "Splitter.h" -#include "ui/FlatButton.h" -#include "ui/Menu.h" - -SideBarActions::SideBarActions(QWidget *parent) - : QWidget{parent} -{ - QFont f; - f.setPointSizeF(f.pointSizeF()); - - const int fontHeight = QFontMetrics(f).height(); - const int contentHeight = fontHeight * 2.5; - - setFixedHeight(contentHeight); - - 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_->setToolTip(tr("User settings")); - 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_->setToolTip(tr("Start a new chat")); - 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())); - }); - - roomDirectory_ = new FlatButton(this); - roomDirectory_->setToolTip(tr("Room directory")); - roomDirectory_->setEnabled(false); - roomDirectory_->setIcon(joinRoomIcon); - roomDirectory_->setCornerRadius(conf::sidebarActions::iconSize / 2); - roomDirectory_->setIconSize( - QSize(conf::sidebarActions::iconSize, conf::sidebarActions::iconSize)); - - layout_->addWidget(createRoomBtn_); - layout_->addWidget(roomDirectory_); - layout_->addWidget(settingsBtn_); - - connect(settingsBtn_, &QPushButton::clicked, this, &SideBarActions::showSettings); -} - -void -SideBarActions::resizeEvent(QResizeEvent *event) -{ - Q_UNUSED(event); - - const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{}); - - if (width() <= sidebarSizes.small) { - roomDirectory_->hide(); - createRoomBtn_->hide(); - } else { - roomDirectory_->show(); - createRoomBtn_->show(); - } -} - -void -SideBarActions::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} diff --git a/src/SideBarActions.h b/src/SideBarActions.h deleted file mode 100644
index 566aa76b..00000000 --- a/src/SideBarActions.h +++ /dev/null
@@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QAction> -#include <QHBoxLayout> -#include <QWidget> - -namespace mtx { -namespace requests { -struct CreateRoom; -} -} - -class Menu; -class FlatButton; -class QResizeEvent; - -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 *roomDirectory_; - - QColor borderColor_; -}; diff --git a/src/Splitter.cpp b/src/Splitter.cpp deleted file mode 100644
index 15e3f5c5..00000000 --- a/src/Splitter.cpp +++ /dev/null
@@ -1,166 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <QSettings> - -#include "Logging.h" -#include "Splitter.h" - -constexpr auto MaxWidth = (1 << 24) - 1; - -Splitter::Splitter(QWidget *parent) - : QSplitter(parent) - , sz_{splitter::calculateSidebarSizes(QFont{})} -{ - connect(this, &QSplitter::splitterMoved, this, &Splitter::onSplitterMoved); - setChildrenCollapsible(false); -} - -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 <= sz_.small) { - if (left) { - left->setMinimumWidth(sz_.small); - left->setMaximumWidth(sz_.small); - return; - } - } else if (savedWidth < sz_.normal) { - savedWidth = sz_.normal; - } - - left->setMinimumWidth(sz_.normal); - left->setMaximumWidth(2 * sz_.normal); - setSizes({savedWidth, fallback - savedWidth}); - - 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) { - nhlog::ui()->warn("Splitter needs at least two children"); - return; - } - - if (s[0] == sz_.normal) { - 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(sz_.small); - left->setMaximumWidth(sz_.small); - - rightMoveCount_ = 0; - } - } - } else if (s[0] == sz_.small) { - 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() >= sz_.collapsePoint + sz_.normal) { - left->setMinimumWidth(sz_.normal); - left->setMaximumWidth(2 * sz_.normal); - - 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() == sz_.small) { - left->setMinimumWidth(sz_.small); - left->setMaximumWidth(sz_.small); - } else { - left->setMinimumWidth(sz_.normal); - left->setMaximumWidth(2 * sz_.normal); - } - } -} - -void -Splitter::showFullRoomList() -{ - auto left = widget(0); - auto right = widget(1); - - right->hide(); - - left->show(); - left->setMaximumWidth(MaxWidth); -} - -splitter::SideBarSizes -splitter::calculateSidebarSizes(const QFont &f) -{ - const auto height = static_cast<double>(QFontMetrics{f}.lineSpacing()); - - SideBarSizes sz; - sz.small = std::ceil(3.8 * height); - sz.normal = std::ceil(16 * height); - sz.groups = std::ceil(3 * height); - sz.collapsePoint = 2 * sz.normal; - - return sz; -} diff --git a/src/Splitter.h b/src/Splitter.h deleted file mode 100644
index 94622f89..00000000 --- a/src/Splitter.h +++ /dev/null
@@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QSplitter> - -namespace splitter { -struct SideBarSizes -{ - int small; - int normal; - int groups; - int collapsePoint; -}; - -SideBarSizes -calculateSidebarSizes(const QFont &f); -} - -class Splitter : public QSplitter -{ - Q_OBJECT -public: - explicit Splitter(QWidget *parent = nullptr); - ~Splitter() override; - - 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; - - splitter::SideBarSizes sz_; -}; diff --git a/src/UserInfoWidget.cpp b/src/UserInfoWidget.cpp deleted file mode 100644
index 3d526b8b..00000000 --- a/src/UserInfoWidget.cpp +++ /dev/null
@@ -1,219 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <QInputDialog> -#include <QLabel> -#include <QMenu> -#include <QPainter> -#include <QStyle> -#include <QStyleOption> -#include <QTimer> - -#include <iostream> - -#include "ChatPage.h" -#include "Config.h" -#include "MainWindow.h" -#include "Splitter.h" -#include "UserInfoWidget.h" -#include "UserSettingsPage.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") -{ - QFont f; - f.setPointSizeF(f.pointSizeF()); - - const int fontHeight = QFontMetrics(f).height(); - const int widgetMargin = fontHeight / 3; - const int contentHeight = fontHeight * 3; - - logoutButtonSize_ = std::min(fontHeight, 20); - - setFixedHeight(contentHeight + widgetMargin); - - topLayout_ = new QHBoxLayout(this); - topLayout_->setSpacing(0); - topLayout_->setMargin(widgetMargin); - - avatarLayout_ = new QHBoxLayout(); - textLayout_ = new QVBoxLayout(); - textLayout_->setSpacing(widgetMargin / 2); - textLayout_->setContentsMargins(widgetMargin * 2, widgetMargin, widgetMargin, widgetMargin); - - userAvatar_ = new Avatar(this, fontHeight * 2.5); - userAvatar_->setObjectName("userAvatar"); - userAvatar_->setLetter(QChar('?')); - - QFont nameFont; - nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); - nameFont.setWeight(QFont::Medium); - - displayNameLabel_ = new QLabel(this); - displayNameLabel_->setFont(nameFont); - displayNameLabel_->setObjectName("displayNameLabel"); - displayNameLabel_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignTop); - - userIdLabel_ = new QLabel(this); - userIdLabel_->setFont(f); - userIdLabel_->setObjectName("userIdLabel"); - userIdLabel_->setAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignVCenter); - - avatarLayout_->addWidget(userAvatar_); - textLayout_->addWidget(displayNameLabel_, 0, Qt::AlignBottom); - textLayout_->addWidget(userIdLabel_, 0, Qt::AlignTop); - - topLayout_->addLayout(avatarLayout_); - topLayout_->addLayout(textLayout_); - topLayout_->addStretch(1); - - buttonLayout_ = new QHBoxLayout(); - buttonLayout_->setSpacing(0); - buttonLayout_->setMargin(0); - - logoutButton_ = new FlatButton(this); - logoutButton_->setToolTip(tr("Logout")); - 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, []() { - MainWindow::instance()->openLogoutDialog(); - }); - - menu = new QMenu(this); - - auto setStatusAction = menu->addAction(tr("Set custom status message")); - connect(setStatusAction, &QAction::triggered, this, [this]() { - bool ok = false; - QString text = QInputDialog::getText(this, - tr("Custom status message"), - tr("Status:"), - QLineEdit::Normal, - ChatPage::instance()->status(), - &ok); - if (ok) - ChatPage::instance()->setStatus(text); - }); - - auto userProfileAction = menu->addAction(tr("User Profile Settings")); - connect( - userProfileAction, &QAction::triggered, this, [this]() { emit openGlobalUserProfile(); }); - -#if 0 // disable presence menu until issues in synapse are resolved - auto setAutoPresence = menu->addAction(tr("Set presence automatically")); - connect(setAutoPresence, &QAction::triggered, this, []() { - ChatPage::instance()->userSettings()->setPresence( - UserSettings::Presence::AutomaticPresence); - ChatPage::instance()->setStatus(ChatPage::instance()->status()); - }); - auto setOnline = menu->addAction(tr("Online")); - connect(setOnline, &QAction::triggered, this, []() { - ChatPage::instance()->userSettings()->setPresence(UserSettings::Presence::Online); - ChatPage::instance()->setStatus(ChatPage::instance()->status()); - }); - auto setUnavailable = menu->addAction(tr("Unavailable")); - connect(setUnavailable, &QAction::triggered, this, []() { - ChatPage::instance()->userSettings()->setPresence( - UserSettings::Presence::Unavailable); - ChatPage::instance()->setStatus(ChatPage::instance()->status()); - }); - auto setOffline = menu->addAction(tr("Offline")); - connect(setOffline, &QAction::triggered, this, []() { - ChatPage::instance()->userSettings()->setPresence(UserSettings::Presence::Offline); - ChatPage::instance()->setStatus(ChatPage::instance()->status()); - }); -#endif -} - -void -UserInfoWidget::contextMenuEvent(QContextMenuEvent *event) -{ - menu->popup(event->globalPos()); -} - -void -UserInfoWidget::resizeEvent(QResizeEvent *event) -{ - Q_UNUSED(event); - - const auto sz = splitter::calculateSidebarSizes(QFont{}); - - if (width() <= sz.small) { - topLayout_->setContentsMargins(0, 0, logoutButtonSize_, 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::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); - update(); -} - -void -UserInfoWidget::setAvatar(const QString &url) -{ - userAvatar_->setImage(url); - update(); -} - -void -UserInfoWidget::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} diff --git a/src/UserInfoWidget.h b/src/UserInfoWidget.h deleted file mode 100644
index 5aec1cda..00000000 --- a/src/UserInfoWidget.h +++ /dev/null
@@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr> -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QWidget> - -class Avatar; -class FlatButton; -class OverlayModal; - -class QLabel; -class QHBoxLayout; -class QVBoxLayout; -class QMenu; - -class UserInfoWidget : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor) - -public: - UserInfoWidget(QWidget *parent = nullptr); - - void setDisplayName(const QString &name); - void setUserId(const QString &userid); - void setAvatar(const QString &url); - - void reset(); - - QColor borderColor() const { return borderColor_; } - void setBorderColor(QColor &color) { borderColor_ = color; } - -protected: - void resizeEvent(QResizeEvent *event) override; - void paintEvent(QPaintEvent *event) override; - void contextMenuEvent(QContextMenuEvent *) override; - -signals: - void openGlobalUserProfile(); - -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_; - - QMenu *menu = nullptr; -}; diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 0edc1288..9b906555 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp
@@ -64,10 +64,14 @@ void UserSettings::load(std::optional<QString> profile) { QSettings settings; - tray_ = settings.value("user/window/tray", false).toBool(); + tray_ = settings.value("user/window/tray", false).toBool(); + startInTray_ = settings.value("user/window/start_in_tray", false).toBool(); + + roomListWidth_ = settings.value("user/sidebar/room_list_width", -1).toInt(); + communityListWidth_ = settings.value("user/sidebar/community_list_width", -1).toInt(); + hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool(); hasAlertOnNotification_ = settings.value("user/alert_on_notification", false).toBool(); - startInTray_ = settings.value("user/window/start_in_tray", false).toBool(); groupView_ = settings.value("user/group_view", true).toBool(); hiddenTags_ = settings.value("user/hidden_tags", QStringList{}).toStringList(); buttonsInTimeline_ = settings.value("user/timeline/buttons", true).toBool(); @@ -248,6 +252,24 @@ UserSettings::setTimelineMaxWidth(int state) emit timelineMaxWidthChanged(state); save(); } +void +UserSettings::setCommunityListWidth(int state) +{ + if (state == communityListWidth_) + return; + communityListWidth_ = state; + emit communityListWidthChanged(state); + save(); +} +void +UserSettings::setRoomListWidth(int state) +{ + if (state == roomListWidth_) + return; + roomListWidth_ = state; + emit roomListWidthChanged(state); + save(); +} void UserSettings::setDesktopNotifications(bool state) @@ -545,49 +567,14 @@ UserSettings::applyTheme() { QFile stylefile; - static QPalette original; if (this->theme() == "light") { stylefile.setFileName(":/styles/styles/nheko.qss"); - QPalette lightActive( - /*windowText*/ QColor("#333"), - /*button*/ QColor("white"), - /*light*/ QColor(0xef, 0xef, 0xef), - /*dark*/ QColor(110, 110, 110), - /*mid*/ QColor(220, 220, 220), - /*text*/ QColor("#333"), - /*bright_text*/ QColor("#333"), - /*base*/ QColor("#fff"), - /*window*/ QColor("white")); - lightActive.setColor(QPalette::AlternateBase, QColor("#eee")); - lightActive.setColor(QPalette::Highlight, QColor("#38a3d8")); - lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color()); - lightActive.setColor(QPalette::ToolTipText, lightActive.text().color()); - lightActive.setColor(QPalette::Link, QColor("#0077b5")); - lightActive.setColor(QPalette::ButtonText, QColor("#333")); - QApplication::setPalette(lightActive); } else if (this->theme() == "dark") { stylefile.setFileName(":/styles/styles/nheko-dark.qss"); - QPalette darkActive( - /*windowText*/ QColor("#caccd1"), - /*button*/ QColor(0xff, 0xff, 0xff), - /*light*/ QColor("#caccd1"), - /*dark*/ QColor(110, 110, 110), - /*mid*/ QColor("#202228"), - /*text*/ QColor("#caccd1"), - /*bright_text*/ QColor(0xff, 0xff, 0xff), - /*base*/ QColor("#202228"), - /*window*/ QColor("#2d3139")); - darkActive.setColor(QPalette::AlternateBase, QColor("#2d3139")); - darkActive.setColor(QPalette::Highlight, QColor("#38a3d8")); - darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color()); - darkActive.setColor(QPalette::ToolTipText, darkActive.text().color()); - darkActive.setColor(QPalette::Link, QColor("#38a3d8")); - darkActive.setColor(QPalette::ButtonText, "#727274"); - QApplication::setPalette(darkActive); } else { stylefile.setFileName(":/styles/styles/system.qss"); - QApplication::setPalette(original); } + QApplication::setPalette(Theme::paletteFromTheme(this->theme().toStdString())); stylefile.open(QFile::ReadOnly); QString stylesheet = QString(stylefile.readAll()); @@ -606,6 +593,11 @@ UserSettings::save() settings.setValue("start_in_tray", startInTray_); settings.endGroup(); // window + settings.beginGroup("sidebar"); + settings.setValue("community_list_width", communityListWidth_); + settings.setValue("room_list_width", roomListWidth_); + settings.endGroup(); // window + settings.beginGroup("timeline"); settings.setValue("buttons", buttonsInTimeline_); settings.setValue("message_hover_highlight", messageHoverHighlight_); diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index 3ad0293b..acb08569 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h
@@ -61,6 +61,10 @@ class UserSettings : public QObject NOTIFY privacyScreenTimeoutChanged) Q_PROPERTY(int timelineMaxWidth READ timelineMaxWidth WRITE setTimelineMaxWidth NOTIFY timelineMaxWidthChanged) + Q_PROPERTY( + int roomListWidth READ roomListWidth WRITE setRoomListWidth NOTIFY roomListWidthChanged) + Q_PROPERTY(int communityListWidth READ communityListWidth WRITE setCommunityListWidth NOTIFY + communityListWidthChanged) Q_PROPERTY(bool mobileMode READ mobileMode WRITE setMobileMode NOTIFY mobileModeChanged) Q_PROPERTY(double fontSize READ fontSize WRITE setFontSize NOTIFY fontSizeChanged) Q_PROPERTY(QString font READ font WRITE setFontFamily NOTIFY fontChanged) @@ -129,6 +133,8 @@ public: void setSortByImportance(bool state); void setButtonsInTimeline(bool state); void setTimelineMaxWidth(int state); + void setCommunityListWidth(int state); + void setRoomListWidth(int state); void setDesktopNotifications(bool state); void setAlertOnNotification(bool state); void setAvatarCircles(bool state); @@ -178,6 +184,8 @@ public: return hasDesktopNotifications() || hasAlertOnNotification(); } int timelineMaxWidth() const { return timelineMaxWidth_; } + int communityListWidth() const { return communityListWidth_; } + int roomListWidth() const { return roomListWidth_; } double fontSize() const { return baseFontSize_; } QString font() const { return font_; } QString emojiFont() const @@ -227,6 +235,8 @@ signals: void privacyScreenChanged(bool state); void privacyScreenTimeoutChanged(int state); void timelineMaxWidthChanged(int state); + void roomListWidthChanged(int state); + void communityListWidthChanged(int state); void mobileModeChanged(bool mode); void fontSizeChanged(double state); void fontChanged(QString state); @@ -276,6 +286,8 @@ private: bool shareKeysWithTrustedUsers_; bool mobileMode_; int timelineMaxWidth_; + int roomListWidth_; + int communityListWidth_; double baseFontSize_; QString font_; QString emojiFont_; diff --git a/src/popups/PopupItem.cpp b/src/popups/PopupItem.cpp deleted file mode 100644
index 2daa6143..00000000 --- a/src/popups/PopupItem.cpp +++ /dev/null
@@ -1,89 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <QLabel> -#include <QPaintEvent> -#include <QPainter> -#include <QStyleOption> - -#include "../Utils.h" -#include "../ui/Avatar.h" -#include "PopupItem.h" - -constexpr int PopupHMargin = 4; -constexpr int PopupItemMargin = 3; - -PopupItem::PopupItem(QWidget *parent) - : QWidget(parent) - , avatar_{new Avatar(this, conf::popup::avatar)} - , hovering_{false} -{ - setMouseTracking(true); - setAttribute(Qt::WA_Hover); - - topLayout_ = new QHBoxLayout(this); - topLayout_->setContentsMargins( - PopupHMargin, PopupItemMargin, PopupHMargin, PopupItemMargin); - - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); -} - -void -PopupItem::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); - - if (underMouse() || hovering_) - p.fillRect(rect(), hoverColor_); -} - -RoomItem::RoomItem(QWidget *parent, const RoomSearchResult &res) - : PopupItem(parent) - , roomId_{QString::fromStdString(res.room_id)} -{ - auto name = QFontMetrics(QFont()).elidedText( - QString::fromStdString(res.info.name), Qt::ElideRight, parentWidget()->width() - 10); - - avatar_->setLetter(utils::firstChar(name)); - - roomName_ = new QLabel(name, this); - roomName_->setMargin(0); - - topLayout_->addWidget(avatar_); - topLayout_->addWidget(roomName_, 1); - - if (!res.info.avatar_url.empty()) - avatar_->setImage(QString::fromStdString(res.info.avatar_url)); -} - -void -RoomItem::updateItem(const RoomSearchResult &result) -{ - roomId_ = QString::fromStdString(std::move(result.room_id)); - - auto name = - QFontMetrics(QFont()).elidedText(QString::fromStdString(std::move(result.info.name)), - Qt::ElideRight, - parentWidget()->width() - 10); - - roomName_->setText(name); - - // if there is not an avatar set for the room, we want to at least show the letter - // correctly! - avatar_->setLetter(utils::firstChar(name)); - if (!result.info.avatar_url.empty()) - avatar_->setImage(QString::fromStdString(result.info.avatar_url)); -} - -void -RoomItem::mousePressEvent(QMouseEvent *event) -{ - if (event->buttons() != Qt::RightButton) - emit clicked(selectedText()); - - QWidget::mousePressEvent(event); -} diff --git a/src/popups/PopupItem.h b/src/popups/PopupItem.h deleted file mode 100644
index fc24915e..00000000 --- a/src/popups/PopupItem.h +++ /dev/null
@@ -1,66 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QWidget> - -#include "../AvatarProvider.h" -#include "../ChatPage.h" - -class Avatar; -struct SearchResult; -class QLabel; -class QHBoxLayout; - -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 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_; -}; diff --git a/src/popups/SuggestionsPopup.cpp b/src/popups/SuggestionsPopup.cpp deleted file mode 100644
index 7b545d61..00000000 --- a/src/popups/SuggestionsPopup.cpp +++ /dev/null
@@ -1,164 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <QPaintEvent> -#include <QPainter> -#include <QStyleOption> - -#include "../Config.h" -#include "../Utils.h" -#include "../ui/Avatar.h" -#include "../ui/DropShadow.h" -#include "ChatPage.h" -#include "PopupItem.h" -#include "SuggestionsPopup.h" - -SuggestionsPopup::SuggestionsPopup(QWidget *parent) - : QWidget(parent) -{ - setAttribute(Qt::WA_ShowWithoutActivating, true); - setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint); - - layout_ = new QVBoxLayout(this); - layout_->setMargin(0); - layout_->setSpacing(0); -} - -QString -SuggestionsPopup::displayName(QString room, QString user) -{ - return cache::displayName(room, user); -} - -void -SuggestionsPopup::addRooms(const std::vector<RoomSearchResult> &rooms) -{ - if (rooms.empty()) { - hide(); - return; - } - - const int layoutCount = (int)layout_->count(); - const int roomCount = (int)rooms.size(); - - // Remove the extra widgets from the layout. - if (roomCount < layoutCount) - removeLayoutItemsAfter(roomCount - 1); - - for (int i = 0; i < roomCount; ++i) { - auto item = layout_->itemAt(i); - - // Create a new widget if there isn't already one in that - // layout position. - if (!item) { - auto room = new RoomItem(this, rooms.at(i)); - connect(room, &RoomItem::clicked, this, &SuggestionsPopup::itemSelected); - layout_->addWidget(room); - } else { - // Update the current widget with the new data. - auto room = qobject_cast<RoomItem *>(item->widget()); - if (room) - room->updateItem(rooms.at(i)); - } - } - - resetSelection(); - adjustSize(); - - resize(geometry().width(), 40 * (int)rooms.size()); - - selectNextSuggestion(); -} - -void -SuggestionsPopup::hoverSelection() -{ - resetHovering(); - setHovering(selectedItem_); - update(); -} - -void -SuggestionsPopup::selectHoveredSuggestion() -{ - const auto item = layout_->itemAt(selectedItem_); - if (!item) - return; - - const auto &widget = qobject_cast<RoomItem *>(item->widget()); - emit itemSelected(displayName(ChatPage::instance()->currentRoom(), widget->selectedText())); - - resetSelection(); -} - -void -SuggestionsPopup::selectNextSuggestion() -{ - selectedItem_++; - if (selectedItem_ >= layout_->count()) - selectFirstItem(); - - hoverSelection(); -} - -void -SuggestionsPopup::selectPreviousSuggestion() -{ - selectedItem_--; - if (selectedItem_ < 0) - selectLastItem(); - - hoverSelection(); -} - -void -SuggestionsPopup::resetHovering() -{ - for (int i = 0; i < layout_->count(); ++i) { - const auto item = qobject_cast<PopupItem *>(layout_->itemAt(i)->widget()); - - if (item) - item->setHovering(false); - } -} - -void -SuggestionsPopup::setHovering(int pos) -{ - const auto &item = layout_->itemAt(pos); - const auto &widget = qobject_cast<PopupItem *>(item->widget()); - - if (widget) - widget->setHovering(true); -} - -void -SuggestionsPopup::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -SuggestionsPopup::selectLastItem() -{ - selectedItem_ = layout_->count() - 1; -} - -void -SuggestionsPopup::removeLayoutItemsAfter(size_t startingPos) -{ - size_t posToRemove = layout_->count() - 1; - - QLayoutItem *item; - while (startingPos <= posToRemove && - (item = layout_->takeAt((int)posToRemove)) != nullptr) { - delete item->widget(); - delete item; - - posToRemove = layout_->count() - 1; - } -} diff --git a/src/popups/SuggestionsPopup.h b/src/popups/SuggestionsPopup.h deleted file mode 100644
index 281edddb..00000000 --- a/src/popups/SuggestionsPopup.h +++ /dev/null
@@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <QWidget> - -#include "CacheStructs.h" - -class QVBoxLayout; -class QLayoutItem; - -class SuggestionsPopup : public QWidget -{ - Q_OBJECT - -public: - explicit SuggestionsPopup(QWidget *parent = nullptr); - - void selectHoveredSuggestion(); - -public slots: - void addRooms(const std::vector<RoomSearchResult> &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: - QString displayName(QString roomid, QString userid); - void hoverSelection(); - void resetSelection() { selectedItem_ = -1; } - void selectFirstItem() { selectedItem_ = 0; } - void selectLastItem(); - void removeLayoutItemsAfter(size_t startingPos); - - QVBoxLayout *layout_; - - //! Counter for tab completion (cycling). - int selectedItem_ = -1; -}; diff --git a/src/popups/UserMentions.cpp b/src/popups/UserMentions.cpp deleted file mode 100644
index 56b57503..00000000 --- a/src/popups/UserMentions.cpp +++ /dev/null
@@ -1,178 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include <QPaintEvent> -#include <QPainter> -#include <QScrollArea> -#include <QStyleOption> -#include <QTabWidget> -#include <QTimer> -#include <QVBoxLayout> - -#include "Cache.h" -#include "ChatPage.h" -#include "EventAccessors.h" -#include "Logging.h" -#include "UserMentions.h" - -using namespace popups; - -UserMentions::UserMentions(QWidget *parent) - : QWidget{parent} -{ - setAttribute(Qt::WA_ShowWithoutActivating, true); - setWindowFlags(Qt::FramelessWindowHint | Qt::Popup); - - tab_layout_ = new QTabWidget(this); - - top_layout_ = new QVBoxLayout(this); - top_layout_->setSpacing(0); - top_layout_->setMargin(0); - - local_scroll_area_ = new QScrollArea(this); - local_scroll_area_->setWidgetResizable(true); - local_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - local_scroll_widget_ = new QWidget(this); - local_scroll_widget_->setObjectName("local_scroll_widget"); - - all_scroll_area_ = new QScrollArea(this); - all_scroll_area_->setWidgetResizable(true); - all_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - all_scroll_widget_ = new QWidget(this); - all_scroll_widget_->setObjectName("all_scroll_widget"); - - // Height of the typing display. - QFont f; - f.setPointSizeF(f.pointSizeF() * 0.9); - const int bottomMargin = QFontMetrics(f).height() + 6; - - local_scroll_layout_ = new QVBoxLayout(local_scroll_widget_); - local_scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin); - local_scroll_layout_->setSpacing(0); - local_scroll_layout_->setObjectName("localscrollarea"); - - all_scroll_layout_ = new QVBoxLayout(all_scroll_widget_); - all_scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin); - all_scroll_layout_->setSpacing(0); - all_scroll_layout_->setObjectName("allscrollarea"); - - local_scroll_area_->setWidget(local_scroll_widget_); - local_scroll_area_->setAlignment(Qt::AlignBottom); - - all_scroll_area_->setWidget(all_scroll_widget_); - all_scroll_area_->setAlignment(Qt::AlignBottom); - - tab_layout_->addTab(local_scroll_area_, tr("This Room")); - tab_layout_->addTab(all_scroll_area_, tr("All Rooms")); - top_layout_->addWidget(tab_layout_); - - setLayout(top_layout_); -} - -void -UserMentions::initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs) -{ - nhlog::ui()->debug("Initializing " + std::to_string(notifs.size()) + " notifications."); - - for (const auto &item : notifs) { - for (const auto &notif : item.notifications) { - const auto event_id = - QString::fromStdString(mtx::accessors::event_id(notif.event)); - - try { - const auto room_id = QString::fromStdString(notif.room_id); - const auto user_id = - QString::fromStdString(mtx::accessors::sender(notif.event)); - const auto body = - QString::fromStdString(mtx::accessors::body(notif.event)); - - pushItem(event_id, - user_id, - body, - room_id, - ChatPage::instance()->currentRoom()); - - } catch (const lmdb::error &e) { - nhlog::db()->warn("error while sending desktop notification: {}", - e.what()); - } - } - } -} - -void -UserMentions::showPopup() -{ - for (auto widget : all_scroll_layout_->findChildren<QWidget *>()) { - delete widget; - } - for (auto widget : local_scroll_layout_->findChildren<QWidget *>()) { - delete widget; - } - - auto notifs = cache::getTimelineMentions(); - - initializeMentions(notifs); - show(); -} - -void -UserMentions::pushItem(const QString &event_id, - const QString &user_id, - const QString &body, - const QString &room_id, - const QString &current_room_id) -{ - (void)event_id; - (void)user_id; - (void)body; - (void)room_id; - (void)current_room_id; - // setUpdatesEnabled(false); - // - // // Add to the 'all' section - // TimelineItem *view_item = new TimelineItem( - // mtx::events::MessageType::Text, user_id, body, true, room_id, - // all_scroll_widget_); - // view_item->setEventId(event_id); - // view_item->hide(); - // - // all_scroll_layout_->addWidget(view_item); - // QTimer::singleShot(0, this, [view_item, this]() { - // view_item->show(); - // view_item->adjustSize(); - // setUpdatesEnabled(true); - // }); - // - // // if it matches the current room... add it to the current room as well. - // if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) { - // // Add to the 'local' section - // TimelineItem *local_view_item = new - // TimelineItem(mtx::events::MessageType::Text, - // user_id, - // body, - // true, - // room_id, - // local_scroll_widget_); - // local_view_item->setEventId(event_id); - // local_view_item->hide(); - // local_scroll_layout_->addWidget(local_view_item); - // - // QTimer::singleShot(0, this, [local_view_item]() { - // local_view_item->show(); - // local_view_item->adjustSize(); - // }); - // } -} - -void -UserMentions::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} diff --git a/src/popups/UserMentions.h b/src/popups/UserMentions.h deleted file mode 100644
index f0b662d8..00000000 --- a/src/popups/UserMentions.h +++ /dev/null
@@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include <mtx/responses/notifications.hpp> - -#include <QMap> -#include <QString> -#include <QWidget> - -class QPaintEvent; -class QTabWidget; -class QScrollArea; -class QVBoxLayout; - -namespace popups { - -class UserMentions : public QWidget -{ - Q_OBJECT -public: - UserMentions(QWidget *parent = nullptr); - - void initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs); - void showPopup(); - -protected: - void paintEvent(QPaintEvent *) override; - -private: - void pushItem(const QString &event_id, - const QString &user_id, - const QString &body, - const QString &room_id, - const QString &current_room_id); - QTabWidget *tab_layout_; - QVBoxLayout *top_layout_; - QVBoxLayout *local_scroll_layout_; - QVBoxLayout *all_scroll_layout_; - - QScrollArea *local_scroll_area_; - QWidget *local_scroll_widget_; - - QScrollArea *all_scroll_area_; - QWidget *all_scroll_widget_; -}; -} diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp new file mode 100644
index 00000000..96a450ea --- /dev/null +++ b/src/timeline/CommunitiesModel.cpp
@@ -0,0 +1,188 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "CommunitiesModel.h" + +#include <set> + +#include "Cache.h" +#include "UserSettingsPage.h" + +CommunitiesModel::CommunitiesModel(QObject *parent) + : QAbstractListModel(parent) +{} + +QHash<int, QByteArray> +CommunitiesModel::roleNames() const +{ + return { + {AvatarUrl, "avatarUrl"}, + {DisplayName, "displayName"}, + {Tooltip, "tooltip"}, + {ChildrenHidden, "childrenHidden"}, + {Hidden, "hidden"}, + {Id, "id"}, + }; +} + +QVariant +CommunitiesModel::data(const QModelIndex &index, int role) const +{ + if (index.row() == 0) { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/world.png"); + case CommunitiesModel::Roles::DisplayName: + return tr("All rooms"); + case CommunitiesModel::Roles::Tooltip: + return tr("Shows all rooms without filtering."); + case CommunitiesModel::Roles::ChildrenHidden: + return false; + case CommunitiesModel::Roles::Hidden: + return false; + case CommunitiesModel::Roles::Id: + return ""; + } + } else if (index.row() - 1 < tags_.size()) { + auto tag = tags_.at(index.row() - 1); + if (tag == "m.favourite") { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/star.png"); + case CommunitiesModel::Roles::DisplayName: + return tr("Favourites"); + case CommunitiesModel::Roles::Tooltip: + return tr("Rooms you have favourited."); + } + } else if (tag == "m.lowpriority") { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/star.png"); + case CommunitiesModel::Roles::DisplayName: + return tr("Low Priority"); + case CommunitiesModel::Roles::Tooltip: + return tr("Rooms with low priority."); + } + } else if (tag == "m.server_notice") { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/tag.png"); + case CommunitiesModel::Roles::DisplayName: + return tr("Server Notices"); + case CommunitiesModel::Roles::Tooltip: + return tr("Messages from your server or administrator."); + } + } else { + switch (role) { + case CommunitiesModel::Roles::AvatarUrl: + return QString(":/icons/icons/ui/tag.png"); + case CommunitiesModel::Roles::DisplayName: + return tag.mid(2); + case CommunitiesModel::Roles::Tooltip: + return tag.mid(2); + } + } + + switch (role) { + case CommunitiesModel::Roles::Hidden: + return hiddentTagIds_.contains("tag:" + tag); + case CommunitiesModel::Roles::ChildrenHidden: + return true; + case CommunitiesModel::Roles::Id: + return "tag:" + tag; + } + } + return QVariant(); +} + +void +CommunitiesModel::initializeSidebar() +{ + std::set<std::string> ts; + for (const auto &e : cache::roomInfo()) { + for (const auto &t : e.tags) { + if (t.find("u.") == 0 || t.find("m." == 0)) { + ts.insert(t); + } + } + } + + beginResetModel(); + tags_.clear(); + for (const auto &t : ts) + tags_.push_back(QString::fromStdString(t)); + + hiddentTagIds_ = UserSettings::instance()->hiddenTags(); + endResetModel(); + + emit tagsChanged(); + emit hiddenTagsChanged(); +} + +void +CommunitiesModel::clear() +{ + beginResetModel(); + tags_.clear(); + endResetModel(); + resetCurrentTagId(); + + emit tagsChanged(); +} + +void +CommunitiesModel::sync(const mtx::responses::Rooms &rooms) +{ + bool tagsUpdated = false; + + for (const auto &[roomid, room] : rooms.join) { + (void)roomid; + for (const auto &e : room.account_data.events) + if (std::holds_alternative< + mtx::events::AccountDataEvent<mtx::events::account_data::Tags>>(e)) { + tagsUpdated = true; + } + } + + if (tagsUpdated) + initializeSidebar(); +} + +void +CommunitiesModel::setCurrentTagId(QString tagId) +{ + if (tagId.startsWith("tag:")) { + auto tag = tagId.mid(4); + for (const auto &t : tags_) { + if (t == tag) { + this->currentTagId_ = tagId; + emit currentTagIdChanged(currentTagId_); + return; + } + } + } + + this->currentTagId_ = ""; + emit currentTagIdChanged(currentTagId_); +} + +void +CommunitiesModel::toggleTagId(QString tagId) +{ + if (hiddentTagIds_.contains(tagId)) { + hiddentTagIds_.removeOne(tagId); + UserSettings::instance()->setHiddenTags(hiddentTagIds_); + } else { + hiddentTagIds_.push_back(tagId); + UserSettings::instance()->setHiddenTags(hiddentTagIds_); + } + + if (tagId.startsWith("tag:")) { + auto idx = tags_.indexOf(tagId.mid(4)); + if (idx != -1) + emit dataChanged(index(idx), index(idx), {Hidden}); + } + + emit hiddenTagsChanged(); +} diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h new file mode 100644
index 00000000..c98b5955 --- /dev/null +++ b/src/timeline/CommunitiesModel.h
@@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <QAbstractListModel> +#include <QHash> +#include <QString> +#include <QStringList> + +#include <mtx/responses/sync.hpp> + +class CommunitiesModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QString currentTagId READ currentTagId WRITE setCurrentTagId NOTIFY + currentTagIdChanged RESET resetCurrentTagId) + Q_PROPERTY(QStringList tags READ tags NOTIFY tagsChanged) + +public: + enum Roles + { + AvatarUrl = Qt::UserRole, + DisplayName, + Tooltip, + ChildrenHidden, + Hidden, + Id, + }; + + CommunitiesModel(QObject *parent = nullptr); + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return 1 + tags_.size(); + } + QVariant data(const QModelIndex &index, int role) const override; + +public slots: + void initializeSidebar(); + void sync(const mtx::responses::Rooms &rooms); + void clear(); + QString currentTagId() const { return currentTagId_; } + void setCurrentTagId(QString tagId); + void resetCurrentTagId() + { + currentTagId_.clear(); + emit currentTagIdChanged(currentTagId_); + } + QStringList tags() const { return tags_; } + void toggleTagId(QString tagId); + +signals: + void currentTagIdChanged(QString tagId); + void hiddenTagsChanged(); + void tagsChanged(); + +private: + QStringList tags_; + QString currentTagId_; + QStringList hiddentTagIds_; +}; diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 883d384c..4a9f0fff 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp
@@ -770,7 +770,7 @@ EventStore::decryptEvent(const IdIndex &idx, } mtx::events::collections::TimelineEvents * -EventStore::get(std::string_view id, std::string_view related_to, bool decrypt, bool resolve_edits) +EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool resolve_edits) { if (this->thread() != QThread::currentThread()) nhlog::db()->warn("{} called from a different thread!", __func__); @@ -778,7 +778,7 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt, if (id.empty()) return nullptr; - IdIndex index{room_id_, std::string(id)}; + IdIndex index{room_id_, std::move(id)}; if (resolve_edits) { auto edits_ = edits(index.id); if (!edits_.empty()) { @@ -796,14 +796,12 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt, http::client()->get_event( room_id_, index.id, - [this, - relatedTo = std::string(related_to.data(), related_to.size()), - id = index.id](const mtx::events::collections::TimelineEvents &timeline, - mtx::http::RequestErr err) { + [this, relatedTo = std::string(related_to), id = index.id]( + const mtx::events::collections::TimelineEvents &timeline, + mtx::http::RequestErr err) { if (err) { nhlog::net()->error( - "Failed to retrieve event with id {}, which " - "was " + "Failed to retrieve event with id {}, which was " "requested to show the replyTo for event {}", relatedTo, id); diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
index c7a7588b..d9bb86cb 100644 --- a/src/timeline/EventStore.h +++ b/src/timeline/EventStore.h
@@ -70,7 +70,7 @@ public: // optionally returns the event or nullptr and fetches it, after which it emits a // relatedFetched event - mtx::events::collections::TimelineEvents *get(std::string_view id, + mtx::events::collections::TimelineEvents *get(std::string id, std::string_view related_to, bool decrypt = true, bool resolve_edits = true); diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index cda38b75..c309daab 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp
@@ -20,6 +20,7 @@ #include "Cache.h" #include "ChatPage.h" #include "CompletionProxyModel.h" +#include "Config.h" #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" @@ -508,8 +509,7 @@ InputBar::command(QString command, QString args) } else if (command == "react") { auto eventId = room->reply(); if (!eventId.isEmpty()) - ChatPage::instance()->timelineManager()->queueReactionMessage( - eventId, args.trimmed()); + reaction(eventId, args.trimmed()); } else if (command == "join") { ChatPage::instance()->joinRoom(args); } else if (command == "part" || command == "leave") { @@ -715,3 +715,35 @@ InputBar::stopTyping() } }); } + +void +InputBar::reaction(const QString &reactedEvent, const QString &reactionKey) +{ + auto reactions = room->reactions(reactedEvent.toStdString()); + + QString selfReactedEvent; + for (const auto &reaction : reactions) { + if (reactionKey == reaction.key_) { + selfReactedEvent = reaction.selfReactedEvent_; + break; + } + } + + if (selfReactedEvent.startsWith("m")) + return; + + // If selfReactedEvent is empty, that means we haven't previously reacted + if (selfReactedEvent.isEmpty()) { + mtx::events::msg::Reaction reaction; + mtx::common::Relation rel; + rel.rel_type = mtx::common::RelationType::Annotation; + rel.event_id = reactedEvent.toStdString(); + rel.key = reactionKey.toStdString(); + reaction.relations.relations.push_back(rel); + + room->sendMessageEvent(reaction, mtx::events::EventType::Reaction); + // Otherwise, we have previously reacted and the reaction should be redacted + } else { + room->redactEvent(selfReactedEvent); + } +} diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index 9db16bae..c9728379 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h
@@ -56,6 +56,7 @@ public slots: void message(QString body, MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED, bool rainbowify = false); + void reaction(const QString &reactedEvent, const QString &reactionKey); private slots: void startTyping(); diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp new file mode 100644
index 00000000..0f980c6c --- /dev/null +++ b/src/timeline/RoomlistModel.cpp
@@ -0,0 +1,590 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "RoomlistModel.h" + +#include "Cache_p.h" +#include "ChatPage.h" +#include "MatrixClient.h" +#include "MxcImageProvider.h" +#include "TimelineModel.h" +#include "TimelineViewManager.h" +#include "UserSettingsPage.h" + +RoomlistModel::RoomlistModel(TimelineViewManager *parent) + : QAbstractListModel(parent) + , manager(parent) +{ + connect(ChatPage::instance(), &ChatPage::decryptSidebarChanged, this, [this]() { + auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar(); + QHash<QString, QSharedPointer<TimelineModel>>::iterator i; + for (i = models.begin(); i != models.end(); ++i) { + auto ptr = i.value(); + + if (!ptr.isNull()) { + ptr->setDecryptDescription(decrypt); + ptr->updateLastMessage(); + } + } + }); + + connect(this, + &RoomlistModel::totalUnreadMessageCountUpdated, + ChatPage::instance(), + &ChatPage::unreadMessages); +} + +QHash<int, QByteArray> +RoomlistModel::roleNames() const +{ + return { + {AvatarUrl, "avatarUrl"}, + {RoomName, "roomName"}, + {RoomId, "roomId"}, + {LastMessage, "lastMessage"}, + {Time, "time"}, + {Timestamp, "timestamp"}, + {HasUnreadMessages, "hasUnreadMessages"}, + {HasLoudNotification, "hasLoudNotification"}, + {NotificationCount, "notificationCount"}, + {IsInvite, "isInvite"}, + {IsSpace, "isSpace"}, + {Tags, "tags"}, + }; +} + +QVariant +RoomlistModel::data(const QModelIndex &index, int role) const +{ + if (index.row() >= 0 && static_cast<size_t>(index.row()) < roomids.size()) { + auto roomid = roomids.at(index.row()); + + if (models.contains(roomid)) { + auto room = models.value(roomid); + switch (role) { + case Roles::AvatarUrl: + return room->roomAvatarUrl(); + case Roles::RoomName: + return room->plainRoomName(); + case Roles::RoomId: + return room->roomId(); + case Roles::LastMessage: + return room->lastMessage().body; + case Roles::Time: + return room->lastMessage().descriptiveTime; + case Roles::Timestamp: + return QVariant( + static_cast<quint64>(room->lastMessage().timestamp)); + case Roles::HasUnreadMessages: + return this->roomReadStatus.count(roomid) && + this->roomReadStatus.at(roomid); + case Roles::HasLoudNotification: + return room->hasMentions(); + case Roles::NotificationCount: + return room->notificationCount(); + case Roles::IsInvite: + case Roles::IsSpace: + return false; + case Roles::Tags: { + auto info = cache::singleRoomInfo(roomid.toStdString()); + QStringList list; + for (const auto &t : info.tags) + list.push_back(QString::fromStdString(t)); + return list; + } + default: + return {}; + } + } else if (invites.contains(roomid)) { + auto room = invites.value(roomid); + switch (role) { + case Roles::AvatarUrl: + return QString::fromStdString(room.avatar_url); + case Roles::RoomName: + return QString::fromStdString(room.name); + case Roles::RoomId: + return roomid; + case Roles::LastMessage: + return room.msgInfo.body; + case Roles::Time: + return room.msgInfo.descriptiveTime; + case Roles::Timestamp: + return QVariant(static_cast<quint64>(room.msgInfo.timestamp)); + case Roles::HasUnreadMessages: + case Roles::HasLoudNotification: + return false; + case Roles::NotificationCount: + return 0; + case Roles::IsInvite: + return true; + case Roles::IsSpace: + return false; + case Roles::Tags: + return QStringList(); + default: + return {}; + } + } else { + return {}; + } + } else { + return {}; + } +} + +void +RoomlistModel::updateReadStatus(const std::map<QString, bool> roomReadStatus_) +{ + std::vector<int> roomsToUpdate; + roomsToUpdate.resize(roomReadStatus_.size()); + for (const auto &[roomid, roomUnread] : roomReadStatus_) { + if (roomUnread != roomReadStatus[roomid]) { + roomsToUpdate.push_back(this->roomidToIndex(roomid)); + } + + this->roomReadStatus[roomid] = roomUnread; + } + + for (auto idx : roomsToUpdate) { + emit dataChanged(index(idx), + index(idx), + { + Roles::HasUnreadMessages, + }); + } +} +void +RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification) +{ + if (!models.contains(room_id)) { + // ensure we get read status updates and are only connected once + connect(cache::client(), + &Cache::roomReadStatus, + this, + &RoomlistModel::updateReadStatus, + Qt::UniqueConnection); + + QSharedPointer<TimelineModel> newRoom(new TimelineModel(manager, room_id)); + newRoom->setDecryptDescription( + ChatPage::instance()->userSettings()->decryptSidebar()); + + connect(newRoom.data(), + &TimelineModel::newEncryptedImage, + manager->imageProvider(), + &MxcImageProvider::addEncryptionInfo); + connect(newRoom.data(), + &TimelineModel::forwardToRoom, + manager, + &TimelineViewManager::forwardMessageToRoom); + connect( + newRoom.data(), &TimelineModel::lastMessageChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::HasLoudNotification, + Roles::LastMessage, + Roles::Timestamp, + Roles::NotificationCount, + Qt::DisplayRole, + }); + }); + connect( + newRoom.data(), &TimelineModel::roomAvatarUrlChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::AvatarUrl, + }); + }); + connect(newRoom.data(), &TimelineModel::roomNameChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::RoomName, + }); + }); + connect( + newRoom.data(), &TimelineModel::notificationsChanged, this, [room_id, this]() { + auto idx = this->roomidToIndex(room_id); + emit dataChanged(index(idx), + index(idx), + { + Roles::HasLoudNotification, + Roles::NotificationCount, + Qt::DisplayRole, + }); + + int total_unread_msgs = 0; + + for (const auto &room : models) { + if (!room.isNull()) + total_unread_msgs += room->notificationCount(); + } + + emit totalUnreadMessageCountUpdated(total_unread_msgs); + }); + + newRoom->updateLastMessage(); + + bool wasInvite = invites.contains(room_id); + if (!suppressInsertNotification && !wasInvite) + beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size()); + + models.insert(room_id, std::move(newRoom)); + + if (wasInvite) { + auto idx = roomidToIndex(room_id); + invites.remove(room_id); + emit dataChanged(index(idx), index(idx)); + } else { + roomids.push_back(room_id); + } + + if (!suppressInsertNotification && !wasInvite) + endInsertRows(); + } +} + +void +RoomlistModel::sync(const mtx::responses::Rooms &rooms) +{ + for (const auto &[room_id, room] : rooms.join) { + // addRoom will only add the room, if it doesn't exist + addRoom(QString::fromStdString(room_id)); + const auto &room_model = models.value(QString::fromStdString(room_id)); + room_model->sync(room); + // room_model->addEvents(room.timeline); + connect(room_model.data(), + &TimelineModel::newCallEvent, + manager->callManager(), + &CallManager::syncEvent, + Qt::UniqueConnection); + + if (ChatPage::instance()->userSettings()->typingNotifications()) { + for (const auto &ev : room.ephemeral.events) { + if (auto t = std::get_if< + mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>( + &ev)) { + std::vector<QString> typing; + typing.reserve(t->content.user_ids.size()); + for (const auto &user : t->content.user_ids) { + if (user != http::client()->user_id().to_string()) + typing.push_back( + QString::fromStdString(user)); + } + room_model->updateTypingUsers(typing); + } + } + } + } + + for (const auto &[room_id, room] : rooms.leave) { + (void)room; + auto idx = this->roomidToIndex(QString::fromStdString(room_id)); + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + if (models.contains(QString::fromStdString(room_id))) + models.remove(QString::fromStdString(room_id)); + else if (invites.contains(QString::fromStdString(room_id))) + invites.remove(QString::fromStdString(room_id)); + endRemoveRows(); + } + } + + for (const auto &[room_id, room] : rooms.invite) { + (void)room; + auto qroomid = QString::fromStdString(room_id); + + auto invite = cache::client()->invite(room_id); + if (!invite) + continue; + + if (invites.contains(qroomid)) { + invites[qroomid] = *invite; + auto idx = roomidToIndex(qroomid); + emit dataChanged(index(idx), index(idx)); + } else { + beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size()); + invites.insert(qroomid, *invite); + roomids.push_back(std::move(qroomid)); + endInsertRows(); + } + } +} + +void +RoomlistModel::initializeRooms() +{ + beginResetModel(); + models.clear(); + roomids.clear(); + invites.clear(); + currentRoom_ = nullptr; + + invites = cache::client()->invites(); + for (const auto &id : invites.keys()) + roomids.push_back(id); + + for (const auto &id : cache::client()->roomIds()) + addRoom(id, true); + + endResetModel(); +} + +void +RoomlistModel::clear() +{ + beginResetModel(); + models.clear(); + invites.clear(); + roomids.clear(); + currentRoom_ = nullptr; + emit currentRoomChanged(); + endResetModel(); +} + +void +RoomlistModel::acceptInvite(QString roomid) +{ + if (invites.contains(roomid)) { + auto idx = roomidToIndex(roomid); + + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + invites.remove(roomid); + endRemoveRows(); + ChatPage::instance()->joinRoom(roomid); + } + } +} +void +RoomlistModel::declineInvite(QString roomid) +{ + if (invites.contains(roomid)) { + auto idx = roomidToIndex(roomid); + + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + invites.remove(roomid); + endRemoveRows(); + ChatPage::instance()->leaveRoom(roomid); + } + } +} +void +RoomlistModel::leave(QString roomid) +{ + if (models.contains(roomid)) { + auto idx = roomidToIndex(roomid); + + if (idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + roomids.erase(roomids.begin() + idx); + models.remove(roomid); + endRemoveRows(); + ChatPage::instance()->leaveRoom(roomid); + } + } +} + +void +RoomlistModel::setCurrentRoom(QString roomid) +{ + nhlog::ui()->debug("Trying to switch to: {}", roomid.toStdString()); + if (models.contains(roomid)) { + currentRoom_ = models.value(roomid); + emit currentRoomChanged(); + nhlog::ui()->debug("Switched to: {}", roomid.toStdString()); + } +} + +namespace { +enum NotificationImportance : short +{ + ImportanceDisabled = -1, + AllEventsRead = 0, + NewMessage = 1, + NewMentions = 2, + Invite = 3 +}; +} + +short int +FilteredRoomlistModel::calculateImportance(const QModelIndex &idx) const +{ + // Returns the degree of importance of the unread messages in the room. + // If sorting by importance is disabled in settings, this only ever + // returns ImportanceDisabled or Invite + if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) { + return Invite; + } else if (!this->sortByImportance) { + return ImportanceDisabled; + } else if (sourceModel()->data(idx, RoomlistModel::HasLoudNotification).toBool()) { + return NewMentions; + } else if (sourceModel()->data(idx, RoomlistModel::NotificationCount).toInt() > 0) { + return NewMessage; + } else { + return AllEventsRead; + } +} +bool +FilteredRoomlistModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + QModelIndex const left_idx = sourceModel()->index(left.row(), 0, QModelIndex()); + QModelIndex const right_idx = sourceModel()->index(right.row(), 0, QModelIndex()); + + // Sort by "importance" (i.e. invites before mentions before + // notifs before new events before old events), then secondly + // by recency. + + // Checking importance first + const auto a_importance = calculateImportance(left_idx); + const auto b_importance = calculateImportance(right_idx); + if (a_importance != b_importance) { + return a_importance > b_importance; + } + + // Now sort by recency + // Zero if empty, otherwise the time that the event occured + uint64_t a_recency = sourceModel()->data(left_idx, RoomlistModel::Timestamp).toULongLong(); + uint64_t b_recency = sourceModel()->data(right_idx, RoomlistModel::Timestamp).toULongLong(); + + if (a_recency != b_recency) + return a_recency > b_recency; + else + return left.row() < right.row(); +} + +FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *parent) + : QSortFilterProxyModel(parent) + , roomlistmodel(model) +{ + this->sortByImportance = UserSettings::instance()->sortByImportance(); + setSourceModel(model); + setDynamicSortFilter(true); + + QObject::connect(UserSettings::instance().get(), + &UserSettings::roomSortingChanged, + this, + [this](bool sortByImportance_) { + this->sortByImportance = sortByImportance_; + invalidate(); + }); + + connect(roomlistmodel, + &RoomlistModel::currentRoomChanged, + this, + &FilteredRoomlistModel::currentRoomChanged); + + sort(0); +} + +void +FilteredRoomlistModel::updateHiddenTagsAndSpaces() +{ + hiddenTags.clear(); + hiddenSpaces.clear(); + for (const auto &t : UserSettings::instance()->hiddenTags()) { + if (t.startsWith("tag:")) + hiddenTags.push_back(t.mid(4)); + else if (t.startsWith("space:")) + hiddenSpaces.push_back(t.mid(6)); + } + + invalidateFilter(); +} + +bool +FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const +{ + if (filterType == FilterBy::Nothing) { + if (!hiddenTags.empty()) { + auto tags = + sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags) + .toStringList(); + + for (const auto &t : tags) + if (hiddenTags.contains(t)) + return false; + } + + return true; + } else if (filterType == FilterBy::Tag) { + auto tags = sourceModel() + ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags) + .toStringList(); + + if (!tags.contains(filterStr)) + return false; + else if (!hiddenTags.empty()) { + for (const auto &t : tags) + if (t != filterStr && hiddenTags.contains(t)) + return false; + } + return true; + } else { + return true; + } +} + +void +FilteredRoomlistModel::toggleTag(QString roomid, QString tag, bool on) +{ + if (on) { + http::client()->put_tag( + roomid.toStdString(), tag.toStdString(), {}, [tag](mtx::http::RequestErr err) { + if (err) { + nhlog::ui()->error("Failed to add tag: {}, {}", + tag.toStdString(), + err->matrix_error.error); + } + }); + } else { + http::client()->delete_tag( + roomid.toStdString(), tag.toStdString(), [tag](mtx::http::RequestErr err) { + if (err) { + nhlog::ui()->error("Failed to delete tag: {}, {}", + tag.toStdString(), + err->matrix_error.error); + } + }); + } +} + +void +FilteredRoomlistModel::nextRoom() +{ + auto r = currentRoom(); + + if (r) { + int idx = roomidToIndex(r->roomId()); + idx++; + if (idx < rowCount()) { + setCurrentRoom( + data(index(idx, 0), RoomlistModel::Roles::RoomId).toString()); + } + } +} + +void +FilteredRoomlistModel::previousRoom() +{ + auto r = currentRoom(); + + if (r) { + int idx = roomidToIndex(r->roomId()); + idx--; + if (idx > 0) { + setCurrentRoom( + data(index(idx, 0), RoomlistModel::Roles::RoomId).toString()); + } + } +} diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h new file mode 100644
index 00000000..b0244886 --- /dev/null +++ b/src/timeline/RoomlistModel.h
@@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <CacheStructs.h> +#include <QAbstractListModel> +#include <QHash> +#include <QSharedPointer> +#include <QSortFilterProxyModel> +#include <QString> +#include <set> + +#include <mtx/responses/sync.hpp> + +#include "TimelineModel.h" + +class TimelineViewManager; + +class RoomlistModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET + resetCurrentRoom) +public: + enum Roles + { + AvatarUrl = Qt::UserRole, + RoomName, + RoomId, + LastMessage, + Time, + Timestamp, + HasUnreadMessages, + HasLoudNotification, + NotificationCount, + IsInvite, + IsSpace, + Tags, + }; + + RoomlistModel(TimelineViewManager *parent = nullptr); + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return (int)roomids.size(); + } + QVariant data(const QModelIndex &index, int role) const override; + QSharedPointer<TimelineModel> getRoomById(QString id) const + { + if (models.contains(id)) + return models.value(id); + else + return {}; + } + +public slots: + void initializeRooms(); + void sync(const mtx::responses::Rooms &rooms); + void clear(); + int roomidToIndex(QString roomid) + { + for (int i = 0; i < (int)roomids.size(); i++) { + if (roomids[i] == roomid) + return i; + } + + return -1; + } + void acceptInvite(QString roomid); + void declineInvite(QString roomid); + void leave(QString roomid); + TimelineModel *currentRoom() const { return currentRoom_.get(); } + void setCurrentRoom(QString roomid); + void resetCurrentRoom() + { + currentRoom_ = nullptr; + emit currentRoomChanged(); + } + +private slots: + void updateReadStatus(const std::map<QString, bool> roomReadStatus_); + +signals: + void totalUnreadMessageCountUpdated(int unreadMessages); + void currentRoomChanged(); + +private: + void addRoom(const QString &room_id, bool suppressInsertNotification = false); + + TimelineViewManager *manager = nullptr; + std::vector<QString> roomids; + QHash<QString, RoomInfo> invites; + QHash<QString, QSharedPointer<TimelineModel>> models; + std::map<QString, bool> roomReadStatus; + + QSharedPointer<TimelineModel> currentRoom_; + + friend class FilteredRoomlistModel; +}; + +class FilteredRoomlistModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged RESET + resetCurrentRoom) +public: + FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr); + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + bool filterAcceptsRow(int sourceRow, const QModelIndex &) const override; + +public slots: + int roomidToIndex(QString roomid) + { + return mapFromSource(roomlistmodel->index(roomlistmodel->roomidToIndex(roomid))) + .row(); + } + void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); } + void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); } + void leave(QString roomid) { roomlistmodel->leave(roomid); } + void toggleTag(QString roomid, QString tag, bool on); + + TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); } + void setCurrentRoom(QString roomid) { roomlistmodel->setCurrentRoom(std::move(roomid)); } + void resetCurrentRoom() { roomlistmodel->resetCurrentRoom(); } + + void nextRoom(); + void previousRoom(); + + void updateFilterTag(QString tagId) + { + if (tagId.startsWith("tag:")) { + filterType = FilterBy::Tag; + filterStr = tagId.mid(4); + } else { + filterType = FilterBy::Nothing; + filterStr.clear(); + } + + invalidateFilter(); + } + + void updateHiddenTagsAndSpaces(); + +signals: + void currentRoomChanged(); + +private: + short int calculateImportance(const QModelIndex &idx) const; + RoomlistModel *roomlistmodel; + bool sortByImportance = true; + + enum class FilterBy + { + Tag, + Space, + Nothing, + }; + QString filterStr = ""; + FilterBy filterType = FilterBy::Nothing; + QStringList hiddenTags, hiddenSpaces; +}; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 8df17457..f29f929e 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp
@@ -318,6 +318,8 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj , room_id_(room_id) , manager_(manager) { + lastMessage_.timestamp = 0; + connect( this, &TimelineModel::redactionFailed, @@ -572,7 +574,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r !event_id(event).empty() && event_id(event).front() == '$'); case IsEncrypted: { auto id = event_id(event); - auto encrypted_event = events.get(id, id, false); + auto encrypted_event = events.get(id, "", false); return encrypted_event && std::holds_alternative< mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( @@ -581,7 +583,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r case Trustlevel: { auto id = event_id(event); - auto encrypted_event = events.get(id, id, false); + auto encrypted_event = events.get(id, "", false); if (encrypted_event) { if (auto encrypted = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( @@ -724,6 +726,20 @@ TimelineModel::fetchMore(const QModelIndex &) } void +TimelineModel::sync(const mtx::responses::JoinedRoom &room) +{ + this->syncState(room.state); + this->addEvents(room.timeline); + + if (room.unread_notifications.highlight_count != highlight_count || + room.unread_notifications.notification_count != notification_count) { + notification_count = room.unread_notifications.notification_count; + highlight_count = room.unread_notifications.highlight_count; + emit notificationsChanged(); + } +} + +void TimelineModel::syncState(const mtx::responses::State &s) { using namespace mtx::events; @@ -866,14 +882,17 @@ TimelineModel::updateLastMessage() if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) { auto time = mtx::accessors::origin_server_ts(*event); uint64_t ts = time.toMSecsSinceEpoch(); - emit manager_->updateRoomsLastMessage( - room_id_, + auto description = DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)), QString::fromStdString(http::client()->user_id().to_string()), tr("You joined this room."), utils::descriptiveTime(time), ts, - time}); + time}; + if (description != lastMessage_) { + lastMessage_ = description; + emit lastMessageChanged(); + } return; } if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event)) @@ -884,7 +903,10 @@ TimelineModel::updateLastMessage() QString::fromStdString(http::client()->user_id().to_string()), cache::displayName(room_id_, QString::fromStdString(mtx::accessors::sender(*event)))); - emit manager_->updateRoomsLastMessage(room_id_, description); + if (description != lastMessage_) { + lastMessage_ = description; + emit lastMessageChanged(); + } return; } } @@ -1867,6 +1889,17 @@ TimelineModel::roomName() const } QString +TimelineModel::plainRoomName() const +{ + auto info = cache::getRoomInfo({room_id_.toStdString()}); + + if (!info.count(room_id_)) + return ""; + else + return QString::fromStdString(info[room_id_].name); +} + +QString TimelineModel::roomAvatarUrl() const { auto info = cache::getRoomInfo({room_id_.toStdString()}); diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 92fccd2d..3ebbe120 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h
@@ -14,6 +14,7 @@ #include <mtxclient/http/errors.hpp> #include "CacheCryptoStructs.h" +#include "CacheStructs.h" #include "EventStore.h" #include "InputBar.h" #include "Permissions.h" @@ -253,12 +254,15 @@ public: } void updateLastMessage(); + void sync(const mtx::responses::JoinedRoom &room); void addEvents(const mtx::responses::Timeline &events); void syncState(const mtx::responses::State &state); template<class T> void sendMessageEvent(const T &content, mtx::events::EventType eventType); RelatedInfo relatedInfo(QString id); + DescInfo lastMessage() const { return lastMessage_; } + public slots: void setCurrentIndex(int index); int currentIndex() const { return idToIndex(currentId); } @@ -303,12 +307,16 @@ public slots: } QString roomName() const; + QString plainRoomName() const; QString roomTopic() const; InputBar *input() { return &input_; } Permissions *permissions() { return &permissions_; } QString roomAvatarUrl() const; QString roomId() const { return room_id_; } + bool hasMentions() { return highlight_count > 0; } + int notificationCount() { return notification_count; } + QString scrollTarget() const; private slots: @@ -328,6 +336,9 @@ signals: void newCallEvent(const mtx::events::collections::TimelineEvents &event); void scrollToIndex(int index); + void lastMessageChanged(); + void notificationsChanged(); + void openRoomSettingsDialog(RoomSettings *settings); void newMessageToSend(mtx::events::collections::TimelineEvents event); @@ -372,7 +383,11 @@ private: QString eventIdToShow; int showEventTimerCounter = 0; + DescInfo lastMessage_{}; + friend struct SendMessageVisitor; + + int notification_count = 0, highlight_count = 0; }; template<class T> diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 0785e3e1..a45294d1 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp
@@ -4,7 +4,6 @@ #include "TimelineViewManager.h" -#include <QDesktopServices> #include <QDropEvent> #include <QMetaType> #include <QPalette> @@ -32,6 +31,7 @@ #include "emoji/Provider.h" #include "ui/NhekoCursorShape.h" #include "ui/NhekoDropArea.h" +#include "ui/NhekoGlobalObject.h" Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents) Q_DECLARE_METATYPE(std::vector<DeviceInfo>) @@ -87,21 +87,6 @@ removeReplyFallback(mtx::events::Event<T> &e) } void -TimelineViewManager::updateEncryptedDescriptions() -{ - auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar(); - QHash<QString, QSharedPointer<TimelineModel>>::iterator i; - for (i = models.begin(); i != models.end(); ++i) { - auto ptr = i.value(); - - if (!ptr.isNull()) { - ptr->setDecryptDescription(decrypt); - ptr->updateLastMessage(); - } - } -} - -void TimelineViewManager::updateColorPalette() { userColors.clear(); @@ -143,10 +128,13 @@ TimelineViewManager::userStatus(QString id) const } TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *parent) - : imgProvider(new MxcImageProvider()) + : QObject(parent) + , imgProvider(new MxcImageProvider()) , colorImgProvider(new ColorImageProvider()) , blurhashProvider(new BlurhashProvider()) , callManager_(callManager) + , rooms_(new RoomlistModel(this)) + , communities_(new CommunitiesModel(this)) { qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>(); qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>(); @@ -204,6 +192,26 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); return ptr; }); + qmlRegisterSingletonType<RoomlistModel>( + "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * { + auto ptr = new FilteredRoomlistModel(self->rooms_); + + connect(self->communities_, + &CommunitiesModel::currentTagIdChanged, + ptr, + &FilteredRoomlistModel::updateFilterTag); + connect(self->communities_, + &CommunitiesModel::hiddenTagsChanged, + ptr, + &FilteredRoomlistModel::updateHiddenTagsAndSpaces); + return ptr; + }); + qmlRegisterSingletonType<RoomlistModel>( + "im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * { + auto ptr = self->communities_; + QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); + return ptr; + }); qmlRegisterSingletonType<UserSettings>( "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * { auto ptr = ChatPage::instance()->userSettings().data(); @@ -220,6 +228,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par "im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * { return new Clipboard(); }); + qmlRegisterSingletonType<Nheko>( + "im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * { + return new Nheko(); + }); qRegisterMetaType<mtx::events::collections::TimelineEvents>(); qRegisterMetaType<std::vector<DeviceInfo>>(); @@ -235,7 +247,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par "Error: Only enums"); #ifdef USE_QUICK_VIEW - view = new QQuickView(); + view = new QQuickView(parent); container = QWidget::createWindowContainer(view, parent); #else view = new QQuickWidget(parent); @@ -252,13 +264,9 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par view->engine()->addImageProvider("MxcImage", imgProvider); view->engine()->addImageProvider("colorimage", colorImgProvider); view->engine()->addImageProvider("blurhash", blurhashProvider); - view->setSource(QUrl("qrc:///qml/TimelineView.qml")); + view->setSource(QUrl("qrc:///qml/Root.qml")); connect(parent, &ChatPage::themeChanged, this, &TimelineViewManager::updateColorPalette); - connect(parent, - &ChatPage::decryptSidebarChanged, - this, - &TimelineViewManager::updateEncryptedDescriptions); connect( dynamic_cast<ChatPage *>(parent), &ChatPage::receivedRoomDeviceVerificationRequest, @@ -329,100 +337,28 @@ TimelineViewManager::setVideoCallItem() } void -TimelineViewManager::sync(const mtx::responses::Rooms &rooms) +TimelineViewManager::sync(const mtx::responses::Rooms &rooms_res) { - for (const auto &[room_id, room] : rooms.join) { - // addRoom will only add the room, if it doesn't exist - addRoom(QString::fromStdString(room_id)); - const auto &room_model = models.value(QString::fromStdString(room_id)); - if (!isInitialSync_) - connect(room_model.data(), - &TimelineModel::newCallEvent, - callManager_, - &CallManager::syncEvent); - room_model->syncState(room.state); - room_model->addEvents(room.timeline); - if (!isInitialSync_) - disconnect(room_model.data(), - &TimelineModel::newCallEvent, - callManager_, - &CallManager::syncEvent); + this->rooms_->sync(rooms_res); + this->communities_->sync(rooms_res); - if (ChatPage::instance()->userSettings()->typingNotifications()) { - for (const auto &ev : room.ephemeral.events) { - if (auto t = std::get_if< - mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>( - &ev)) { - std::vector<QString> typing; - typing.reserve(t->content.user_ids.size()); - for (const auto &user : t->content.user_ids) { - if (user != http::client()->user_id().to_string()) - typing.push_back( - QString::fromStdString(user)); - } - room_model->updateTypingUsers(typing); - } - } - } - } - - this->isInitialSync_ = false; - emit initialSyncChanged(false); -} - -void -TimelineViewManager::addRoom(const QString &room_id) -{ - if (!models.contains(room_id)) { - QSharedPointer<TimelineModel> newRoom(new TimelineModel(this, room_id)); - newRoom->setDecryptDescription( - ChatPage::instance()->userSettings()->decryptSidebar()); - - connect(newRoom.data(), - &TimelineModel::newEncryptedImage, - imgProvider, - &MxcImageProvider::addEncryptionInfo); - connect(newRoom.data(), - &TimelineModel::forwardToRoom, - this, - &TimelineViewManager::forwardMessageToRoom); - models.insert(room_id, std::move(newRoom)); + if (isInitialSync_) { + this->isInitialSync_ = false; + emit initialSyncChanged(false); } } void -TimelineViewManager::setHistoryView(const QString &room_id) -{ - nhlog::ui()->info("Trying to activate room {}", room_id.toStdString()); - - auto room = models.find(room_id); - if (room != models.end()) { - timeline_ = room.value().data(); - emit activeTimelineChanged(timeline_); - container->setFocus(); - nhlog::ui()->info("Activated room {}", room_id.toStdString()); - } -} - -void -TimelineViewManager::highlightRoom(const QString &room_id) -{ - ChatPage::instance()->highlightRoom(room_id); -} - -void TimelineViewManager::showEvent(const QString &room_id, const QString &event_id) { - auto room = models.find(room_id); - if (room != models.end()) { - if (timeline_ != room.value().data()) { - timeline_ = room.value().data(); - emit activeTimelineChanged(timeline_); + if (auto room = rooms_->getRoomById(room_id)) { + if (rooms_->currentRoom() != room) { + rooms_->setCurrentRoom(room_id); container->setFocus(); nhlog::ui()->info("Activated room {}", room_id.toStdString()); } - timeline_->showEvent(event_id); + room->showEvent(event_id); } } @@ -457,12 +393,14 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) auto imgDialog = new dialogs::ImageOverlay(pixmap); imgDialog->showFullScreen(); - connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId, imgDialog]() { + + auto room = rooms_->currentRoom(); + connect(imgDialog, &dialogs::ImageOverlay::saving, room, [eventId, imgDialog, room]() { // hide the overlay while presenting the save dialog for better // cross platform support. imgDialog->hide(); - if (!timeline_->saveMedia(eventId)) { + if (!room->saveMedia(eventId)) { imgDialog->show(); } else { imgDialog->close(); @@ -471,70 +409,20 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) } void -TimelineViewManager::openLink(QString link) const -{ - QUrl url(link); - if (url.scheme() == "https" && url.host() == "matrix.to") { - // handle matrix.to links internally - QString p = url.fragment(QUrl::FullyEncoded); - if (p.startsWith("/")) - p.remove(0, 1); - - auto temp = p.split("?"); - QString query; - if (temp.size() >= 2) - query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8()); - - temp = temp.first().split("/"); - auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8()); - QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8()); - if (!identifier.isEmpty()) { - if (identifier.startsWith("@")) { - QByteArray uri = - "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!query.isEmpty()) - uri.append("?" + query.toUtf8()); - ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); - } else if (identifier.startsWith("#")) { - QByteArray uri = - "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!eventId.isEmpty()) - uri.append("/e/" + - QUrl::toPercentEncoding(eventId.remove(0, 1))); - if (!query.isEmpty()) - uri.append("?" + query.toUtf8()); - ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); - } else if (identifier.startsWith("!")) { - QByteArray uri = "matrix:roomid/" + - QUrl::toPercentEncoding(identifier.remove(0, 1)); - if (!eventId.isEmpty()) - uri.append("/e/" + - QUrl::toPercentEncoding(eventId.remove(0, 1))); - if (!query.isEmpty()) - uri.append("?" + query.toUtf8()); - ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); - } - } - } else { - QDesktopServices::openUrl(url); - } -} - -void TimelineViewManager::openInviteUsersDialog() { MainWindow::instance()->openInviteUsersDialog( [this](const QStringList &invitees) { emit inviteUsers(invitees); }); } void -TimelineViewManager::openMemberListDialog() const +TimelineViewManager::openMemberListDialog(QString roomid) const { - MainWindow::instance()->openMemberListDialog(timeline_->roomId()); + MainWindow::instance()->openMemberListDialog(roomid); } void -TimelineViewManager::openLeaveRoomDialog() const +TimelineViewManager::openLeaveRoomDialog(QString roomid) const { - MainWindow::instance()->openLeaveRoomDialog(timeline_->roomId()); + MainWindow::instance()->openLeaveRoomDialog(roomid); } void @@ -550,17 +438,21 @@ TimelineViewManager::verifyUser(QString userid) if (std::find(room_members.begin(), room_members.end(), (userid).toStdString()) != room_members.end()) { - auto model = models.value(QString::fromStdString(room_id)); - auto flow = DeviceVerificationFlow::InitiateUserVerification( - this, model.data(), userid); - connect(model.data(), - &TimelineModel::updateFlowEventId, - this, - [this, flow](std::string eventId) { - dvList[QString::fromStdString(eventId)] = flow; - }); - emit newDeviceVerificationRequest(flow.data()); - return; + if (auto model = + rooms_->getRoomById(QString::fromStdString(room_id))) { + auto flow = + DeviceVerificationFlow::InitiateUserVerification( + this, model.data(), userid); + connect(model.data(), + &TimelineModel::updateFlowEventId, + this, + [this, flow](std::string eventId) { + dvList[QString::fromStdString(eventId)] = + flow; + }); + emit newDeviceVerificationRequest(flow.data()); + return; + } } } } @@ -593,26 +485,24 @@ void TimelineViewManager::updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids) { - auto room = models.find(room_id); - if (room != models.end()) { - room.value()->markEventsAsRead(event_ids); + if (auto room = rooms_->getRoomById(room_id)) { + room->markEventsAsRead(event_ids); } } void TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::string &session_id) { - auto room = models.find(QString::fromStdString(room_id)); - if (room != models.end()) { - room.value()->receivedSessionKey(session_id); + if (auto room = rooms_->getRoomById(QString::fromStdString(room_id))) { + room->receivedSessionKey(session_id); } } void -TimelineViewManager::initWithMessages(const std::vector<QString> &roomIds) +TimelineViewManager::initializeRoomlist() { - for (const auto &roomId : roomIds) - addRoom(roomId); + rooms_->initializeRooms(); + communities_->initializeSidebar(); } void @@ -620,74 +510,42 @@ TimelineViewManager::queueReply(const QString &roomid, const QString &repliedToEvent, const QString &replyBody) { - auto room = models.find(roomid); - if (room != models.end()) { - room.value()->setReply(repliedToEvent); - room.value()->input()->message(replyBody); + if (auto room = rooms_->getRoomById(roomid)) { + room->setReply(repliedToEvent); + room->input()->message(replyBody); } } void -TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QString &reactionKey) -{ - if (!timeline_) - return; - - auto reactions = timeline_->reactions(reactedEvent.toStdString()); - - QString selfReactedEvent; - for (const auto &reaction : reactions) { - if (reactionKey == reaction.key_) { - selfReactedEvent = reaction.selfReactedEvent_; - break; - } - } - - if (selfReactedEvent.startsWith("m")) - return; - - // If selfReactedEvent is empty, that means we haven't previously reacted - if (selfReactedEvent.isEmpty()) { - mtx::events::msg::Reaction reaction; - mtx::common::Relation rel; - rel.rel_type = mtx::common::RelationType::Annotation; - rel.event_id = reactedEvent.toStdString(); - rel.key = reactionKey.toStdString(); - reaction.relations.relations.push_back(rel); - - timeline_->sendMessageEvent(reaction, mtx::events::EventType::Reaction); - // Otherwise, we have previously reacted and the reaction should be redacted - } else { - timeline_->redactEvent(selfReactedEvent); - } -} -void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &callInvite) { - models.value(roomid)->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite); + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite); } void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &callCandidates) { - models.value(roomid)->sendMessageEvent(callCandidates, - mtx::events::EventType::CallCandidates); + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callCandidates, mtx::events::EventType::CallCandidates); } void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &callAnswer) { - models.value(roomid)->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer); + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer); } void TimelineViewManager::queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &callHangUp) { - models.value(roomid)->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp); + if (auto room = rooms_->getRoomById(roomid)) + room->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp); } void @@ -738,7 +596,7 @@ void TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId) { - auto room = models.find(roomId); + auto room = rooms_->getRoomById(roomId); auto content = mtx::accessors::url(*e); std::optional<mtx::crypto::EncryptedFile> encryptionInfo = mtx::accessors::file(*e); @@ -781,12 +639,15 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven ev.content.url = url; } - auto room = models.find(roomId); - removeReplyFallback(ev); - ev.content.relations.relations.clear(); - room.value()->sendMessageEvent( - ev.content, - mtx::events::EventType::RoomMessage); + if (auto room = rooms_->getRoomById(roomId)) { + removeReplyFallback(ev); + ev.content.relations.relations + .clear(); + room->sendMessageEvent( + ev.content, + mtx::events::EventType:: + RoomMessage); + } } }, *e); @@ -804,8 +665,7 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven mtx::events::EventType::RoomMessage) { e.content.relations.relations.clear(); removeReplyFallback(e); - room.value()->sendMessageEvent(e.content, - mtx::events::EventType::RoomMessage); + room->sendMessageEvent(e.content, mtx::events::EventType::RoomMessage); } }, *e); diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index b23a61db..556bcf4c 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h
@@ -22,6 +22,8 @@ #include "WebRTCSession.h" #include "emoji/EmojiModel.h" #include "emoji/Provider.h" +#include "timeline/CommunitiesModel.h" +#include "timeline/RoomlistModel.h" class MxcImageProvider; class BlurhashProvider; @@ -35,12 +37,8 @@ class TimelineViewManager : public QObject Q_OBJECT Q_PROPERTY( - TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) - Q_PROPERTY( bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged) Q_PROPERTY( - bool isNarrowView MEMBER isNarrowView_ READ isNarrowView NOTIFY narrowViewChanged) - Q_PROPERTY( bool isWindowFocused MEMBER isWindowFocused_ READ isWindowFocused NOTIFY focusChanged) public: @@ -48,48 +46,38 @@ public: QWidget *getWidget() const { return container; } void sync(const mtx::responses::Rooms &rooms); - void addRoom(const QString &room_id); - void clearAll() - { - timeline_ = nullptr; - emit activeTimelineChanged(nullptr); - models.clear(); - } + MxcImageProvider *imageProvider() { return imgProvider; } + CallManager *callManager() { return callManager_; } + + void clearAll() { rooms_->clear(); } - Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } - bool isNarrowView() const { return isNarrowView_; } bool isWindowFocused() const { return isWindowFocused_; } Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId); Q_INVOKABLE QColor userColor(QString id, QColor background); Q_INVOKABLE QString escapeEmoji(QString str) const; + Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); } Q_INVOKABLE QString userPresence(QString id) const; Q_INVOKABLE QString userStatus(QString id) const; - Q_INVOKABLE void openLink(QString link) const; - Q_INVOKABLE void focusMessageInput(); Q_INVOKABLE void openInviteUsersDialog(); - Q_INVOKABLE void openMemberListDialog() const; - Q_INVOKABLE void openLeaveRoomDialog() const; + Q_INVOKABLE void openMemberListDialog(QString roomid) const; + Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const; Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow); void verifyUser(QString userid); void verifyDevice(QString userid, QString deviceid); signals: - void clearRoomMessageCount(QString roomid); - void updateRoomsLastMessage(QString roomid, const DescInfo &info); void activeTimelineChanged(TimelineModel *timeline); void initialSyncChanged(bool isInitialSync); void replyingEventChanged(QString replyingEvent); void replyClosed(); void newDeviceVerificationRequest(DeviceVerificationFlow *flow); void inviteUsers(QStringList users); - void showRoomList(); - void narrowViewChanged(); void focusChanged(); void focusInput(); void openImageOverlayInternalCb(QString eventId, QImage img); @@ -98,58 +86,32 @@ signals: public slots: void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); void receivedSessionKey(const std::string &room_id, const std::string &session_id); - void initWithMessages(const std::vector<QString> &roomIds); + void initializeRoomlist(); void chatFocusChanged(bool focused) { isWindowFocused_ = focused; emit focusChanged(); } - void setHistoryView(const QString &room_id); - void highlightRoom(const QString &room_id); void showEvent(const QString &room_id, const QString &event_id); void focusTimeline(); - TimelineModel *getHistoryView(const QString &room_id) - { - auto room = models.find(room_id); - if (room != models.end()) - return room.value().data(); - else - return nullptr; - } void updateColorPalette(); void queueReply(const QString &roomid, const QString &repliedToEvent, const QString &replyBody); - void queueReactionMessage(const QString &reactedEvent, const QString &reactionKey); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &); void queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &); - void updateEncryptedDescriptions(); void setVideoCallItem(); - void enableBackButton() - { - if (isNarrowView_) - return; - isNarrowView_ = true; - emit narrowViewChanged(); - } - void disableBackButton() - { - if (!isNarrowView_) - return; - isNarrowView_ = false; - emit narrowViewChanged(); - } - - void backToRooms() { emit showRoomList(); } QObject *completerFor(QString completerName, QString roomId = ""); void forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId); + RoomlistModel *rooms() { return rooms_; } + private slots: void openImageOverlayInternal(QString eventId, QImage img); @@ -165,14 +127,14 @@ private: ColorImageProvider *colorImgProvider; BlurhashProvider *blurhashProvider; - QHash<QString, QSharedPointer<TimelineModel>> models; - TimelineModel *timeline_ = nullptr; CallManager *callManager_ = nullptr; bool isInitialSync_ = true; - bool isNarrowView_ = false; bool isWindowFocused_ = false; + RoomlistModel *rooms_ = nullptr; + CommunitiesModel *communities_ = nullptr; + QHash<QString, QColor> userColors; QHash<QString, QSharedPointer<DeviceVerificationFlow>> dvList; diff --git a/src/ui/NhekoDropArea.cpp b/src/ui/NhekoDropArea.cpp
index 54f48d3c..bbcedd7e 100644 --- a/src/ui/NhekoDropArea.cpp +++ b/src/ui/NhekoDropArea.cpp
@@ -35,7 +35,7 @@ void NhekoDropArea::dropEvent(QDropEvent *event) { if (event) { - auto model = ChatPage::instance()->timelineManager()->getHistoryView(roomid_); + auto model = ChatPage::instance()->timelineManager()->rooms()->getRoomById(roomid_); if (model) { model->input()->insertMimeData(event->mimeData()); } diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp new file mode 100644
index 00000000..fea10839 --- /dev/null +++ b/src/ui/NhekoGlobalObject.cpp
@@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "NhekoGlobalObject.h" + +#include <QDesktopServices> +#include <QUrl> + +#include "Cache_p.h" +#include "ChatPage.h" +#include "Logging.h" +#include "MainWindow.h" +#include "UserSettingsPage.h" +#include "Utils.h" + +Nheko::Nheko() +{ + connect( + UserSettings::instance().get(), &UserSettings::themeChanged, this, &Nheko::colorsChanged); + connect(ChatPage::instance(), &ChatPage::contentLoaded, this, &Nheko::updateUserProfile); +} + +void +Nheko::updateUserProfile() +{ + if (cache::client() && cache::client()->isInitialized()) + currentUser_.reset( + new UserProfile("", utils::localUser(), ChatPage::instance()->timelineManager())); + else + currentUser_.reset(); + emit profileChanged(); +} + +QPalette +Nheko::colors() const +{ + return Theme::paletteFromTheme(UserSettings::instance()->theme().toStdString()); +} + +QPalette +Nheko::inactiveColors() const +{ + auto p = colors(); + p.setCurrentColorGroup(QPalette::ColorGroup::Inactive); + return p; +} + +Theme +Nheko::theme() const +{ + return Theme(UserSettings::instance()->theme().toStdString()); +} + +void +Nheko::openLink(QString link) const +{ + QUrl url(link); + if (url.scheme() == "https" && url.host() == "matrix.to") { + // handle matrix.to links internally + QString p = url.fragment(QUrl::FullyEncoded); + if (p.startsWith("/")) + p.remove(0, 1); + + auto temp = p.split("?"); + QString query; + if (temp.size() >= 2) + query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8()); + + temp = temp.first().split("/"); + auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8()); + QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8()); + if (!identifier.isEmpty()) { + if (identifier.startsWith("@")) { + QByteArray uri = + "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); + if (!query.isEmpty()) + uri.append("?" + query.toUtf8()); + ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); + } else if (identifier.startsWith("#")) { + QByteArray uri = + "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); + if (!eventId.isEmpty()) + uri.append("/e/" + + QUrl::toPercentEncoding(eventId.remove(0, 1))); + if (!query.isEmpty()) + uri.append("?" + query.toUtf8()); + ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); + } else if (identifier.startsWith("!")) { + QByteArray uri = "matrix:roomid/" + + QUrl::toPercentEncoding(identifier.remove(0, 1)); + if (!eventId.isEmpty()) + uri.append("/e/" + + QUrl::toPercentEncoding(eventId.remove(0, 1))); + if (!query.isEmpty()) + uri.append("?" + query.toUtf8()); + ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); + } + } + } else { + QDesktopServices::openUrl(url); + } +} +void +Nheko::setStatusMessage(QString msg) const +{ + ChatPage::instance()->setStatus(msg); +} + +UserProfile * +Nheko::currentUser() const +{ + nhlog::ui()->debug("Profile requested"); + + return currentUser_.get(); +} + +void +Nheko::showUserSettingsPage() const +{ + ChatPage::instance()->showUserSettingsPage(); +} + +void +Nheko::openLogoutDialog() const +{ + MainWindow::instance()->openLogoutDialog(); +} + +void +Nheko::openCreateRoomDialog() const +{ + MainWindow::instance()->openCreateRoomDialog( + [](const mtx::requests::CreateRoom &req) { ChatPage::instance()->createRoom(req); }); +} + +void +Nheko::openJoinRoomDialog() const +{ + MainWindow::instance()->openJoinRoomDialog( + [](const QString &room_id) { ChatPage::instance()->joinRoom(room_id); }); +} diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h new file mode 100644
index 00000000..14135fd1 --- /dev/null +++ b/src/ui/NhekoGlobalObject.h
@@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <QObject> +#include <QPalette> + +#include "Theme.h" +#include "UserProfile.h" + +class Nheko : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QPalette colors READ colors NOTIFY colorsChanged) + Q_PROPERTY(QPalette inactiveColors READ inactiveColors NOTIFY colorsChanged) + Q_PROPERTY(Theme theme READ theme NOTIFY colorsChanged) + Q_PROPERTY(int avatarSize READ avatarSize CONSTANT) + Q_PROPERTY(int paddingSmall READ paddingSmall CONSTANT) + Q_PROPERTY(int paddingMedium READ paddingMedium CONSTANT) + Q_PROPERTY(int paddingLarge READ paddingLarge CONSTANT) + + Q_PROPERTY(UserProfile *currentUser READ currentUser NOTIFY profileChanged) + +public: + Nheko(); + + QPalette colors() const; + QPalette inactiveColors() const; + Theme theme() const; + + int avatarSize() const { return 40; } + + int paddingSmall() const { return 4; } + int paddingMedium() const { return 8; } + int paddingLarge() const { return 20; } + UserProfile *currentUser() const; + + Q_INVOKABLE void openLink(QString link) const; + Q_INVOKABLE void setStatusMessage(QString msg) const; + Q_INVOKABLE void showUserSettingsPage() const; + Q_INVOKABLE void openLogoutDialog() const; + Q_INVOKABLE void openCreateRoomDialog() const; + Q_INVOKABLE void openJoinRoomDialog() const; + +public slots: + void updateUserProfile(); + +signals: + void colorsChanged(); + void profileChanged(); + +private: + QScopedPointer<UserProfile> currentUser_; +}; diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp
index 4341bd63..26119393 100644 --- a/src/ui/Theme.cpp +++ b/src/ui/Theme.cpp
@@ -2,76 +2,73 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -#include <QDebug> - #include "Theme.h" -Theme::Theme(QObject *parent) - : QObject(parent) -{ - setColor("Black", ui::Color::Black); - - setColor("BrightWhite", ui::Color::BrightWhite); - setColor("FadedWhite", ui::Color::FadedWhite); - setColor("MediumWhite", ui::Color::MediumWhite); - - setColor("BrightGreen", ui::Color::BrightGreen); - setColor("DarkGreen", ui::Color::DarkGreen); - setColor("LightGreen", ui::Color::LightGreen); - - setColor("Gray", ui::Color::Gray); - setColor("Red", ui::Color::Red); - setColor("Blue", ui::Color::Blue); - - setColor("Transparent", ui::Color::Transparent); -} - -QColor -Theme::rgba(int r, int g, int b, qreal a) const -{ - QColor color(r, g, b); - color.setAlphaF(a); - - return color; -} +Q_DECLARE_METATYPE(Theme) -QColor -Theme::getColor(const QString &key) const +QPalette +Theme::paletteFromTheme(std::string_view theme) { - if (!colors_.contains(key)) { - qWarning() << "Color with key" << key << "could not be found"; - return QColor(); + [[maybe_unused]] static auto meta = qRegisterMetaType<Theme>("Theme"); + static QPalette original; + if (theme == "light") { + QPalette lightActive( + /*windowText*/ QColor("#333"), + /*button*/ QColor("white"), + /*light*/ QColor(0xef, 0xef, 0xef), + /*dark*/ QColor(70, 77, 93), + /*mid*/ QColor(220, 220, 220), + /*text*/ QColor("#333"), + /*bright_text*/ QColor("#f2f5f8"), + /*base*/ QColor("#fff"), + /*window*/ QColor("white")); + lightActive.setColor(QPalette::AlternateBase, QColor("#eee")); + lightActive.setColor(QPalette::Highlight, QColor("#38a3d8")); + lightActive.setColor(QPalette::HighlightedText, QColor("#f4f4f5")); + lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color()); + lightActive.setColor(QPalette::ToolTipText, lightActive.text().color()); + lightActive.setColor(QPalette::Link, QColor("#0077b5")); + lightActive.setColor(QPalette::ButtonText, QColor("#555459")); + return lightActive; + } else if (theme == "dark") { + QPalette darkActive( + /*windowText*/ QColor("#caccd1"), + /*button*/ QColor(0xff, 0xff, 0xff), + /*light*/ QColor("#caccd1"), + /*dark*/ QColor(60, 70, 77), + /*mid*/ QColor("#202228"), + /*text*/ QColor("#caccd1"), + /*bright_text*/ QColor("#f4f5f8"), + /*base*/ QColor("#202228"), + /*window*/ QColor("#2d3139")); + darkActive.setColor(QPalette::AlternateBase, QColor("#2d3139")); + darkActive.setColor(QPalette::Highlight, QColor("#38a3d8")); + darkActive.setColor(QPalette::HighlightedText, QColor("#f4f5f8")); + darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color()); + darkActive.setColor(QPalette::ToolTipText, darkActive.text().color()); + darkActive.setColor(QPalette::Link, QColor("#38a3d8")); + darkActive.setColor(QPalette::ButtonText, "#727274"); + return darkActive; + } else { + return original; } - - return colors_.value(key); } -void -Theme::setColor(const QString &key, const QColor &color) +Theme::Theme(std::string_view theme) { - colors_.insert(key, color); -} - -void -Theme::setColor(const QString &key, ui::Color color) -{ - static const QColor palette[] = { - QColor("#171919"), - - QColor("#EBEBEB"), - QColor("#C9C9C9"), - QColor("#929292"), - - QColor("#1C3133"), - QColor("#577275"), - QColor("#46A451"), - - QColor("#5D6565"), - QColor("#E22826"), - QColor("#81B3A9"), - - rgba(0, 0, 0, 0), - }; - - colors_.insert(key, palette[static_cast<int>(color)]); + auto p = paletteFromTheme(theme); + separator_ = p.mid().color(); + if (theme == "light") { + sidebarBackground_ = QColor("#233649"); + alternateButton_ = QColor("#ccc"); + red_ = QColor("#a82353"); + } else if (theme == "dark") { + sidebarBackground_ = QColor("#2d3139"); + alternateButton_ = QColor("#414A59"); + red_ = QColor("#a82353"); + } else { + sidebarBackground_ = p.window().color(); + alternateButton_ = p.dark().color(); + red_ = QColor("red"); + } } diff --git a/src/ui/Theme.h b/src/ui/Theme.h
index 3243c076..b5bcd4dd 100644 --- a/src/ui/Theme.h +++ b/src/ui/Theme.h
@@ -5,8 +5,7 @@ #pragma once #include <QColor> -#include <QHash> -#include <QObject> +#include <QPalette> namespace ui { enum class AvatarType @@ -60,36 +59,25 @@ enum class ProgressType IndeterminateProgress }; -enum class Color -{ - Black, - BrightWhite, - FadedWhite, - MediumWhite, - DarkGreen, - LightGreen, - BrightGreen, - Gray, - Red, - Blue, - Transparent -}; - } // namespace ui -class Theme : public QObject +class Theme : public QPalette { - Q_OBJECT + Q_GADGET + Q_PROPERTY(QColor sidebarBackground READ sidebarBackground CONSTANT) + Q_PROPERTY(QColor alternateButton READ alternateButton CONSTANT) + Q_PROPERTY(QColor separator READ separator CONSTANT) + Q_PROPERTY(QColor red READ red CONSTANT) public: - explicit Theme(QObject *parent = nullptr); + Theme() {} + explicit Theme(std::string_view theme); + static QPalette paletteFromTheme(std::string_view theme); - QColor getColor(const QString &key) const; - - void setColor(const QString &key, const QColor &color); - void setColor(const QString &key, ui::Color color); + QColor sidebarBackground() const { return sidebarBackground_; } + QColor alternateButton() const { return alternateButton_; } + QColor separator() const { return separator_; } + QColor red() const { return red_; } private: - QColor rgba(int r, int g, int b, qreal a) const; - - QHash<QString, QColor> colors_; + QColor sidebarBackground_, separator_, red_, alternateButton_; }; diff --git a/src/ui/ThemeManager.cpp b/src/ui/ThemeManager.cpp
index 834f5083..b7b3df40 100644 --- a/src/ui/ThemeManager.cpp +++ b/src/ui/ThemeManager.cpp
@@ -6,18 +6,37 @@ #include "ThemeManager.h" -ThemeManager::ThemeManager() { setTheme(new Theme); } - -void -ThemeManager::setTheme(Theme *theme) -{ - theme_ = theme; - theme_->setParent(this); -} +ThemeManager::ThemeManager() {} QColor ThemeManager::themeColor(const QString &key) const { - Q_ASSERT(theme_); - return theme_->getColor(key); + if (key == "Black") + return QColor("#171919"); + + else if (key == "BrightWhite") + return QColor("#EBEBEB"); + else if (key == "FadedWhite") + return QColor("#C9C9C9"); + else if (key == "MediumWhite") + return QColor("#929292"); + + else if (key == "BrightGreen") + return QColor("#1C3133"); + else if (key == "DarkGreen") + return QColor("#577275"); + else if (key == "LightGreen") + return QColor("#46A451"); + + else if (key == "Gray") + return QColor("#5D6565"); + else if (key == "Red") + return QColor("#E22826"); + else if (key == "Blue") + return QColor("#81B3A9"); + + else if (key == "Transparent") + return QColor(0, 0, 0, 0); + + return (QColor(0, 0, 0, 0)); } diff --git a/src/ui/ThemeManager.h b/src/ui/ThemeManager.h
index f2099730..cbb355fd 100644 --- a/src/ui/ThemeManager.h +++ b/src/ui/ThemeManager.h
@@ -6,8 +6,6 @@ #include <QCommonStyle> -#include "Theme.h" - class ThemeManager : public QCommonStyle { Q_OBJECT @@ -15,7 +13,6 @@ class ThemeManager : public QCommonStyle public: inline static ThemeManager &instance(); - void setTheme(Theme *theme); QColor themeColor(const QString &key) const; private: @@ -23,8 +20,6 @@ private: ThemeManager(ThemeManager const &); void operator=(ThemeManager const &); - - Theme *theme_; }; inline ThemeManager & diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp
index 0f330964..3d9c4b6a 100644 --- a/src/ui/UserProfile.cpp +++ b/src/ui/UserProfile.cpp
@@ -27,9 +27,22 @@ UserProfile::UserProfile(QString roomid, , manager(manager_) , model(parent) { - fetchDeviceList(this->userid_); globalAvatarUrl = ""; + connect(this, + &UserProfile::globalUsernameRetrieved, + this, + &UserProfile::setGlobalUsername, + Qt::QueuedConnection); + + if (isGlobalUserProfile()) { + getGlobalProfileData(); + } + + if (!cache::client() || !cache::client()->isDatabaseReady() || + !ChatPage::instance()->timelineManager()) + return; + connect(cache::client(), &Cache::verificationStatusChanged, this, @@ -53,17 +66,9 @@ UserProfile::UserProfile(QString roomid, : verification::VERIFIED; } deviceList_.reset(deviceList_.deviceList_); + emit devicesChanged(); }); - - connect(this, - &UserProfile::globalUsernameRetrieved, - this, - &UserProfile::setGlobalUsername, - Qt::QueuedConnection); - - if (isGlobalUserProfile()) { - getGlobalProfileData(); - } + fetchDeviceList(this->userid_); } QHash<int, QByteArray> @@ -123,10 +128,7 @@ UserProfile::displayName() QString UserProfile::avatarUrl() { - return (isGlobalUserProfile() && globalAvatarUrl != "") - ? globalAvatarUrl - : cache::avatarUrl(roomid_, userid_); - ; + return isGlobalUserProfile() ? globalAvatarUrl : cache::avatarUrl(roomid_, userid_); } bool @@ -157,6 +159,9 @@ UserProfile::fetchDeviceList(const QString &userID) { auto localUser = utils::localUser(); + if (!cache::client() || !cache::client()->isDatabaseReady()) + return; + cache::client()->query_keys( userID.toStdString(), [other_user_id = userID.toStdString(), this](const UserKeyCache &other_user_keys, @@ -217,6 +222,7 @@ UserProfile::fetchDeviceList(const QString &userID) } this->deviceList_.queueReset(std::move(deviceInfo)); + emit devicesChanged(); }); }); } diff --git a/src/ui/UserProfile.h b/src/ui/UserProfile.h
index bf71d0de..721d7230 100644 --- a/src/ui/UserProfile.h +++ b/src/ui/UserProfile.h
@@ -90,7 +90,7 @@ class UserProfile : public QObject Q_PROPERTY(QString displayName READ displayName NOTIFY displayNameChanged) Q_PROPERTY(QString userid READ userid CONSTANT) Q_PROPERTY(QString avatarUrl READ avatarUrl NOTIFY avatarUrlChanged) - Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList CONSTANT) + Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList NOTIFY devicesChanged) Q_PROPERTY(bool isGlobalUserProfile READ isGlobalUserProfile CONSTANT) Q_PROPERTY(int userVerified READ getUserStatus NOTIFY userStatusChanged) Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged) @@ -133,6 +133,7 @@ signals: void avatarUrlChanged(); void displayError(const QString &errorMessage); void globalUsernameRetrieved(const QString &globalUser); + void devicesChanged(); public slots: void updateAvatarUrl();