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

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

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

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:
return "image://colorimage/:/icons/icons/ui/lock.png?green";
case Crypto.TOFU:
return "image://colorimage/:/icons/icons/ui/lock.png?" + colors.buttonText;
return "image://colorimage/:/icons/icons/ui/lock.png?" + Nheko.colors.buttonText;
default:
return "image://colorimage/:/icons/icons/ui/lock.png?#dd3d3d";
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@
import "./delegates"
import "./emoji"
import Qt.labs.platform 1.1 as Platform
import QtGraphicalEffects 1.0
import QtQuick 2.12
import QtQuick.Controls 2.3
@ -13,7 +14,7 @@ import im.nheko 1.0
ScrollView {
clip: false
palette: colors
palette: Nheko.colors
padding: 8
ScrollBar.horizontal.visible: false
@ -22,7 +23,7 @@ ScrollView {
property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2
model: TimelineManager.timeline
model: room
boundsBehavior: Flickable.StopAtBounds
pixelAligned: true
spacing: 4
@ -51,8 +52,8 @@ ScrollView {
z: 10
height: row.implicitHeight + padding * 2
width: row.implicitWidth + padding * 2
color: colors.window
border.color: colors.buttonText
color: Nheko.colors.window
border.color: Nheko.colors.buttonText
border.width: 1
radius: padding
@ -74,7 +75,7 @@ ScrollView {
id: editButton
visible: !!row.model && row.model.isEditable
buttonTextColor: colors.buttonText
buttonTextColor: Nheko.colors.buttonText
width: 16
hoverEnabled: true
image: ":/icons/icons/ui/edit.png"
@ -220,7 +221,7 @@ ScrollView {
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
visible: modelData && modelData.previousMessageDay !== modelData.day
text: modelData ? chat.model.formatDateSeparator(modelData.timestamp) : ""
color: colors.text
color: Nheko.colors.text
height: Math.round(fontMetrics.height * 1.4)
width: contentWidth * 1.2
horizontalAlignment: Text.AlignHCenter
@ -228,7 +229,7 @@ ScrollView {
background: Rectangle {
radius: parent.height / 2
color: colors.window
color: Nheko.colors.window
}
}
@ -240,8 +241,8 @@ ScrollView {
Avatar {
id: messageUserAvatar
width: avatarSize
height: avatarSize
width: Nheko.avatarSize
height: Nheko.avatarSize
url: modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : ""
displayName: modelData ? modelData.userName : ""
userid: modelData ? modelData.userId : ""
@ -267,7 +268,7 @@ ScrollView {
id: userName
text: modelData ? TimelineManager.escapeEmoji(modelData.userName) : ""
color: TimelineManager.userColor(modelData ? modelData.userId : "", colors.window)
color: TimelineManager.userColor(modelData ? modelData.userId : "", Nheko.colors.window)
textFormat: Text.RichText
ToolTip.visible: displayNameHover.hovered
ToolTip.text: modelData ? modelData.userId : ""
@ -288,11 +289,11 @@ ScrollView {
}
Label {
color: colors.buttonText
color: Nheko.colors.buttonText
text: modelData ? TimelineManager.userStatus(modelData.userId) : ""
textFormat: Text.PlainText
elide: Text.ElideRight
width: chat.delegateMaxWidth - parent.spacing * 2 - userName.implicitWidth - avatarSize
width: chat.delegateMaxWidth - parent.spacing * 2 - userName.implicitWidth - Nheko.avatarSize
font.italic: true
}
@ -317,7 +318,7 @@ ScrollView {
opacity: 0
visible: true
anchors.fill: timelinerow
color: colors.highlight
color: Nheko.colors.highlight
states: State {
name: "revealed"
@ -413,4 +414,141 @@ ScrollView {
}
Platform.Menu {
id: messageContextMenu
property string eventId
property string link
property string text
property int eventType
property bool isEncrypted
property bool isEditable
property bool isSender
function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) {
eventId = eventId_;
eventType = eventType_;
isEncrypted = isEncrypted_;
isEditable = isEditable_;
isSender = isSender_;
if (text_)
text = text_;
else
text = "";
if (link_)
link = link_;
else
link = "";
if (showAt_)
open(showAt_);
else
open();
}
Platform.MenuItem {
visible: messageContextMenu.text
enabled: visible
text: qsTr("&Copy")
onTriggered: Clipboard.text = messageContextMenu.text
}
Platform.MenuItem {
visible: messageContextMenu.link
enabled: visible
text: qsTr("Copy &link location")
onTriggered: Clipboard.text = messageContextMenu.link
}
Platform.MenuItem {
id: reactionOption
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
text: qsTr("Re&act")
onTriggered: emojiPopup.show(null, function(emoji) {
room.input.reaction(messageContextMenu.eventId, emoji);
})
}
Platform.MenuItem {
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
text: qsTr("Repl&y")
onTriggered: room.replyAction(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
enabled: visible
text: qsTr("&Edit")
onTriggered: room.editAction(messageContextMenu.eventId)
}
Platform.MenuItem {
text: qsTr("Read receip&ts")
onTriggered: room.readReceiptsAction(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage
text: qsTr("&Forward")
onTriggered: {
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(messageContextMenu.eventId);
forwardMess.open();
}
}
Platform.MenuItem {
text: qsTr("&Mark as read")
}
Platform.MenuItem {
text: qsTr("View raw message")
onTriggered: room.viewRawMessage(messageContextMenu.eventId)
}
Platform.MenuItem {
// TODO(Nico): Fix this still being iterated over, when using keyboard to select options
visible: messageContextMenu.isEncrypted
enabled: visible
text: qsTr("View decrypted raw message")
onTriggered: room.viewDecryptedRawMessage(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: (room ? room.permissions.canRedact() : false) || messageContextMenu.isSender
text: qsTr("Remo&ve message")
onTriggered: room.redactEvent(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
enabled: visible
text: qsTr("&Save as")
onTriggered: room.saveMedia(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
enabled: visible
text: qsTr("&Open in external program")
onTriggered: room.openMedia(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventId
enabled: visible
text: qsTr("Copy link to eve&nt")
onTriggered: room.copyLinkToEvent(messageContextMenu.eventId)
}
}
Component {
id: forwardCompleterComponent
ForwardCompleter {
}
}
}

View file

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

View file

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

View file

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

View file

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

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

123
resources/qml/Root.qml Normal file
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: {
if (model.state == MtxEvent.Read)
TimelineManager.timeline.readReceiptsAction(model.id);
room.readReceiptsAction(model.id);
}
image: {

View file

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

View file

@ -9,382 +9,137 @@ import "./voip"
import Qt.labs.platform 1.1 as Platform
import QtGraphicalEffects 1.0
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Controls 2.5
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import im.nheko 1.0
import im.nheko.EmojiModel 1.0
Page {
id: timelineRoot
Item {
id: timelineView
property var colors: currentActivePalette
property var systemInactive
property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive
readonly property int avatarSize: 40
property real highlightHue: colors.highlight.hslHue
property real highlightSat: colors.highlight.hslSaturation
property real highlightLight: colors.highlight.hslLightness
property var room: null
property bool showBackButton: false
palette: colors
FontMetrics {
id: fontMetrics
Label {
visible: !room && !TimelineManager.isInitialSync
anchors.centerIn: parent
text: qsTr("No room open")
font.pointSize: 24
color: Nheko.colors.text
}
EmojiPicker {
id: emojiPopup
colors: palette
model: TimelineManager.completerFor("allemoji", "")
BusyIndicator {
visible: running
anchors.centerIn: parent
running: TimelineManager.isInitialSync
height: 200
width: 200
z: 3
}
Component {
id: userProfileComponent
ColumnLayout {
id: timelineLayout
UserProfile {
}
}
Component {
id: roomSettingsComponent
RoomSettings {
}
}
Component {
id: mobileCallInviteDialog
CallInvite {
}
}
Component {
id: quickSwitcherComponent
QuickSwitcher {
}
}
Component {
id: forwardCompleterComponent
ForwardCompleter {
}
}
Shortcut {
sequence: "Ctrl+K"
onActivated: {
var quickSwitch = quickSwitcherComponent.createObject(timelineRoot);
TimelineManager.focusTimeline();
quickSwitch.open();
}
}
Platform.Menu {
id: messageContextMenu
property string eventId
property string link
property string text
property int eventType
property bool isEncrypted
property bool isEditable
property bool isSender
function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) {
eventId = eventId_;
eventType = eventType_;
isEncrypted = isEncrypted_;
isEditable = isEditable_;
isSender = isSender_;
if (text_)
text = text_;
else
text = "";
if (link_)
link = link_;
else
link = "";
if (showAt_)
open(showAt_);
else
open();
}
Platform.MenuItem {
visible: messageContextMenu.text
enabled: visible
text: qsTr("&Copy")
onTriggered: Clipboard.text = messageContextMenu.text
}
Platform.MenuItem {
visible: messageContextMenu.link
enabled: visible
text: qsTr("Copy &link location")
onTriggered: Clipboard.text = messageContextMenu.link
}
Platform.MenuItem {
id: reactionOption
visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.Reaction) : false
text: qsTr("Re&act")
onTriggered: emojiPopup.show(null, function(emoji) {
TimelineManager.queueReactionMessage(messageContextMenu.eventId, emoji);
})
}
Platform.MenuItem {
visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false
text: qsTr("Repl&y")
onTriggered: TimelineManager.timeline.replyAction(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.isEditable && (TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false)
enabled: visible
text: qsTr("&Edit")
onTriggered: TimelineManager.timeline.editAction(messageContextMenu.eventId)
}
Platform.MenuItem {
text: qsTr("Read receip&ts")
onTriggered: TimelineManager.timeline.readReceiptsAction(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage
text: qsTr("&Forward")
onTriggered: {
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(messageContextMenu.eventId);
forwardMess.open();
}
}
Platform.MenuItem {
text: qsTr("&Mark as read")
}
Platform.MenuItem {
text: qsTr("View raw message")
onTriggered: TimelineManager.timeline.viewRawMessage(messageContextMenu.eventId)
}
Platform.MenuItem {
// TODO(Nico): Fix this still being iterated over, when using keyboard to select options
visible: messageContextMenu.isEncrypted
enabled: visible
text: qsTr("View decrypted raw message")
onTriggered: TimelineManager.timeline.viewDecryptedRawMessage(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: (TimelineManager.timeline ? TimelineManager.timeline.permissions.canRedact() : false) || messageContextMenu.isSender
text: qsTr("Remo&ve message")
onTriggered: TimelineManager.timeline.redactEvent(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
enabled: visible
text: qsTr("&Save as")
onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
enabled: visible
text: qsTr("&Open in external program")
onTriggered: TimelineManager.timeline.openMedia(messageContextMenu.eventId)
}
Platform.MenuItem {
visible: messageContextMenu.eventId
enabled: visible
text: qsTr("Copy link to eve&nt")
onTriggered: TimelineManager.timeline.copyLinkToEvent(messageContextMenu.eventId)
}
}
Rectangle {
visible: room != null
anchors.fill: parent
color: colors.window
Component {
id: deviceVerificationDialog
DeviceVerification {
}
spacing: 0
TopBar {
showBackButton: timelineView.showBackButton
}
Connections {
target: TimelineManager
onNewDeviceVerificationRequest: {
var dialog = deviceVerificationDialog.createObject(timelineRoot, {
"flow": flow
});
dialog.show();
}
onOpenProfile: {
var userProfile = userProfileComponent.createObject(timelineRoot, {
"profile": profile
});
userProfile.show();
}
Rectangle {
Layout.fillWidth: true
height: 1
z: 3
color: Nheko.theme.separator
}
Connections {
target: TimelineManager.timeline
onOpenRoomSettingsDialog: {
var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
"roomSettings": settings
});
roomSettings.show();
}
}
Rectangle {
id: msgView
Layout.fillWidth: true
Layout.fillHeight: true
color: Nheko.colors.base
ColumnLayout {
anchors.fill: parent
spacing: 0
StackLayout {
id: stackLayout
currentIndex: 0
Connections {
function onRoomChanged() {
stackLayout.currentIndex = 0;
}
target: timelineView
}
MessageView {
Layout.fillWidth: true
implicitHeight: msgView.height - typingIndicator.height
}
Loader {
source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : ""
onLoaded: TimelineManager.setVideoCallItem()
}
Connections {
target: CallManager
onNewInviteState: {
if (CallManager.haveCallInvite && Settings.mobileMode) {
var dialog = mobileCallInviteDialog.createObject(msgView);
dialog.open();
}
TypingIndicator {
id: typingIndicator
}
}
}
Label {
visible: !TimelineManager.timeline && !TimelineManager.isInitialSync
anchors.centerIn: parent
text: qsTr("No room open")
font.pointSize: 24
color: colors.text
}
CallInviteBar {
id: callInviteBar
BusyIndicator {
visible: running
anchors.centerIn: parent
running: TimelineManager.isInitialSync
height: 200
width: 200
Layout.fillWidth: true
z: 3
}
ColumnLayout {
id: timelineLayout
visible: TimelineManager.timeline != null
anchors.fill: parent
spacing: 0
TopBar {
}
Rectangle {
Layout.fillWidth: true
height: 1
z: 3
color: colors.mid
}
Rectangle {
id: msgView
Layout.fillWidth: true
Layout.fillHeight: true
color: colors.base
ColumnLayout {
anchors.fill: parent
spacing: 0
StackLayout {
id: stackLayout
currentIndex: 0
Connections {
function onActiveTimelineChanged() {
stackLayout.currentIndex = 0;
}
target: TimelineManager
}
MessageView {
Layout.fillWidth: true
Layout.fillHeight: true
}
Loader {
source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : ""
onLoaded: TimelineManager.setVideoCallItem()
}
}
TypingIndicator {
}
}
}
CallInviteBar {
id: callInviteBar
Layout.fillWidth: true
z: 3
}
ActiveCallBar {
Layout.fillWidth: true
z: 3
}
Rectangle {
Layout.fillWidth: true
z: 3
height: 1
color: colors.mid
}
ReplyPopup {
}
MessageInput {
}
ActiveCallBar {
Layout.fillWidth: true
z: 3
}
NhekoDropArea {
anchors.fill: parent
roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : ""
Rectangle {
Layout.fillWidth: true
z: 3
height: 1
color: Nheko.theme.separator
}
ReplyPopup {
}
MessageInput {
}
}
PrivacyScreen {
NhekoDropArea {
anchors.fill: parent
visible: Settings.privacyScreen
screenTimeout: Settings.privacyScreenTimeout
timelineRoot: timelineLayout
roomid: room ? room.roomId() : ""
}
systemInactive: SystemPalette {
colorGroup: SystemPalette.Disabled
Connections {
target: room
onOpenRoomSettingsDialog: {
var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
"roomSettings": settings
});
roomSettings.show();
}
}
}

View file

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

View file

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

View file

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

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

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

View file

@ -58,7 +58,7 @@ Item {
NoticeMessage {
formatted: TimelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody
color: TimelineManager.userColor(modelData.userId, colors.window)
color: TimelineManager.userColor(modelData.userId, Nheko.colors.window)
}
}
@ -207,7 +207,7 @@ Item {
roleValue: MtxEvent.PowerLevels
NoticeMessage {
text: TimelineManager.timeline.formatPowerLevelEvent(model.data.id)
text: room.formatPowerLevelEvent(model.data.id)
}
}
@ -216,7 +216,7 @@ Item {
roleValue: MtxEvent.RoomJoinRules
NoticeMessage {
text: TimelineManager.timeline.formatJoinRuleEvent(model.data.id)
text: room.formatJoinRuleEvent(model.data.id)
}
}
@ -225,7 +225,7 @@ Item {
roleValue: MtxEvent.RoomHistoryVisibility
NoticeMessage {
text: TimelineManager.timeline.formatHistoryVisibilityEvent(model.data.id)
text: room.formatHistoryVisibilityEvent(model.data.id)
}
}
@ -234,7 +234,7 @@ Item {
roleValue: MtxEvent.RoomGuestAccess
NoticeMessage {
text: TimelineManager.timeline.formatGuestAccessEvent(model.data.id)
text: room.formatGuestAccessEvent(model.data.id)
}
}
@ -243,7 +243,7 @@ Item {
roleValue: MtxEvent.Member
NoticeMessage {
text: TimelineManager.timeline.formatMemberEvent(model.data.id)
text: room.formatMemberEvent(model.data.id)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,9 +9,26 @@ MatrixText {
property string formatted: model.data.formattedBody
property string copyText: selectedText ? getText(selectionStart, selectionEnd) : model.data.body
text: "<style type=\"text/css\">a { color:" + colors.link + ";}\ncode { background-color: " + colors.alternateBase + ";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap; background-color: " + colors.alternateBase + "'>")
// table border-collapse doesn't seem to work
text: "
<style type=\"text/css\">
a { color:" + Nheko.colors.link + ";}
code { background-color: " + Nheko.colors.alternateBase + ";}
table {
border-width: 1px;
border-collapse: collapse;
border-style: solid;
}
table th,
table td {
bgcolor: " + Nheko.colors.alternateBase + ";
border-collapse: collapse;
border: 1px solid " + Nheko.colors.text + ";
}
</style>
" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap; background-color: " + Nheko.colors.alternateBase + "'>").replace("<del>", "<s>").replace("</del>", "</s>").replace("<strike>", "<s>").replace("</strike>", "</s>")
width: parent ? parent.width : undefined
height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
height: isReply ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : undefined
clip: isReply
selectByMouse: !Settings.mobileMode && !isReply
font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize

View file

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

View file

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

View file

@ -19,7 +19,7 @@ Pane {
Layout.fillWidth: true
wrapMode: Text.Wrap
text: qsTr("Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification!")
color: colors.text
color: Nheko.colors.text
verticalAlignment: Text.AlignVCenter
}
@ -29,19 +29,19 @@ Pane {
Label {
font.pixelSize: Qt.application.font.pixelSize * 2
text: flow.sasList[0]
color: colors.text
color: Nheko.colors.text
}
Label {
font.pixelSize: Qt.application.font.pixelSize * 2
text: flow.sasList[1]
color: colors.text
color: Nheko.colors.text
}
Label {
font.pixelSize: Qt.application.font.pixelSize * 2
text: flow.sasList[2]
color: colors.text
color: Nheko.colors.text
}
}

View file

@ -19,7 +19,7 @@ Pane {
Layout.fillWidth: true
wrapMode: Text.Wrap
text: qsTr("Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification!")
color: colors.text
color: Nheko.colors.text
verticalAlignment: Text.AlignVCenter
}
@ -374,13 +374,13 @@ Pane {
text: col.emoji.emoji
font.pixelSize: Qt.application.font.pixelSize * 2
font.family: Settings.emojiFont
color: colors.text
color: Nheko.colors.text
}
Label {
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
text: col.emoji.description
color: colors.text
color: Nheko.colors.text
}
}

View file

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

View file

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

View file

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

View file

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

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"
onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) {
TimelineManager.queueReactionMessage(event_id, emoji);
room.input.reaction(event_id, emoji);
TimelineManager.focusMessageInput();
})
}

View file

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

View file

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

View file

@ -9,7 +9,7 @@ import im.nheko 1.0
Popup {
modal: true
palette: colors
palette: Nheko.colors
// only set the anchors on Qt 5.12 or higher
// see https://doc.qt.io/qt-5/qml-qtquick-controls2-popup.html#anchors.centerIn-prop
Component.onCompleted: {
@ -31,7 +31,7 @@ Popup {
Image {
Layout.preferredWidth: 22
Layout.preferredHeight: 22
source: "image://colorimage/:/icons/icons/ui/microphone-unmute.png?" + colors.windowText
source: "image://colorimage/:/icons/icons/ui/microphone-unmute.png?" + Nheko.colors.windowText
}
ComboBox {
@ -49,7 +49,7 @@ Popup {
Image {
Layout.preferredWidth: 22
Layout.preferredHeight: 22
source: "image://colorimage/:/icons/icons/ui/video-call.png?" + colors.windowText
source: "image://colorimage/:/icons/icons/ui/video-call.png?" + Nheko.colors.windowText
}
ComboBox {
@ -81,8 +81,8 @@ Popup {
}
background: Rectangle {
color: colors.window
border.color: colors.windowText
color: Nheko.colors.window
border.color: Nheko.colors.windowText
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -253,6 +253,8 @@ Cache::setup()
outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
txn.commit();
databaseReady_ = true;
}
void
@ -788,6 +790,7 @@ Cache::nextBatchToken()
void
Cache::deleteData()
{
this->databaseReady_ = false;
// TODO: We need to remove the env_ while not accepting new requests.
lmdb::dbi_close(env_, syncStateDb_);
lmdb::dbi_close(env_, roomsDb_);
@ -2042,21 +2045,57 @@ Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id)
return fallbackDesc;
}
std::map<QString, bool>
QHash<QString, RoomInfo>
Cache::invites()
{
std::map<QString, bool> result;
QHash<QString, RoomInfo> result;
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
auto cursor = lmdb::cursor::open(txn, invitesDb_);
std::string_view room_id, unused;
std::string_view room_id, room_data;
while (cursor.get(room_id, unused, MDB_NEXT))
result.emplace(QString::fromStdString(std::string(room_id)), true);
while (cursor.get(room_id, room_data, MDB_NEXT)) {
try {
RoomInfo tmp = json::parse(room_data);
tmp.member_count = getInviteMembersDb(txn, std::string(room_id)).size(txn);
result.insert(QString::fromStdString(std::string(room_id)), std::move(tmp));
} catch (const json::exception &e) {
nhlog::db()->warn("failed to parse room info for invite: "
"room_id ({}), {}: {}",
room_id,
std::string(room_data),
e.what());
}
}
cursor.close();
txn.commit();
return result;
}
std::optional<RoomInfo>
Cache::invite(std::string_view roomid)
{
std::optional<RoomInfo> result;
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
std::string_view room_data;
if (invitesDb_.get(txn, roomid, room_data)) {
try {
RoomInfo tmp = json::parse(room_data);
tmp.member_count = getInviteMembersDb(txn, std::string(roomid)).size(txn);
result = std::move(tmp);
} catch (const json::exception &e) {
nhlog::db()->warn("failed to parse room info for invite: "
"room_id ({}), {}: {}",
roomid,
std::string(room_data),
e.what());
}
}
return result;
}
@ -2426,7 +2465,7 @@ Cache::joinedRooms()
std::optional<MemberInfo>
Cache::getMember(const std::string &room_id, const std::string &user_id)
{
if (user_id.empty())
if (user_id.empty() || !env_.handle())
return std::nullopt;
try {
@ -2440,7 +2479,8 @@ Cache::getMember(const std::string &room_id, const std::string &user_id)
return m;
}
} catch (std::exception &e) {
nhlog::db()->warn("Failed to read member ({}): {}", user_id, e.what());
nhlog::db()->warn(
"Failed to read member ({}) in room ({}): {}", user_id, room_id, e.what());
}
return std::nullopt;
}
@ -3412,6 +3452,10 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
if (!updateToWrite.master_keys.keys.empty() &&
update.master_keys.keys != updateToWrite.master_keys.keys) {
nhlog::db()->debug("Master key of {} changed:\nold: {}\nnew: {}",
user,
updateToWrite.master_keys.keys.size(),
update.master_keys.keys.size());
updateToWrite.master_key_changed = true;
}
@ -3466,6 +3510,7 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
}
}
}
for (auto &[user_id, update] : updates) {
(void)update;
if (user_id == local_user) {
@ -3473,9 +3518,8 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
(void)status;
emit verificationStatusChanged(user);
}
} else {
emit verificationStatusChanged(user_id);
}
emit verificationStatusChanged(user_id);
}
}
@ -3549,10 +3593,23 @@ Cache::query_keys(const std::string &user_id,
last_changed = cache_->last_changed;
req.token = last_changed;
// use context object so that we can disconnect again
QObject *context{new QObject(this)};
QObject::connect(this,
&Cache::verificationStatusChanged,
context,
[cb, user_id, context_ = context](std::string updated_user) mutable {
if (user_id == updated_user) {
context_->deleteLater();
auto keys = cache::userKeys(user_id);
cb(keys.value_or(UserKeyCache{}), {});
}
});
http::client()->query_keys(
req,
[cb, user_id, last_changed](const mtx::responses::QueryKeys &res,
mtx::http::RequestErr err) {
[cb, user_id, last_changed, this](const mtx::responses::QueryKeys &res,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to query device keys: {},{}",
mtx::errors::to_string(err->matrix_error.errcode),
@ -3561,10 +3618,7 @@ Cache::query_keys(const std::string &user_id,
return;
}
cache::updateUserKeys(last_changed, res);
auto keys = cache::userKeys(user_id);
cb(keys.value_or(UserKeyCache{}), err);
emit userKeysUpdate(last_changed, res);
});
}
@ -3999,6 +4053,8 @@ avatarUrl(const QString &room_id, const QString &user_id)
mtx::presence::PresenceState
presenceState(const std::string &user_id)
{
if (!instance_)
return {};
return instance_->presenceState(user_id);
}
std::string
@ -4049,7 +4105,7 @@ roomInfo(bool withInvites)
{
return instance_->roomInfo(withInvites);
}
std::map<QString, bool>
QHash<QString, RoomInfo>
invites()
{
return instance_->invites();

View file

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

View file

@ -50,6 +50,19 @@ struct DescInfo
QDateTime datetime;
};
inline bool
operator==(const DescInfo &a, const DescInfo &b)
{
return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) ==
std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime);
}
inline bool
operator!=(const DescInfo &a, const DescInfo &b)
{
return std::tie(a.timestamp, a.event_id, a.userid, a.body, a.descriptiveTime) !=
std::tie(b.timestamp, b.event_id, b.userid, b.body, b.descriptiveTime);
}
//! UI info associated with a room.
struct RoomInfo
{

View file

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

View file

@ -23,10 +23,6 @@
#include "MainWindow.h"
#include "MatrixClient.h"
#include "Olm.h"
#include "RoomList.h"
#include "SideBarActions.h"
#include "Splitter.h"
#include "UserInfoWidget.h"
#include "UserSettingsPage.h"
#include "Utils.h"
#include "ui/OverlayModal.h"
@ -36,7 +32,6 @@
#include "notifications/Manager.h"
#include "dialogs/ReadReceipts.h"
#include "popups/UserMentions.h"
#include "timeline/TimelineViewManager.h"
#include "blurhash.hpp"
@ -76,62 +71,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
topLayout_->setSpacing(0);
topLayout_->setMargin(0);
communitiesList_ = new CommunitiesList(this);
topLayout_->addWidget(communitiesList_);
splitter = new Splitter(this);
splitter->setHandleWidth(0);
topLayout_->addWidget(splitter);
// SideBar
sideBar_ = new QFrame(this);
sideBar_->setObjectName("sideBar");
sideBar_->setMinimumWidth(::splitter::calculateSidebarSizes(QFont{}).normal);
sideBarLayout_ = new QVBoxLayout(sideBar_);
sideBarLayout_->setSpacing(0);
sideBarLayout_->setMargin(0);
sideBarTopWidget_ = new QWidget(sideBar_);
sidebarActions_ = new SideBarActions(this);
connect(
sidebarActions_, &SideBarActions::showSettings, this, &ChatPage::showUserSettingsPage);
connect(sidebarActions_, &SideBarActions::joinRoom, this, &ChatPage::joinRoom);
connect(sidebarActions_, &SideBarActions::createRoom, this, &ChatPage::createRoom);
user_info_widget_ = new UserInfoWidget(sideBar_);
connect(user_info_widget_, &UserInfoWidget::openGlobalUserProfile, this, [this]() {
UserProfile *userProfile = new UserProfile("", utils::localUser(), view_manager_);
emit view_manager_->openProfile(userProfile);
});
user_mentions_popup_ = new popups::UserMentions();
room_list_ = new RoomList(userSettings, sideBar_);
connect(room_list_, &RoomList::joinRoom, this, &ChatPage::joinRoom);
sideBarLayout_->addWidget(user_info_widget_);
sideBarLayout_->addWidget(room_list_);
sideBarLayout_->addWidget(sidebarActions_);
sideBarTopWidgetLayout_ = new QVBoxLayout(sideBarTopWidget_);
sideBarTopWidgetLayout_->setSpacing(0);
sideBarTopWidgetLayout_->setMargin(0);
// Content
content_ = new QFrame(this);
content_->setObjectName("mainContent");
contentLayout_ = new QVBoxLayout(content_);
contentLayout_->setSpacing(0);
contentLayout_->setMargin(0);
view_manager_ = new TimelineViewManager(callManager_, this);
contentLayout_->addWidget(view_manager_->getWidget());
// Splitter
splitter->addWidget(sideBar_);
splitter->addWidget(content_);
splitter->restoreSizes(parent->width());
topLayout_->addWidget(view_manager_->getWidget());
connect(this,
&ChatPage::downloadedSecrets,
@ -153,17 +95,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
trySync();
});
connect(
new QShortcut(QKeySequence("Ctrl+Down"), this), &QShortcut::activated, this, [this]() {
if (isVisible())
room_list_->nextRoom();
});
connect(
new QShortcut(QKeySequence("Ctrl+Up"), this), &QShortcut::activated, this, [this]() {
if (isVisible())
room_list_->previousRoom();
});
connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL);
connect(&connectivityTimer_, &QTimer::timeout, this, [=]() {
if (http::client()->access_token().empty()) {
@ -185,10 +116,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
connect(this, &ChatPage::loggedOut, this, &ChatPage::logout);
connect(
view_manager_, &TimelineViewManager::showRoomList, splitter, &Splitter::showFullRoomList);
connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList users) {
const auto room_id = current_room_.toStdString();
const auto room_id = currentRoom().toStdString();
for (int ii = 0; ii < users.size(); ++ii) {
QTimer::singleShot(ii * 500, this, [this, room_id, ii, users]() {
@ -211,36 +140,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
}
});
connect(room_list_, &RoomList::roomChanged, this, [this](QString room_id) {
this->current_room_ = room_id;
});
connect(room_list_, &RoomList::roomChanged, splitter, &Splitter::showChatView);
connect(
room_list_, &RoomList::roomChanged, view_manager_, &TimelineViewManager::setHistoryView);
connect(room_list_, &RoomList::acceptInvite, this, [this](const QString &room_id) {
joinRoom(room_id);
room_list_->removeRoom(room_id, currentRoom() == room_id);
});
connect(room_list_, &RoomList::declineInvite, this, [this](const QString &room_id) {
leaveRoom(room_id);
room_list_->removeRoom(room_id, currentRoom() == room_id);
});
connect(view_manager_,
&TimelineViewManager::updateRoomsLastMessage,
room_list_,
&RoomList::updateRoomDescription);
connect(room_list_,
SIGNAL(totalUnreadMessageCountUpdated(int)),
this,
SIGNAL(unreadMessages(int)));
connect(
this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities);
connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection);
connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications);
@ -255,60 +154,31 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
}
});
connect(communitiesList_,
&CommunitiesList::communityChanged,
this,
[this](const QString &groupId) {
current_community_ = groupId;
if (groupId == "world") {
auto hidden = communitiesList_->hiddenTagsAndCommunities();
std::set<QString> roomsToHide = communitiesList_->roomList(groupId);
for (const auto &hiddenTag : hidden) {
auto temp = communitiesList_->roomList(hiddenTag);
roomsToHide.insert(temp.begin(), temp.end());
}
room_list_->removeFilter(roomsToHide);
} else {
auto hidden = communitiesList_->hiddenTagsAndCommunities();
hidden.erase(current_community_);
auto roomsToShow = communitiesList_->roomList(groupId);
for (const auto &hiddenTag : hidden) {
for (const auto &r : communitiesList_->roomList(hiddenTag))
roomsToShow.erase(r);
}
room_list_->applyFilter(roomsToShow);
}
});
connect(&notificationsManager,
&NotificationsManager::notificationClicked,
this,
[this](const QString &roomid, const QString &eventid) {
Q_UNUSED(eventid)
room_list_->highlightSelectedRoom(roomid);
view_manager_->rooms()->setCurrentRoom(roomid);
activateWindow();
});
connect(&notificationsManager,
&NotificationsManager::sendNotificationReply,
this,
[this](const QString &roomid, const QString &eventid, const QString &body) {
view_manager_->rooms()->setCurrentRoom(roomid);
view_manager_->queueReply(roomid, eventid, body);
room_list_->highlightSelectedRoom(roomid);
activateWindow();
});
setGroupViewState(userSettings_->groupView());
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
// ensure the qml context is shutdown before we destroy all other singletons
// Otherwise Qml will try to access the room list or settings, after they have been
// destroyed
topLayout_->removeWidget(view_manager_->getWidget());
delete view_manager_->getWidget();
});
connect(userSettings_.data(),
&UserSettings::groupViewStateChanged,
this,
&ChatPage::setGroupViewState);
connect(this, &ChatPage::initializeRoomList, room_list_, &RoomList::initialize);
connect(
this,
&ChatPage::initializeViews,
@ -318,31 +188,14 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
connect(this,
&ChatPage::initializeEmptyViews,
view_manager_,
&TimelineViewManager::initWithMessages);
connect(this,
&ChatPage::initializeMentions,
user_mentions_popup_,
&popups::UserMentions::initializeMentions);
&TimelineViewManager::initializeRoomlist);
connect(
this, &ChatPage::chatFocusChanged, view_manager_, &TimelineViewManager::chatFocusChanged);
connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) {
try {
room_list_->cleanupInvites(cache::invites());
} catch (const lmdb::error &e) {
nhlog::db()->error("failed to retrieve invites: {}", e.what());
}
view_manager_->sync(rooms);
removeLeftRooms(rooms.leave);
bool hasNotifications = false;
for (const auto &room : rooms.join) {
auto room_id = QString::fromStdString(room.first);
updateRoomNotificationCount(
room_id,
room.second.unread_notifications.notification_count,
room.second.unread_notifications.highlight_count);
if (room.second.unread_notifications.notification_count > 0)
hasNotifications = true;
}
@ -365,16 +218,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
emit notificationsRetrieved(std::move(res));
});
});
connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync);
connect(this, &ChatPage::syncTags, communitiesList_, &CommunitiesList::syncTags);
// Callbacks to update the user info (top left corner of the page).
connect(this, &ChatPage::setUserAvatar, user_info_widget_, &UserInfoWidget::setAvatar);
connect(this, &ChatPage::setUserDisplayName, this, [this](const QString &name) {
auto userid = utils::localUser();
user_info_widget_->setUserId(userid);
user_info_widget_->setDisplayName(name);
});
connect(
this, &ChatPage::tryInitialSyncCb, this, &ChatPage::tryInitialSync, Qt::QueuedConnection);
@ -427,8 +270,6 @@ ChatPage::dropToLoginPage(const QString &msg)
void
ChatPage::resetUI()
{
room_list_->clear();
user_info_widget_->reset();
view_manager_->clearAll();
emit unreadMessages(0);
@ -481,9 +322,6 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
view_manager_,
&TimelineViewManager::updateReadReceipts);
connect(
cache::client(), &Cache::roomReadStatus, room_list_, &RoomList::updateReadStatus);
connect(cache::client(),
&Cache::removeNotification,
&notificationsManager,
@ -559,10 +397,8 @@ ChatPage::loadStateFromCache()
try {
olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY);
emit initializeEmptyViews(cache::client()->roomIds());
emit initializeRoomList(cache::roomInfo());
emit initializeEmptyViews();
emit initializeMentions(cache::getTimelineMentions());
emit syncTags(cache::roomInfo().toStdMap());
cache::calculateRoomReadStatus();
@ -600,38 +436,6 @@ ChatPage::removeRoom(const QString &room_id)
nhlog::db()->critical("failure while removing room: {}", e.what());
// TODO: Notify the user.
}
room_list_->removeRoom(room_id, room_id == current_room_);
}
void
ChatPage::removeLeftRooms(const std::map<std::string, mtx::responses::LeftRoom> &rooms)
{
for (auto it = rooms.cbegin(); it != rooms.cend(); ++it) {
const auto room_id = QString::fromStdString(it->first);
room_list_->removeRoom(room_id, room_id == current_room_);
}
}
void
ChatPage::setGroupViewState(bool isEnabled)
{
if (!isEnabled) {
communitiesList_->communityChanged("world");
communitiesList_->hide();
return;
}
communitiesList_->show();
}
void
ChatPage::updateRoomNotificationCount(const QString &room_id,
uint16_t notification_count,
uint16_t highlight_count)
{
room_list_->updateUnreadMessageCount(room_id, notification_count, highlight_count);
}
void
@ -679,18 +483,6 @@ ChatPage::sendNotifications(const mtx::responses::Notifications &res)
}
}
void
ChatPage::showNotificationsDialog(const QPoint &widgetPos)
{
auto notifDialog = user_mentions_popup_;
notifDialog->setGeometry(
widgetPos.x() - (width() / 10), widgetPos.y() + 25, width() / 5, height() / 2);
notifDialog->raise();
notifDialog->showPopup();
}
void
ChatPage::tryInitialSync()
{
@ -789,11 +581,9 @@ ChatPage::startInitialSync()
olm::handle_to_device_messages(res.to_device.events);
emit initializeViews(std::move(res.rooms));
emit initializeRoomList(cache::roomInfo());
emit initializeMentions(cache::getTimelineMentions());
cache::calculateRoomReadStatus();
emit syncTags(cache::roomInfo().toStdMap());
} catch (const lmdb::error &e) {
nhlog::db()->error("failed to save state after initial sync: {}",
e.what());
@ -830,12 +620,8 @@ ChatPage::handleSyncResponse(const mtx::responses::Sync &res, const std::string
auto updates = cache::getRoomInfo(cache::client()->roomsWithStateUpdates(res));
emit syncRoomlist(updates);
emit syncUI(res.rooms);
emit syncTags(cache::getRoomInfo(cache::client()->roomsWithTagUpdates(res)));
// if we process a lot of syncs (1 every 200ms), this means we clean the
// db every 100s
static int syncCounter = 0;
@ -939,7 +725,7 @@ ChatPage::joinRoomVia(const std::string &room_id,
emit showNotification(tr("Failed to remove invite: %1").arg(e.what()));
}
room_list_->highlightSelectedRoom(QString::fromStdString(room_id));
view_manager_->rooms()->setCurrentRoom(QString::fromStdString(room_id));
});
}
@ -987,19 +773,18 @@ ChatPage::leaveRoom(const QString &room_id)
void
ChatPage::changeRoom(const QString &room_id)
{
view_manager_->setHistoryView(room_id);
room_list_->highlightSelectedRoom(room_id);
view_manager_->rooms()->setCurrentRoom(room_id);
}
void
ChatPage::inviteUser(QString userid, QString reason)
{
auto room = current_room_;
auto room = currentRoom();
if (QMessageBox::question(this,
tr("Confirm invite"),
tr("Do you really want to invite %1 (%2)?")
.arg(cache::displayName(current_room_, userid))
.arg(cache::displayName(room, userid))
.arg(userid)) != QMessageBox::Yes)
return;
@ -1021,12 +806,12 @@ ChatPage::inviteUser(QString userid, QString reason)
void
ChatPage::kickUser(QString userid, QString reason)
{
auto room = current_room_;
auto room = currentRoom();
if (QMessageBox::question(this,
tr("Confirm kick"),
tr("Do you really want to kick %1 (%2)?")
.arg(cache::displayName(current_room_, userid))
.arg(cache::displayName(room, userid))
.arg(userid)) != QMessageBox::Yes)
return;
@ -1048,12 +833,12 @@ ChatPage::kickUser(QString userid, QString reason)
void
ChatPage::banUser(QString userid, QString reason)
{
auto room = current_room_;
auto room = currentRoom();
if (QMessageBox::question(this,
tr("Confirm ban"),
tr("Do you really want to ban %1 (%2)?")
.arg(cache::displayName(current_room_, userid))
.arg(cache::displayName(room, userid))
.arg(userid)) != QMessageBox::Yes)
return;
@ -1075,12 +860,12 @@ ChatPage::banUser(QString userid, QString reason)
void
ChatPage::unbanUser(QString userid, QString reason)
{
auto room = current_room_;
auto room = currentRoom();
if (QMessageBox::question(this,
tr("Confirm unban"),
tr("Do you really want to unban %1 (%2)?")
.arg(cache::displayName(current_room_, userid))
.arg(cache::displayName(room, userid))
.arg(userid)) != QMessageBox::Yes)
return;
@ -1182,57 +967,6 @@ ChatPage::getProfileInfo()
emit setUserAvatar(QString::fromStdString(res.avatar_url));
});
http::client()->joined_groups(
[this](const mtx::responses::JoinedGroups &res, mtx::http::RequestErr err) {
if (err) {
nhlog::net()->critical("failed to retrieve joined groups: {} {}",
static_cast<int>(err->status_code),
err->matrix_error.error);
emit updateGroupsInfo({});
return;
}
emit updateGroupsInfo(res);
});
}
bool
ChatPage::isRoomActive(const QString &room_id)
{
return isActiveWindow() && content_->isVisible() && currentRoom() == room_id;
}
void
ChatPage::hideSideBars()
{
// Don't hide side bar, if we are currently only showing the side bar!
if (view_manager_->getWidget()->isVisible()) {
communitiesList_->hide();
sideBar_->hide();
}
view_manager_->enableBackButton();
}
void
ChatPage::showSideBars()
{
if (userSettings_->groupView())
communitiesList_->show();
sideBar_->show();
view_manager_->disableBackButton();
content_->show();
}
uint64_t
ChatPage::timelineWidth()
{
int sidebarWidth = sideBar_->minimumSize().width();
sidebarWidth += communitiesList_->minimumSize().width();
nhlog::ui()->info("timelineWidth: {}", size().width() - sidebarWidth);
return size().width() - sidebarWidth;
}
void
@ -1318,7 +1052,8 @@ ChatPage::startChat(QString userid)
if (std::find(room_members.begin(),
room_members.end(),
(userid).toStdString()) != room_members.end()) {
room_list_->highlightSelectedRoom(QString::fromStdString(room_id));
view_manager_->rooms()->setCurrentRoom(
QString::fromStdString(room_id));
return;
}
}
@ -1408,7 +1143,8 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
if (sigil1 == "u") {
if (action.isEmpty()) {
view_manager_->activeTimeline()->openUserProfile(mxid1);
if (auto t = view_manager_->rooms()->currentRoom())
t->openUserProfile(mxid1);
} else if (action == "chat") {
this->startChat(mxid1);
}
@ -1418,7 +1154,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
for (auto roomid : joined_rooms) {
if (roomid == targetRoomId) {
room_list_->highlightSelectedRoom(mxid1);
view_manager_->rooms()->setCurrentRoom(mxid1);
if (!mxid2.isEmpty())
view_manager_->showEvent(mxid1, mxid2);
return;
@ -1436,7 +1172,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
auto aliases = cache::client()->getRoomAliases(roomid);
if (aliases) {
if (aliases->alias == targetRoomAlias) {
room_list_->highlightSelectedRoom(
view_manager_->rooms()->setCurrentRoom(
QString::fromStdString(roomid));
if (!mxid2.isEmpty())
view_manager_->showEvent(
@ -1458,8 +1194,17 @@ ChatPage::handleMatrixUri(const QUrl &uri)
handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8());
}
void
ChatPage::highlightRoom(const QString &room_id)
bool
ChatPage::isRoomActive(const QString &room_id)
{
room_list_->highlightSelectedRoom(room_id);
return isActiveWindow() && currentRoom() == room_id;
}
QString
ChatPage::currentRoom() const
{
if (view_manager_->rooms()->currentRoom())
return view_manager_->rooms()->currentRoom()->roomId();
else
return "";
}

View file

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

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

View file

@ -22,7 +22,6 @@
#include "MainWindow.h"
#include "MatrixClient.h"
#include "RegisterPage.h"
#include "Splitter.h"
#include "TrayIcon.h"
#include "UserSettingsPage.h"
#include "Utils.h"
@ -109,10 +108,6 @@ MainWindow::MainWindow(QWidget *parent)
userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool)));
connect(
userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged);
connect(userSettingsPage_,
&UserSettingsPage::decryptSidebarChanged,
chat_page_,
&ChatPage::decryptSidebarChanged);
connect(trayIcon_,
SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
this,
@ -176,20 +171,6 @@ MainWindow::setWindowTitle(int notificationCount)
QMainWindow::setWindowTitle(name);
}
void
MainWindow::showEvent(QShowEvent *event)
{
adjustSideBars();
QMainWindow::showEvent(event);
}
void
MainWindow::resizeEvent(QResizeEvent *event)
{
adjustSideBars();
QMainWindow::resizeEvent(event);
}
bool
MainWindow::event(QEvent *event)
{
@ -203,22 +184,6 @@ MainWindow::event(QEvent *event)
return QMainWindow::event(event);
}
void
MainWindow::adjustSideBars()
{
const auto sz = splitter::calculateSidebarSizes(QFont{});
const uint64_t timelineWidth = chat_page_->timelineWidth();
const uint64_t minAvailableWidth = sz.collapsePoint + sz.groups;
nhlog::ui()->info("timelineWidth: {}, min {}", timelineWidth, minAvailableWidth);
if (timelineWidth < minAvailableWidth) {
chat_page_->hideSideBars();
} else {
chat_page_->showSideBars();
}
}
void
MainWindow::restoreWindowSize()
{
@ -295,6 +260,7 @@ MainWindow::showChatPage()
&Cache::secretChanged,
userSettingsPage_,
&UserSettingsPage::updateSecretStatus);
emit reload();
}
void

View file

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

View file

@ -206,8 +206,11 @@ handle_olm_message(const OlmMessage &msg)
for (const auto &cipher : msg.ciphertext) {
// We skip messages not meant for the current device.
if (cipher.first != my_key)
if (cipher.first != my_key) {
nhlog::crypto()->debug(
"Skipping message for {} since we are {}.", cipher.first, my_key);
continue;
}
const auto type = cipher.second.type;
nhlog::crypto()->info("type: {}", type == 0 ? "OLM_PRE_KEY" : "OLM_MESSAGE");
@ -661,8 +664,10 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip
for (const auto &id : session_ids) {
auto session = cache::getOlmSession(sender_key, id);
if (!session)
if (!session) {
nhlog::crypto()->warn("Unknown olm session: {}:{}", sender_key, id);
continue;
}
mtx::crypto::BinaryBuf text;

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)
{
QSettings settings;
tray_ = settings.value("user/window/tray", false).toBool();
tray_ = settings.value("user/window/tray", false).toBool();
startInTray_ = settings.value("user/window/start_in_tray", false).toBool();
roomListWidth_ = settings.value("user/sidebar/room_list_width", -1).toInt();
communityListWidth_ = settings.value("user/sidebar/community_list_width", -1).toInt();
hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool();
hasAlertOnNotification_ = settings.value("user/alert_on_notification", false).toBool();
startInTray_ = settings.value("user/window/start_in_tray", false).toBool();
groupView_ = settings.value("user/group_view", true).toBool();
hiddenTags_ = settings.value("user/hidden_tags", QStringList{}).toStringList();
buttonsInTimeline_ = settings.value("user/timeline/buttons", true).toBool();
@ -248,6 +252,24 @@ UserSettings::setTimelineMaxWidth(int state)
emit timelineMaxWidthChanged(state);
save();
}
void
UserSettings::setCommunityListWidth(int state)
{
if (state == communityListWidth_)
return;
communityListWidth_ = state;
emit communityListWidthChanged(state);
save();
}
void
UserSettings::setRoomListWidth(int state)
{
if (state == roomListWidth_)
return;
roomListWidth_ = state;
emit roomListWidthChanged(state);
save();
}
void
UserSettings::setDesktopNotifications(bool state)
@ -545,49 +567,14 @@ UserSettings::applyTheme()
{
QFile stylefile;
static QPalette original;
if (this->theme() == "light") {
stylefile.setFileName(":/styles/styles/nheko.qss");
QPalette lightActive(
/*windowText*/ QColor("#333"),
/*button*/ QColor("white"),
/*light*/ QColor(0xef, 0xef, 0xef),
/*dark*/ QColor(110, 110, 110),
/*mid*/ QColor(220, 220, 220),
/*text*/ QColor("#333"),
/*bright_text*/ QColor("#333"),
/*base*/ QColor("#fff"),
/*window*/ QColor("white"));
lightActive.setColor(QPalette::AlternateBase, QColor("#eee"));
lightActive.setColor(QPalette::Highlight, QColor("#38a3d8"));
lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color());
lightActive.setColor(QPalette::ToolTipText, lightActive.text().color());
lightActive.setColor(QPalette::Link, QColor("#0077b5"));
lightActive.setColor(QPalette::ButtonText, QColor("#333"));
QApplication::setPalette(lightActive);
} else if (this->theme() == "dark") {
stylefile.setFileName(":/styles/styles/nheko-dark.qss");
QPalette darkActive(
/*windowText*/ QColor("#caccd1"),
/*button*/ QColor(0xff, 0xff, 0xff),
/*light*/ QColor("#caccd1"),
/*dark*/ QColor(110, 110, 110),
/*mid*/ QColor("#202228"),
/*text*/ QColor("#caccd1"),
/*bright_text*/ QColor(0xff, 0xff, 0xff),
/*base*/ QColor("#202228"),
/*window*/ QColor("#2d3139"));
darkActive.setColor(QPalette::AlternateBase, QColor("#2d3139"));
darkActive.setColor(QPalette::Highlight, QColor("#38a3d8"));
darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color());
darkActive.setColor(QPalette::ToolTipText, darkActive.text().color());
darkActive.setColor(QPalette::Link, QColor("#38a3d8"));
darkActive.setColor(QPalette::ButtonText, "#727274");
QApplication::setPalette(darkActive);
} else {
stylefile.setFileName(":/styles/styles/system.qss");
QApplication::setPalette(original);
}
QApplication::setPalette(Theme::paletteFromTheme(this->theme().toStdString()));
stylefile.open(QFile::ReadOnly);
QString stylesheet = QString(stylefile.readAll());
@ -606,6 +593,11 @@ UserSettings::save()
settings.setValue("start_in_tray", startInTray_);
settings.endGroup(); // window
settings.beginGroup("sidebar");
settings.setValue("community_list_width", communityListWidth_);
settings.setValue("room_list_width", roomListWidth_);
settings.endGroup(); // window
settings.beginGroup("timeline");
settings.setValue("buttons", buttonsInTimeline_);
settings.setValue("message_hover_highlight", messageHoverHighlight_);

View file

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

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

View file

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

View file

@ -20,6 +20,7 @@
#include "Cache.h"
#include "ChatPage.h"
#include "CompletionProxyModel.h"
#include "Config.h"
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
@ -508,8 +509,7 @@ InputBar::command(QString command, QString args)
} else if (command == "react") {
auto eventId = room->reply();
if (!eventId.isEmpty())
ChatPage::instance()->timelineManager()->queueReactionMessage(
eventId, args.trimmed());
reaction(eventId, args.trimmed());
} else if (command == "join") {
ChatPage::instance()->joinRoom(args);
} else if (command == "part" || command == "leave") {
@ -715,3 +715,35 @@ InputBar::stopTyping()
}
});
}
void
InputBar::reaction(const QString &reactedEvent, const QString &reactionKey)
{
auto reactions = room->reactions(reactedEvent.toStdString());
QString selfReactedEvent;
for (const auto &reaction : reactions) {
if (reactionKey == reaction.key_) {
selfReactedEvent = reaction.selfReactedEvent_;
break;
}
}
if (selfReactedEvent.startsWith("m"))
return;
// If selfReactedEvent is empty, that means we haven't previously reacted
if (selfReactedEvent.isEmpty()) {
mtx::events::msg::Reaction reaction;
mtx::common::Relation rel;
rel.rel_type = mtx::common::RelationType::Annotation;
rel.event_id = reactedEvent.toStdString();
rel.key = reactionKey.toStdString();
reaction.relations.relations.push_back(rel);
room->sendMessageEvent(reaction, mtx::events::EventType::Reaction);
// Otherwise, we have previously reacted and the reaction should be redacted
} else {
room->redactEvent(selfReactedEvent);
}
}

View file

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

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

View file

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

View file

@ -4,7 +4,6 @@
#include "TimelineViewManager.h"
#include <QDesktopServices>
#include <QDropEvent>
#include <QMetaType>
#include <QPalette>
@ -32,6 +31,7 @@
#include "emoji/Provider.h"
#include "ui/NhekoCursorShape.h"
#include "ui/NhekoDropArea.h"
#include "ui/NhekoGlobalObject.h"
Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
Q_DECLARE_METATYPE(std::vector<DeviceInfo>)
@ -86,21 +86,6 @@ removeReplyFallback(mtx::events::Event<T> &e)
}
}
void
TimelineViewManager::updateEncryptedDescriptions()
{
auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar();
QHash<QString, QSharedPointer<TimelineModel>>::iterator i;
for (i = models.begin(); i != models.end(); ++i) {
auto ptr = i.value();
if (!ptr.isNull()) {
ptr->setDecryptDescription(decrypt);
ptr->updateLastMessage();
}
}
}
void
TimelineViewManager::updateColorPalette()
{
@ -143,10 +128,13 @@ TimelineViewManager::userStatus(QString id) const
}
TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *parent)
: imgProvider(new MxcImageProvider())
: QObject(parent)
, imgProvider(new MxcImageProvider())
, colorImgProvider(new ColorImageProvider())
, blurhashProvider(new BlurhashProvider())
, callManager_(callManager)
, rooms_(new RoomlistModel(this))
, communities_(new CommunitiesModel(this))
{
qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>();
qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>();
@ -204,6 +192,26 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
return ptr;
});
qmlRegisterSingletonType<RoomlistModel>(
"im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = new FilteredRoomlistModel(self->rooms_);
connect(self->communities_,
&CommunitiesModel::currentTagIdChanged,
ptr,
&FilteredRoomlistModel::updateFilterTag);
connect(self->communities_,
&CommunitiesModel::hiddenTagsChanged,
ptr,
&FilteredRoomlistModel::updateHiddenTagsAndSpaces);
return ptr;
});
qmlRegisterSingletonType<RoomlistModel>(
"im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = self->communities_;
QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
return ptr;
});
qmlRegisterSingletonType<UserSettings>(
"im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
auto ptr = ChatPage::instance()->userSettings().data();
@ -220,6 +228,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
"im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new Clipboard();
});
qmlRegisterSingletonType<Nheko>(
"im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * {
return new Nheko();
});
qRegisterMetaType<mtx::events::collections::TimelineEvents>();
qRegisterMetaType<std::vector<DeviceInfo>>();
@ -235,7 +247,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
"Error: Only enums");
#ifdef USE_QUICK_VIEW
view = new QQuickView();
view = new QQuickView(parent);
container = QWidget::createWindowContainer(view, parent);
#else
view = new QQuickWidget(parent);
@ -252,13 +264,9 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
view->engine()->addImageProvider("MxcImage", imgProvider);
view->engine()->addImageProvider("colorimage", colorImgProvider);
view->engine()->addImageProvider("blurhash", blurhashProvider);
view->setSource(QUrl("qrc:///qml/TimelineView.qml"));
view->setSource(QUrl("qrc:///qml/Root.qml"));
connect(parent, &ChatPage::themeChanged, this, &TimelineViewManager::updateColorPalette);
connect(parent,
&ChatPage::decryptSidebarChanged,
this,
&TimelineViewManager::updateEncryptedDescriptions);
connect(
dynamic_cast<ChatPage *>(parent),
&ChatPage::receivedRoomDeviceVerificationRequest,
@ -329,100 +337,28 @@ TimelineViewManager::setVideoCallItem()
}
void
TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
TimelineViewManager::sync(const mtx::responses::Rooms &rooms_res)
{
for (const auto &[room_id, room] : rooms.join) {
// addRoom will only add the room, if it doesn't exist
addRoom(QString::fromStdString(room_id));
const auto &room_model = models.value(QString::fromStdString(room_id));
if (!isInitialSync_)
connect(room_model.data(),
&TimelineModel::newCallEvent,
callManager_,
&CallManager::syncEvent);
room_model->syncState(room.state);
room_model->addEvents(room.timeline);
if (!isInitialSync_)
disconnect(room_model.data(),
&TimelineModel::newCallEvent,
callManager_,
&CallManager::syncEvent);
this->rooms_->sync(rooms_res);
this->communities_->sync(rooms_res);
if (ChatPage::instance()->userSettings()->typingNotifications()) {
for (const auto &ev : room.ephemeral.events) {
if (auto t = std::get_if<
mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>(
&ev)) {
std::vector<QString> typing;
typing.reserve(t->content.user_ids.size());
for (const auto &user : t->content.user_ids) {
if (user != http::client()->user_id().to_string())
typing.push_back(
QString::fromStdString(user));
}
room_model->updateTypingUsers(typing);
}
}
}
if (isInitialSync_) {
this->isInitialSync_ = false;
emit initialSyncChanged(false);
}
this->isInitialSync_ = false;
emit initialSyncChanged(false);
}
void
TimelineViewManager::addRoom(const QString &room_id)
{
if (!models.contains(room_id)) {
QSharedPointer<TimelineModel> newRoom(new TimelineModel(this, room_id));
newRoom->setDecryptDescription(
ChatPage::instance()->userSettings()->decryptSidebar());
connect(newRoom.data(),
&TimelineModel::newEncryptedImage,
imgProvider,
&MxcImageProvider::addEncryptionInfo);
connect(newRoom.data(),
&TimelineModel::forwardToRoom,
this,
&TimelineViewManager::forwardMessageToRoom);
models.insert(room_id, std::move(newRoom));
}
}
void
TimelineViewManager::setHistoryView(const QString &room_id)
{
nhlog::ui()->info("Trying to activate room {}", room_id.toStdString());
auto room = models.find(room_id);
if (room != models.end()) {
timeline_ = room.value().data();
emit activeTimelineChanged(timeline_);
container->setFocus();
nhlog::ui()->info("Activated room {}", room_id.toStdString());
}
}
void
TimelineViewManager::highlightRoom(const QString &room_id)
{
ChatPage::instance()->highlightRoom(room_id);
}
void
TimelineViewManager::showEvent(const QString &room_id, const QString &event_id)
{
auto room = models.find(room_id);
if (room != models.end()) {
if (timeline_ != room.value().data()) {
timeline_ = room.value().data();
emit activeTimelineChanged(timeline_);
if (auto room = rooms_->getRoomById(room_id)) {
if (rooms_->currentRoom() != room) {
rooms_->setCurrentRoom(room_id);
container->setFocus();
nhlog::ui()->info("Activated room {}", room_id.toStdString());
}
timeline_->showEvent(event_id);
room->showEvent(event_id);
}
}
@ -457,12 +393,14 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img)
auto imgDialog = new dialogs::ImageOverlay(pixmap);
imgDialog->showFullScreen();
connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId, imgDialog]() {
auto room = rooms_->currentRoom();
connect(imgDialog, &dialogs::ImageOverlay::saving, room, [eventId, imgDialog, room]() {
// hide the overlay while presenting the save dialog for better
// cross platform support.
imgDialog->hide();
if (!timeline_->saveMedia(eventId)) {
if (!room->saveMedia(eventId)) {
imgDialog->show();
} else {
imgDialog->close();
@ -470,56 +408,6 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img)
});
}
void
TimelineViewManager::openLink(QString link) const
{
QUrl url(link);
if (url.scheme() == "https" && url.host() == "matrix.to") {
// handle matrix.to links internally
QString p = url.fragment(QUrl::FullyEncoded);
if (p.startsWith("/"))
p.remove(0, 1);
auto temp = p.split("?");
QString query;
if (temp.size() >= 2)
query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8());
temp = temp.first().split("/");
auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8());
QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8());
if (!identifier.isEmpty()) {
if (identifier.startsWith("@")) {
QByteArray uri =
"matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
if (!query.isEmpty())
uri.append("?" + query.toUtf8());
ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri));
} else if (identifier.startsWith("#")) {
QByteArray uri =
"matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
if (!eventId.isEmpty())
uri.append("/e/" +
QUrl::toPercentEncoding(eventId.remove(0, 1)));
if (!query.isEmpty())
uri.append("?" + query.toUtf8());
ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri));
} else if (identifier.startsWith("!")) {
QByteArray uri = "matrix:roomid/" +
QUrl::toPercentEncoding(identifier.remove(0, 1));
if (!eventId.isEmpty())
uri.append("/e/" +
QUrl::toPercentEncoding(eventId.remove(0, 1)));
if (!query.isEmpty())
uri.append("?" + query.toUtf8());
ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri));
}
}
} else {
QDesktopServices::openUrl(url);
}
}
void
TimelineViewManager::openInviteUsersDialog()
{
@ -527,14 +415,14 @@ TimelineViewManager::openInviteUsersDialog()
[this](const QStringList &invitees) { emit inviteUsers(invitees); });
}
void
TimelineViewManager::openMemberListDialog() const
TimelineViewManager::openMemberListDialog(QString roomid) const
{
MainWindow::instance()->openMemberListDialog(timeline_->roomId());
MainWindow::instance()->openMemberListDialog(roomid);
}
void
TimelineViewManager::openLeaveRoomDialog() const
TimelineViewManager::openLeaveRoomDialog(QString roomid) const
{
MainWindow::instance()->openLeaveRoomDialog(timeline_->roomId());
MainWindow::instance()->openLeaveRoomDialog(roomid);
}
void
@ -550,17 +438,21 @@ TimelineViewManager::verifyUser(QString userid)
if (std::find(room_members.begin(),
room_members.end(),
(userid).toStdString()) != room_members.end()) {
auto model = models.value(QString::fromStdString(room_id));
auto flow = DeviceVerificationFlow::InitiateUserVerification(
this, model.data(), userid);
connect(model.data(),
&TimelineModel::updateFlowEventId,
this,
[this, flow](std::string eventId) {
dvList[QString::fromStdString(eventId)] = flow;
});
emit newDeviceVerificationRequest(flow.data());
return;
if (auto model =
rooms_->getRoomById(QString::fromStdString(room_id))) {
auto flow =
DeviceVerificationFlow::InitiateUserVerification(
this, model.data(), userid);
connect(model.data(),
&TimelineModel::updateFlowEventId,
this,
[this, flow](std::string eventId) {
dvList[QString::fromStdString(eventId)] =
flow;
});
emit newDeviceVerificationRequest(flow.data());
return;
}
}
}
}
@ -593,26 +485,24 @@ void
TimelineViewManager::updateReadReceipts(const QString &room_id,
const std::vector<QString> &event_ids)
{
auto room = models.find(room_id);
if (room != models.end()) {
room.value()->markEventsAsRead(event_ids);
if (auto room = rooms_->getRoomById(room_id)) {
room->markEventsAsRead(event_ids);
}
}
void
TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::string &session_id)
{
auto room = models.find(QString::fromStdString(room_id));
if (room != models.end()) {
room.value()->receivedSessionKey(session_id);
if (auto room = rooms_->getRoomById(QString::fromStdString(room_id))) {
room->receivedSessionKey(session_id);
}
}
void
TimelineViewManager::initWithMessages(const std::vector<QString> &roomIds)
TimelineViewManager::initializeRoomlist()
{
for (const auto &roomId : roomIds)
addRoom(roomId);
rooms_->initializeRooms();
communities_->initializeSidebar();
}
void
@ -620,74 +510,42 @@ TimelineViewManager::queueReply(const QString &roomid,
const QString &repliedToEvent,
const QString &replyBody)
{
auto room = models.find(roomid);
if (room != models.end()) {
room.value()->setReply(repliedToEvent);
room.value()->input()->message(replyBody);
if (auto room = rooms_->getRoomById(roomid)) {
room->setReply(repliedToEvent);
room->input()->message(replyBody);
}
}
void
TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QString &reactionKey)
{
if (!timeline_)
return;
auto reactions = timeline_->reactions(reactedEvent.toStdString());
QString selfReactedEvent;
for (const auto &reaction : reactions) {
if (reactionKey == reaction.key_) {
selfReactedEvent = reaction.selfReactedEvent_;
break;
}
}
if (selfReactedEvent.startsWith("m"))
return;
// If selfReactedEvent is empty, that means we haven't previously reacted
if (selfReactedEvent.isEmpty()) {
mtx::events::msg::Reaction reaction;
mtx::common::Relation rel;
rel.rel_type = mtx::common::RelationType::Annotation;
rel.event_id = reactedEvent.toStdString();
rel.key = reactionKey.toStdString();
reaction.relations.relations.push_back(rel);
timeline_->sendMessageEvent(reaction, mtx::events::EventType::Reaction);
// Otherwise, we have previously reacted and the reaction should be redacted
} else {
timeline_->redactEvent(selfReactedEvent);
}
}
void
TimelineViewManager::queueCallMessage(const QString &roomid,
const mtx::events::msg::CallInvite &callInvite)
{
models.value(roomid)->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite);
if (auto room = rooms_->getRoomById(roomid))
room->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite);
}
void
TimelineViewManager::queueCallMessage(const QString &roomid,
const mtx::events::msg::CallCandidates &callCandidates)
{
models.value(roomid)->sendMessageEvent(callCandidates,
mtx::events::EventType::CallCandidates);
if (auto room = rooms_->getRoomById(roomid))
room->sendMessageEvent(callCandidates, mtx::events::EventType::CallCandidates);
}
void
TimelineViewManager::queueCallMessage(const QString &roomid,
const mtx::events::msg::CallAnswer &callAnswer)
{
models.value(roomid)->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer);
if (auto room = rooms_->getRoomById(roomid))
room->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer);
}
void
TimelineViewManager::queueCallMessage(const QString &roomid,
const mtx::events::msg::CallHangUp &callHangUp)
{
models.value(roomid)->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp);
if (auto room = rooms_->getRoomById(roomid))
room->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp);
}
void
@ -738,7 +596,7 @@ void
TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e,
QString roomId)
{
auto room = models.find(roomId);
auto room = rooms_->getRoomById(roomId);
auto content = mtx::accessors::url(*e);
std::optional<mtx::crypto::EncryptedFile> encryptionInfo = mtx::accessors::file(*e);
@ -781,12 +639,15 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven
ev.content.url = url;
}
auto room = models.find(roomId);
removeReplyFallback(ev);
ev.content.relations.relations.clear();
room.value()->sendMessageEvent(
ev.content,
mtx::events::EventType::RoomMessage);
if (auto room = rooms_->getRoomById(roomId)) {
removeReplyFallback(ev);
ev.content.relations.relations
.clear();
room->sendMessageEvent(
ev.content,
mtx::events::EventType::
RoomMessage);
}
}
},
*e);
@ -804,8 +665,7 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven
mtx::events::EventType::RoomMessage) {
e.content.relations.relations.clear();
removeReplyFallback(e);
room.value()->sendMessageEvent(e.content,
mtx::events::EventType::RoomMessage);
room->sendMessageEvent(e.content, mtx::events::EventType::RoomMessage);
}
},
*e);

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