mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-10-30 09:30:47 +03:00
Merge pull request #605 from Nheko-Reborn/qml-roomlist
Qml roomlist and stuff
This commit is contained in:
commit
5b4566d3f9
110 changed files with 3487 additions and 4865 deletions
|
@ -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
|
||||
)
|
||||
|
||||
#
|
||||
|
|
1
resources/icons/ui/user-friends-solid.svg
Normal file
1
resources/icons/ui/user-friends-solid.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 747 B |
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
94
resources/qml/ChatPage.qml
Normal file
94
resources/qml/ChatPage.qml
Normal file
|
@ -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
|
||||
}
|
||||
|
||||
}
|
160
resources/qml/CommunitiesList.qml
Normal file
160
resources/qml/CommunitiesList.qml
Normal file
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
28
resources/qml/ElidedLabel.qml
Normal file
28
resources/qml/ElidedLabel.qml
Normal file
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
604
resources/qml/RoomList.qml
Normal file
604
resources/qml/RoomList.qml
Normal file
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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 ;
|
||||
|
|
123
resources/qml/Root.qml
Normal file
123
resources/qml/Root.qml
Normal file
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -31,7 +31,7 @@ ImageButton {
|
|||
}
|
||||
onClicked: {
|
||||
if (model.state == MtxEvent.Read)
|
||||
TimelineManager.timeline.readReceiptsAction(model.id);
|
||||
room.readReceiptsAction(model.id);
|
||||
|
||||
}
|
||||
image: {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
property var room: null
|
||||
property bool showBackButton: false
|
||||
|
||||
palette: colors
|
||||
|
||||
FontMetrics {
|
||||
id: fontMetrics
|
||||
Label {
|
||||
visible: !room && !TimelineManager.isInitialSync
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("No room open")
|
||||
font.pointSize: 24
|
||||
color: Nheko.colors.text
|
||||
}
|
||||
|
||||
EmojiPicker {
|
||||
id: emojiPopup
|
||||
|
||||
colors: palette
|
||||
model: TimelineManager.completerFor("allemoji", "")
|
||||
BusyIndicator {
|
||||
visible: running
|
||||
anchors.centerIn: parent
|
||||
running: TimelineManager.isInitialSync
|
||||
height: 200
|
||||
width: 200
|
||||
z: 3
|
||||
}
|
||||
|
||||
Component {
|
||||
id: userProfileComponent
|
||||
ColumnLayout {
|
||||
id: timelineLayout
|
||||
|
||||
UserProfile {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: roomSettingsComponent
|
||||
|
||||
RoomSettings {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mobileCallInviteDialog
|
||||
|
||||
CallInvite {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: quickSwitcherComponent
|
||||
|
||||
QuickSwitcher {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Component {
|
||||
id: forwardCompleterComponent
|
||||
|
||||
ForwardCompleter {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+K"
|
||||
onActivated: {
|
||||
var quickSwitch = quickSwitcherComponent.createObject(timelineRoot);
|
||||
TimelineManager.focusTimeline();
|
||||
quickSwitch.open();
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
}
|
||||
spacing: 0
|
||||
|
||||
TopBar {
|
||||
showBackButton: timelineView.showBackButton
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: TimelineManager
|
||||
onNewDeviceVerificationRequest: {
|
||||
var dialog = deviceVerificationDialog.createObject(timelineRoot, {
|
||||
"flow": flow
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
onOpenProfile: {
|
||||
var userProfile = userProfileComponent.createObject(timelineRoot, {
|
||||
"profile": profile
|
||||
});
|
||||
userProfile.show();
|
||||
}
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
z: 3
|
||||
color: Nheko.theme.separator
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: TimelineManager.timeline
|
||||
onOpenRoomSettingsDialog: {
|
||||
var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
|
||||
"roomSettings": settings
|
||||
});
|
||||
roomSettings.show();
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: msgView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Nheko.colors.base
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
StackLayout {
|
||||
id: stackLayout
|
||||
|
||||
currentIndex: 0
|
||||
|
||||
Connections {
|
||||
function onRoomChanged() {
|
||||
stackLayout.currentIndex = 0;
|
||||
}
|
||||
|
||||
target: timelineView
|
||||
}
|
||||
|
||||
MessageView {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: msgView.height - typingIndicator.height
|
||||
}
|
||||
|
||||
Loader {
|
||||
source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : ""
|
||||
onLoaded: TimelineManager.setVideoCallItem()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CallManager
|
||||
onNewInviteState: {
|
||||
if (CallManager.haveCallInvite && Settings.mobileMode) {
|
||||
var dialog = mobileCallInviteDialog.createObject(msgView);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
TypingIndicator {
|
||||
id: typingIndicator
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: !TimelineManager.timeline && !TimelineManager.isInitialSync
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("No room open")
|
||||
font.pointSize: 24
|
||||
color: colors.text
|
||||
}
|
||||
CallInviteBar {
|
||||
id: callInviteBar
|
||||
|
||||
BusyIndicator {
|
||||
visible: running
|
||||
anchors.centerIn: parent
|
||||
running: TimelineManager.isInitialSync
|
||||
height: 200
|
||||
width: 200
|
||||
Layout.fillWidth: true
|
||||
z: 3
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: timelineLayout
|
||||
|
||||
visible: TimelineManager.timeline != null
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
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
|
||||
|
||||
currentIndex: 0
|
||||
|
||||
Connections {
|
||||
function onActiveTimelineChanged() {
|
||||
stackLayout.currentIndex = 0;
|
||||
}
|
||||
|
||||
target: TimelineManager
|
||||
}
|
||||
|
||||
MessageView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : ""
|
||||
onLoaded: TimelineManager.setVideoCallItem()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TypingIndicator {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
CallInviteBar {
|
||||
id: callInviteBar
|
||||
|
||||
Layout.fillWidth: true
|
||||
z: 3
|
||||
}
|
||||
|
||||
ActiveCallBar {
|
||||
Layout.fillWidth: true
|
||||
z: 3
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
z: 3
|
||||
height: 1
|
||||
color: colors.mid
|
||||
}
|
||||
|
||||
ReplyPopup {
|
||||
}
|
||||
|
||||
MessageInput {
|
||||
}
|
||||
|
||||
ActiveCallBar {
|
||||
Layout.fillWidth: true
|
||||
z: 3
|
||||
}
|
||||
|
||||
NhekoDropArea {
|
||||
anchors.fill: parent
|
||||
roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : ""
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
z: 3
|
||||
height: 1
|
||||
color: Nheko.theme.separator
|
||||
}
|
||||
|
||||
ReplyPopup {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
137
resources/qml/components/AdaptiveLayout.qml
Normal file
137
resources/qml/components/AdaptiveLayout.qml
Normal file
|
@ -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
|
||||
}
|
||||
|
||||
}
|
26
resources/qml/components/AdaptiveLayoutElement.qml
Normal file
26
resources/qml/components/AdaptiveLayoutElement.qml
Normal file
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ Pane {
|
|||
return "Unknown verification error.";
|
||||
}
|
||||
}
|
||||
color: colors.text
|
||||
color: Nheko.colors.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
53
resources/qml/dialogs/InputDialog.qml
Normal file
53
resources/qml/dialogs/InputDialog.qml
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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 {
|
||||
|
|
337
src/ChatPage.cpp
337
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(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(userSettings_.data(),
|
||||
&UserSettings::groupViewStateChanged,
|
||||
this,
|
||||
&ChatPage::setGroupViewState);
|
||||
|
||||
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
|
||||
|
@ -679,18 +483,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()
|
||||
{
|
||||
|
@ -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 "";
|
||||
}
|
||||
|
|
|
@ -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_;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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_;
|
||||
};
|
|
@ -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)"));
|
||||
}
|
||||
}
|
|
@ -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_;
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
@ -203,22 +184,6 @@ MainWindow::event(QEvent *event)
|
|||
return QMainWindow::event(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()
|
||||
{
|
||||
|
@ -295,6 +260,7 @@ MainWindow::showChatPage()
|
|||
&Cache::secretChanged,
|
||||
userSettingsPage_,
|
||||
&UserSettingsPage::updateSecretStatus);
|
||||
emit reload();
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
};
|
540
src/RoomList.cpp
540
src/RoomList.cpp
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
101
src/RoomList.h
101
src/RoomList.h
|
@ -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;
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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_;
|
||||
};
|
166
src/Splitter.cpp
166
src/Splitter.cpp
|
@ -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;
|
||||
}
|
|
@ -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_;
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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_);
|
||||
|
|
|
@ -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_;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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_;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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_;
|
||||
};
|
||||
}
|
188
src/timeline/CommunitiesModel.cpp
Normal file
188
src/timeline/CommunitiesModel.cpp
Normal file
|
@ -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();
|
||||
}
|
64
src/timeline/CommunitiesModel.h
Normal file
64
src/timeline/CommunitiesModel.h
Normal file
|
@ -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_;
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
590
src/timeline/RoomlistModel.cpp
Normal file
590
src/timeline/RoomlistModel.cpp
Normal file
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
164
src/timeline/RoomlistModel.h
Normal file
164
src/timeline/RoomlistModel.h
Normal file
|
@ -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;
|
||||
};
|
|
@ -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>>(
|
||||
|
@ -723,6 +725,20 @@ TimelineModel::fetchMore(const QModelIndex &)
|
|||
events.fetchMore();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -1866,6 +1888,17 @@ TimelineModel::roomName() const
|
|||
QString::fromStdString(info[room_id_].name).toHtmlEscaped());
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>)
|
||||
|
@ -86,21 +86,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()
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isInitialSync_) {
|
||||
this->isInitialSync_ = false;
|
||||
emit initialSyncChanged(false);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
@ -470,56 +408,6 @@ 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()
|
||||
{
|
||||
|
@ -527,14 +415,14 @@ TimelineViewManager::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);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue