Merge pull request #605 from Nheko-Reborn/qml-roomlist

Qml roomlist and stuff
This commit is contained in:
DeepBlueV7.X 2021-06-13 01:44:25 +00:00 committed by GitHub
commit 5b4566d3f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
110 changed files with 3487 additions and 4865 deletions

View file

@ -137,7 +137,7 @@ endif()
# #
# Discover Qt dependencies. # 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(Qt5QuickCompiler)
find_package(Qt5DBus) find_package(Qt5DBus)
@ -265,6 +265,7 @@ set(SRC_FILES
# Timeline # Timeline
src/timeline/CommunitiesModel.cpp
src/timeline/EventStore.cpp src/timeline/EventStore.cpp
src/timeline/InputBar.cpp src/timeline/InputBar.cpp
src/timeline/Reaction.cpp src/timeline/Reaction.cpp
@ -272,6 +273,7 @@ set(SRC_FILES
src/timeline/TimelineModel.cpp src/timeline/TimelineModel.cpp
src/timeline/DelegateChooser.cpp src/timeline/DelegateChooser.cpp
src/timeline/Permissions.cpp src/timeline/Permissions.cpp
src/timeline/RoomlistModel.cpp
# UI components # UI components
src/ui/Avatar.cpp src/ui/Avatar.cpp
@ -284,11 +286,13 @@ set(SRC_FILES
src/ui/LoadingIndicator.cpp src/ui/LoadingIndicator.cpp
src/ui/NhekoCursorShape.cpp src/ui/NhekoCursorShape.cpp
src/ui/NhekoDropArea.cpp src/ui/NhekoDropArea.cpp
src/ui/NhekoGlobalObject.cpp
src/ui/OverlayModal.cpp src/ui/OverlayModal.cpp
src/ui/OverlayWidget.cpp src/ui/OverlayWidget.cpp
src/ui/RaisedButton.cpp src/ui/RaisedButton.cpp
src/ui/Ripple.cpp src/ui/Ripple.cpp
src/ui/RippleOverlay.cpp src/ui/RippleOverlay.cpp
src/ui/RoomSettings.cpp
src/ui/SnackBar.cpp src/ui/SnackBar.cpp
src/ui/TextField.cpp src/ui/TextField.cpp
src/ui/TextLabel.cpp src/ui/TextLabel.cpp
@ -296,7 +300,6 @@ set(SRC_FILES
src/ui/ThemeManager.cpp src/ui/ThemeManager.cpp
src/ui/ToggleButton.cpp src/ui/ToggleButton.cpp
src/ui/UserProfile.cpp src/ui/UserProfile.cpp
src/ui/RoomSettings.cpp
# Generic notification stuff # Generic notification stuff
src/notifications/Manager.cpp src/notifications/Manager.cpp
@ -309,8 +312,6 @@ set(SRC_FILES
src/ChatPage.cpp src/ChatPage.cpp
src/Clipboard.cpp src/Clipboard.cpp
src/ColorImageProvider.cpp src/ColorImageProvider.cpp
src/CommunitiesList.cpp
src/CommunitiesListItem.cpp
src/CompletionProxyModel.cpp src/CompletionProxyModel.cpp
src/DeviceVerificationFlow.cpp src/DeviceVerificationFlow.cpp
src/EventAccessors.cpp src/EventAccessors.cpp
@ -322,22 +323,14 @@ set(SRC_FILES
src/MxcImageProvider.cpp src/MxcImageProvider.cpp
src/Olm.cpp src/Olm.cpp
src/RegisterPage.cpp src/RegisterPage.cpp
src/RoomInfoListItem.cpp
src/RoomList.cpp
src/SSOHandler.cpp src/SSOHandler.cpp
src/SideBarActions.cpp
src/Splitter.cpp
src/TrayIcon.cpp src/TrayIcon.cpp
src/UserInfoWidget.cpp
src/UserSettingsPage.cpp src/UserSettingsPage.cpp
src/UsersModel.cpp src/UsersModel.cpp
src/RoomsModel.cpp src/RoomsModel.cpp
src/Utils.cpp src/Utils.cpp
src/WebRTCSession.cpp src/WebRTCSession.cpp
src/WelcomePage.cpp src/WelcomePage.cpp
src/popups/PopupItem.cpp
src/popups/SuggestionsPopup.cpp
src/popups/UserMentions.cpp
src/main.cpp src/main.cpp
third_party/blurhash/blurhash.cpp third_party/blurhash/blurhash.cpp
@ -489,6 +482,7 @@ qt5_wrap_cpp(MOC_HEADERS
src/emoji/Provider.h src/emoji/Provider.h
# Timeline # Timeline
src/timeline/CommunitiesModel.h
src/timeline/EventStore.h src/timeline/EventStore.h
src/timeline/InputBar.h src/timeline/InputBar.h
src/timeline/Reaction.h src/timeline/Reaction.h
@ -496,30 +490,32 @@ qt5_wrap_cpp(MOC_HEADERS
src/timeline/TimelineModel.h src/timeline/TimelineModel.h
src/timeline/DelegateChooser.h src/timeline/DelegateChooser.h
src/timeline/Permissions.h src/timeline/Permissions.h
src/timeline/RoomlistModel.h
# UI components # UI components
src/ui/Avatar.h src/ui/Avatar.h
src/ui/Badge.h src/ui/Badge.h
src/ui/LoadingIndicator.h
src/ui/InfoMessage.h
src/ui/FlatButton.h src/ui/FlatButton.h
src/ui/Label.h
src/ui/FloatingButton.h src/ui/FloatingButton.h
src/ui/InfoMessage.h
src/ui/Label.h
src/ui/LoadingIndicator.h
src/ui/Menu.h src/ui/Menu.h
src/ui/NhekoCursorShape.h src/ui/NhekoCursorShape.h
src/ui/NhekoDropArea.h src/ui/NhekoDropArea.h
src/ui/NhekoGlobalObject.h
src/ui/OverlayWidget.h src/ui/OverlayWidget.h
src/ui/SnackBar.h
src/ui/RaisedButton.h src/ui/RaisedButton.h
src/ui/Ripple.h src/ui/Ripple.h
src/ui/RippleOverlay.h src/ui/RippleOverlay.h
src/ui/RoomSettings.h
src/ui/SnackBar.h
src/ui/TextField.h src/ui/TextField.h
src/ui/TextLabel.h src/ui/TextLabel.h
src/ui/ToggleButton.h
src/ui/Theme.h src/ui/Theme.h
src/ui/ThemeManager.h src/ui/ThemeManager.h
src/ui/ToggleButton.h
src/ui/UserProfile.h src/ui/UserProfile.h
src/ui/RoomSettings.h
src/notifications/Manager.h src/notifications/Manager.h
@ -531,8 +527,6 @@ qt5_wrap_cpp(MOC_HEADERS
src/CallManager.h src/CallManager.h
src/ChatPage.h src/ChatPage.h
src/Clipboard.h src/Clipboard.h
src/CommunitiesList.h
src/CommunitiesListItem.h
src/CompletionProxyModel.h src/CompletionProxyModel.h
src/DeviceVerificationFlow.h src/DeviceVerificationFlow.h
src/InviteeItem.h src/InviteeItem.h
@ -540,21 +534,13 @@ qt5_wrap_cpp(MOC_HEADERS
src/MainWindow.h src/MainWindow.h
src/MxcImageProvider.h src/MxcImageProvider.h
src/RegisterPage.h src/RegisterPage.h
src/RoomInfoListItem.h
src/RoomList.h
src/SSOHandler.h src/SSOHandler.h
src/SideBarActions.h
src/Splitter.h
src/TrayIcon.h src/TrayIcon.h
src/UserInfoWidget.h
src/UserSettingsPage.h src/UserSettingsPage.h
src/UsersModel.h src/UsersModel.h
src/RoomsModel.h src/RoomsModel.h
src/WebRTCSession.h src/WebRTCSession.h
src/WelcomePage.h src/WelcomePage.h
src/popups/PopupItem.h
src/popups/SuggestionsPopup.h
src/popups/UserMentions.h
) )
# #

View 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

View file

@ -14,18 +14,21 @@ Rectangle {
property alias url: img.source property alias url: img.source
property string userid property string userid
property string displayName property string displayName
property alias textColor: label.color
signal clicked(var mouse) signal clicked(var mouse)
width: 48 width: 48
height: 48 height: 48
radius: Settings.avatarCircles ? height / 2 : 3 radius: Settings.avatarCircles ? height / 2 : 3
color: colors.alternateBase color: Nheko.colors.alternateBase
Component.onCompleted: { Component.onCompleted: {
mouseArea.clicked.connect(clicked); mouseArea.clicked.connect(clicked);
} }
Label { Label {
id: label
anchors.fill: parent anchors.fill: parent
text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "") text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "")
textFormat: Text.RichText textFormat: Text.RichText
@ -33,7 +36,7 @@ Rectangle {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
visible: img.status != Image.Ready visible: img.status != Image.Ready
color: colors.text color: Nheko.colors.text
} }
Image { Image {
@ -55,7 +58,7 @@ Rectangle {
Ripple { Ripple {
rippleTarget: mouseArea 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)
} }
} }

View 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
}
}

View 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
}
}

View file

@ -70,7 +70,7 @@ Popup {
onCompleterNameChanged: { onCompleterNameChanged: {
if (completerName) { if (completerName) {
if (completerName == "user") if (completerName == "user")
completer = TimelineManager.completerFor(completerName, TimelineManager.timeline.roomId()); completer = TimelineManager.completerFor(completerName, room.roomId());
else else
completer = TimelineManager.completerFor(completerName); completer = TimelineManager.completerFor(completerName);
completer.setSearchString(""); completer.setSearchString("");
@ -83,8 +83,8 @@ Popup {
height: listView.contentHeight + 2 // + 2 for the padding on top and bottom height: listView.contentHeight + 2 // + 2 for the padding on top and bottom
Connections { Connections {
onTimelineChanged: completer = null onRoomChanged: completer = null
target: TimelineManager target: timelineView
} }
ListView { ListView {
@ -100,7 +100,7 @@ Popup {
delegate: Rectangle { delegate: Rectangle {
property variant modelData: model 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 height: chooser.childrenRect.height + 2 * popup.rowMargin
implicitWidth: fullWidth ? popup.width : chooser.childrenRect.width + 4 implicitWidth: fullWidth ? popup.width : chooser.childrenRect.width + 4
@ -119,7 +119,7 @@ Popup {
Ripple { Ripple {
rippleTarget: mouseArea 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 { Label {
text: model.displayName text: model.displayName
color: model.index == popup.currentIndex ? colors.highlightedText : colors.text color: model.index == popup.currentIndex ? Nheko.colors.highlightedText : Nheko.colors.text
} }
Label { Label {
text: "(" + model.userid + ")" 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 { Label {
text: model.unicode 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 font: Settings.emojiFont
} }
Label { Label {
text: model.shortName 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 { Label {
text: model.roomName text: model.roomName
font.pixelSize: popup.avatarHeight * 0.5 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 { Label {
text: model.roomName 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 { Label {
text: "(" + model.roomAlias + ")" 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 { background: Rectangle {
color: colors.base color: Nheko.colors.base
implicitHeight: popup.contentHeight implicitHeight: popup.contentHeight
implicitWidth: popup.contentWidth implicitWidth: popup.contentWidth
border.color: colors.mid border.color: Nheko.colors.mid
} }
} }

View 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
}
}

View file

@ -20,7 +20,7 @@ Image {
case Crypto.Verified: case Crypto.Verified:
return "image://colorimage/:/icons/icons/ui/lock.png?green"; return "image://colorimage/:/icons/icons/ui/lock.png?green";
case Crypto.TOFU: 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: default:
return "image://colorimage/:/icons/icons/ui/lock.png?#dd3d3d"; return "image://colorimage/:/icons/icons/ui/lock.png?#dd3d3d";
} }

View file

@ -19,7 +19,7 @@ Popup {
x: Math.round(parent.width / 2 - width / 2) x: Math.round(parent.width / 2 - width / 2)
y: Math.round(parent.height / 2 - height / 2) y: Math.round(parent.height / 2 - height / 2)
modal: true modal: true
palette: colors palette: Nheko.colors
parent: Overlay.overlay parent: Overlay.overlay
width: implicitWidth >= (timelineRoot.width * 0.8) ? implicitWidth : (timelineRoot.width * 0.8) width: implicitWidth >= (timelineRoot.width * 0.8) ? implicitWidth : (timelineRoot.width * 0.8)
height: implicitHeight + completerPopup.height + padding * 2 height: implicitHeight + completerPopup.height + padding * 2
@ -44,22 +44,22 @@ Popup {
text: qsTr("Forward Message") text: qsTr("Forward Message")
font.bold: true font.bold: true
bottomPadding: 10 bottomPadding: 10
color: colors.text color: Nheko.colors.text
} }
Reply { Reply {
id: replyPreview 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 { MatrixTextField {
id: roomTextInput id: roomTextInput
width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2 width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2
color: colors.text color: Nheko.colors.text
onTextEdited: { onTextEdited: {
completerPopup.completer.searchString = text; completerPopup.completer.searchString = text;
} }
@ -95,7 +95,7 @@ Popup {
Connections { Connections {
onCompletionSelected: { onCompletionSelected: {
TimelineManager.timeline.forwardMessage(messageContextMenu.eventId, id); room.forwardMessage(messageContextMenu.eventId, id);
forwardMessagePopup.close(); forwardMessagePopup.close();
} }
onCountChanged: { onCountChanged: {
@ -107,11 +107,11 @@ Popup {
} }
background: Rectangle { background: Rectangle {
color: colors.window color: Nheko.colors.window
} }
Overlay.modal: Rectangle { 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)
} }
} }

View file

@ -12,8 +12,8 @@ AbstractButton {
property alias cursor: mouseArea.cursorShape property alias cursor: mouseArea.cursorShape
property string image: undefined property string image: undefined
property color highlightColor: colors.highlight property color highlightColor: Nheko.colors.highlight
property color buttonTextColor: colors.buttonText property color buttonTextColor: Nheko.colors.buttonText
property bool changeColorOnHover: true property bool changeColorOnHover: true
focusPolicy: Qt.NoFocus focusPolicy: Qt.NoFocus
@ -26,6 +26,7 @@ AbstractButton {
// Workaround, can't get icon.source working for now... // Workaround, can't get icon.source working for now...
anchors.fill: parent anchors.fill: parent
source: image != "" ? ("image://colorimage/" + image + "?" + ((button.hovered && changeColorOnHover) ? highlightColor : buttonTextColor)) : "" source: image != "" ? ("image://colorimage/" + image + "?" + ((button.hovered && changeColorOnHover) ? highlightColor : buttonTextColor)) : ""
fillMode: Image.PreserveAspectFit
} }
CursorShape { CursorShape {

View file

@ -13,8 +13,8 @@ TextEdit {
wrapMode: Text.Wrap wrapMode: Text.Wrap
selectByMouse: !Settings.mobileMode selectByMouse: !Settings.mobileMode
enabled: selectByMouse enabled: selectByMouse
color: colors.text color: Nheko.colors.text
onLinkActivated: TimelineManager.openLink(link) onLinkActivated: Nheko.openLink(link)
ToolTip.visible: hoveredLink ToolTip.visible: hoveredLink
ToolTip.text: hoveredLink ToolTip.text: hoveredLink

View file

@ -5,18 +5,20 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import im.nheko 1.0
TextField { TextField {
id: input id: input
palette: colors palette: Nheko.colors
color: Nheko.colors.text
Rectangle { Rectangle {
id: blueBar id: blueBar
anchors.top: parent.bottom anchors.top: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: colors.highlight color: Nheko.colors.highlight
height: 1 height: 1
width: parent.width width: parent.width
@ -27,7 +29,7 @@ TextField {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
height: parent.height + 1 height: parent.height + 1
width: 0 width: 0
color: colors.text color: Nheko.colors.text
states: State { states: State {
name: "focused" name: "focused"
@ -60,7 +62,7 @@ TextField {
} }
background: Rectangle { background: Rectangle {
color: colors.base color: Nheko.colors.base
} }
} }

View file

@ -12,7 +12,7 @@ import im.nheko 1.0
Rectangle { Rectangle {
id: inputBar id: inputBar
color: colors.window color: Nheko.colors.window
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: row.implicitHeight Layout.preferredHeight: row.implicitHeight
Layout.minimumHeight: 40 Layout.minimumHeight: 40
@ -28,7 +28,7 @@ Rectangle {
RowLayout { RowLayout {
id: row 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 anchors.fill: parent
ImageButton { ImageButton {
@ -43,7 +43,7 @@ Rectangle {
ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call") ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call")
Layout.margins: 8 Layout.margins: 8
onClicked: { onClicked: {
if (TimelineManager.timeline) { if (room) {
if (CallManager.haveCallInvite) { if (CallManager.haveCallInvite) {
return ; return ;
} else if (CallManager.isOnCall) { } else if (CallManager.isOnCall) {
@ -63,14 +63,14 @@ Rectangle {
height: 22 height: 22
image: ":/icons/icons/ui/paper-clip-outline.png" image: ":/icons/icons/ui/paper-clip-outline.png"
Layout.margins: 8 Layout.margins: 8
onClicked: TimelineManager.timeline.input.openFileSelection() onClicked: room.input.openFileSelection()
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: qsTr("Send a file") ToolTip.text: qsTr("Send a file")
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: colors.window color: Nheko.colors.window
visible: TimelineManager.timeline && TimelineManager.timeline.input.uploading visible: room && room.input.uploading
NhekoBusyIndicator { NhekoBusyIndicator {
anchors.fill: parent anchors.fill: parent
@ -116,23 +116,23 @@ Rectangle {
selectByMouse: true selectByMouse: true
placeholderText: qsTr("Write a message...") placeholderText: qsTr("Write a message...")
placeholderTextColor: colors.buttonText placeholderTextColor: Nheko.colors.buttonText
color: colors.text color: Nheko.colors.text
width: textInput.width width: textInput.width
wrapMode: TextEdit.Wrap wrapMode: TextEdit.Wrap
padding: 8 padding: 8
focus: true focus: true
onTextChanged: { onTextChanged: {
if (TimelineManager.timeline) if (room)
TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text); room.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
forceActiveFocus(); forceActiveFocus();
} }
onCursorPositionChanged: { onCursorPositionChanged: {
if (!TimelineManager.timeline) if (!room)
return ; return ;
TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text); room.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
if (cursorPosition <= completerTriggeredAt) { if (cursorPosition <= completerTriggeredAt) {
completerTriggeredAt = -1; completerTriggeredAt = -1;
popup.close(); popup.close();
@ -141,13 +141,13 @@ Rectangle {
popup.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)); popup.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition));
} }
onSelectionStartChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onSelectionStartChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
onSelectionEndChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text) onSelectionEndChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
// Ensure that we get escape key press events first. // 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.onShortcutOverride: event.accepted = (completerTriggeredAt != -1 && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter))
Keys.onPressed: { Keys.onPressed: {
if (event.matches(StandardKey.Paste)) { if (event.matches(StandardKey.Paste)) {
TimelineManager.timeline.input.paste(false); room.input.paste(false);
event.accepted = true; event.accepted = true;
} else if (event.key == Qt.Key_Space) { } else if (event.key == Qt.Key_Space) {
// close popup if user enters space after colon // close popup if user enters space after colon
@ -160,9 +160,9 @@ Rectangle {
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) { } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) {
messageInput.clear(); messageInput.clear();
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) { } 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) { } 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) { } else if (event.key == Qt.Key_At) {
messageInput.openCompleter(cursorPosition, "user"); messageInput.openCompleter(cursorPosition, "user");
popup.open(); popup.open();
@ -188,7 +188,7 @@ Rectangle {
return ; return ;
} }
} }
TimelineManager.timeline.input.send(); room.input.send();
event.accepted = true; event.accepted = true;
} else if (event.key == Qt.Key_Tab) { } else if (event.key == Qt.Key_Tab) {
event.accepted = true; event.accepted = true;
@ -223,11 +223,11 @@ Rectangle {
} else if (event.key == Qt.Key_Up && event.modifiers == Qt.NoModifier) { } else if (event.key == Qt.Key_Up && event.modifiers == Qt.NoModifier) {
if (cursorPosition == 0) { if (cursorPosition == 0) {
event.accepted = true; 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) { while (true) {
var id = TimelineManager.timeline.indexToId(idx); var id = room.indexToId(idx);
if (!id || TimelineManager.timeline.getDump(id, "").isEditable) { if (!id || room.getDump(id, "").isEditable) {
TimelineManager.timeline.edit = id; room.edit = id;
cursorPosition = 0; cursorPosition = 0;
Qt.callLater(positionCursorAtEnd); Qt.callLater(positionCursorAtEnd);
break; break;
@ -239,13 +239,13 @@ Rectangle {
positionCursorAtStart(); positionCursorAtStart();
} }
} else if (event.key == Qt.Key_Down && event.modifiers == Qt.NoModifier) { } 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; event.accepted = true;
var idx = TimelineManager.timeline.idToIndex(TimelineManager.timeline.edit) - 1; var idx = room.idToIndex(room.edit) - 1;
while (true) { while (true) {
var id = TimelineManager.timeline.indexToId(idx); var id = room.indexToId(idx);
if (!id || TimelineManager.timeline.getDump(id, "").isEditable) { if (!id || room.getDump(id, "").isEditable) {
TimelineManager.timeline.edit = id; room.edit = id;
Qt.callLater(positionCursorAtStart); Qt.callLater(positionCursorAtStart);
break; break;
} }
@ -260,14 +260,14 @@ Rectangle {
background: null background: null
Connections { Connections {
onActiveTimelineChanged: { onRoomChanged: {
messageInput.clear(); messageInput.clear();
messageInput.append(TimelineManager.timeline.input.text()); messageInput.append(room.input.text());
messageInput.completerTriggeredAt = -1; messageInput.completerTriggeredAt = -1;
popup.completerName = ""; popup.completerName = "";
messageInput.forceActiveFocus(); messageInput.forceActiveFocus();
} }
target: TimelineManager target: timelineView
} }
Connections { Connections {
@ -292,14 +292,14 @@ Rectangle {
messageInput.text = newText; messageInput.text = newText;
messageInput.cursorPosition = newText.length; messageInput.cursorPosition = newText.length;
} }
target: TimelineManager.timeline ? TimelineManager.timeline.input : null target: room ? room.input : null
} }
Connections { Connections {
ignoreUnknownSignals: true ignoreUnknownSignals: true
onReplyChanged: messageInput.forceActiveFocus() onReplyChanged: messageInput.forceActiveFocus()
onEditChanged: messageInput.forceActiveFocus() onEditChanged: messageInput.forceActiveFocus()
target: TimelineManager.timeline target: room
} }
Connections { Connections {
@ -312,7 +312,7 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.MiddleButton acceptedButtons: Qt.MiddleButton
cursorShape: Qt.IBeamCursor cursorShape: Qt.IBeamCursor
onClicked: TimelineManager.timeline.input.paste(true) onClicked: room.input.paste(true)
} }
} }
@ -347,7 +347,7 @@ Rectangle {
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: qsTr("Send") ToolTip.text: qsTr("Send")
onClicked: { onClicked: {
TimelineManager.timeline.input.send(); room.input.send();
} }
} }
@ -355,9 +355,9 @@ Rectangle {
Text { Text {
anchors.centerIn: parent 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") text: qsTr("You don't have permission to send messages in this room")
color: colors.text color: Nheko.colors.text
} }
} }

View file

@ -4,6 +4,7 @@
import "./delegates" import "./delegates"
import "./emoji" import "./emoji"
import Qt.labs.platform 1.1 as Platform
import QtGraphicalEffects 1.0 import QtGraphicalEffects 1.0
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
@ -13,7 +14,7 @@ import im.nheko 1.0
ScrollView { ScrollView {
clip: false clip: false
palette: colors palette: Nheko.colors
padding: 8 padding: 8
ScrollBar.horizontal.visible: false 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 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 boundsBehavior: Flickable.StopAtBounds
pixelAligned: true pixelAligned: true
spacing: 4 spacing: 4
@ -51,8 +52,8 @@ ScrollView {
z: 10 z: 10
height: row.implicitHeight + padding * 2 height: row.implicitHeight + padding * 2
width: row.implicitWidth + padding * 2 width: row.implicitWidth + padding * 2
color: colors.window color: Nheko.colors.window
border.color: colors.buttonText border.color: Nheko.colors.buttonText
border.width: 1 border.width: 1
radius: padding radius: padding
@ -74,7 +75,7 @@ ScrollView {
id: editButton id: editButton
visible: !!row.model && row.model.isEditable visible: !!row.model && row.model.isEditable
buttonTextColor: colors.buttonText buttonTextColor: Nheko.colors.buttonText
width: 16 width: 16
hoverEnabled: true hoverEnabled: true
image: ":/icons/icons/ui/edit.png" image: ":/icons/icons/ui/edit.png"
@ -220,7 +221,7 @@ ScrollView {
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
visible: modelData && modelData.previousMessageDay !== modelData.day visible: modelData && modelData.previousMessageDay !== modelData.day
text: modelData ? chat.model.formatDateSeparator(modelData.timestamp) : "" text: modelData ? chat.model.formatDateSeparator(modelData.timestamp) : ""
color: colors.text color: Nheko.colors.text
height: Math.round(fontMetrics.height * 1.4) height: Math.round(fontMetrics.height * 1.4)
width: contentWidth * 1.2 width: contentWidth * 1.2
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@ -228,7 +229,7 @@ ScrollView {
background: Rectangle { background: Rectangle {
radius: parent.height / 2 radius: parent.height / 2
color: colors.window color: Nheko.colors.window
} }
} }
@ -240,8 +241,8 @@ ScrollView {
Avatar { Avatar {
id: messageUserAvatar id: messageUserAvatar
width: avatarSize width: Nheko.avatarSize
height: avatarSize height: Nheko.avatarSize
url: modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : "" url: modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : ""
displayName: modelData ? modelData.userName : "" displayName: modelData ? modelData.userName : ""
userid: modelData ? modelData.userId : "" userid: modelData ? modelData.userId : ""
@ -267,7 +268,7 @@ ScrollView {
id: userName id: userName
text: modelData ? TimelineManager.escapeEmoji(modelData.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 textFormat: Text.RichText
ToolTip.visible: displayNameHover.hovered ToolTip.visible: displayNameHover.hovered
ToolTip.text: modelData ? modelData.userId : "" ToolTip.text: modelData ? modelData.userId : ""
@ -288,11 +289,11 @@ ScrollView {
} }
Label { Label {
color: colors.buttonText color: Nheko.colors.buttonText
text: modelData ? TimelineManager.userStatus(modelData.userId) : "" text: modelData ? TimelineManager.userStatus(modelData.userId) : ""
textFormat: Text.PlainText textFormat: Text.PlainText
elide: Text.ElideRight 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 font.italic: true
} }
@ -317,7 +318,7 @@ ScrollView {
opacity: 0 opacity: 0
visible: true visible: true
anchors.fill: timelinerow anchors.fill: timelinerow
color: colors.highlight color: Nheko.colors.highlight
states: State { states: State {
name: "revealed" 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 {
}
}
} }

View file

@ -5,6 +5,7 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import im.nheko 1.0
BusyIndicator { BusyIndicator {
id: control id: control
@ -38,7 +39,7 @@ BusyIndicator {
implicitWidth: radius * 2 implicitWidth: radius * 2
implicitHeight: radius * 2 implicitHeight: radius * 2
radius: item.height / 6 radius: item.height / 6
color: colors.text color: Nheko.colors.text
opacity: (index + 2) / (repeater.count + 2) opacity: (index + 2) / (repeater.count + 2)
transform: [ transform: [
Translate { Translate {

View file

@ -19,7 +19,7 @@ Popup {
modal: true modal: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
parent: Overlay.overlay parent: Overlay.overlay
palette: colors palette: Nheko.colors
onOpened: { onOpened: {
completerPopup.open(); completerPopup.open();
roomTextInput.forceActiveFocus(); roomTextInput.forceActiveFocus();
@ -34,7 +34,7 @@ Popup {
anchors.fill: parent anchors.fill: parent
font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6) font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6)
padding: textMargin padding: textMargin
color: colors.text color: Nheko.colors.text
onTextEdited: { onTextEdited: {
completerPopup.completer.searchString = text; completerPopup.completer.searchString = text;
} }
@ -72,8 +72,7 @@ Popup {
Connections { Connections {
onCompletionSelected: { onCompletionSelected: {
TimelineManager.setHistoryView(id); Rooms.setCurrentRoom(id);
TimelineManager.highlightRoom(id);
quickSwitcher.close(); quickSwitcher.close();
} }
onCountChanged: { onCountChanged: {

View file

@ -12,9 +12,9 @@ Flow {
id: reactionFlow id: reactionFlow
// highlight colors for selfReactedEvent background // highlight colors for selfReactedEvent background
property real highlightHue: colors.highlight.hslHue property real highlightHue: Nheko.colors.highlight.hslHue
property real highlightSat: colors.highlight.hslSaturation property real highlightSat: Nheko.colors.highlight.hslSaturation
property real highlightLight: colors.highlight.hslLightness property real highlightLight: Nheko.colors.highlight.hslLightness
property string eventId property string eventId
property alias reactions: repeater.model property alias reactions: repeater.model
@ -35,7 +35,7 @@ Flow {
ToolTip.text: modelData.users ToolTip.text: modelData.users
onClicked: { onClicked: {
console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent); 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 { contentItem: Row {
@ -59,7 +59,7 @@ Flow {
anchors.baseline: reactionCounter.baseline anchors.baseline: reactionCounter.baseline
text: textMetrics.elidedText + (textMetrics.elidedText == modelData.key ? "" : "…") text: textMetrics.elidedText + (textMetrics.elidedText == modelData.key ? "" : "…")
font.family: Settings.emojiFont font.family: Settings.emojiFont
color: reaction.hovered ? colors.highlight : colors.text color: reaction.hovered ? Nheko.colors.highlight : Nheko.colors.text
maximumLineCount: 1 maximumLineCount: 1
} }
@ -68,7 +68,7 @@ Flow {
height: Math.floor(reactionCounter.implicitHeight * 1.4) height: Math.floor(reactionCounter.implicitHeight * 1.4)
width: 1 width: 1
color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text color: (reaction.hovered || modelData.selfReactedEvent !== '') ? Nheko.colors.highlight : Nheko.colors.text
} }
Text { Text {
@ -77,7 +77,7 @@ Flow {
anchors.verticalCenter: divider.verticalCenter anchors.verticalCenter: divider.verticalCenter
text: modelData.count text: modelData.count
font: reaction.font 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 anchors.centerIn: parent
implicitWidth: reaction.implicitWidth implicitWidth: reaction.implicitWidth
implicitHeight: reaction.implicitHeight implicitHeight: reaction.implicitHeight
border.color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text border.color: (reaction.hovered || modelData.selfReactedEvent !== '') ? Nheko.colors.highlight : Nheko.colors.text
color: modelData.selfReactedEvent !== '' ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : colors.window color: modelData.selfReactedEvent !== '' ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : Nheko.colors.window
border.width: 1 border.width: 1
radius: reaction.height / 2 radius: reaction.height / 2
} }

View file

@ -11,13 +11,11 @@ import im.nheko 1.0
Rectangle { Rectangle {
id: replyPopup id: replyPopup
property var room: TimelineManager.timeline
Layout.fillWidth: true Layout.fillWidth: true
visible: room && (room.reply || room.edit) visible: room && (room.reply || room.edit)
// Height of child, plus margins, plus border // Height of child, plus margins, plus border
implicitHeight: (room && room.reply ? replyPreview.height : closeEditButton.height) + 10 implicitHeight: (room && room.reply ? replyPreview.height : closeEditButton.height) + 10
color: colors.window color: Nheko.colors.window
z: 3 z: 3
Reply { Reply {
@ -31,7 +29,7 @@ Rectangle {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
modelData: room ? room.getDump(room.reply, room.id) : { modelData: room ? room.getDump(room.reply, room.id) : {
} }
userColor: TimelineManager.userColor(modelData.userId, colors.window) userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.window)
} }
ImageButton { ImageButton {

604
resources/qml/RoomList.qml Normal file
View 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()
}
}
}
}
}

View file

@ -18,9 +18,9 @@ ApplicationWindow {
y: MainWindow.y + (MainWindow.height / 2) - (height / 2) y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
minimumWidth: 420 minimumWidth: 420
minimumHeight: 650 minimumHeight: 650
palette: colors palette: Nheko.colors
color: colors.window color: Nheko.colors.window
modality: Qt.WindowModal modality: Qt.NonModal
flags: Qt.Dialog flags: Qt.Dialog
title: qsTr("Room Settings") title: qsTr("Room Settings")
@ -126,9 +126,9 @@ ApplicationWindow {
readOnly: true readOnly: true
background: null background: null
selectByMouse: true selectByMouse: true
color: colors.text color: Nheko.colors.text
horizontalAlignment: TextEdit.AlignHCenter horizontalAlignment: TextEdit.AlignHCenter
onLinkActivated: TimelineManager.openLink(link) onLinkActivated: Nheko.openLink(link)
CursorShape { CursorShape {
anchors.fill: parent anchors.fill: parent
@ -205,7 +205,7 @@ ApplicationWindow {
title: qsTr("End-to-End Encryption") title: qsTr("End-to-End Encryption")
text: qsTr("Encryption is currently experimental and things might break unexpectedly. <br> text: qsTr("Encryption is currently experimental and things might break unexpectedly. <br>
Please take note that it can't be disabled afterwards.") Please take note that it can't be disabled afterwards.")
modality: Qt.WindowModal modality: Qt.NonModal
onAccepted: { onAccepted: {
if (roomSettings.isEncryptionEnabled) if (roomSettings.isEncryptionEnabled)
return ; return ;

123
resources/qml/Root.qml Normal file
View 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
}
}

View file

@ -31,7 +31,7 @@ ImageButton {
} }
onClicked: { onClicked: {
if (model.state == MtxEvent.Read) if (model.state == MtxEvent.Read)
TimelineManager.timeline.readReceiptsAction(model.id); room.readReceiptsAction(model.id);
} }
image: { image: {

View file

@ -16,7 +16,7 @@ Item {
height: row.height height: row.height
Rectangle { Rectangle {
color: (Settings.messageHoverHighlight && hoverHandler.hovered) ? colors.alternateBase : "transparent" color: (Settings.messageHoverHighlight && hoverHandler.hovered) ? Nheko.colors.alternateBase : "transparent"
anchors.fill: row anchors.fill: row
} }
@ -42,7 +42,7 @@ Item {
id: row id: row
anchors.rightMargin: 1 anchors.rightMargin: 1
anchors.leftMargin: avatarSize + 16 anchors.leftMargin: Nheko.avatarSize + 16
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@ -57,7 +57,7 @@ Item {
Reply { Reply {
visible: model.replyTo visible: model.replyTo
modelData: chat.model.getDump(model.replyTo, model.id) 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 // actual message content
@ -101,7 +101,7 @@ Item {
width: 16 width: 16
sourceSize.width: 16 sourceSize.width: 16
sourceSize.height: 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.visible: editHovered.hovered
ToolTip.text: qsTr("Edited") ToolTip.text: qsTr("Edited")
@ -115,7 +115,7 @@ Item {
Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.alignment: Qt.AlignRight | Qt.AlignTop
text: model.timestamp.toLocaleTimeString(Locale.ShortFormat) text: model.timestamp.toLocaleTimeString(Locale.ShortFormat)
width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth) width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth)
color: inactiveColors.text color: Nheko.inactiveColors.text
ToolTip.visible: ma.hovered ToolTip.visible: ma.hovered
ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate)

View file

@ -9,382 +9,137 @@ import "./voip"
import Qt.labs.platform 1.1 as Platform import Qt.labs.platform 1.1 as Platform
import QtGraphicalEffects 1.0 import QtGraphicalEffects 1.0
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.3 import QtQuick.Controls 2.5
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import im.nheko 1.0 import im.nheko 1.0
import im.nheko.EmojiModel 1.0 import im.nheko.EmojiModel 1.0
Page { Item {
id: timelineRoot id: timelineView
property var colors: currentActivePalette property var room: null
property var systemInactive property bool showBackButton: false
property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive
readonly property int avatarSize: 40
property real highlightHue: colors.highlight.hslHue
property real highlightSat: colors.highlight.hslSaturation
property real highlightLight: colors.highlight.hslLightness
palette: colors Label {
visible: !room && !TimelineManager.isInitialSync
FontMetrics { anchors.centerIn: parent
id: fontMetrics text: qsTr("No room open")
font.pointSize: 24
color: Nheko.colors.text
} }
EmojiPicker { BusyIndicator {
id: emojiPopup visible: running
anchors.centerIn: parent
colors: palette running: TimelineManager.isInitialSync
model: TimelineManager.completerFor("allemoji", "") height: 200
width: 200
z: 3
} }
Component { ColumnLayout {
id: userProfileComponent id: timelineLayout
UserProfile { visible: room != null
}
}
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 {
anchors.fill: parent anchors.fill: parent
color: colors.window spacing: 0
Component {
id: deviceVerificationDialog
DeviceVerification {
}
TopBar {
showBackButton: timelineView.showBackButton
} }
Connections { Rectangle {
target: TimelineManager Layout.fillWidth: true
onNewDeviceVerificationRequest: { height: 1
var dialog = deviceVerificationDialog.createObject(timelineRoot, { z: 3
"flow": flow color: Nheko.theme.separator
});
dialog.show();
}
onOpenProfile: {
var userProfile = userProfileComponent.createObject(timelineRoot, {
"profile": profile
});
userProfile.show();
}
} }
Connections { Rectangle {
target: TimelineManager.timeline id: msgView
onOpenRoomSettingsDialog: {
var roomSettings = roomSettingsComponent.createObject(timelineRoot, { Layout.fillWidth: true
"roomSettings": settings Layout.fillHeight: true
}); color: Nheko.colors.base
roomSettings.show();
} 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 { CallInviteBar {
visible: !TimelineManager.timeline && !TimelineManager.isInitialSync id: callInviteBar
anchors.centerIn: parent
text: qsTr("No room open")
font.pointSize: 24
color: colors.text
}
BusyIndicator { Layout.fillWidth: true
visible: running
anchors.centerIn: parent
running: TimelineManager.isInitialSync
height: 200
width: 200
z: 3 z: 3
} }
ColumnLayout { ActiveCallBar {
id: timelineLayout Layout.fillWidth: true
z: 3
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 {
}
} }
NhekoDropArea { Rectangle {
anchors.fill: parent Layout.fillWidth: true
roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : "" z: 3
height: 1
color: Nheko.theme.separator
}
ReplyPopup {
}
MessageInput {
} }
} }
PrivacyScreen { NhekoDropArea {
anchors.fill: parent anchors.fill: parent
visible: Settings.privacyScreen roomid: room ? room.roomId() : ""
screenTimeout: Settings.privacyScreenTimeout
timelineRoot: timelineLayout
} }
systemInactive: SystemPalette { Connections {
colorGroup: SystemPalette.Disabled target: room
onOpenRoomSettingsDialog: {
var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
"roomSettings": settings
});
roomSettings.show();
}
} }
} }

View file

@ -11,16 +11,16 @@ import im.nheko 1.0
Rectangle { Rectangle {
id: topBar id: topBar
property var room: TimelineManager.timeline property bool showBackButton: false
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: topLayout.height + 16 implicitHeight: topLayout.height + Nheko.paddingMedium * 2
z: 3 z: 3
color: colors.window color: Nheko.colors.window
TapHandler { TapHandler {
onSingleTapped: { onSingleTapped: {
TimelineManager.timeline.openRoomSettings(); room.openRoomSettings();
eventPoint.accepted = true; eventPoint.accepted = true;
} }
gesturePolicy: TapHandler.ReleaseWithinBounds gesturePolicy: TapHandler.ReleaseWithinBounds
@ -33,7 +33,7 @@ Rectangle {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.margins: 8 anchors.margins: Nheko.paddingMedium
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
ImageButton { ImageButton {
@ -43,13 +43,13 @@ Rectangle {
Layout.row: 0 Layout.row: 0
Layout.rowSpan: 2 Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
width: avatarSize width: Nheko.avatarSize
height: avatarSize height: Nheko.avatarSize
visible: TimelineManager.isNarrowView visible: showBackButton
image: ":/icons/icons/ui/angle-pointing-to-left.png" image: ":/icons/icons/ui/angle-pointing-to-left.png"
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: qsTr("Back to room list") ToolTip.text: qsTr("Back to room list")
onClicked: TimelineManager.backToRooms() onClicked: Rooms.resetCurrentRoom()
} }
Avatar { Avatar {
@ -57,18 +57,18 @@ Rectangle {
Layout.row: 0 Layout.row: 0
Layout.rowSpan: 2 Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
width: avatarSize width: Nheko.avatarSize
height: avatarSize height: Nheko.avatarSize
url: room ? room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : "" url: room ? room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : ""
displayName: room ? room.roomName : qsTr("No room selected") displayName: room ? room.roomName : qsTr("No room selected")
onClicked: TimelineManager.timeline.openRoomSettings() onClicked: room.openRoomSettings()
} }
Label { Label {
Layout.fillWidth: true Layout.fillWidth: true
Layout.column: 2 Layout.column: 2
Layout.row: 0 Layout.row: 0
color: colors.text color: Nheko.colors.text
font.pointSize: fontMetrics.font.pointSize * 1.1 font.pointSize: fontMetrics.font.pointSize * 1.1
text: room ? room.roomName : qsTr("No room selected") text: room ? room.roomName : qsTr("No room selected")
maximumLineCount: 1 maximumLineCount: 1
@ -101,24 +101,24 @@ Rectangle {
id: roomOptionsMenu id: roomOptionsMenu
Platform.MenuItem { Platform.MenuItem {
visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canInvite() : false visible: room ? room.permissions.canInvite() : false
text: qsTr("Invite users") text: qsTr("Invite users")
onTriggered: TimelineManager.openInviteUsersDialog() onTriggered: TimelineManager.openInviteUsersDialog()
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Members") text: qsTr("Members")
onTriggered: TimelineManager.openMemberListDialog() onTriggered: TimelineManager.openMemberListDialog(room.roomId())
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Leave room") text: qsTr("Leave room")
onTriggered: TimelineManager.openLeaveRoomDialog() onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId())
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Settings") text: qsTr("Settings")
onTriggered: TimelineManager.timeline.openRoomSettings() onTriggered: room.openRoomSettings()
} }
} }

View file

@ -8,8 +8,6 @@ import QtQuick.Layouts 1.2
import im.nheko 1.0 import im.nheko 1.0
Item { Item {
property var room: TimelineManager.timeline
implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height) implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height)
Layout.fillWidth: true Layout.fillWidth: true
@ -17,7 +15,7 @@ Item {
id: typingRect id: typingRect
visible: (room && room.typingUsers.length > 0) visible: (room && room.typingUsers.length > 0)
color: colors.base color: Nheko.colors.base
anchors.fill: parent anchors.fill: parent
z: 3 z: 3
@ -29,8 +27,8 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 10 anchors.rightMargin: 10
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
color: colors.text color: Nheko.colors.text
text: room ? room.formatTypingUsers(room.typingUsers, colors.base) : "" text: room ? room.formatTypingUsers(room.typingUsers, Nheko.colors.base) : ""
textFormat: Text.RichText textFormat: Text.RichText
} }

View file

@ -19,10 +19,10 @@ ApplicationWindow {
height: 650 height: 650
width: 420 width: 420
minimumHeight: 420 minimumHeight: 420
palette: colors palette: Nheko.colors
color: colors.window color: Nheko.colors.window
title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile") title: profile.isGlobalUserProfile ? qsTr("Global User Profile") : qsTr("Room User Profile")
modality: Qt.WindowModal modality: Qt.NonModal
flags: Qt.Dialog flags: Qt.Dialog
Shortcut { Shortcut {
@ -97,7 +97,7 @@ ApplicationWindow {
readOnly: !isUsernameEditingAllowed readOnly: !isUsernameEditingAllowed
text: profile.displayName text: profile.displayName
font.pixelSize: 20 font.pixelSize: 20
color: TimelineManager.userColor(profile.userid, colors.window) color: TimelineManager.userColor(profile.userid, Nheko.colors.window)
font.bold: true font.bold: true
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
selectByMouse: true selectByMouse: true
@ -145,7 +145,7 @@ ApplicationWindow {
Image { Image {
Layout.preferredHeight: 16 Layout.preferredHeight: 16
Layout.preferredWidth: 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 visible: profile.userVerified != Crypto.Unverified
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
@ -218,7 +218,7 @@ ApplicationWindow {
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
elide: Text.ElideRight elide: Text.ElideRight
font.bold: true font.bold: true
color: colors.text color: Nheko.colors.text
text: model.deviceId text: model.deviceId
} }
@ -226,7 +226,7 @@ ApplicationWindow {
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
elide: Text.ElideRight elide: Text.ElideRight
color: colors.text color: Nheko.colors.text
text: model.deviceName text: model.deviceName
} }

View 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
}
}

View 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;
});
}
}

View file

@ -20,7 +20,7 @@ Item {
Rectangle { Rectangle {
id: button id: button
color: colors.light color: Nheko.colors.light
radius: 22 radius: 22
height: 44 height: 44
width: 44 width: 44
@ -34,7 +34,7 @@ Item {
} }
TapHandler { TapHandler {
onSingleTapped: TimelineManager.timeline.saveMedia(model.data.id) onSingleTapped: room.saveMedia(model.data.id)
gesturePolicy: TapHandler.ReleaseWithinBounds gesturePolicy: TapHandler.ReleaseWithinBounds
} }
@ -55,7 +55,7 @@ Item {
text: model.data.filename text: model.data.filename
textFormat: Text.PlainText textFormat: Text.PlainText
elide: Text.ElideRight elide: Text.ElideRight
color: colors.text color: Nheko.colors.text
} }
Text { Text {
@ -65,7 +65,7 @@ Item {
text: model.data.filesize text: model.data.filesize
textFormat: Text.PlainText textFormat: Text.PlainText
elide: Text.ElideRight elide: Text.ElideRight
color: colors.text color: Nheko.colors.text
} }
} }
@ -73,7 +73,7 @@ Item {
} }
Rectangle { Rectangle {
color: colors.alternateBase color: Nheko.colors.alternateBase
z: -1 z: -1
radius: 10 radius: 10
height: row.height + 24 height: row.height + 24

View file

@ -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 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 tempHeight: tempWidth * model.data.proportionalHeight
property double divisor: model.isReply ? 5 : 3 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) height: Math.round(tooHigh ? timelineView.height / divisor : tempHeight)
width: Math.round(tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth) width: Math.round(tooHigh ? (timelineView.height / divisor) / model.data.proportionalHeight : tempWidth)
Image { Image {
id: blurhash id: blurhash
anchors.fill: parent anchors.fill: parent
visible: img.status != Image.Ready 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 asynchronous: true
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
sourceSize.width: parent.width sourceSize.width: parent.width
@ -61,7 +61,7 @@ Item {
width: parent.width width: parent.width
implicitHeight: imgcaption.implicitHeight implicitHeight: imgcaption.implicitHeight
anchors.bottom: overlay.bottom anchors.bottom: overlay.bottom
color: colors.window color: Nheko.colors.window
opacity: 0.75 opacity: 0.75
} }
@ -74,7 +74,7 @@ Item {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
// See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530 // See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530
text: model.data.filename ? model.data.filename : model.data.body text: model.data.filename ? model.data.filename : model.data.body
color: colors.text color: Nheko.colors.text
} }
} }

View file

@ -58,7 +58,7 @@ Item {
NoticeMessage { NoticeMessage {
formatted: TimelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody 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 roleValue: MtxEvent.PowerLevels
NoticeMessage { NoticeMessage {
text: TimelineManager.timeline.formatPowerLevelEvent(model.data.id) text: room.formatPowerLevelEvent(model.data.id)
} }
} }
@ -216,7 +216,7 @@ Item {
roleValue: MtxEvent.RoomJoinRules roleValue: MtxEvent.RoomJoinRules
NoticeMessage { NoticeMessage {
text: TimelineManager.timeline.formatJoinRuleEvent(model.data.id) text: room.formatJoinRuleEvent(model.data.id)
} }
} }
@ -225,7 +225,7 @@ Item {
roleValue: MtxEvent.RoomHistoryVisibility roleValue: MtxEvent.RoomHistoryVisibility
NoticeMessage { NoticeMessage {
text: TimelineManager.timeline.formatHistoryVisibilityEvent(model.data.id) text: room.formatHistoryVisibilityEvent(model.data.id)
} }
} }
@ -234,7 +234,7 @@ Item {
roleValue: MtxEvent.RoomGuestAccess roleValue: MtxEvent.RoomGuestAccess
NoticeMessage { NoticeMessage {
text: TimelineManager.timeline.formatGuestAccessEvent(model.data.id) text: room.formatGuestAccessEvent(model.data.id)
} }
} }
@ -243,7 +243,7 @@ Item {
roleValue: MtxEvent.Member roleValue: MtxEvent.Member
NoticeMessage { NoticeMessage {
text: TimelineManager.timeline.formatMemberEvent(model.data.id) text: room.formatMemberEvent(model.data.id)
} }
} }

View file

@ -2,7 +2,9 @@
// //
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import im.nheko 1.0
TextMessage { TextMessage {
font.italic: true font.italic: true
color: colors.buttonText color: Nheko.colors.buttonText
} }

View file

@ -4,16 +4,17 @@
import QtQuick 2.5 import QtQuick 2.5
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
import im.nheko 1.0
Label { Label {
color: colors.brightText color: Nheko.colors.brightText
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
height: contentHeight * 1.2 height: contentHeight * 1.2
width: contentWidth * 1.2 width: contentWidth * 1.2
background: Rectangle { background: Rectangle {
radius: parent.height / 2 radius: parent.height / 2
color: colors.alternateBase color: Nheko.colors.alternateBase
} }
} }

View file

@ -3,9 +3,10 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import ".." import ".."
import im.nheko 1.0
MatrixText { MatrixText {
text: qsTr("unimplemented event: ") + model.data.typeString text: qsTr("unimplemented event: ") + model.data.typeString
width: parent ? parent.width : undefined width: parent ? parent.width : undefined
color: inactiveColors.text color: Nheko.inactiveColors.text
} }

View file

@ -13,7 +13,7 @@ Rectangle {
id: bg id: bg
radius: 10 radius: 10
color: colors.alternateBase color: Nheko.colors.alternateBase
height: Math.round(content.height + 24) height: Math.round(content.height + 24)
width: parent ? parent.width : undefined 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 tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? 400 : model.data.width)
property double tempHeight: tempWidth * model.data.proportionalHeight property double tempHeight: tempWidth * model.data.proportionalHeight
property double divisor: model.isReply ? 4 : 2 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 visible: model.data.type == MtxEvent.VideoMessage
height: tooHigh ? timelineRoot.height / divisor : tempHeight height: tooHigh ? timelineView.height / divisor : tempHeight
width: tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth width: tooHigh ? (timelineView.height / divisor) / model.data.proportionalHeight : tempWidth
Image { Image {
anchors.fill: parent anchors.fill: parent
@ -58,7 +58,7 @@ Rectangle {
id: positionText id: positionText
text: "--:--:--" text: "--:--:--"
color: colors.text color: Nheko.colors.text
} }
Slider { Slider {
@ -92,14 +92,14 @@ Rectangle {
to: media.duration to: media.duration
onMoved: media.seek(value) onMoved: media.seek(value)
onValueChanged: updatePositionTexts() onValueChanged: updatePositionTexts()
palette: colors palette: Nheko.colors
} }
Text { Text {
id: durationText id: durationText
text: "--:--:--" text: "--:--:--"
color: colors.text color: Nheko.colors.text
} }
} }
@ -112,7 +112,7 @@ Rectangle {
id: button id: button
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
//color: colors.window //color: Nheko.colors.window
//radius: 22 //radius: 22
height: 32 height: 32
width: 32 width: 32
@ -121,7 +121,7 @@ Rectangle {
onClicked: { onClicked: {
switch (button.state) { switch (button.state) {
case "": case "":
TimelineManager.timeline.cacheMedia(model.data.id); room.cacheMedia(model.data.id);
break; break;
case "stopped": case "stopped":
media.play(); media.play();
@ -174,7 +174,7 @@ Rectangle {
} }
Connections { Connections {
target: TimelineManager.timeline target: room
onMediaCached: { onMediaCached: {
if (mxcUrl == model.data.url) { if (mxcUrl == model.data.url) {
media.source = cacheUrl; media.source = cacheUrl;
@ -194,7 +194,7 @@ Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
text: model.data.body text: model.data.body
elide: Text.ElideRight elide: Text.ElideRight
color: colors.text color: Nheko.colors.text
} }
Text { Text {
@ -202,7 +202,7 @@ Rectangle {
text: model.data.filesize text: model.data.filesize
textFormat: Text.PlainText textFormat: Text.PlainText
elide: Text.ElideRight elide: Text.ElideRight
color: colors.text color: Nheko.colors.text
} }
} }

View file

@ -33,7 +33,7 @@ Item {
anchors.top: replyContainer.top anchors.top: replyContainer.top
anchors.bottom: replyContainer.bottom anchors.bottom: replyContainer.bottom
width: 4 width: 4
color: TimelineManager.userColor(reply.modelData.userId, colors.window) color: TimelineManager.userColor(reply.modelData.userId, Nheko.colors.window)
} }
Column { Column {

View file

@ -9,9 +9,26 @@ MatrixText {
property string formatted: model.data.formattedBody property string formatted: model.data.formattedBody
property string copyText: selectedText ? getText(selectionStart, selectionEnd) : model.data.body 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 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 clip: isReply
selectByMouse: !Settings.mobileMode && !isReply selectByMouse: !Settings.mobileMode && !isReply
font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize

View file

@ -21,7 +21,7 @@ Pane {
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.Wrap wrapMode: Text.Wrap
text: qsTr("Waiting for other side to complete verification.") text: qsTr("Waiting for other side to complete verification.")
color: colors.text color: Nheko.colors.text
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }

View file

@ -15,8 +15,8 @@ ApplicationWindow {
onClosing: TimelineManager.removeVerificationFlow(flow) onClosing: TimelineManager.removeVerificationFlow(flow)
title: stack.currentItem.title title: stack.currentItem.title
flags: Qt.Dialog flags: Qt.Dialog
modality: Qt.WindowModal modality: Qt.NonModal
palette: colors palette: Nheko.colors
height: stack.implicitHeight height: stack.implicitHeight
width: stack.implicitWidth width: stack.implicitWidth
x: MainWindow.x + (MainWindow.width / 2) - (width / 2) x: MainWindow.x + (MainWindow.width / 2) - (width / 2)

View file

@ -19,7 +19,7 @@ Pane {
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.Wrap 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!") 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 verticalAlignment: Text.AlignVCenter
} }
@ -29,19 +29,19 @@ Pane {
Label { Label {
font.pixelSize: Qt.application.font.pixelSize * 2 font.pixelSize: Qt.application.font.pixelSize * 2
text: flow.sasList[0] text: flow.sasList[0]
color: colors.text color: Nheko.colors.text
} }
Label { Label {
font.pixelSize: Qt.application.font.pixelSize * 2 font.pixelSize: Qt.application.font.pixelSize * 2
text: flow.sasList[1] text: flow.sasList[1]
color: colors.text color: Nheko.colors.text
} }
Label { Label {
font.pixelSize: Qt.application.font.pixelSize * 2 font.pixelSize: Qt.application.font.pixelSize * 2
text: flow.sasList[2] text: flow.sasList[2]
color: colors.text color: Nheko.colors.text
} }
} }

View file

@ -19,7 +19,7 @@ Pane {
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.Wrap 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!") 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 verticalAlignment: Text.AlignVCenter
} }
@ -374,13 +374,13 @@ Pane {
text: col.emoji.emoji text: col.emoji.emoji
font.pixelSize: Qt.application.font.pixelSize * 2 font.pixelSize: Qt.application.font.pixelSize * 2
font.family: Settings.emojiFont font.family: Settings.emojiFont
color: colors.text color: Nheko.colors.text
} }
Label { Label {
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
text: col.emoji.description text: col.emoji.description
color: colors.text color: Nheko.colors.text
} }
} }

View file

@ -38,7 +38,7 @@ Pane {
return "Unknown verification error."; return "Unknown verification error.";
} }
} }
color: colors.text color: Nheko.colors.text
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }

View file

@ -35,7 +35,7 @@ Pane {
return qsTr("Your device (%1) has requested to be verified.").arg(flow.deviceId); return qsTr("Your device (%1) has requested to be verified.").arg(flow.deviceId);
} }
} }
color: colors.text color: Nheko.colors.text
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }

View file

@ -5,6 +5,7 @@
import QtQuick 2.3 import QtQuick 2.3
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Layouts 1.10 import QtQuick.Layouts 1.10
import im.nheko 1.0
Pane { Pane {
property string title: qsTr("Successful Verification") property string title: qsTr("Successful Verification")
@ -20,7 +21,7 @@ Pane {
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.Wrap wrapMode: Text.Wrap
text: qsTr("Verification successful! Both sides verified their devices!") text: qsTr("Verification successful! Both sides verified their devices!")
color: colors.text color: Nheko.colors.text
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }

View file

@ -30,13 +30,13 @@ Pane {
return qsTr("Waiting for other side to complete the verification process."); return qsTr("Waiting for other side to complete the verification process.");
} }
} }
color: colors.text color: Nheko.colors.text
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
BusyIndicator { BusyIndicator {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
palette: colors palette: Nheko.colors
} }
RowLayout { RowLayout {

View 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();
}
}
}

View file

@ -17,7 +17,7 @@ ImageButton {
image: ":/icons/icons/ui/smile.png" image: ":/icons/icons/ui/smile.png"
onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) { onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) {
TimelineManager.queueReactionMessage(event_id, emoji); room.input.reaction(event_id, emoji);
TimelineManager.focusMessageInput(); TimelineManager.focusMessageInput();
}) })
} }

View file

@ -18,9 +18,9 @@ Menu {
property alias model: gridView.model property alias model: gridView.model
property var textArea property var textArea
property string emojiCategory: "people" property string emojiCategory: "people"
property real highlightHue: colors.highlight.hslHue property real highlightHue: Nheko.colors.highlight.hslHue
property real highlightSat: colors.highlight.hslSaturation property real highlightSat: Nheko.colors.highlight.hslSaturation
property real highlightLight: colors.highlight.hslLightness property real highlightLight: Nheko.colors.highlight.hslLightness
function show(showAt, callback) { function show(showAt, callback) {
console.debug("Showing emojiPicker"); console.debug("Showing emojiPicker");
@ -80,7 +80,7 @@ Menu {
id: clearSearch id: clearSearch
visible: emojiSearch.text !== '' 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 focusPolicy: Qt.NoFocus
onClicked: emojiSearch.clear() onClicked: emojiSearch.clear()
@ -146,7 +146,7 @@ Menu {
background: Rectangle { background: Rectangle {
anchors.fill: parent anchors.fill: parent
color: hovered ? colors.highlight : 'transparent' color: hovered ? Nheko.colors.highlight : 'transparent'
radius: 5 radius: 5
} }
@ -163,7 +163,7 @@ Menu {
visible: emojiSearch.text === '' visible: emojiSearch.text === ''
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 1 Layout.preferredHeight: 1
color: emojiPopup.colors.alternateBase color: emojiPopup.Nheko.colors.alternateBase
} }
// Category picker row // Category picker row
@ -265,14 +265,14 @@ Menu {
fillMode: Image.Pad fillMode: Image.Pad
sourceSize.width: 32 sourceSize.width: 32
sourceSize.height: 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 { background: Rectangle {
anchors.fill: parent anchors.fill: parent
color: emojiPopup.model.category === model.category ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : 'transparent' color: emojiPopup.model.category === model.category ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : 'transparent'
radius: 5 radius: 5
border.color: emojiPopup.model.category === model.category ? colors.highlight : 'transparent' border.color: emojiPopup.model.category === model.category ? Nheko.colors.highlight : 'transparent'
} }
} }

View file

@ -31,11 +31,11 @@ Rectangle {
anchors.leftMargin: 8 anchors.leftMargin: 8
Avatar { Avatar {
width: avatarSize width: Nheko.avatarSize
height: avatarSize height: Nheko.avatarSize
url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
displayName: CallManager.callParty displayName: CallManager.callParty
onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id) onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
} }
Label { Label {

View file

@ -9,7 +9,7 @@ import im.nheko 1.0
Popup { Popup {
modal: true modal: true
palette: colors palette: Nheko.colors
// only set the anchors on Qt 5.12 or higher // 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 // see https://doc.qt.io/qt-5/qml-qtquick-controls2-popup.html#anchors.centerIn-prop
Component.onCompleted: { Component.onCompleted: {
@ -31,7 +31,7 @@ Popup {
Image { Image {
Layout.preferredWidth: 22 Layout.preferredWidth: 22
Layout.preferredHeight: 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 { ComboBox {
@ -49,7 +49,7 @@ Popup {
Image { Image {
Layout.preferredWidth: 22 Layout.preferredWidth: 22
Layout.preferredHeight: 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 { ComboBox {
@ -81,8 +81,8 @@ Popup {
} }
background: Rectangle { background: Rectangle {
color: colors.window color: Nheko.colors.window
border.color: colors.windowText border.color: Nheko.colors.windowText
} }
} }

View file

@ -12,7 +12,7 @@ Popup {
closePolicy: Popup.NoAutoClose closePolicy: Popup.NoAutoClose
width: parent.width width: parent.width
height: parent.height height: parent.height
palette: colors palette: Nheko.colors
Component { Component {
id: deviceError id: deviceError
@ -41,7 +41,7 @@ Popup {
Layout.topMargin: msgView.height / 25 Layout.topMargin: msgView.height / 25
text: CallManager.callParty text: CallManager.callParty
font.pointSize: fontMetrics.font.pointSize * 2 font.pointSize: fontMetrics.font.pointSize * 2
color: colors.windowText color: Nheko.colors.windowText
} }
Avatar { Avatar {
@ -62,14 +62,14 @@ Popup {
Layout.alignment: Qt.AlignCenter Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: msgView.height / 10 Layout.preferredWidth: msgView.height / 10
Layout.preferredHeight: msgView.height / 10 Layout.preferredHeight: msgView.height / 10
source: "image://colorimage/" + image + "?" + colors.windowText source: "image://colorimage/" + image + "?" + Nheko.colors.windowText
} }
Label { Label {
Layout.alignment: Qt.AlignCenter Layout.alignment: Qt.AlignCenter
text: CallManager.callType == CallType.VIDEO ? qsTr("Video Call") : qsTr("Voice Call") text: CallManager.callType == CallType.VIDEO ? qsTr("Video Call") : qsTr("Voice Call")
font.pointSize: fontMetrics.font.pointSize * 2 font.pointSize: fontMetrics.font.pointSize * 2
color: colors.windowText color: Nheko.colors.windowText
} }
} }
@ -88,7 +88,7 @@ Popup {
Image { Image {
Layout.preferredWidth: deviceCombos.imageSize Layout.preferredWidth: deviceCombos.imageSize
Layout.preferredHeight: 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 { ComboBox {
@ -107,7 +107,7 @@ Popup {
Image { Image {
Layout.preferredWidth: deviceCombos.imageSize Layout.preferredWidth: deviceCombos.imageSize
Layout.preferredHeight: 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 { ComboBox {
@ -194,8 +194,8 @@ Popup {
} }
background: Rectangle { background: Rectangle {
color: colors.window color: Nheko.colors.window
border.color: colors.windowText border.color: Nheko.colors.windowText
} }
} }

View file

@ -38,11 +38,11 @@ Rectangle {
anchors.leftMargin: 8 anchors.leftMargin: 8
Avatar { Avatar {
width: avatarSize width: Nheko.avatarSize
height: avatarSize height: Nheko.avatarSize
url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
displayName: CallManager.callParty displayName: CallManager.callParty
onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id) onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
} }
Label { Label {
@ -88,7 +88,7 @@ Rectangle {
Layout.rightMargin: 4 Layout.rightMargin: 4
icon.source: CallManager.callType == CallType.VIDEO ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png" icon.source: CallManager.callType == CallType.VIDEO ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
text: qsTr("Accept") text: qsTr("Accept")
palette: colors palette: Nheko.colors
onClicked: { onClicked: {
if (CallManager.mics.length == 0) { if (CallManager.mics.length == 0) {
var dialog = deviceError.createObject(timelineRoot, { var dialog = deviceError.createObject(timelineRoot, {
@ -121,7 +121,7 @@ Rectangle {
Layout.rightMargin: 16 Layout.rightMargin: 16
icon.source: "qrc:/icons/icons/ui/end-call.png" icon.source: "qrc:/icons/icons/ui/end-call.png"
text: qsTr("Decline") text: qsTr("Decline")
palette: colors palette: Nheko.colors
onClicked: { onClicked: {
CallManager.hangUp(); CallManager.hangUp();
} }

View file

@ -24,19 +24,19 @@ Popup {
Image { Image {
Layout.preferredWidth: 16 Layout.preferredWidth: 16
Layout.preferredHeight: 16 Layout.preferredHeight: 16
source: "image://colorimage/" + image + "?" + colors.windowText source: "image://colorimage/" + image + "?" + Nheko.colors.windowText
} }
Label { Label {
text: errorString text: errorString
color: colors.windowText color: Nheko.colors.windowText
} }
} }
background: Rectangle { background: Rectangle {
color: colors.window color: Nheko.colors.window
border.color: colors.windowText border.color: Nheko.colors.windowText
} }
} }

View file

@ -17,7 +17,7 @@ Popup {
anchors.centerIn = parent; anchors.centerIn = parent;
} }
palette: colors palette: Nheko.colors
Component { Component {
id: deviceError id: deviceError
@ -45,8 +45,8 @@ Popup {
Layout.leftMargin: 8 Layout.leftMargin: 8
Label { Label {
text: qsTr("Place a call to %1?").arg(TimelineManager.timeline.roomName) text: qsTr("Place a call to %1?").arg(room.roomName)
color: colors.windowText color: Nheko.colors.windowText
} }
Item { Item {
@ -75,11 +75,11 @@ Popup {
Avatar { Avatar {
Layout.rightMargin: cameraCombo.visible ? 16 : 64 Layout.rightMargin: cameraCombo.visible ? 16 : 64
width: avatarSize width: Nheko.avatarSize
height: avatarSize height: Nheko.avatarSize
url: TimelineManager.timeline.roomAvatarUrl.replace("mxc://", "image://MxcImage/") url: room.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
displayName: TimelineManager.timeline.roomName displayName: room.roomName
onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id) onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
} }
Button { Button {
@ -88,7 +88,7 @@ Popup {
onClicked: { onClicked: {
if (buttonLayout.validateMic()) { if (buttonLayout.validateMic()) {
Settings.microphone = micCombo.currentText; Settings.microphone = micCombo.currentText;
CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.VOICE); CallManager.sendInvite(room.roomId(), CallType.VOICE);
close(); close();
} }
} }
@ -102,7 +102,7 @@ Popup {
if (buttonLayout.validateMic()) { if (buttonLayout.validateMic()) {
Settings.microphone = micCombo.currentText; Settings.microphone = micCombo.currentText;
Settings.camera = cameraCombo.currentText; Settings.camera = cameraCombo.currentText;
CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.VIDEO); CallManager.sendInvite(room.roomId(), CallType.VIDEO);
close(); close();
} }
} }
@ -139,7 +139,7 @@ Popup {
Image { Image {
Layout.preferredWidth: 22 Layout.preferredWidth: 22
Layout.preferredHeight: 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 { ComboBox {
@ -160,7 +160,7 @@ Popup {
Image { Image {
Layout.preferredWidth: 22 Layout.preferredWidth: 22
Layout.preferredHeight: 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 { ComboBox {
@ -177,8 +177,8 @@ Popup {
} }
background: Rectangle { background: Rectangle {
color: colors.window color: Nheko.colors.window
border.color: colors.windowText border.color: Nheko.colors.windowText
} }
} }

View file

@ -18,7 +18,7 @@ Popup {
frameRateCombo.currentIndex = frameRateCombo.find(Settings.screenShareFrameRate); frameRateCombo.currentIndex = frameRateCombo.find(Settings.screenShareFrameRate);
} }
palette: colors palette: Nheko.colors
ColumnLayout { ColumnLayout {
Label { Label {
@ -27,8 +27,8 @@ Popup {
Layout.leftMargin: 8 Layout.leftMargin: 8
Layout.rightMargin: 8 Layout.rightMargin: 8
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
text: qsTr("Share desktop with %1?").arg(TimelineManager.timeline.roomName) text: qsTr("Share desktop with %1?").arg(room.roomName)
color: colors.windowText color: Nheko.colors.windowText
} }
RowLayout { RowLayout {
@ -39,7 +39,7 @@ Popup {
Label { Label {
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
text: qsTr("Window:") text: qsTr("Window:")
color: colors.windowText color: Nheko.colors.windowText
} }
ComboBox { ComboBox {
@ -59,7 +59,7 @@ Popup {
Label { Label {
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
text: qsTr("Frame rate:") text: qsTr("Frame rate:")
color: colors.windowText color: Nheko.colors.windowText
} }
ComboBox { ComboBox {
@ -136,7 +136,7 @@ Popup {
Settings.screenSharePiP = pipCheckBox.checked; Settings.screenSharePiP = pipCheckBox.checked;
Settings.screenShareRemoteVideo = remoteVideoCheckBox.checked; Settings.screenShareRemoteVideo = remoteVideoCheckBox.checked;
Settings.screenShareHideCursor = hideCursorCheckBox.checked; Settings.screenShareHideCursor = hideCursorCheckBox.checked;
CallManager.sendInvite(TimelineManager.timeline.roomId(), CallType.SCREEN, windowCombo.currentIndex); CallManager.sendInvite(room.roomId(), CallType.SCREEN, windowCombo.currentIndex);
close(); close();
} }
} }
@ -161,8 +161,8 @@ Popup {
} }
background: Rectangle { background: Rectangle {
color: colors.window color: Nheko.colors.window
border.color: colors.windowText border.color: Nheko.colors.windowText
} }
} }

View file

@ -123,11 +123,16 @@
<qresource prefix="/"> <qresource prefix="/">
<file>qtquickcontrols2.conf</file> <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/TimelineView.qml</file>
<file>qml/Avatar.qml</file> <file>qml/Avatar.qml</file>
<file>qml/Completer.qml</file> <file>qml/Completer.qml</file>
<file>qml/EncryptionIndicator.qml</file> <file>qml/EncryptionIndicator.qml</file>
<file>qml/ImageButton.qml</file> <file>qml/ImageButton.qml</file>
<file>qml/ElidedLabel.qml</file>
<file>qml/MatrixText.qml</file> <file>qml/MatrixText.qml</file>
<file>qml/MatrixTextField.qml</file> <file>qml/MatrixTextField.qml</file>
<file>qml/ToggleButton.qml</file> <file>qml/ToggleButton.qml</file>
@ -164,6 +169,7 @@
<file>qml/device-verification/NewVerificationRequest.qml</file> <file>qml/device-verification/NewVerificationRequest.qml</file>
<file>qml/device-verification/Failed.qml</file> <file>qml/device-verification/Failed.qml</file>
<file>qml/device-verification/Success.qml</file> <file>qml/device-verification/Success.qml</file>
<file>qml/dialogs/InputDialog.qml</file>
<file>qml/ui/Ripple.qml</file> <file>qml/ui/Ripple.qml</file>
<file>qml/voip/ActiveCallBar.qml</file> <file>qml/voip/ActiveCallBar.qml</file>
<file>qml/voip/CallDevices.qml</file> <file>qml/voip/CallDevices.qml</file>
@ -173,6 +179,8 @@
<file>qml/voip/PlaceCall.qml</file> <file>qml/voip/PlaceCall.qml</file>
<file>qml/voip/ScreenShare.qml</file> <file>qml/voip/ScreenShare.qml</file>
<file>qml/voip/VideoCall.qml</file> <file>qml/voip/VideoCall.qml</file>
<file>qml/components/AdaptiveLayout.qml</file>
<file>qml/components/AdaptiveLayoutElement.qml</file>
</qresource> </qresource>
<qresource prefix="/media"> <qresource prefix="/media">
<file>media/ring.ogg</file> <file>media/ring.ogg</file>

View file

@ -253,6 +253,8 @@ Cache::setup()
outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE); outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
txn.commit(); txn.commit();
databaseReady_ = true;
} }
void void
@ -788,6 +790,7 @@ Cache::nextBatchToken()
void void
Cache::deleteData() Cache::deleteData()
{ {
this->databaseReady_ = false;
// TODO: We need to remove the env_ while not accepting new requests. // TODO: We need to remove the env_ while not accepting new requests.
lmdb::dbi_close(env_, syncStateDb_); lmdb::dbi_close(env_, syncStateDb_);
lmdb::dbi_close(env_, roomsDb_); lmdb::dbi_close(env_, roomsDb_);
@ -2042,21 +2045,57 @@ Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id)
return fallbackDesc; return fallbackDesc;
} }
std::map<QString, bool> QHash<QString, RoomInfo>
Cache::invites() Cache::invites()
{ {
std::map<QString, bool> result; QHash<QString, RoomInfo> result;
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
auto cursor = lmdb::cursor::open(txn, invitesDb_); 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)) while (cursor.get(room_id, room_data, MDB_NEXT)) {
result.emplace(QString::fromStdString(std::string(room_id)), true); 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(); 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; return result;
} }
@ -2426,7 +2465,7 @@ Cache::joinedRooms()
std::optional<MemberInfo> std::optional<MemberInfo>
Cache::getMember(const std::string &room_id, const std::string &user_id) 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; return std::nullopt;
try { try {
@ -2440,7 +2479,8 @@ Cache::getMember(const std::string &room_id, const std::string &user_id)
return m; return m;
} }
} catch (std::exception &e) { } 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; return std::nullopt;
} }
@ -3412,6 +3452,10 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
if (!updateToWrite.master_keys.keys.empty() && if (!updateToWrite.master_keys.keys.empty() &&
update.master_keys.keys != updateToWrite.master_keys.keys) { 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; 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) { for (auto &[user_id, update] : updates) {
(void)update; (void)update;
if (user_id == local_user) { if (user_id == local_user) {
@ -3473,9 +3518,8 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
(void)status; (void)status;
emit verificationStatusChanged(user); 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; last_changed = cache_->last_changed;
req.token = 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( http::client()->query_keys(
req, req,
[cb, user_id, last_changed](const mtx::responses::QueryKeys &res, [cb, user_id, last_changed, this](const mtx::responses::QueryKeys &res,
mtx::http::RequestErr err) { mtx::http::RequestErr err) {
if (err) { if (err) {
nhlog::net()->warn("failed to query device keys: {},{}", nhlog::net()->warn("failed to query device keys: {},{}",
mtx::errors::to_string(err->matrix_error.errcode), mtx::errors::to_string(err->matrix_error.errcode),
@ -3561,10 +3618,7 @@ Cache::query_keys(const std::string &user_id,
return; return;
} }
cache::updateUserKeys(last_changed, res); emit userKeysUpdate(last_changed, res);
auto keys = cache::userKeys(user_id);
cb(keys.value_or(UserKeyCache{}), err);
}); });
} }
@ -3999,6 +4053,8 @@ avatarUrl(const QString &room_id, const QString &user_id)
mtx::presence::PresenceState mtx::presence::PresenceState
presenceState(const std::string &user_id) presenceState(const std::string &user_id)
{ {
if (!instance_)
return {};
return instance_->presenceState(user_id); return instance_->presenceState(user_id);
} }
std::string std::string
@ -4049,7 +4105,7 @@ roomInfo(bool withInvites)
{ {
return instance_->roomInfo(withInvites); return instance_->roomInfo(withInvites);
} }
std::map<QString, bool> QHash<QString, RoomInfo>
invites() invites()
{ {
return instance_->invites(); return instance_->invites();

View file

@ -62,7 +62,7 @@ joinedRooms();
QMap<QString, RoomInfo> QMap<QString, RoomInfo>
roomInfo(bool withInvites = true); roomInfo(bool withInvites = true);
std::map<QString, bool> QHash<QString, RoomInfo>
invites(); invites();
//! Calculate & return the name of the room. //! Calculate & return the name of the room.

View file

@ -50,6 +50,19 @@ struct DescInfo
QDateTime datetime; 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. //! UI info associated with a room.
struct RoomInfo struct RoomInfo
{ {

View file

@ -70,7 +70,8 @@ public:
QMap<QString, RoomInfo> roomInfo(bool withInvites = true); QMap<QString, RoomInfo> roomInfo(bool withInvites = true);
std::optional<mtx::events::state::CanonicalAlias> getRoomAliases(const std::string &roomid); 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. //! Calculate & return the name of the room.
QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
@ -100,6 +101,7 @@ public:
void saveState(const mtx::responses::Sync &res); void saveState(const mtx::responses::Sync &res);
bool isInitialized(); bool isInitialized();
bool isDatabaseReady() { return databaseReady_ && isInitialized(); }
std::string nextBatchToken(); std::string nextBatchToken();
@ -620,6 +622,8 @@ private:
QString cacheDirectory_; QString cacheDirectory_;
VerificationStorage verification_storage; VerificationStorage verification_storage;
bool databaseReady_ = false;
}; };
namespace cache { namespace cache {

View file

@ -23,10 +23,6 @@
#include "MainWindow.h" #include "MainWindow.h"
#include "MatrixClient.h" #include "MatrixClient.h"
#include "Olm.h" #include "Olm.h"
#include "RoomList.h"
#include "SideBarActions.h"
#include "Splitter.h"
#include "UserInfoWidget.h"
#include "UserSettingsPage.h" #include "UserSettingsPage.h"
#include "Utils.h" #include "Utils.h"
#include "ui/OverlayModal.h" #include "ui/OverlayModal.h"
@ -36,7 +32,6 @@
#include "notifications/Manager.h" #include "notifications/Manager.h"
#include "dialogs/ReadReceipts.h" #include "dialogs/ReadReceipts.h"
#include "popups/UserMentions.h"
#include "timeline/TimelineViewManager.h" #include "timeline/TimelineViewManager.h"
#include "blurhash.hpp" #include "blurhash.hpp"
@ -76,62 +71,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
topLayout_->setSpacing(0); topLayout_->setSpacing(0);
topLayout_->setMargin(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); view_manager_ = new TimelineViewManager(callManager_, this);
contentLayout_->addWidget(view_manager_->getWidget()); topLayout_->addWidget(view_manager_->getWidget());
// Splitter
splitter->addWidget(sideBar_);
splitter->addWidget(content_);
splitter->restoreSizes(parent->width());
connect(this, connect(this,
&ChatPage::downloadedSecrets, &ChatPage::downloadedSecrets,
@ -153,17 +95,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
trySync(); 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); connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL);
connect(&connectivityTimer_, &QTimer::timeout, this, [=]() { connect(&connectivityTimer_, &QTimer::timeout, this, [=]() {
if (http::client()->access_token().empty()) { 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(this, &ChatPage::loggedOut, this, &ChatPage::logout);
connect(
view_manager_, &TimelineViewManager::showRoomList, splitter, &Splitter::showFullRoomList);
connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList users) { 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) { for (int ii = 0; ii < users.size(); ++ii) {
QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() { 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::leftRoom, this, &ChatPage::removeRoom);
connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection); connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection);
connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications); connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications);
@ -255,60 +154,31 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
} }
}); });
connect(communitiesList_,
&CommunitiesList::communityChanged,
this,
[this](const QString &groupId) {
current_community_ = groupId;
if (groupId == "world") {
auto hidden = communitiesList_->hiddenTagsAndCommunities();
std::set<QString> roomsToHide = communitiesList_->roomList(groupId);
for (const auto &hiddenTag : hidden) {
auto temp = communitiesList_->roomList(hiddenTag);
roomsToHide.insert(temp.begin(), temp.end());
}
room_list_->removeFilter(roomsToHide);
} else {
auto hidden = communitiesList_->hiddenTagsAndCommunities();
hidden.erase(current_community_);
auto roomsToShow = communitiesList_->roomList(groupId);
for (const auto &hiddenTag : hidden) {
for (const auto &r : communitiesList_->roomList(hiddenTag))
roomsToShow.erase(r);
}
room_list_->applyFilter(roomsToShow);
}
});
connect(&notificationsManager, connect(&notificationsManager,
&NotificationsManager::notificationClicked, &NotificationsManager::notificationClicked,
this, this,
[this](const QString &roomid, const QString &eventid) { [this](const QString &roomid, const QString &eventid) {
Q_UNUSED(eventid) Q_UNUSED(eventid)
room_list_->highlightSelectedRoom(roomid); view_manager_->rooms()->setCurrentRoom(roomid);
activateWindow(); activateWindow();
}); });
connect(&notificationsManager, connect(&notificationsManager,
&NotificationsManager::sendNotificationReply, &NotificationsManager::sendNotificationReply,
this, this,
[this](const QString &roomid, const QString &eventid, const QString &body) { [this](const QString &roomid, const QString &eventid, const QString &body) {
view_manager_->rooms()->setCurrentRoom(roomid);
view_manager_->queueReply(roomid, eventid, body); view_manager_->queueReply(roomid, eventid, body);
room_list_->highlightSelectedRoom(roomid);
activateWindow(); 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( connect(
this, this,
&ChatPage::initializeViews, &ChatPage::initializeViews,
@ -318,31 +188,14 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
connect(this, connect(this,
&ChatPage::initializeEmptyViews, &ChatPage::initializeEmptyViews,
view_manager_, view_manager_,
&TimelineViewManager::initWithMessages); &TimelineViewManager::initializeRoomlist);
connect(this,
&ChatPage::initializeMentions,
user_mentions_popup_,
&popups::UserMentions::initializeMentions);
connect( connect(
this, &ChatPage::chatFocusChanged, view_manager_, &TimelineViewManager::chatFocusChanged); this, &ChatPage::chatFocusChanged, view_manager_, &TimelineViewManager::chatFocusChanged);
connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) { 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); view_manager_->sync(rooms);
removeLeftRooms(rooms.leave);
bool hasNotifications = false; bool hasNotifications = false;
for (const auto &room : rooms.join) { 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) if (room.second.unread_notifications.notification_count > 0)
hasNotifications = true; hasNotifications = true;
} }
@ -365,16 +218,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
emit notificationsRetrieved(std::move(res)); 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( connect(
this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync, Qt::QueuedConnection); this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync, Qt::QueuedConnection);
@ -427,8 +270,6 @@ ChatPage::dropToLoginPage(const QString &msg)
void void
ChatPage::resetUI() ChatPage::resetUI()
{ {
room_list_->clear();
user_info_widget_->reset();
view_manager_->clearAll(); view_manager_->clearAll();
emit unreadMessages(0); emit unreadMessages(0);
@ -481,9 +322,6 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
view_manager_, view_manager_,
&TimelineViewManager::updateReadReceipts); &TimelineViewManager::updateReadReceipts);
connect(
cache::client(), &Cache::roomReadStatus, room_list_, &RoomList::updateReadStatus);
connect(cache::client(), connect(cache::client(),
&Cache::removeNotification, &Cache::removeNotification,
&notificationsManager, &notificationsManager,
@ -559,10 +397,8 @@ ChatPage::loadStateFromCache()
try { try {
olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY); olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY);
emit initializeEmptyViews(cache::client()->roomIds()); emit initializeEmptyViews();
emit initializeRoomList(cache::roomInfo());
emit initializeMentions(cache::getTimelineMentions()); emit initializeMentions(cache::getTimelineMentions());
emit syncTags(cache::roomInfo().toStdMap());
cache::calculateRoomReadStatus(); cache::calculateRoomReadStatus();
@ -600,38 +436,6 @@ ChatPage::removeRoom(const QString &room_id)
nhlog::db()->critical("failure while removing room: {}", e.what()); nhlog::db()->critical("failure while removing room: {}", e.what());
// TODO: Notify the user. // 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 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 void
ChatPage::tryInitialSync() ChatPage::tryInitialSync()
{ {
@ -789,11 +581,9 @@ ChatPage::startInitialSync()
olm::handle_to_device_messages(res.to_device.events); olm::handle_to_device_messages(res.to_device.events);
emit initializeViews(std::move(res.rooms)); emit initializeViews(std::move(res.rooms));
emit initializeRoomList(cache::roomInfo());
emit initializeMentions(cache::getTimelineMentions()); emit initializeMentions(cache::getTimelineMentions());
cache::calculateRoomReadStatus(); cache::calculateRoomReadStatus();
emit syncTags(cache::roomInfo().toStdMap());
} catch (const lmdb::error &e) { } catch (const lmdb::error &e) {
nhlog::db()->error("failed to save state after initial sync: {}", nhlog::db()->error("failed to save state after initial sync: {}",
e.what()); e.what());
@ -830,12 +620,8 @@ ChatPage::handleSyncResponse(const mtx::responses::Sync &res, const std::string
auto updates = cache::getRoomInfo(cache::client()->roomsWithStateUpdates(res)); auto updates = cache::getRoomInfo(cache::client()->roomsWithStateUpdates(res));
emit syncRoomlist(updates);
emit syncUI(res.rooms); 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 // if we process a lot of syncs (1 every 200ms), this means we clean the
// db every 100s // db every 100s
static int syncCounter = 0; 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())); 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 void
ChatPage::changeRoom(const QString &room_id) ChatPage::changeRoom(const QString &room_id)
{ {
view_manager_->setHistoryView(room_id); view_manager_->rooms()->setCurrentRoom(room_id);
room_list_->highlightSelectedRoom(room_id);
} }
void void
ChatPage::inviteUser(QString userid, QString reason) ChatPage::inviteUser(QString userid, QString reason)
{ {
auto room = current_room_; auto room = currentRoom();
if (QMessageBox::question(this, if (QMessageBox::question(this,
tr("Confirm invite"), tr("Confirm invite"),
tr("Do you really want to invite %1 (%2)?") tr("Do you really want to invite %1 (%2)?")
.arg(cache::displayName(current_room_, userid)) .arg(cache::displayName(room, userid))
.arg(userid)) != QMessageBox::Yes) .arg(userid)) != QMessageBox::Yes)
return; return;
@ -1021,12 +806,12 @@ ChatPage::inviteUser(QString userid, QString reason)
void void
ChatPage::kickUser(QString userid, QString reason) ChatPage::kickUser(QString userid, QString reason)
{ {
auto room = current_room_; auto room = currentRoom();
if (QMessageBox::question(this, if (QMessageBox::question(this,
tr("Confirm kick"), tr("Confirm kick"),
tr("Do you really want to kick %1 (%2)?") tr("Do you really want to kick %1 (%2)?")
.arg(cache::displayName(current_room_, userid)) .arg(cache::displayName(room, userid))
.arg(userid)) != QMessageBox::Yes) .arg(userid)) != QMessageBox::Yes)
return; return;
@ -1048,12 +833,12 @@ ChatPage::kickUser(QString userid, QString reason)
void void
ChatPage::banUser(QString userid, QString reason) ChatPage::banUser(QString userid, QString reason)
{ {
auto room = current_room_; auto room = currentRoom();
if (QMessageBox::question(this, if (QMessageBox::question(this,
tr("Confirm ban"), tr("Confirm ban"),
tr("Do you really want to ban %1 (%2)?") tr("Do you really want to ban %1 (%2)?")
.arg(cache::displayName(current_room_, userid)) .arg(cache::displayName(room, userid))
.arg(userid)) != QMessageBox::Yes) .arg(userid)) != QMessageBox::Yes)
return; return;
@ -1075,12 +860,12 @@ ChatPage::banUser(QString userid, QString reason)
void void
ChatPage::unbanUser(QString userid, QString reason) ChatPage::unbanUser(QString userid, QString reason)
{ {
auto room = current_room_; auto room = currentRoom();
if (QMessageBox::question(this, if (QMessageBox::question(this,
tr("Confirm unban"), tr("Confirm unban"),
tr("Do you really want to unban %1 (%2)?") tr("Do you really want to unban %1 (%2)?")
.arg(cache::displayName(current_room_, userid)) .arg(cache::displayName(room, userid))
.arg(userid)) != QMessageBox::Yes) .arg(userid)) != QMessageBox::Yes)
return; return;
@ -1182,57 +967,6 @@ ChatPage::getProfileInfo()
emit setUserAvatar(QString::fromStdString(res.avatar_url)); 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 void
@ -1318,7 +1052,8 @@ ChatPage::startChat(QString userid)
if (std::find(room_members.begin(), if (std::find(room_members.begin(),
room_members.end(), room_members.end(),
(userid).toStdString()) != room_members.end()) { (userid).toStdString()) != room_members.end()) {
room_list_->highlightSelectedRoom(QString::fromStdString(room_id)); view_manager_->rooms()->setCurrentRoom(
QString::fromStdString(room_id));
return; return;
} }
} }
@ -1408,7 +1143,8 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
if (sigil1 == "u") { if (sigil1 == "u") {
if (action.isEmpty()) { if (action.isEmpty()) {
view_manager_->activeTimeline()->openUserProfile(mxid1); if (auto t = view_manager_->rooms()->currentRoom())
t->openUserProfile(mxid1);
} else if (action == "chat") { } else if (action == "chat") {
this->startChat(mxid1); this->startChat(mxid1);
} }
@ -1418,7 +1154,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
for (auto roomid : joined_rooms) { for (auto roomid : joined_rooms) {
if (roomid == targetRoomId) { if (roomid == targetRoomId) {
room_list_->highlightSelectedRoom(mxid1); view_manager_->rooms()->setCurrentRoom(mxid1);
if (!mxid2.isEmpty()) if (!mxid2.isEmpty())
view_manager_->showEvent(mxid1, mxid2); view_manager_->showEvent(mxid1, mxid2);
return; return;
@ -1436,7 +1172,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
auto aliases = cache::client()->getRoomAliases(roomid); auto aliases = cache::client()->getRoomAliases(roomid);
if (aliases) { if (aliases) {
if (aliases->alias == targetRoomAlias) { if (aliases->alias == targetRoomAlias) {
room_list_->highlightSelectedRoom( view_manager_->rooms()->setCurrentRoom(
QString::fromStdString(roomid)); QString::fromStdString(roomid));
if (!mxid2.isEmpty()) if (!mxid2.isEmpty())
view_manager_->showEvent( view_manager_->showEvent(
@ -1458,8 +1194,17 @@ ChatPage::handleMatrixUri(const QUrl &uri)
handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8()); handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8());
} }
void bool
ChatPage::highlightRoom(const QString &room_id) 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 "";
} }

View file

@ -27,15 +27,10 @@
#include "CacheCryptoStructs.h" #include "CacheCryptoStructs.h"
#include "CacheStructs.h" #include "CacheStructs.h"
#include "CommunitiesList.h"
#include "notifications/Manager.h" #include "notifications/Manager.h"
class OverlayModal; class OverlayModal;
class RoomList;
class SideBarActions;
class Splitter;
class TimelineViewManager; class TimelineViewManager;
class UserInfoWidget;
class UserSettings; class UserSettings;
class NotificationsManager; class NotificationsManager;
class TimelineModel; class TimelineModel;
@ -53,11 +48,6 @@ struct Notifications;
struct Sync; struct Sync;
struct Timeline; struct Timeline;
struct Rooms; struct Rooms;
struct LeftRoom;
}
namespace popups {
class UserMentions;
} }
using SecretsToDecrypt = std::map<std::string, mtx::secret_storage::AesHmacSha2EncryptedData>; using SecretsToDecrypt = std::map<std::string, mtx::secret_storage::AesHmacSha2EncryptedData>;
@ -71,7 +61,6 @@ public:
// Initialize all the components of the UI. // Initialize all the components of the UI.
void bootstrap(QString userid, QString homeserver, QString token); void bootstrap(QString userid, QString homeserver, QString token);
QString currentRoom() const { return current_room_; }
static ChatPage *instance() { return instance_; } static ChatPage *instance() { return instance_; }
@ -80,14 +69,6 @@ public:
TimelineViewManager *timelineManager() { return view_manager_; } TimelineViewManager *timelineManager() { return view_manager_; }
void deleteConfigs(); 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(); void initiateLogout();
QString status() const; QString status() const;
@ -95,6 +76,9 @@ public:
mtx::presence::PresenceState currentPresence() const; mtx::presence::PresenceState currentPresence() const;
// TODO(Nico): Get rid of this!
QString currentRoom() const;
public slots: public slots:
void handleMatrixUri(const QByteArray &uri); void handleMatrixUri(const QByteArray &uri);
void handleMatrixUri(const QUrl &uri); void handleMatrixUri(const QUrl &uri);
@ -102,7 +86,6 @@ public slots:
void startChat(QString userid); void startChat(QString userid);
void leaveRoom(const QString &room_id); void leaveRoom(const QString &room_id);
void createRoom(const mtx::requests::CreateRoom &req); void createRoom(const mtx::requests::CreateRoom &req);
void highlightRoom(const QString &room_id);
void joinRoom(const QString &room); void joinRoom(const QString &room);
void joinRoomVia(const std::string &room_id, void joinRoomVia(const std::string &room_id,
const std::vector<std::string> &via, const std::vector<std::string> &via,
@ -145,13 +128,10 @@ signals:
void leftRoom(const QString &room_id); void leftRoom(const QString &room_id);
void newRoom(const QString &room_id); void newRoom(const QString &room_id);
void initializeRoomList(QMap<QString, RoomInfo>);
void initializeViews(const mtx::responses::Rooms &rooms); void initializeViews(const mtx::responses::Rooms &rooms);
void initializeEmptyViews(const std::vector<QString> &roomIds); void initializeEmptyViews();
void initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs); void initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs);
void syncUI(const mtx::responses::Rooms &rooms); 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 dropToLoginPageCb(const QString &msg);
void notifyMessage(const QString &roomid, void notifyMessage(const QString &roomid,
@ -161,7 +141,6 @@ signals:
const QString &message, const QString &message,
const QImage &icon); const QImage &icon);
void updateGroupsInfo(const mtx::responses::JoinedGroups &groups);
void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state); void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state);
void themeChanged(); void themeChanged();
void decryptSidebarChanged(); void decryptSidebarChanged();
@ -213,56 +192,25 @@ private:
using Membership = mtx::events::StateEvent<mtx::events::state::Member>; using Membership = mtx::events::StateEvent<mtx::events::state::Member>;
using Memberships = std::map<std::string, Membership>; using Memberships = std::map<std::string, Membership>;
using LeftRooms = std::map<std::string, mtx::responses::LeftRoom>;
void removeLeftRooms(const LeftRooms &rooms);
void loadStateFromCache(); void loadStateFromCache();
void resetUI(); void resetUI();
//! Decides whether or not to hide the group's sidebar.
void setGroupViewState(bool isEnabled);
template<class Collection> template<class Collection>
Memberships getMemberships(const std::vector<Collection> &events) const; 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. //! Send desktop notification for the received messages.
void sendNotifications(const mtx::responses::Notifications &); void sendNotifications(const mtx::responses::Notifications &);
void showNotificationsDialog(const QPoint &point);
template<typename T> template<typename T>
void connectCallMessage(); void connectCallMessage();
QHBoxLayout *topLayout_; QHBoxLayout *topLayout_;
Splitter *splitter;
QWidget *sideBar_;
QVBoxLayout *sideBarLayout_;
QWidget *sideBarTopWidget_;
QVBoxLayout *sideBarTopWidgetLayout_;
QFrame *content_;
QVBoxLayout *contentLayout_;
CommunitiesList *communitiesList_;
RoomList *room_list_;
TimelineViewManager *view_manager_; TimelineViewManager *view_manager_;
SideBarActions *sidebarActions_;
QTimer connectivityTimer_; QTimer connectivityTimer_;
std::atomic_bool isConnected_; std::atomic_bool isConnected_;
QString current_room_;
QString current_community_;
UserInfoWidget *user_info_widget_;
popups::UserMentions *user_mentions_popup_;
// Global user settings. // Global user settings.
QSharedPointer<UserSettings> userSettings_; QSharedPointer<UserSettings> userSettings_;

View file

@ -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);
}

View file

@ -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_;
};

View file

@ -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)"));
}
}

View file

@ -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_;
};

View file

@ -19,7 +19,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model,
, max_completions_(max_completions) , max_completions_(max_completions)
{ {
setSourceModel(model); setSourceModel(model);
QRegularExpression splitPoints("\\s+|-"); QChar splitPoints(' ');
// insert all the full texts // insert all the full texts
for (int i = 0; i < sourceModel()->rowCount(); i++) { for (int i = 0; i < sourceModel()->rowCount(); i++) {
@ -48,7 +48,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model,
.toString() .toString()
.toLower(); .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 if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14
trie_.insert(e.toUcs4(), i); trie_.insert(e.toUcs4(), i);
} }
@ -59,7 +59,7 @@ CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model,
.toLower(); .toLower();
if (!string2.isEmpty()) { 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 if (!e.isEmpty()) // NOTE(Nico): Use Qt::SkipEmptyParts in Qt 5.14
trie_.insert(e.toUcs4(), i); trie_.insert(e.toUcs4(), i);
} }

View file

@ -22,7 +22,6 @@
#include "MainWindow.h" #include "MainWindow.h"
#include "MatrixClient.h" #include "MatrixClient.h"
#include "RegisterPage.h" #include "RegisterPage.h"
#include "Splitter.h"
#include "TrayIcon.h" #include "TrayIcon.h"
#include "UserSettingsPage.h" #include "UserSettingsPage.h"
#include "Utils.h" #include "Utils.h"
@ -109,10 +108,6 @@ MainWindow::MainWindow(QWidget *parent)
userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool))); userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool)));
connect( connect(
userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged); userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged);
connect(userSettingsPage_,
&UserSettingsPage::decryptSidebarChanged,
chat_page_,
&ChatPage::decryptSidebarChanged);
connect(trayIcon_, connect(trayIcon_,
SIGNAL(activated(QSystemTrayIcon::ActivationReason)), SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
this, this,
@ -176,20 +171,6 @@ MainWindow::setWindowTitle(int notificationCount)
QMainWindow::setWindowTitle(name); QMainWindow::setWindowTitle(name);
} }
void
MainWindow::showEvent(QShowEvent *event)
{
adjustSideBars();
QMainWindow::showEvent(event);
}
void
MainWindow::resizeEvent(QResizeEvent *event)
{
adjustSideBars();
QMainWindow::resizeEvent(event);
}
bool bool
MainWindow::event(QEvent *event) MainWindow::event(QEvent *event)
{ {
@ -203,22 +184,6 @@ MainWindow::event(QEvent *event)
return QMainWindow::event(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 void
MainWindow::restoreWindowSize() MainWindow::restoreWindowSize()
{ {
@ -295,6 +260,7 @@ MainWindow::showChatPage()
&Cache::secretChanged, &Cache::secretChanged,
userSettingsPage_, userSettingsPage_,
&UserSettingsPage::updateSecretStatus); &UserSettingsPage::updateSecretStatus);
emit reload();
} }
void void

View file

@ -77,13 +77,9 @@ public:
protected: protected:
void closeEvent(QCloseEvent *event) override; void closeEvent(QCloseEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void showEvent(QShowEvent *event) override;
bool event(QEvent *event) override; bool event(QEvent *event) override;
private slots: private slots:
//! Show or hide the sidebars based on window's size.
void adjustSideBars();
//! Handle interaction with the tray icon. //! Handle interaction with the tray icon.
void iconActivated(QSystemTrayIcon::ActivationReason reason); void iconActivated(QSystemTrayIcon::ActivationReason reason);
@ -109,6 +105,7 @@ private slots:
signals: signals:
void focusChanged(const bool focused); void focusChanged(const bool focused);
void reload();
private: private:
bool loadJdenticonPlugin(); bool loadJdenticonPlugin();

View file

@ -206,8 +206,11 @@ handle_olm_message(const OlmMessage &msg)
for (const auto &cipher : msg.ciphertext) { for (const auto &cipher : msg.ciphertext) {
// We skip messages not meant for the current device. // 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; continue;
}
const auto type = cipher.second.type; const auto type = cipher.second.type;
nhlog::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE"); 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) { for (const auto &id : session_ids) {
auto session = cache::getOlmSession(sender_key, id); auto session = cache::getOlmSession(sender_key, id);
if (!session) if (!session) {
nhlog::crypto()->warn("Unknown olm session: {}:{}", sender_key, id);
continue; continue;
}
mtx::crypto::BinaryBuf text; mtx::crypto::BinaryBuf text;

View file

@ -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();
}

View file

@ -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;
};

View file

@ -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);
}
}
}

View file

@ -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;
};

View file

@ -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);
}

View file

@ -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_;
};

View file

@ -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;
}

View file

@ -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_;
};

View file

@ -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);
}

View file

@ -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;
};

View file

@ -64,10 +64,14 @@ void
UserSettings::load(std::optional<QString> profile) UserSettings::load(std::optional<QString> profile)
{ {
QSettings settings; 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(); hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool();
hasAlertOnNotification_ = settings.value("user/alert_on_notification", false).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(); groupView_ = settings.value("user/group_view", true).toBool();
hiddenTags_ = settings.value("user/hidden_tags", QStringList{}).toStringList(); hiddenTags_ = settings.value("user/hidden_tags", QStringList{}).toStringList();
buttonsInTimeline_ = settings.value("user/timeline/buttons", true).toBool(); buttonsInTimeline_ = settings.value("user/timeline/buttons", true).toBool();
@ -248,6 +252,24 @@ UserSettings::setTimelineMaxWidth(int state)
emit timelineMaxWidthChanged(state); emit timelineMaxWidthChanged(state);
save(); 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 void
UserSettings::setDesktopNotifications(bool state) UserSettings::setDesktopNotifications(bool state)
@ -545,49 +567,14 @@ UserSettings::applyTheme()
{ {
QFile stylefile; QFile stylefile;
static QPalette original;
if (this->theme() == "light") { if (this->theme() == "light") {
stylefile.setFileName(":/styles/styles/nheko.qss"); 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") { } else if (this->theme() == "dark") {
stylefile.setFileName(":/styles/styles/nheko-dark.qss"); 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 { } else {
stylefile.setFileName(":/styles/styles/system.qss"); stylefile.setFileName(":/styles/styles/system.qss");
QApplication::setPalette(original);
} }
QApplication::setPalette(Theme::paletteFromTheme(this->theme().toStdString()));
stylefile.open(QFile::ReadOnly); stylefile.open(QFile::ReadOnly);
QString stylesheet = QString(stylefile.readAll()); QString stylesheet = QString(stylefile.readAll());
@ -606,6 +593,11 @@ UserSettings::save()
settings.setValue("start_in_tray", startInTray_); settings.setValue("start_in_tray", startInTray_);
settings.endGroup(); // window 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.beginGroup("timeline");
settings.setValue("buttons", buttonsInTimeline_); settings.setValue("buttons", buttonsInTimeline_);
settings.setValue("message_hover_highlight", messageHoverHighlight_); settings.setValue("message_hover_highlight", messageHoverHighlight_);

View file

@ -61,6 +61,10 @@ class UserSettings : public QObject
NOTIFY privacyScreenTimeoutChanged) NOTIFY privacyScreenTimeoutChanged)
Q_PROPERTY(int timelineMaxWidth READ timelineMaxWidth WRITE setTimelineMaxWidth NOTIFY Q_PROPERTY(int timelineMaxWidth READ timelineMaxWidth WRITE setTimelineMaxWidth NOTIFY
timelineMaxWidthChanged) 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(bool mobileMode READ mobileMode WRITE setMobileMode NOTIFY mobileModeChanged)
Q_PROPERTY(double fontSize READ fontSize WRITE setFontSize NOTIFY fontSizeChanged) Q_PROPERTY(double fontSize READ fontSize WRITE setFontSize NOTIFY fontSizeChanged)
Q_PROPERTY(QString font READ font WRITE setFontFamily NOTIFY fontChanged) Q_PROPERTY(QString font READ font WRITE setFontFamily NOTIFY fontChanged)
@ -129,6 +133,8 @@ public:
void setSortByImportance(bool state); void setSortByImportance(bool state);
void setButtonsInTimeline(bool state); void setButtonsInTimeline(bool state);
void setTimelineMaxWidth(int state); void setTimelineMaxWidth(int state);
void setCommunityListWidth(int state);
void setRoomListWidth(int state);
void setDesktopNotifications(bool state); void setDesktopNotifications(bool state);
void setAlertOnNotification(bool state); void setAlertOnNotification(bool state);
void setAvatarCircles(bool state); void setAvatarCircles(bool state);
@ -178,6 +184,8 @@ public:
return hasDesktopNotifications() || hasAlertOnNotification(); return hasDesktopNotifications() || hasAlertOnNotification();
} }
int timelineMaxWidth() const { return timelineMaxWidth_; } int timelineMaxWidth() const { return timelineMaxWidth_; }
int communityListWidth() const { return communityListWidth_; }
int roomListWidth() const { return roomListWidth_; }
double fontSize() const { return baseFontSize_; } double fontSize() const { return baseFontSize_; }
QString font() const { return font_; } QString font() const { return font_; }
QString emojiFont() const QString emojiFont() const
@ -227,6 +235,8 @@ signals:
void privacyScreenChanged(bool state); void privacyScreenChanged(bool state);
void privacyScreenTimeoutChanged(int state); void privacyScreenTimeoutChanged(int state);
void timelineMaxWidthChanged(int state); void timelineMaxWidthChanged(int state);
void roomListWidthChanged(int state);
void communityListWidthChanged(int state);
void mobileModeChanged(bool mode); void mobileModeChanged(bool mode);
void fontSizeChanged(double state); void fontSizeChanged(double state);
void fontChanged(QString state); void fontChanged(QString state);
@ -276,6 +286,8 @@ private:
bool shareKeysWithTrustedUsers_; bool shareKeysWithTrustedUsers_;
bool mobileMode_; bool mobileMode_;
int timelineMaxWidth_; int timelineMaxWidth_;
int roomListWidth_;
int communityListWidth_;
double baseFontSize_; double baseFontSize_;
QString font_; QString font_;
QString emojiFont_; QString emojiFont_;

View file

@ -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);
}

View file

@ -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_;
};

View file

@ -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;
}
}

View file

@ -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;
};

View file

@ -1,178 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QPaintEvent>
#include <QPainter>
#include <QScrollArea>
#include <QStyleOption>
#include <QTabWidget>
#include <QTimer>
#include <QVBoxLayout>
#include "Cache.h"
#include "ChatPage.h"
#include "EventAccessors.h"
#include "Logging.h"
#include "UserMentions.h"
using namespace popups;
UserMentions::UserMentions(QWidget *parent)
: QWidget{parent}
{
setAttribute(Qt::WA_ShowWithoutActivating, true);
setWindowFlags(Qt::FramelessWindowHint | Qt::Popup);
tab_layout_ = new QTabWidget(this);
top_layout_ = new QVBoxLayout(this);
top_layout_->setSpacing(0);
top_layout_->setMargin(0);
local_scroll_area_ = new QScrollArea(this);
local_scroll_area_->setWidgetResizable(true);
local_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
local_scroll_widget_ = new QWidget(this);
local_scroll_widget_->setObjectName("local_scroll_widget");
all_scroll_area_ = new QScrollArea(this);
all_scroll_area_->setWidgetResizable(true);
all_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
all_scroll_widget_ = new QWidget(this);
all_scroll_widget_->setObjectName("all_scroll_widget");
// Height of the typing display.
QFont f;
f.setPointSizeF(f.pointSizeF() * 0.9);
const int bottomMargin = QFontMetrics(f).height() + 6;
local_scroll_layout_ = new QVBoxLayout(local_scroll_widget_);
local_scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin);
local_scroll_layout_->setSpacing(0);
local_scroll_layout_->setObjectName("localscrollarea");
all_scroll_layout_ = new QVBoxLayout(all_scroll_widget_);
all_scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin);
all_scroll_layout_->setSpacing(0);
all_scroll_layout_->setObjectName("allscrollarea");
local_scroll_area_->setWidget(local_scroll_widget_);
local_scroll_area_->setAlignment(Qt::AlignBottom);
all_scroll_area_->setWidget(all_scroll_widget_);
all_scroll_area_->setAlignment(Qt::AlignBottom);
tab_layout_->addTab(local_scroll_area_, tr("This Room"));
tab_layout_->addTab(all_scroll_area_, tr("All Rooms"));
top_layout_->addWidget(tab_layout_);
setLayout(top_layout_);
}
void
UserMentions::initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs)
{
nhlog::ui()->debug("Initializing " + std::to_string(notifs.size()) + " notifications.");
for (const auto &item : notifs) {
for (const auto &notif : item.notifications) {
const auto event_id =
QString::fromStdString(mtx::accessors::event_id(notif.event));
try {
const auto room_id = QString::fromStdString(notif.room_id);
const auto user_id =
QString::fromStdString(mtx::accessors::sender(notif.event));
const auto body =
QString::fromStdString(mtx::accessors::body(notif.event));
pushItem(event_id,
user_id,
body,
room_id,
ChatPage::instance()->currentRoom());
} catch (const lmdb::error &e) {
nhlog::db()->warn("error while sending desktop notification: {}",
e.what());
}
}
}
}
void
UserMentions::showPopup()
{
for (auto widget : all_scroll_layout_->findChildren<QWidget *>()) {
delete widget;
}
for (auto widget : local_scroll_layout_->findChildren<QWidget *>()) {
delete widget;
}
auto notifs = cache::getTimelineMentions();
initializeMentions(notifs);
show();
}
void
UserMentions::pushItem(const QString &event_id,
const QString &user_id,
const QString &body,
const QString &room_id,
const QString &current_room_id)
{
(void)event_id;
(void)user_id;
(void)body;
(void)room_id;
(void)current_room_id;
// setUpdatesEnabled(false);
//
// // Add to the 'all' section
// TimelineItem *view_item = new TimelineItem(
// mtx::events::MessageType::Text, user_id, body, true, room_id,
// all_scroll_widget_);
// view_item->setEventId(event_id);
// view_item->hide();
//
// all_scroll_layout_->addWidget(view_item);
// QTimer::singleShot(0, this, [view_item, this]() {
// view_item->show();
// view_item->adjustSize();
// setUpdatesEnabled(true);
// });
//
// // if it matches the current room... add it to the current room as well.
// if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) {
// // Add to the 'local' section
// TimelineItem *local_view_item = new
// TimelineItem(mtx::events::MessageType::Text,
// user_id,
// body,
// true,
// room_id,
// local_scroll_widget_);
// local_view_item->setEventId(event_id);
// local_view_item->hide();
// local_scroll_layout_->addWidget(local_view_item);
//
// QTimer::singleShot(0, this, [local_view_item]() {
// local_view_item->show();
// local_view_item->adjustSize();
// });
// }
}
void
UserMentions::paintEvent(QPaintEvent *)
{
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}

View file

@ -1,49 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <mtx/responses/notifications.hpp>
#include <QMap>
#include <QString>
#include <QWidget>
class QPaintEvent;
class QTabWidget;
class QScrollArea;
class QVBoxLayout;
namespace popups {
class UserMentions : public QWidget
{
Q_OBJECT
public:
UserMentions(QWidget *parent = nullptr);
void initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs);
void showPopup();
protected:
void paintEvent(QPaintEvent *) override;
private:
void pushItem(const QString &event_id,
const QString &user_id,
const QString &body,
const QString &room_id,
const QString &current_room_id);
QTabWidget *tab_layout_;
QVBoxLayout *top_layout_;
QVBoxLayout *local_scroll_layout_;
QVBoxLayout *all_scroll_layout_;
QScrollArea *local_scroll_area_;
QWidget *local_scroll_widget_;
QScrollArea *all_scroll_area_;
QWidget *all_scroll_widget_;
};
}

View 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();
}

View 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_;
};

View file

@ -770,7 +770,7 @@ EventStore::decryptEvent(const IdIndex &idx,
} }
mtx::events::collections::TimelineEvents * 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()) if (this->thread() != QThread::currentThread())
nhlog::db()->warn("{} called from a different thread!", __func__); 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()) if (id.empty())
return nullptr; return nullptr;
IdIndex index{room_id_, std::string(id)}; IdIndex index{room_id_, std::move(id)};
if (resolve_edits) { if (resolve_edits) {
auto edits_ = edits(index.id); auto edits_ = edits(index.id);
if (!edits_.empty()) { if (!edits_.empty()) {
@ -796,14 +796,12 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt,
http::client()->get_event( http::client()->get_event(
room_id_, room_id_,
index.id, index.id,
[this, [this, relatedTo = std::string(related_to), id = index.id](
relatedTo = std::string(related_to.data(), related_to.size()), const mtx::events::collections::TimelineEvents &timeline,
id = index.id](const mtx::events::collections::TimelineEvents &timeline, mtx::http::RequestErr err) {
mtx::http::RequestErr err) {
if (err) { if (err) {
nhlog::net()->error( nhlog::net()->error(
"Failed to retrieve event with id {}, which " "Failed to retrieve event with id {}, which was "
"was "
"requested to show the replyTo for event {}", "requested to show the replyTo for event {}",
relatedTo, relatedTo,
id); id);

View file

@ -70,7 +70,7 @@ public:
// optionally returns the event or nullptr and fetches it, after which it emits a // optionally returns the event or nullptr and fetches it, after which it emits a
// relatedFetched event // relatedFetched event
mtx::events::collections::TimelineEvents *get(std::string_view id, mtx::events::collections::TimelineEvents *get(std::string id,
std::string_view related_to, std::string_view related_to,
bool decrypt = true, bool decrypt = true,
bool resolve_edits = true); bool resolve_edits = true);

View file

@ -20,6 +20,7 @@
#include "Cache.h" #include "Cache.h"
#include "ChatPage.h" #include "ChatPage.h"
#include "CompletionProxyModel.h" #include "CompletionProxyModel.h"
#include "Config.h"
#include "Logging.h" #include "Logging.h"
#include "MainWindow.h" #include "MainWindow.h"
#include "MatrixClient.h" #include "MatrixClient.h"
@ -508,8 +509,7 @@ InputBar::command(QString command, QString args)
} else if (command == "react") { } else if (command == "react") {
auto eventId = room->reply(); auto eventId = room->reply();
if (!eventId.isEmpty()) if (!eventId.isEmpty())
ChatPage::instance()->timelineManager()->queueReactionMessage( reaction(eventId, args.trimmed());
eventId, args.trimmed());
} else if (command == "join") { } else if (command == "join") {
ChatPage::instance()->joinRoom(args); ChatPage::instance()->joinRoom(args);
} else if (command == "part" || command == "leave") { } 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);
}
}

View file

@ -56,6 +56,7 @@ public slots:
void message(QString body, void message(QString body,
MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED, MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED,
bool rainbowify = false); bool rainbowify = false);
void reaction(const QString &reactedEvent, const QString &reactionKey);
private slots: private slots:
void startTyping(); void startTyping();

View 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());
}
}
}

View 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;
};

View file

@ -318,6 +318,8 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
, room_id_(room_id) , room_id_(room_id)
, manager_(manager) , manager_(manager)
{ {
lastMessage_.timestamp = 0;
connect( connect(
this, this,
&TimelineModel::redactionFailed, &TimelineModel::redactionFailed,
@ -572,7 +574,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
!event_id(event).empty() && event_id(event).front() == '$'); !event_id(event).empty() && event_id(event).front() == '$');
case IsEncrypted: { case IsEncrypted: {
auto id = event_id(event); auto id = event_id(event);
auto encrypted_event = events.get(id, id, false); auto encrypted_event = events.get(id, "", false);
return encrypted_event && return encrypted_event &&
std::holds_alternative< std::holds_alternative<
mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
@ -581,7 +583,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
case Trustlevel: { case Trustlevel: {
auto id = event_id(event); auto id = event_id(event);
auto encrypted_event = events.get(id, id, false); auto encrypted_event = events.get(id, "", false);
if (encrypted_event) { if (encrypted_event) {
if (auto encrypted = if (auto encrypted =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
@ -723,6 +725,20 @@ TimelineModel::fetchMore(const QModelIndex &)
events.fetchMore(); 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 void
TimelineModel::syncState(const mtx::responses::State &s) TimelineModel::syncState(const mtx::responses::State &s)
{ {
@ -866,14 +882,17 @@ TimelineModel::updateLastMessage()
if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) { if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) {
auto time = mtx::accessors::origin_server_ts(*event); auto time = mtx::accessors::origin_server_ts(*event);
uint64_t ts = time.toMSecsSinceEpoch(); uint64_t ts = time.toMSecsSinceEpoch();
emit manager_->updateRoomsLastMessage( auto description =
room_id_,
DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)), DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)),
QString::fromStdString(http::client()->user_id().to_string()), QString::fromStdString(http::client()->user_id().to_string()),
tr("You joined this room."), tr("You joined this room."),
utils::descriptiveTime(time), utils::descriptiveTime(time),
ts, ts,
time}); time};
if (description != lastMessage_) {
lastMessage_ = description;
emit lastMessageChanged();
}
return; return;
} }
if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event)) 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()), QString::fromStdString(http::client()->user_id().to_string()),
cache::displayName(room_id_, cache::displayName(room_id_,
QString::fromStdString(mtx::accessors::sender(*event)))); QString::fromStdString(mtx::accessors::sender(*event))));
emit manager_->updateRoomsLastMessage(room_id_, description); if (description != lastMessage_) {
lastMessage_ = description;
emit lastMessageChanged();
}
return; return;
} }
} }
@ -1866,6 +1888,17 @@ TimelineModel::roomName() const
QString::fromStdString(info[room_id_].name).toHtmlEscaped()); 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 QString
TimelineModel::roomAvatarUrl() const TimelineModel::roomAvatarUrl() const
{ {

View file

@ -14,6 +14,7 @@
#include <mtxclient/http/errors.hpp> #include <mtxclient/http/errors.hpp>
#include "CacheCryptoStructs.h" #include "CacheCryptoStructs.h"
#include "CacheStructs.h"
#include "EventStore.h" #include "EventStore.h"
#include "InputBar.h" #include "InputBar.h"
#include "Permissions.h" #include "Permissions.h"
@ -253,12 +254,15 @@ public:
} }
void updateLastMessage(); void updateLastMessage();
void sync(const mtx::responses::JoinedRoom &room);
void addEvents(const mtx::responses::Timeline &events); void addEvents(const mtx::responses::Timeline &events);
void syncState(const mtx::responses::State &state); void syncState(const mtx::responses::State &state);
template<class T> template<class T>
void sendMessageEvent(const T &content, mtx::events::EventType eventType); void sendMessageEvent(const T &content, mtx::events::EventType eventType);
RelatedInfo relatedInfo(QString id); RelatedInfo relatedInfo(QString id);
DescInfo lastMessage() const { return lastMessage_; }
public slots: public slots:
void setCurrentIndex(int index); void setCurrentIndex(int index);
int currentIndex() const { return idToIndex(currentId); } int currentIndex() const { return idToIndex(currentId); }
@ -303,12 +307,16 @@ public slots:
} }
QString roomName() const; QString roomName() const;
QString plainRoomName() const;
QString roomTopic() const; QString roomTopic() const;
InputBar *input() { return &input_; } InputBar *input() { return &input_; }
Permissions *permissions() { return &permissions_; } Permissions *permissions() { return &permissions_; }
QString roomAvatarUrl() const; QString roomAvatarUrl() const;
QString roomId() const { return room_id_; } QString roomId() const { return room_id_; }
bool hasMentions() { return highlight_count > 0; }
int notificationCount() { return notification_count; }
QString scrollTarget() const; QString scrollTarget() const;
private slots: private slots:
@ -328,6 +336,9 @@ signals:
void newCallEvent(const mtx::events::collections::TimelineEvents &event); void newCallEvent(const mtx::events::collections::TimelineEvents &event);
void scrollToIndex(int index); void scrollToIndex(int index);
void lastMessageChanged();
void notificationsChanged();
void openRoomSettingsDialog(RoomSettings *settings); void openRoomSettingsDialog(RoomSettings *settings);
void newMessageToSend(mtx::events::collections::TimelineEvents event); void newMessageToSend(mtx::events::collections::TimelineEvents event);
@ -372,7 +383,11 @@ private:
QString eventIdToShow; QString eventIdToShow;
int showEventTimerCounter = 0; int showEventTimerCounter = 0;
DescInfo lastMessage_{};
friend struct SendMessageVisitor; friend struct SendMessageVisitor;
int notification_count = 0, highlight_count = 0;
}; };
template<class T> template<class T>

View file

@ -4,7 +4,6 @@
#include "TimelineViewManager.h" #include "TimelineViewManager.h"
#include <QDesktopServices>
#include <QDropEvent> #include <QDropEvent>
#include <QMetaType> #include <QMetaType>
#include <QPalette> #include <QPalette>
@ -32,6 +31,7 @@
#include "emoji/Provider.h" #include "emoji/Provider.h"
#include "ui/NhekoCursorShape.h" #include "ui/NhekoCursorShape.h"
#include "ui/NhekoDropArea.h" #include "ui/NhekoDropArea.h"
#include "ui/NhekoGlobalObject.h"
Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents) Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
Q_DECLARE_METATYPE(std::vector<DeviceInfo>) 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 void
TimelineViewManager::updateColorPalette() TimelineViewManager::updateColorPalette()
{ {
@ -143,10 +128,13 @@ TimelineViewManager::userStatus(QString id) const
} }
TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *parent) TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *parent)
: imgProvider(new MxcImageProvider()) : QObject(parent)
, imgProvider(new MxcImageProvider())
, colorImgProvider(new ColorImageProvider()) , colorImgProvider(new ColorImageProvider())
, blurhashProvider(new BlurhashProvider()) , blurhashProvider(new BlurhashProvider())
, callManager_(callManager) , callManager_(callManager)
, rooms_(new RoomlistModel(this))
, communities_(new CommunitiesModel(this))
{ {
qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>(); qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>();
qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>(); qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>();
@ -204,6 +192,26 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership); QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
return ptr; 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>( qmlRegisterSingletonType<UserSettings>(
"im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * { "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = ChatPage::instance()->userSettings().data(); auto ptr = ChatPage::instance()->userSettings().data();
@ -220,6 +228,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
"im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * { "im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new Clipboard(); return new Clipboard();
}); });
qmlRegisterSingletonType<Nheko>(
"im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new Nheko();
});
qRegisterMetaType<mtx::events::collections::TimelineEvents>(); qRegisterMetaType<mtx::events::collections::TimelineEvents>();
qRegisterMetaType<std::vector<DeviceInfo>>(); qRegisterMetaType<std::vector<DeviceInfo>>();
@ -235,7 +247,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
"Error: Only enums"); "Error: Only enums");
#ifdef USE_QUICK_VIEW #ifdef USE_QUICK_VIEW
view = new QQuickView(); view = new QQuickView(parent);
container = QWidget::createWindowContainer(view, parent); container = QWidget::createWindowContainer(view, parent);
#else #else
view = new QQuickWidget(parent); view = new QQuickWidget(parent);
@ -252,13 +264,9 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
view->engine()->addImageProvider("MxcImage", imgProvider); view->engine()->addImageProvider("MxcImage", imgProvider);
view->engine()->addImageProvider("colorimage", colorImgProvider); view->engine()->addImageProvider("colorimage", colorImgProvider);
view->engine()->addImageProvider("blurhash", blurhashProvider); 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::themeChanged, this, &TimelineViewManager::updateColorPalette);
connect(parent,
&ChatPage::decryptSidebarChanged,
this,
&TimelineViewManager::updateEncryptedDescriptions);
connect( connect(
dynamic_cast<ChatPage *>(parent), dynamic_cast<ChatPage *>(parent),
&ChatPage::receivedRoomDeviceVerificationRequest, &ChatPage::receivedRoomDeviceVerificationRequest,
@ -329,100 +337,28 @@ TimelineViewManager::setVideoCallItem()
} }
void void
TimelineViewManager::sync(const mtx::responses::Rooms &rooms) TimelineViewManager::sync(const mtx::responses::Rooms &rooms_res)
{ {
for (const auto &[room_id, room] : rooms.join) { this->rooms_->sync(rooms_res);
// addRoom will only add the room, if it doesn't exist this->communities_->sync(rooms_res);
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);
if (ChatPage::instance()->userSettings()->typingNotifications()) { if (isInitialSync_) {
for (const auto &ev : room.ephemeral.events) { this->isInitialSync_ = false;
if (auto t = std::get_if< emit initialSyncChanged(false);
mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>(
&ev)) {
std::vector<QString> typing;
typing.reserve(t->content.user_ids.size());
for (const auto &user : t->content.user_ids) {
if (user != http::client()->user_id().to_string())
typing.push_back(
QString::fromStdString(user));
}
room_model->updateTypingUsers(typing);
}
}
}
} }
this->isInitialSync_ = false;
emit initialSyncChanged(false);
}
void
TimelineViewManager::addRoom(const QString &room_id)
{
if (!models.contains(room_id)) {
QSharedPointer<TimelineModel> newRoom(new TimelineModel(this, room_id));
newRoom->setDecryptDescription(
ChatPage::instance()->userSettings()->decryptSidebar());
connect(newRoom.data(),
&TimelineModel::newEncryptedImage,
imgProvider,
&MxcImageProvider::addEncryptionInfo);
connect(newRoom.data(),
&TimelineModel::forwardToRoom,
this,
&TimelineViewManager::forwardMessageToRoom);
models.insert(room_id, std::move(newRoom));
}
}
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 void
TimelineViewManager::showEvent(const QString &room_id, const QString &event_id) TimelineViewManager::showEvent(const QString &room_id, const QString &event_id)
{ {
auto room = models.find(room_id); if (auto room = rooms_->getRoomById(room_id)) {
if (room != models.end()) { if (rooms_->currentRoom() != room) {
if (timeline_ != room.value().data()) { rooms_->setCurrentRoom(room_id);
timeline_ = room.value().data();
emit activeTimelineChanged(timeline_);
container->setFocus(); container->setFocus();
nhlog::ui()->info("Activated room {}", room_id.toStdString()); 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); auto imgDialog = new dialogs::ImageOverlay(pixmap);
imgDialog->showFullScreen(); 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 // hide the overlay while presenting the save dialog for better
// cross platform support. // cross platform support.
imgDialog->hide(); imgDialog->hide();
if (!timeline_->saveMedia(eventId)) { if (!room->saveMedia(eventId)) {
imgDialog->show(); imgDialog->show();
} else { } else {
imgDialog->close(); 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 void
TimelineViewManager::openInviteUsersDialog() TimelineViewManager::openInviteUsersDialog()
{ {
@ -527,14 +415,14 @@ TimelineViewManager::openInviteUsersDialog()
[this](const QStringList &invitees) { emit inviteUsers(invitees); }); [this](const QStringList &invitees) { emit inviteUsers(invitees); });
} }
void void
TimelineViewManager::openMemberListDialog() const TimelineViewManager::openMemberListDialog(QString roomid) const
{ {
MainWindow::instance()->openMemberListDialog(timeline_->roomId()); MainWindow::instance()->openMemberListDialog(roomid);
} }
void void
TimelineViewManager::openLeaveRoomDialog() const TimelineViewManager::openLeaveRoomDialog(QString roomid) const
{ {
MainWindow::instance()->openLeaveRoomDialog(timeline_->roomId()); MainWindow::instance()->openLeaveRoomDialog(roomid);
} }
void void
@ -550,17 +438,21 @@ TimelineViewManager::verifyUser(QString userid)
if (std::find(room_members.begin(), if (std::find(room_members.begin(),
room_members.end(), room_members.end(),
(userid).toStdString()) != room_members.end()) { (userid).toStdString()) != room_members.end()) {
auto model = models.value(QString::fromStdString(room_id)); if (auto model =
auto flow = DeviceVerificationFlow::InitiateUserVerification( rooms_->getRoomById(QString::fromStdString(room_id))) {
this, model.data(), userid); auto flow =
connect(model.data(), DeviceVerificationFlow::InitiateUserVerification(
&TimelineModel::updateFlowEventId, this, model.data(), userid);
this, connect(model.data(),
[this, flow](std::string eventId) { &TimelineModel::updateFlowEventId,
dvList[QString::fromStdString(eventId)] = flow; this,
}); [this, flow](std::string eventId) {
emit newDeviceVerificationRequest(flow.data()); dvList[QString::fromStdString(eventId)] =
return; flow;
});
emit newDeviceVerificationRequest(flow.data());
return;
}
} }
} }
} }
@ -593,26 +485,24 @@ void
TimelineViewManager::updateReadReceipts(const QString &room_id, TimelineViewManager::updateReadReceipts(const QString &room_id,
const std::vector<QString> &event_ids) const std::vector<QString> &event_ids)
{ {
auto room = models.find(room_id); if (auto room = rooms_->getRoomById(room_id)) {
if (room != models.end()) { room->markEventsAsRead(event_ids);
room.value()->markEventsAsRead(event_ids);
} }
} }
void void
TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::string &session_id) TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::string &session_id)
{ {
auto room = models.find(QString::fromStdString(room_id)); if (auto room = rooms_->getRoomById(QString::fromStdString(room_id))) {
if (room != models.end()) { room->receivedSessionKey(session_id);
room.value()->receivedSessionKey(session_id);
} }
} }
void void
TimelineViewManager::initWithMessages(const std::vector<QString> &roomIds) TimelineViewManager::initializeRoomlist()
{ {
for (const auto &roomId : roomIds) rooms_->initializeRooms();
addRoom(roomId); communities_->initializeSidebar();
} }
void void
@ -620,74 +510,42 @@ TimelineViewManager::queueReply(const QString &roomid,
const QString &repliedToEvent, const QString &repliedToEvent,
const QString &replyBody) const QString &replyBody)
{ {
auto room = models.find(roomid); if (auto room = rooms_->getRoomById(roomid)) {
if (room != models.end()) { room->setReply(repliedToEvent);
room.value()->setReply(repliedToEvent); room->input()->message(replyBody);
room.value()->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 void
TimelineViewManager::queueCallMessage(const QString &roomid, TimelineViewManager::queueCallMessage(const QString &roomid,
const mtx::events::msg::CallInvite &callInvite) 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 void
TimelineViewManager::queueCallMessage(const QString &roomid, TimelineViewManager::queueCallMessage(const QString &roomid,
const mtx::events::msg::CallCandidates &callCandidates) const mtx::events::msg::CallCandidates &callCandidates)
{ {
models.value(roomid)->sendMessageEvent(callCandidates, if (auto room = rooms_->getRoomById(roomid))
mtx::events::EventType::CallCandidates); room->sendMessageEvent(callCandidates, mtx::events::EventType::CallCandidates);
} }
void void
TimelineViewManager::queueCallMessage(const QString &roomid, TimelineViewManager::queueCallMessage(const QString &roomid,
const mtx::events::msg::CallAnswer &callAnswer) 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 void
TimelineViewManager::queueCallMessage(const QString &roomid, TimelineViewManager::queueCallMessage(const QString &roomid,
const mtx::events::msg::CallHangUp &callHangUp) 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 void
@ -738,7 +596,7 @@ void
TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e,
QString roomId) QString roomId)
{ {
auto room = models.find(roomId); auto room = rooms_->getRoomById(roomId);
auto content = mtx::accessors::url(*e); auto content = mtx::accessors::url(*e);
std::optional<mtx::crypto::EncryptedFile> encryptionInfo = mtx::accessors::file(*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; ev.content.url = url;
} }
auto room = models.find(roomId); if (auto room = rooms_->getRoomById(roomId)) {
removeReplyFallback(ev); removeReplyFallback(ev);
ev.content.relations.relations.clear(); ev.content.relations.relations
room.value()->sendMessageEvent( .clear();
ev.content, room->sendMessageEvent(
mtx::events::EventType::RoomMessage); ev.content,
mtx::events::EventType::
RoomMessage);
}
} }
}, },
*e); *e);
@ -804,8 +665,7 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven
mtx::events::EventType::RoomMessage) { mtx::events::EventType::RoomMessage) {
e.content.relations.relations.clear(); e.content.relations.relations.clear();
removeReplyFallback(e); removeReplyFallback(e);
room.value()->sendMessageEvent(e.content, room->sendMessageEvent(e.content, mtx::events::EventType::RoomMessage);
mtx::events::EventType::RoomMessage);
} }
}, },
*e); *e);

Some files were not shown because too many files have changed in this diff Show more