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(¬ificationsManager,
&NotificationsManager::notificationClicked,
this,
[this](const QString &roomid, const QString &eventid) {
Q_UNUSED(eventid)
- room_list_->highlightSelectedRoom(roomid);
+ view_manager_->rooms()->setCurrentRoom(roomid);
activateWindow();
});
connect(¬ificationsManager,
&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,
¬ificationsManager,
@@ -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> ¬ifs);
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> ¬ifs)
-{
- nhlog::ui()->debug("Initializing " + std::to_string(notifs.size()) + " notifications.");
-
- for (const auto &item : notifs) {
- for (const auto ¬if : 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 ¤t_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> ¬ifs);
- 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 ¤t_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();
|