From a7f8b23b524c5e3af72e42fde118706e94a454f3 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 13 May 2021 08:23:56 +0200 Subject: Make palette global in Qml --- src/timeline/TimelineViewManager.cpp | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src/timeline/TimelineViewManager.cpp') diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 628f3c31..94cef1a7 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -32,6 +32,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) @@ -221,6 +222,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par "im.nheko", 1, 0, "Clipboard", [](QQmlEngine *, QJSEngine *) -> QObject * { return new Clipboard(); }); + qmlRegisterSingletonType( + "im.nheko", 1, 0, "Nheko", [](QQmlEngine *, QJSEngine *) -> QObject * { + return new Nheko(); + }); qRegisterMetaType(); qRegisterMetaType>(); -- cgit 1.5.1 From 22afa122c4697d25fd2f1eb4c7931bcf4ea43f31 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 13 May 2021 08:52:02 +0200 Subject: Move openLink to Nheko globals --- resources/qml/MatrixText.qml | 2 +- resources/qml/RoomSettings.qml | 2 +- src/timeline/TimelineViewManager.cpp | 51 ---------------------------------- src/timeline/TimelineViewManager.h | 2 -- src/ui/NhekoGlobalObject.cpp | 54 ++++++++++++++++++++++++++++++++++++ src/ui/NhekoGlobalObject.h | 3 ++ 6 files changed, 59 insertions(+), 55 deletions(-) (limited to 'src/timeline/TimelineViewManager.cpp') diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index fa1cd98c..167899a5 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -14,7 +14,7 @@ TextEdit { selectByMouse: !Settings.mobileMode enabled: selectByMouse color: Nheko.colors.text - onLinkActivated: TimelineManager.openLink(link) + onLinkActivated: Nheko.openLink(link) ToolTip.visible: hoveredLink ToolTip.text: hoveredLink diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml index ba577f33..14de0edf 100644 --- a/resources/qml/RoomSettings.qml +++ b/resources/qml/RoomSettings.qml @@ -128,7 +128,7 @@ ApplicationWindow { selectByMouse: true color: Nheko.colors.text horizontalAlignment: TextEdit.AlignHCenter - onLinkActivated: TimelineManager.openLink(link) + onLinkActivated: Nheko.openLink(link) CursorShape { anchors.fill: parent diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 94cef1a7..b407a128 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -4,7 +4,6 @@ #include "TimelineViewManager.h" -#include #include #include #include @@ -476,56 +475,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() { diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index b23a61db..0665b663 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -68,8 +68,6 @@ public: Q_INVOKABLE QString userPresence(QString id) const; Q_INVOKABLE QString userStatus(QString id) const; - Q_INVOKABLE void openLink(QString link) const; - Q_INVOKABLE void focusMessageInput(); Q_INVOKABLE void openInviteUsersDialog(); Q_INVOKABLE void openMemberListDialog() const; diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp index 5a2b9788..e5e6825e 100644 --- a/src/ui/NhekoGlobalObject.cpp +++ b/src/ui/NhekoGlobalObject.cpp @@ -4,6 +4,10 @@ #include "NhekoGlobalObject.h" +#include +#include + +#include "ChatPage.h" #include "UserSettingsPage.h" Nheko::Nheko() @@ -25,3 +29,53 @@ Nheko::inactiveColors() const p.setCurrentColorGroup(QPalette::ColorGroup::Inactive); return p; } + +void +Nheko::openLink(QString link) const +{ + QUrl url(link); + if (url.scheme() == "https" && url.host() == "matrix.to") { + // handle matrix.to links internally + QString p = url.fragment(QUrl::FullyEncoded); + if (p.startsWith("/")) + p.remove(0, 1); + + auto temp = p.split("?"); + QString query; + if (temp.size() >= 2) + query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8()); + + temp = temp.first().split("/"); + auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8()); + QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8()); + if (!identifier.isEmpty()) { + if (identifier.startsWith("@")) { + QByteArray uri = + "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); + if (!query.isEmpty()) + uri.append("?" + query.toUtf8()); + ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); + } else if (identifier.startsWith("#")) { + QByteArray uri = + "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1)); + if (!eventId.isEmpty()) + uri.append("/e/" + + QUrl::toPercentEncoding(eventId.remove(0, 1))); + if (!query.isEmpty()) + uri.append("?" + query.toUtf8()); + ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); + } else if (identifier.startsWith("!")) { + QByteArray uri = "matrix:roomid/" + + QUrl::toPercentEncoding(identifier.remove(0, 1)); + if (!eventId.isEmpty()) + uri.append("/e/" + + QUrl::toPercentEncoding(eventId.remove(0, 1))); + if (!query.isEmpty()) + uri.append("?" + query.toUtf8()); + ChatPage::instance()->handleMatrixUri(QUrl::fromEncoded(uri)); + } + } + } else { + QDesktopServices::openUrl(url); + } +} diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h index 76186828..05a0c050 100644 --- a/src/ui/NhekoGlobalObject.h +++ b/src/ui/NhekoGlobalObject.h @@ -20,6 +20,9 @@ public: QPalette colors() const; QPalette inactiveColors() const; + Q_INVOKABLE void openLink(QString link) const; + signals: void colorsChanged(); }; + -- cgit 1.5.1 From 39a43ad4abc84733b6e8a5a244f1055048ce115c Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 14 May 2021 15:23:32 +0200 Subject: Reorganize TimelineView to prepare porting the room list --- resources/qml/ChatPage.qml | 48 +++ resources/qml/ForwardCompleter.qml | 2 +- resources/qml/Root.qml | 260 +++++++++++++++ resources/qml/TimelineView.qml | 389 +++++------------------ resources/qml/delegates/ImageMessage.qml | 6 +- resources/qml/delegates/PlayableMediaMessage.qml | 6 +- resources/qml/delegates/TextMessage.qml | 2 +- resources/res.qrc | 2 + src/timeline/TimelineViewManager.cpp | 2 +- src/ui/NhekoGlobalObject.h | 7 + 10 files changed, 397 insertions(+), 327 deletions(-) create mode 100644 resources/qml/ChatPage.qml create mode 100644 resources/qml/Root.qml (limited to 'src/timeline/TimelineViewManager.cpp') diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml new file mode 100644 index 00000000..a02f0ca9 --- /dev/null +++ b/resources/qml/ChatPage.qml @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.9 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.3 +import im.nheko 1.0 + +Rectangle { + id: chatPage + + color: Nheko.colors.window + + SplitView { + anchors.fill: parent + + Rectangle { + SplitView.minimumWidth: Nheko.avatarSize + Nheko.paddingSmall * 2 + SplitView.preferredWidth: Nheko.avatarSize + Nheko.paddingSmall * 2 + SplitView.maximumWidth: Nheko.avatarSize + Nheko.paddingSmall * 2 + color: "blue" + } + + Rectangle { + SplitView.minimumWidth: Nheko.avatarSize * 3 + Nheko.paddingSmall * 2 + SplitView.preferredWidth: Nheko.avatarSize * 3 + Nheko.paddingSmall * 2 + SplitView.maximumWidth: Nheko.avatarSize * 7 + Nheko.paddingSmall * 2 + color: "red" + } + + TimelineView { + id: timeline + + SplitView.fillWidth: true + SplitView.minimumWidth: 400 + } + + } + + PrivacyScreen { + anchors.fill: parent + visible: Settings.privacyScreen + screenTimeout: Settings.privacyScreenTimeout + timelineRoot: timeline + } + +} diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml index 1ec18540..59bfe94d 100644 --- a/resources/qml/ForwardCompleter.qml +++ b/resources/qml/ForwardCompleter.qml @@ -21,7 +21,7 @@ Popup { modal: true palette: Nheko.colors parent: Overlay.overlay - width: implicitWidth >= (timelineRoot.width * 0.8) ? implicitWidth : (timelineRoot.width * 0.8) + width: implicitWidth >= (timelineView.width * 0.8) ? implicitWidth : (timelineView.width * 0.8) height: implicitHeight + completerPopup.height + padding * 2 leftPadding: 10 rightPadding: 10 diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml new file mode 100644 index 00000000..35b81a1f --- /dev/null +++ b/resources/qml/Root.qml @@ -0,0 +1,260 @@ +// 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.13 +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 { + } + + } + + 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("React") + 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("Reply") + 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 receipts") + 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("Remove 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 event") + onTriggered: TimelineManager.timeline.copyLinkToEvent(messageContextMenu.eventId) + } + + } + + Component { + id: deviceVerificationDialog + + DeviceVerification { + } + + } + + Connections { + target: TimelineManager + onNewDeviceVerificationRequest: { + var dialog = deviceVerificationDialog.createObject(timelineRoot, { + "flow": flow + }); + dialog.show(); + } + onOpenProfile: { + var userProfile = userProfileComponent.createObject(timelineRoot, { + "profile": profile + }); + userProfile.show(); + } + } + + Connections { + target: TimelineManager.timeline + onOpenRoomSettingsDialog: { + var roomSettings = roomSettingsComponent.createObject(timelineRoot, { + "roomSettings": settings + }); + roomSettings.show(); + } + } + + Connections { + target: CallManager + onNewInviteState: { + if (CallManager.haveCallInvite && Settings.mobileMode) { + var dialog = mobileCallInviteDialog.createObject(msgView); + dialog.open(); + } + } + } + + ChatPage { + anchors.fill: parent + } + +} diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index a848cb49..0d0e286d 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -9,370 +9,123 @@ 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.13 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 { - } - - } - - Component { - id: forwardCompleterComponent - - ForwardCompleter { - } +Item { + id: timelineView + Label { + visible: !TimelineManager.timeline && !TimelineManager.isInitialSync + anchors.centerIn: parent + text: qsTr("No room open") + font.pointSize: 24 + color: Nheko.colors.text } - Shortcut { - sequence: "Ctrl+K" - onActivated: { - var quickSwitch = quickSwitcherComponent.createObject(timelineRoot); - TimelineManager.focusTimeline(); - quickSwitch.open(); - } + BusyIndicator { + visible: running + anchors.centerIn: parent + running: TimelineManager.isInitialSync + height: 200 + width: 200 + z: 3 } - Platform.Menu { - id: messageContextMenu - - property string eventId - property string link - property string text - property int eventType - property bool isEncrypted - property bool isEditable - property bool isSender - - function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) { - eventId = eventId_; - eventType = eventType_; - isEncrypted = isEncrypted_; - isEditable = isEditable_; - isSender = isSender_; - if (text_) - text = text_; - else - text = ""; - if (link_) - link = link_; - else - link = ""; - if (showAt_) - open(showAt_); - else - open(); - } - - Platform.MenuItem { - visible: messageContextMenu.text - enabled: visible - text: qsTr("Copy") - onTriggered: Clipboard.text = messageContextMenu.text - } - - Platform.MenuItem { - visible: messageContextMenu.link - enabled: visible - text: qsTr("Copy link location") - onTriggered: Clipboard.text = messageContextMenu.link - } - - Platform.MenuItem { - id: reactionOption + ColumnLayout { + id: timelineLayout - visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.Reaction) : false - text: qsTr("React") - 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("Reply") - 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 receipts") - 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("Remove 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 event") - onTriggered: TimelineManager.timeline.copyLinkToEvent(messageContextMenu.eventId) - } - - } - - Rectangle { + visible: TimelineManager.timeline != null anchors.fill: parent - color: Nheko.colors.window - - Component { - id: deviceVerificationDialog - - DeviceVerification { - } - - } - - Connections { - target: TimelineManager - onNewDeviceVerificationRequest: { - var dialog = deviceVerificationDialog.createObject(timelineRoot, { - "flow": flow - }); - dialog.show(); - } - onOpenProfile: { - var userProfile = userProfileComponent.createObject(timelineRoot, { - "profile": profile - }); - userProfile.show(); - } - } - - Connections { - target: TimelineManager.timeline - onOpenRoomSettingsDialog: { - var roomSettings = roomSettingsComponent.createObject(timelineRoot, { - "roomSettings": settings - }); - roomSettings.show(); - } - } - - Connections { - target: CallManager - onNewInviteState: { - if (CallManager.haveCallInvite && Settings.mobileMode) { - var dialog = mobileCallInviteDialog.createObject(msgView); - dialog.open(); - } - } - } + spacing: 0 - Label { - visible: !TimelineManager.timeline && !TimelineManager.isInitialSync - anchors.centerIn: parent - text: qsTr("No room open") - font.pointSize: 24 - color: Nheko.colors.text + TopBar { } - BusyIndicator { - visible: running - anchors.centerIn: parent - running: TimelineManager.isInitialSync - height: 200 - width: 200 + Rectangle { + Layout.fillWidth: true + height: 1 z: 3 + color: Nheko.colors.mid } - ColumnLayout { - id: timelineLayout + Rectangle { + id: msgView - visible: TimelineManager.timeline != null - anchors.fill: parent - spacing: 0 + Layout.fillWidth: true + Layout.fillHeight: true + color: Nheko.colors.base - TopBar { - } - - Rectangle { - Layout.fillWidth: true - height: 1 - z: 3 - color: Nheko.colors.mid - } - - Rectangle { - id: msgView - - Layout.fillWidth: true - Layout.fillHeight: true - color: Nheko.colors.base - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - StackLayout { - id: stackLayout + ColumnLayout { + anchors.fill: parent + spacing: 0 - currentIndex: 0 + StackLayout { + id: stackLayout - Connections { - function onActiveTimelineChanged() { - stackLayout.currentIndex = 0; - } + currentIndex: 0 - target: TimelineManager + Connections { + function onActiveTimelineChanged() { + stackLayout.currentIndex = 0; } - MessageView { - Layout.fillWidth: true - Layout.fillHeight: true - } - - Loader { - source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : "" - onLoaded: TimelineManager.setVideoCallItem() - } + target: TimelineManager + } + MessageView { + Layout.fillWidth: true + implicitHeight: msgView.height - typingIndicator.height } - TypingIndicator { + Loader { + source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : "" + onLoaded: TimelineManager.setVideoCallItem() } } - } - - CallInviteBar { - id: callInviteBar + TypingIndicator { + id: typingIndicator + } - Layout.fillWidth: true - z: 3 } - ActiveCallBar { - Layout.fillWidth: true - z: 3 - } + } - Rectangle { - Layout.fillWidth: true - z: 3 - height: 1 - color: Nheko.colors.mid - } + CallInviteBar { + id: callInviteBar - ReplyPopup { - } + Layout.fillWidth: true + z: 3 + } - MessageInput { - } + ActiveCallBar { + Layout.fillWidth: true + z: 3 + } + + Rectangle { + Layout.fillWidth: true + z: 3 + height: 1 + color: Nheko.colors.mid + } + ReplyPopup { } - NhekoDropArea { - anchors.fill: parent - roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : "" + MessageInput { } } - PrivacyScreen { + NhekoDropArea { anchors.fill: parent - visible: Settings.privacyScreen - screenTimeout: Settings.privacyScreenTimeout - timelineRoot: timelineLayout + roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : "" } } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index 704af3fe..ce8e779c 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -9,10 +9,10 @@ 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 diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 223c2a34..0234495d 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -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 diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index 810ee3d4..ae622480 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -11,7 +11,7 @@ MatrixText { text: "" + formatted.replace("
", "
")
     width: parent ? parent.width : undefined
-    height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
+    height: isReply ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : undefined
     clip: isReply
     selectByMouse: !Settings.mobileMode && !isReply
     font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
diff --git a/resources/res.qrc b/resources/res.qrc
index 304493b6..8105e966 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -123,6 +123,8 @@
     
         qtquickcontrols2.conf
 
+        qml/Root.qml
+        qml/ChatPage.qml
         qml/TimelineView.qml
         qml/Avatar.qml
         qml/Completer.qml
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index b407a128..e8e57fd8 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -257,7 +257,7 @@ 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,
diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h
index 9875507e..d952c266 100644
--- a/src/ui/NhekoGlobalObject.h
+++ b/src/ui/NhekoGlobalObject.h
@@ -14,6 +14,9 @@ class Nheko : public QObject
         Q_PROPERTY(QPalette colors READ colors NOTIFY colorsChanged)
         Q_PROPERTY(QPalette inactiveColors READ inactiveColors NOTIFY colorsChanged)
         Q_PROPERTY(int avatarSize READ avatarSize CONSTANT)
+        Q_PROPERTY(int paddingSmall READ paddingSmall CONSTANT)
+        Q_PROPERTY(int paddingMedium READ paddingMedium CONSTANT)
+        Q_PROPERTY(int paddingLarge READ paddingLarge CONSTANT)
 
 public:
         Nheko();
@@ -23,6 +26,10 @@ public:
 
         int avatarSize() const { return 40; }
 
+        int paddingSmall() const { return 4; }
+        int paddingMedium() const { return 8; }
+        int paddingLarge() const { return 20; }
+
         Q_INVOKABLE void openLink(QString link) const;
 
 signals:
-- 
cgit 1.5.1


From 10fd2752f9863c43bf7df6c39d7cec1397dfde1c Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Wed, 19 May 2021 19:34:10 +0200
Subject: Some basic room list

---
 CMakeLists.txt                       |   2 +
 resources/qml/ElidedLabel.qml        |  28 ++++++
 resources/qml/ForwardCompleter.qml   |   2 +-
 resources/qml/RoomList.qml           | 171 ++++++++++++++++++++++++++-------
 resources/res.qrc                    |   1 +
 src/timeline/RoomlistModel.cpp       | 146 ++++++++++++++++++++++++++++
 src/timeline/RoomlistModel.h         |  58 +++++++++++
 src/timeline/TimelineViewManager.cpp | 180 ++++++++++++-----------------------
 src/timeline/TimelineViewManager.h   |  17 ++--
 9 files changed, 440 insertions(+), 165 deletions(-)
 create mode 100644 resources/qml/ElidedLabel.qml
 create mode 100644 src/timeline/RoomlistModel.cpp
 create mode 100644 src/timeline/RoomlistModel.h

(limited to 'src/timeline/TimelineViewManager.cpp')

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5155af40..8b43559f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -272,6 +272,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
@@ -497,6 +498,7 @@ 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
diff --git a/resources/qml/ElidedLabel.qml b/resources/qml/ElidedLabel.qml
new file mode 100644
index 00000000..5ae99de7
--- /dev/null
+++ b/resources/qml/ElidedLabel.qml
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.9
+import QtQuick.Controls 2.13
+import im.nheko 1.0
+
+Label {
+    id: root
+
+    property alias fullText: metrics.text
+    property alias elideWidth: metrics.elideWidth
+
+    color: Nheko.colors.text
+    text: metrics.elidedText
+    maximumLineCount: 1
+    elide: Text.ElideRight
+    textFormat: Text.PlainText
+
+    TextMetrics {
+        id: metrics
+
+        font.pointSize: root.font.pointSize
+        elide: Text.ElideRight
+    }
+
+}
diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml
index 59bfe94d..1ec18540 100644
--- a/resources/qml/ForwardCompleter.qml
+++ b/resources/qml/ForwardCompleter.qml
@@ -21,7 +21,7 @@ Popup {
     modal: true
     palette: Nheko.colors
     parent: Overlay.overlay
-    width: implicitWidth >= (timelineView.width * 0.8) ? implicitWidth : (timelineView.width * 0.8)
+    width: implicitWidth >= (timelineRoot.width * 0.8) ? implicitWidth : (timelineRoot.width * 0.8)
     height: implicitHeight + completerPopup.height + padding * 2
     leftPadding: 10
     rightPadding: 10
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 25abb4d1..87a27517 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -8,6 +8,132 @@ import QtQuick.Layouts 1.3
 import im.nheko 1.0
 
 Page {
+    ListView {
+        anchors.left: parent.left
+        anchors.right: parent.right
+        height: parent.height
+        model: Rooms
+
+        ScrollHelper {
+            flickable: parent
+            anchors.fill: parent
+            enabled: !Settings.mobileMode
+        }
+
+        delegate: Rectangle {
+            color: Nheko.colors.window
+            height: fontMetrics.lineSpacing * 2.5 + Nheko.paddingMedium * 2
+            width: ListView.view.width
+
+            RowLayout {
+                //id: userInfoGrid
+
+                spacing: Nheko.paddingMedium
+                anchors.fill: parent
+                anchors.margins: Nheko.paddingMedium
+
+                Avatar {
+                    //userid: Nheko.currentUser.userid
+
+                    id: avatar
+
+                    Layout.alignment: Qt.AlignVCenter
+                    Layout.preferredWidth: fontMetrics.lineSpacing * 2.5
+                    Layout.preferredHeight: fontMetrics.lineSpacing * 2.5
+                    url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
+                    displayName: model.roomName
+                }
+
+                ColumnLayout {
+                    id: textContent
+
+                    Layout.alignment: Qt.AlignLeft
+                    Layout.fillWidth: true
+                    Layout.minimumWidth: 100
+                    width: parent.width - avatar.width
+                    Layout.preferredWidth: parent.width - avatar.width
+                    spacing: 0
+
+                    RowLayout {
+                        Layout.fillWidth: true
+                        spacing: 0
+
+                        ElidedLabel {
+                            Layout.alignment: Qt.AlignBottom
+                            color: Nheko.colors.text
+                            elideWidth: textContent.width - timestamp.width - Nheko.paddingMedium
+                            fullText: model.roomName + ": " + model.notificationCount
+                        }
+
+                        Item {
+                            Layout.fillWidth: true
+                        }
+
+                        Label {
+                            id: timestamp
+
+                            Layout.alignment: Qt.AlignRight | Qt.AlignBottom
+                            font.pixelSize: fontMetrics.font.pixelSize * 0.9
+                            color: Nheko.colors.buttonText
+                            text: "14:32"
+                        }
+
+                    }
+
+                    RowLayout {
+                        Layout.fillWidth: true
+                        spacing: 0
+
+                        ElidedLabel {
+                            color: Nheko.colors.buttonText
+                            font.weight: Font.Thin
+                            font.pixelSize: fontMetrics.font.pixelSize * 0.9
+                            elideWidth: textContent.width - notificationBubble.width
+                            fullText: model.lastMessage
+                        }
+
+                        Item {
+                            Layout.fillWidth: true
+                        }
+
+                        Rectangle {
+                            id: notificationBubble
+
+                            Layout.alignment: Qt.AlignRight
+                            height: fontMetrics.font.pixelSize * 1.3
+                            width: height
+                            radius: height / 2
+                            color: Nheko.colors.highlight
+
+                            Label {
+                                anchors.fill: parent
+                                horizontalAlignment: Text.AlignHCenter
+                                verticalAlignment: Text.AlignVCenter
+                                fontSizeMode: Text.Fit
+                                color: Nheko.colors.highlightedText
+                                text: model.notificationCount
+                            }
+
+                        }
+
+                    }
+
+                }
+
+            }
+
+            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
@@ -34,8 +160,8 @@ Page {
                     id: avatar
 
                     Layout.alignment: Qt.AlignVCenter
-                    Layout.preferredWidth: Nheko.avatarSize
-                    Layout.preferredHeight: Nheko.avatarSize
+                    Layout.preferredWidth: fontMetrics.lineSpacing * 2
+                    Layout.preferredHeight: fontMetrics.lineSpacing * 2
                     url: Nheko.currentUser.avatarUrl.replace("mxc://", "image://MxcImage/")
                     displayName: Nheko.currentUser.displayName
                     userid: Nheko.currentUser.userid
@@ -46,50 +172,25 @@ Page {
 
                     Layout.alignment: Qt.AlignLeft
                     Layout.fillWidth: true
-                    Layout.minimumWidth: 100
-                    width: parent.width - avatar.width - logoutButton.width
-                    Layout.preferredWidth: parent.width - avatar.width - logoutButton.width
+                    width: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2
+                    Layout.preferredWidth: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2
                     spacing: 0
 
-                    Label {
+                    ElidedLabel {
                         Layout.alignment: Qt.AlignBottom
-                        color: Nheko.colors.text
                         font.pointSize: fontMetrics.font.pointSize * 1.1
                         font.weight: Font.DemiBold
-                        text: userNameText.elidedText
-                        maximumLineCount: 1
-                        elide: Text.ElideRight
-                        textFormat: Text.PlainText
-
-                        TextMetrics {
-                            id: userNameText
-
-                            font.pointSize: fontMetrics.font.pointSize * 1.1
-                            elide: Text.ElideRight
-                            elideWidth: col.width
-                            text: Nheko.currentUser.displayName
-                        }
-
+                        fullText: Nheko.currentUser.displayName
+                        elideWidth: col.width
                     }
 
-                    Label {
+                    ElidedLabel {
                         Layout.alignment: Qt.AlignTop
                         color: Nheko.colors.buttonText
                         font.weight: Font.Thin
-                        text: userIdText.elidedText
-                        maximumLineCount: 1
-                        textFormat: Text.PlainText
                         font.pointSize: fontMetrics.font.pointSize * 0.9
-
-                        TextMetrics {
-                            id: userIdText
-
-                            font.pointSize: fontMetrics.font.pointSize * 0.9
-                            elide: Text.ElideRight
-                            elideWidth: col.width
-                            text: Nheko.currentUser.userid
-                        }
-
+                        elideWidth: col.width
+                        fullText: Nheko.currentUser.userid
                     }
 
                 }
diff --git a/resources/res.qrc b/resources/res.qrc
index c146f2d9..79e63810 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -131,6 +131,7 @@
         qml/Completer.qml
         qml/EncryptionIndicator.qml
         qml/ImageButton.qml
+        qml/ElidedLabel.qml
         qml/MatrixText.qml
         qml/MatrixTextField.qml
         qml/ToggleButton.qml
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
new file mode 100644
index 00000000..6a1fc3c5
--- /dev/null
+++ b/src/timeline/RoomlistModel.cpp
@@ -0,0 +1,146 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "RoomlistModel.h"
+
+#include "ChatPage.h"
+#include "MatrixClient.h"
+#include "MxcImageProvider.h"
+#include "TimelineModel.h"
+#include "TimelineViewManager.h"
+#include "UserSettingsPage.h"
+
+RoomlistModel::RoomlistModel(TimelineViewManager *parent)
+  : manager(parent)
+{
+        connect(ChatPage::instance(), &ChatPage::decryptSidebarChanged, this, [this]() {
+                auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar();
+                QHash>::iterator i;
+                for (i = models.begin(); i != models.end(); ++i) {
+                        auto ptr = i.value();
+
+                        if (!ptr.isNull()) {
+                                ptr->setDecryptDescription(decrypt);
+                                ptr->updateLastMessage();
+                        }
+                }
+        });
+}
+
+QHash
+RoomlistModel::roleNames() const
+{
+        return {
+          {AvatarUrl, "avatarUrl"},
+          {RoomName, "roomName"},
+          {LastMessage, "lastMessage"},
+          {HasUnreadMessages, "hasUnreadMessages"},
+          {NotificationCount, "notificationCount"},
+        };
+}
+
+QVariant
+RoomlistModel::data(const QModelIndex &index, int role) const
+{
+        if (index.row() >= 0 && static_cast(index.row()) < roomids.size()) {
+                auto room = models.value(roomids.at(index.row()));
+                switch (role) {
+                case Roles::AvatarUrl:
+                        return room->roomAvatarUrl();
+                case Roles::RoomName:
+                        return room->roomName();
+                case Roles::LastMessage:
+                        return QString("Nico: Hahaha, this is funny!");
+                case Roles::HasUnreadMessages:
+                        return true;
+                case Roles::NotificationCount:
+                        return 5;
+                default:
+                        return {};
+                }
+        } else {
+                return {};
+        }
+}
+
+void
+RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
+{
+        if (!models.contains(room_id)) {
+                QSharedPointer 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);
+
+                if (!suppressInsertNotification)
+                        beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size());
+                models.insert(room_id, std::move(newRoom));
+                roomids.push_back(room_id);
+                if (!suppressInsertNotification)
+                        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->syncState(room.state);
+                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>(
+                                      &ev)) {
+                                        std::vector 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);
+                                }
+                        }
+                }
+        }
+}
+
+void
+RoomlistModel::initializeRooms(const std::vector &roomIds_)
+{
+        beginResetModel();
+        models.clear();
+        roomids.clear();
+        roomids = roomIds_;
+        for (const auto &id : roomIds_)
+                addRoom(id, true);
+        endResetModel();
+}
+
+void
+RoomlistModel::clear()
+{
+        beginResetModel();
+        models.clear();
+        roomids.clear();
+        endResetModel();
+}
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
new file mode 100644
index 00000000..44fcf032
--- /dev/null
+++ b/src/timeline/RoomlistModel.h
@@ -0,0 +1,58 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+class TimelineModel;
+class TimelineViewManager;
+
+class RoomlistModel : public QAbstractListModel
+{
+        Q_OBJECT
+public:
+        enum Roles
+        {
+                AvatarUrl = Qt::UserRole,
+                RoomName,
+                LastMessage,
+                HasUnreadMessages,
+                NotificationCount,
+        };
+
+        RoomlistModel(TimelineViewManager *parent = nullptr);
+        QHash 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 getRoomById(QString id) const
+        {
+                if (models.contains(id))
+                        return models.value(id);
+                else
+                        return {};
+        }
+
+public slots:
+        void initializeRooms(const std::vector &roomids);
+        void sync(const mtx::responses::Rooms &rooms);
+        void clear();
+
+private:
+        void addRoom(const QString &room_id, bool suppressInsertNotification = false);
+
+        TimelineViewManager *manager = nullptr;
+        std::vector roomids;
+        QHash> models;
+};
+
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index e8e57fd8..b0c13b03 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -86,21 +86,6 @@ removeReplyFallback(mtx::events::Event &e)
 }
 }
 
-void
-TimelineViewManager::updateEncryptedDescriptions()
-{
-        auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar();
-        QHash>::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()
 {
@@ -148,6 +133,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
   , colorImgProvider(new ColorImageProvider())
   , blurhashProvider(new BlurhashProvider())
   , callManager_(callManager)
+  , rooms(new RoomlistModel(this))
 {
         qRegisterMetaType();
         qRegisterMetaType();
@@ -205,6 +191,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
                   QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
                   return ptr;
           });
+        qmlRegisterSingletonType(
+          "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
+                  auto ptr = self->rooms;
+                  QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
+                  return ptr;
+          });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
                   auto ptr = ChatPage::instance()->userSettings().data();
@@ -260,10 +252,6 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
         view->setSource(QUrl("qrc:///qml/Root.qml"));
 
         connect(parent, &ChatPage::themeChanged, this, &TimelineViewManager::updateColorPalette);
-        connect(parent,
-                &ChatPage::decryptSidebarChanged,
-                this,
-                &TimelineViewManager::updateEncryptedDescriptions);
         connect(
           dynamic_cast(parent),
           &ChatPage::receivedRoomDeviceVerificationRequest,
@@ -334,64 +322,13 @@ TimelineViewManager::setVideoCallItem()
 }
 
 void
-TimelineViewManager::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));
-                if (!isInitialSync_)
-                        connect(room_model.data(),
-                                &TimelineModel::newCallEvent,
-                                callManager_,
-                                &CallManager::syncEvent);
-                room_model->syncState(room.state);
-                room_model->addEvents(room.timeline);
-                if (!isInitialSync_)
-                        disconnect(room_model.data(),
-                                   &TimelineModel::newCallEvent,
-                                   callManager_,
-                                   &CallManager::syncEvent);
-
-                if (ChatPage::instance()->userSettings()->typingNotifications()) {
-                        for (const auto &ev : room.ephemeral.events) {
-                                if (auto t = std::get_if<
-                                      mtx::events::EphemeralEvent>(
-                                      &ev)) {
-                                        std::vector typing;
-                                        typing.reserve(t->content.user_ids.size());
-                                        for (const auto &user : t->content.user_ids) {
-                                                if (user != http::client()->user_id().to_string())
-                                                        typing.push_back(
-                                                          QString::fromStdString(user));
-                                        }
-                                        room_model->updateTypingUsers(typing);
-                                }
-                        }
-                }
-        }
-
-        this->isInitialSync_ = false;
-        emit initialSyncChanged(false);
-}
+TimelineViewManager::sync(const mtx::responses::Rooms &rooms_)
+{
+        this->rooms->sync(rooms_);
 
-void
-TimelineViewManager::addRoom(const QString &room_id)
-{
-        if (!models.contains(room_id)) {
-                QSharedPointer newRoom(new TimelineModel(this, room_id));
-                newRoom->setDecryptDescription(
-                  ChatPage::instance()->userSettings()->decryptSidebar());
-
-                connect(newRoom.data(),
-                        &TimelineModel::newEncryptedImage,
-                        imgProvider,
-                        &MxcImageProvider::addEncryptionInfo);
-                connect(newRoom.data(),
-                        &TimelineModel::forwardToRoom,
-                        this,
-                        &TimelineViewManager::forwardMessageToRoom);
-                models.insert(room_id, std::move(newRoom));
+        if (isInitialSync_) {
+                this->isInitialSync_ = false;
+                emit initialSyncChanged(false);
         }
 }
 
@@ -400,9 +337,8 @@ 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();
+        if (auto room = rooms->getRoomById(room_id)) {
+                timeline_ = room.get();
                 emit activeTimelineChanged(timeline_);
                 container->setFocus();
                 nhlog::ui()->info("Activated room {}", room_id.toStdString());
@@ -418,10 +354,9 @@ TimelineViewManager::highlightRoom(const QString &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();
+        if (auto room = rooms->getRoomById(room_id)) {
+                if (timeline_ != room) {
+                        timeline_ = room.get();
                         emit activeTimelineChanged(timeline_);
                         container->setFocus();
                         nhlog::ui()->info("Activated room {}", room_id.toStdString());
@@ -505,17 +440,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;
+                                }
                         }
                 }
         }
@@ -548,26 +487,23 @@ void
 TimelineViewManager::updateReadReceipts(const QString &room_id,
                                         const std::vector &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 &roomIds)
 {
-        for (const auto &roomId : roomIds)
-                addRoom(roomId);
+        rooms->initializeRooms(roomIds);
 }
 
 void
@@ -575,10 +511,9 @@ 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);
         }
 }
 
@@ -620,29 +555,32 @@ 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
@@ -693,7 +631,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 encryptionInfo = mtx::accessors::file(*e);
 
@@ -736,12 +674,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);
@@ -759,8 +700,7 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven
                                 mtx::events::EventType::RoomMessage) {
                           e.content.relations.relations.clear();
                           removeReplyFallback(e);
-                          room.value()->sendMessageEvent(e.content,
-                                                         mtx::events::EventType::RoomMessage);
+                          room->sendMessageEvent(e.content, mtx::events::EventType::RoomMessage);
                   }
           },
           *e);
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 0665b663..f4297243 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -22,6 +22,7 @@
 #include "WebRTCSession.h"
 #include "emoji/EmojiModel.h"
 #include "emoji/Provider.h"
+#include "timeline/RoomlistModel.h"
 
 class MxcImageProvider;
 class BlurhashProvider;
@@ -48,13 +49,15 @@ public:
         QWidget *getWidget() const { return container; }
 
         void sync(const mtx::responses::Rooms &rooms);
-        void addRoom(const QString &room_id);
+
+        MxcImageProvider *imageProvider() { return imgProvider; }
+        CallManager *callManager() { return callManager_; }
 
         void clearAll()
         {
                 timeline_ = nullptr;
                 emit activeTimelineChanged(nullptr);
-                models.clear();
+                rooms->clear();
         }
 
         Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
@@ -109,11 +112,7 @@ public slots:
         void focusTimeline();
         TimelineModel *getHistoryView(const QString &room_id)
         {
-                auto room = models.find(room_id);
-                if (room != models.end())
-                        return room.value().data();
-                else
-                        return nullptr;
+                return rooms->getRoomById(room_id).get();
         }
 
         void updateColorPalette();
@@ -126,7 +125,6 @@ public slots:
         void queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
         void queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
 
-        void updateEncryptedDescriptions();
         void setVideoCallItem();
 
         void enableBackButton()
@@ -163,7 +161,6 @@ private:
         ColorImageProvider *colorImgProvider;
         BlurhashProvider *blurhashProvider;
 
-        QHash> models;
         TimelineModel *timeline_  = nullptr;
         CallManager *callManager_ = nullptr;
 
@@ -171,6 +168,8 @@ private:
         bool isNarrowView_    = false;
         bool isWindowFocused_ = false;
 
+        RoomlistModel *rooms = nullptr;
+
         QHash userColors;
 
         QHash> dvList;
-- 
cgit 1.5.1


From beeb60e4a12b47ae619e52629040aff5a8f43db2 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sat, 22 May 2021 00:57:14 +0200
Subject: Sort the room list

---
 resources/qml/RoomList.qml           |  2 +-
 src/timeline/RoomlistModel.cpp       | 93 ++++++++++++++++++++++++++++++++++--
 src/timeline/RoomlistModel.h         | 26 ++++++++++
 src/timeline/TimelineModel.cpp       |  2 +
 src/timeline/TimelineModel.h         |  2 +-
 src/timeline/TimelineViewManager.cpp |  4 +-
 6 files changed, 120 insertions(+), 9 deletions(-)

(limited to 'src/timeline/TimelineViewManager.cpp')

diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index bb8deda6..f2a957c9 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -131,7 +131,7 @@ Page {
                             Layout.alignment: Qt.AlignRight | Qt.AlignBottom
                             font.pixelSize: fontMetrics.font.pixelSize * 0.9
                             color: roomItem.unimportantText
-                            text: model.timestamp
+                            text: model.time
                         }
 
                     }
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 5fc4dc65..afe9679a 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -42,10 +42,13 @@ RoomlistModel::roleNames() const
           {RoomName, "roomName"},
           {RoomId, "roomId"},
           {LastMessage, "lastMessage"},
+          {Time, "time"},
           {Timestamp, "timestamp"},
           {HasUnreadMessages, "hasUnreadMessages"},
           {HasLoudNotification, "hasLoudNotification"},
           {NotificationCount, "notificationCount"},
+          {IsInvite, "isInvite"},
+          {IsSpace, "isSpace"},
         };
 }
 
@@ -64,8 +67,10 @@ RoomlistModel::data(const QModelIndex &index, int role) const
                         return room->roomId();
                 case Roles::LastMessage:
                         return room->lastMessage().body;
-                case Roles::Timestamp:
+                case Roles::Time:
                         return room->lastMessage().descriptiveTime;
+                case Roles::Timestamp:
+                        return QVariant(static_cast(room->lastMessage().timestamp));
                 case Roles::HasUnreadMessages:
                         return this->roomReadStatus.count(roomid) &&
                                this->roomReadStatus.at(roomid);
@@ -73,6 +78,9 @@ RoomlistModel::data(const QModelIndex &index, int role) const
                         return room->hasMentions();
                 case Roles::NotificationCount:
                         return room->notificationCount();
+                case Roles::IsInvite:
+                case Roles::IsSpace:
+                        return false;
                 default:
                         return {};
                 }
@@ -90,9 +98,9 @@ RoomlistModel::updateReadStatus(const std::map roomReadStatus_)
                 if (roomUnread != roomReadStatus[roomid]) {
                         roomsToUpdate.push_back(this->roomidToIndex(roomid));
                 }
-        }
 
-        this->roomReadStatus = roomReadStatus_;
+                this->roomReadStatus[roomid] = roomUnread;
+        }
 
         for (auto idx : roomsToUpdate) {
                 emit dataChanged(index(idx),
@@ -135,6 +143,7 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
                                              Roles::LastMessage,
                                              Roles::Timestamp,
                                              Roles::NotificationCount,
+                                             Qt::DisplayRole,
                                            });
                   });
                 connect(
@@ -162,6 +171,7 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
                                            {
                                              Roles::HasLoudNotification,
                                              Roles::NotificationCount,
+                                             Qt::DisplayRole,
                                            });
 
                           int total_unread_msgs = 0;
@@ -225,7 +235,6 @@ RoomlistModel::initializeRooms(const std::vector &roomIds_)
         beginResetModel();
         models.clear();
         roomids.clear();
-        roomids = roomIds_;
         for (const auto &id : roomIds_)
                 addRoom(id, true);
         endResetModel();
@@ -239,3 +248,79 @@ RoomlistModel::clear()
         roomids.clear();
         endResetModel();
 }
+
+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();
+                         });
+
+        sort(0);
+}
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index c4c9d9ba..c3374bd2 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -7,6 +7,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
@@ -24,10 +25,13 @@ public:
                 RoomName,
                 RoomId,
                 LastMessage,
+                Time,
                 Timestamp,
                 HasUnreadMessages,
                 HasLoudNotification,
                 NotificationCount,
+                IsInvite,
+                IsSpace,
         };
 
         RoomlistModel(TimelineViewManager *parent = nullptr);
@@ -73,4 +77,26 @@ private:
         std::vector roomids;
         QHash> models;
         std::map roomReadStatus;
+
+        friend class FilteredRoomlistModel;
+};
+
+class FilteredRoomlistModel : public QSortFilterProxyModel
+{
+        Q_OBJECT
+public:
+        FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr);
+        bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+
+public slots:
+        int roomidToIndex(QString roomid)
+        {
+                return mapFromSource(roomlistmodel->index(roomlistmodel->roomidToIndex(roomid)))
+                  .row();
+        }
+
+private:
+        short int calculateImportance(const QModelIndex &idx) const;
+        RoomlistModel *roomlistmodel;
+        bool sortByImportance = true;
 };
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 19c3fb30..2625127c 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -318,6 +318,8 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
   , room_id_(room_id)
   , manager_(manager)
 {
+        lastMessage_.timestamp = 0;
+
         connect(
           this,
           &TimelineModel::redactionFailed,
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 5c1065cb..b3d3b663 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -382,7 +382,7 @@ private:
         QString eventIdToShow;
         int showEventTimerCounter = 0;
 
-        DescInfo lastMessage_;
+        DescInfo lastMessage_{};
 
         friend struct SendMessageVisitor;
 
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index b0c13b03..c84e0df8 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -193,9 +193,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
-                  auto ptr = self->rooms;
-                  QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
-                  return ptr;
+                  return new FilteredRoomlistModel(self->rooms);
           });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
-- 
cgit 1.5.1


From c290b0747f34a6f683365f93d64ce93dc4428ca8 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Mon, 24 May 2021 14:04:07 +0200
Subject: Reenable invites

---
 resources/qml/RoomList.qml           |  56 ++++++++++++
 src/Cache.cpp                        |  50 +++++++++--
 src/Cache.h                          |   2 +-
 src/Cache_p.h                        |   3 +-
 src/ChatPage.cpp                     |   4 +-
 src/ChatPage.h                       |   2 +-
 src/RoomList.cpp                     |   2 +-
 src/RoomList.h                       |   2 +-
 src/timeline/RoomlistModel.cpp       | 169 ++++++++++++++++++++++++++++-------
 src/timeline/RoomlistModel.h         |   8 +-
 src/timeline/TimelineViewManager.cpp |   4 +-
 src/timeline/TimelineViewManager.h   |   2 +-
 src/ui/Theme.cpp                     |   3 +
 src/ui/Theme.h                       |   4 +-
 14 files changed, 260 insertions(+), 51 deletions(-)

(limited to 'src/timeline/TimelineViewManager.cpp')

diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 40669eda..e9bb351f 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -141,6 +141,8 @@ Page {
                     RowLayout {
                         Layout.fillWidth: true
                         spacing: 0
+                        visible: !model.isInvite
+                        height: visible ? 0 : undefined
 
                         ElidedLabel {
                             color: roomItem.unimportantText
@@ -182,6 +184,60 @@ Page {
 
                     }
 
+                    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
+                        }
+
+                    }
+
                 }
 
             }
diff --git a/src/Cache.cpp b/src/Cache.cpp
index c41b66cc..4a99dd59 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -2045,21 +2045,57 @@ Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id)
         return fallbackDesc;
 }
 
-std::map
+QHash
 Cache::invites()
 {
-        std::map result;
+        QHash 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
+Cache::invite(std::string_view roomid)
+{
+        std::optional 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;
 }
@@ -4064,7 +4100,7 @@ roomInfo(bool withInvites)
 {
         return instance_->roomInfo(withInvites);
 }
-std::map
+QHash
 invites()
 {
         return instance_->invites();
diff --git a/src/Cache.h b/src/Cache.h
index 427dbafc..74ec9695 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -62,7 +62,7 @@ joinedRooms();
 
 QMap
 roomInfo(bool withInvites = true);
-std::map
+QHash
 invites();
 
 //! Calculate & return the name of the room.
diff --git a/src/Cache_p.h b/src/Cache_p.h
index c55fa601..f2911622 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -70,7 +70,8 @@ public:
 
         QMap roomInfo(bool withInvites = true);
         std::optional getRoomAliases(const std::string &roomid);
-        std::map invites();
+        QHash invites();
+        std::optional invite(std::string_view roomid);
 
         //! Calculate & return the name of the room.
         QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 58b76174..166c03ec 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -313,7 +313,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
         connect(this,
                 &ChatPage::initializeEmptyViews,
                 view_manager_,
-                &TimelineViewManager::initWithMessages);
+                &TimelineViewManager::initializeRoomlist);
         connect(this,
                 &ChatPage::initializeMentions,
                 user_mentions_popup_,
@@ -554,7 +554,7 @@ ChatPage::loadStateFromCache()
         try {
                 olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY);
 
-                emit initializeEmptyViews(cache::client()->roomIds());
+                emit initializeEmptyViews();
                 emit initializeRoomList(cache::roomInfo());
                 emit initializeMentions(cache::getTimelineMentions());
                 emit syncTags(cache::roomInfo().toStdMap());
diff --git a/src/ChatPage.h b/src/ChatPage.h
index 84e7cdff..eb60047d 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -147,7 +147,7 @@ signals:
 
         void initializeRoomList(QMap);
         void initializeViews(const mtx::responses::Rooms &rooms);
-        void initializeEmptyViews(const std::vector &roomIds);
+        void initializeEmptyViews();
         void initializeMentions(const QMap ¬ifs);
         void syncUI(const mtx::responses::Rooms &rooms);
         void syncRoomlist(const std::map &updates);
diff --git a/src/RoomList.cpp b/src/RoomList.cpp
index 5c41a7a1..5839c4a0 100644
--- a/src/RoomList.cpp
+++ b/src/RoomList.cpp
@@ -183,7 +183,7 @@ RoomList::initialize(const QMap &info)
 }
 
 void
-RoomList::cleanupInvites(const std::map &invites)
+RoomList::cleanupInvites(const QHash &invites)
 {
         if (invites.size() == 0)
                 return;
diff --git a/src/RoomList.h b/src/RoomList.h
index 74152c55..af792fd7 100644
--- a/src/RoomList.h
+++ b/src/RoomList.h
@@ -48,7 +48,7 @@ public:
         //! Show all the available rooms.
         void removeFilter(const std::set &roomsToHide);
         void updateRoom(const QString &room_id, const RoomInfo &info);
-        void cleanupInvites(const std::map &invites);
+        void cleanupInvites(const QHash &invites);
 
 signals:
         void roomChanged(const QString &room_id);
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 28c3cf46..f3d4dad7 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -57,31 +57,64 @@ RoomlistModel::data(const QModelIndex &index, int role) const
 {
         if (index.row() >= 0 && static_cast(index.row()) < roomids.size()) {
                 auto roomid = roomids.at(index.row());
-                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(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;
-                default:
+
+                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(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;
+                        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(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;
+                        default:
+                                return {};
+                        }
+                } else {
                         return {};
                 }
         } else {
@@ -109,7 +142,7 @@ RoomlistModel::updateReadStatus(const std::map roomReadStatus_)
                                    Roles::HasUnreadMessages,
                                  });
         }
-};
+}
 void
 RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
 {
@@ -186,11 +219,21 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
 
                 newRoom->updateLastMessage();
 
-                if (!suppressInsertNotification)
+                bool wasInvite = invites.contains(room_id);
+                if (!suppressInsertNotification && !wasInvite)
                         beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size());
+
                 models.insert(room_id, std::move(newRoom));
-                roomids.push_back(room_id);
-                if (!suppressInsertNotification)
+
+                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();
         }
 }
@@ -234,20 +277,50 @@ RoomlistModel::sync(const mtx::responses::Rooms &rooms)
                 if (idx != -1) {
                         beginRemoveRows(QModelIndex(), idx, idx);
                         roomids.erase(roomids.begin() + idx);
-                        models.remove(QString::fromStdString(room_id));
+                        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_id;
+                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(const std::vector &roomIds_)
+RoomlistModel::initializeRooms()
 {
         beginResetModel();
         models.clear();
         roomids.clear();
-        for (const auto &id : roomIds_)
+        invites.clear();
+
+        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();
 }
 
@@ -256,10 +329,42 @@ RoomlistModel::clear()
 {
         beginResetModel();
         models.clear();
+        invites.clear();
         roomids.clear();
         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);
+                }
+        }
+}
+
 namespace {
 enum NotificationImportance : short
 {
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index c3374bd2..ff85614c 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+#include 
 #include 
 #include 
 #include 
@@ -51,7 +52,7 @@ public:
         }
 
 public slots:
-        void initializeRooms(const std::vector &roomids);
+        void initializeRooms();
         void sync(const mtx::responses::Rooms &rooms);
         void clear();
         int roomidToIndex(QString roomid)
@@ -63,6 +64,8 @@ public slots:
 
                 return -1;
         }
+        void acceptInvite(QString roomid);
+        void declineInvite(QString roomid);
 
 private slots:
         void updateReadStatus(const std::map roomReadStatus_);
@@ -75,6 +78,7 @@ private:
 
         TimelineViewManager *manager = nullptr;
         std::vector roomids;
+        QHash invites;
         QHash> models;
         std::map roomReadStatus;
 
@@ -94,6 +98,8 @@ public slots:
                 return mapFromSource(roomlistmodel->index(roomlistmodel->roomidToIndex(roomid)))
                   .row();
         }
+        void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); }
+        void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); }
 
 private:
         short int calculateImportance(const QModelIndex &idx) const;
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index c84e0df8..9fa7f8b6 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -499,9 +499,9 @@ TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::s
 }
 
 void
-TimelineViewManager::initWithMessages(const std::vector &roomIds)
+TimelineViewManager::initializeRoomlist()
 {
-        rooms->initializeRooms(roomIds);
+        rooms->initializeRooms();
 }
 
 void
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 609f5a4a..37e50804 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -100,7 +100,7 @@ signals:
 public slots:
         void updateReadReceipts(const QString &room_id, const std::vector &event_ids);
         void receivedSessionKey(const std::string &room_id, const std::string &session_id);
-        void initWithMessages(const std::vector &roomIds);
+        void initializeRoomlist();
         void chatFocusChanged(bool focused)
         {
                 isWindowFocused_ = focused;
diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp
index b6c9579a..26119393 100644
--- a/src/ui/Theme.cpp
+++ b/src/ui/Theme.cpp
@@ -60,12 +60,15 @@ Theme::Theme(std::string_view theme)
         separator_ = p.mid().color();
         if (theme == "light") {
                 sidebarBackground_ = QColor("#233649");
+                alternateButton_   = QColor("#ccc");
                 red_               = QColor("#a82353");
         } else if (theme == "dark") {
                 sidebarBackground_ = QColor("#2d3139");
+                alternateButton_   = QColor("#414A59");
                 red_               = QColor("#a82353");
         } else {
                 sidebarBackground_ = p.window().color();
+                alternateButton_   = p.dark().color();
                 red_               = QColor("red");
         }
 }
diff --git a/src/ui/Theme.h b/src/ui/Theme.h
index 834571c0..b5bcd4dd 100644
--- a/src/ui/Theme.h
+++ b/src/ui/Theme.h
@@ -65,6 +65,7 @@ class Theme : public QPalette
 {
         Q_GADGET
         Q_PROPERTY(QColor sidebarBackground READ sidebarBackground CONSTANT)
+        Q_PROPERTY(QColor alternateButton READ alternateButton CONSTANT)
         Q_PROPERTY(QColor separator READ separator CONSTANT)
         Q_PROPERTY(QColor red READ red CONSTANT)
 public:
@@ -73,9 +74,10 @@ public:
         static QPalette paletteFromTheme(std::string_view theme);
 
         QColor sidebarBackground() const { return sidebarBackground_; }
+        QColor alternateButton() const { return alternateButton_; }
         QColor separator() const { return separator_; }
         QColor red() const { return red_; }
 
 private:
-        QColor sidebarBackground_, separator_, red_;
+        QColor sidebarBackground_, separator_, red_, alternateButton_;
 };
-- 
cgit 1.5.1


From 298822baeaffdc83386e003099e34819bcd7d18c Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 28 May 2021 22:14:59 +0200
Subject: Move currentRoom/timeline handling to roomlist

---
 resources/qml/ChatPage.qml                       |   1 +
 resources/qml/Completer.qml                      |   6 +-
 resources/qml/ForwardCompleter.qml               |   4 +-
 resources/qml/MessageInput.qml                   |  62 +++++-----
 resources/qml/MessageView.qml                    | 140 ++++++++++++++++++++-
 resources/qml/QuickSwitcher.qml                  |   3 +-
 resources/qml/Reactions.qml                      |   2 +-
 resources/qml/ReplyPopup.qml                     |   2 -
 resources/qml/RoomList.qml                       |  21 +++-
 resources/qml/Root.qml                           | 147 -----------------------
 resources/qml/StatusIndicator.qml                |   2 +-
 resources/qml/TimelineView.qml                   |  22 +++-
 resources/qml/TopBar.qml                         |  14 +--
 resources/qml/TypingIndicator.qml                |   2 -
 resources/qml/delegates/FileMessage.qml          |   2 +-
 resources/qml/delegates/MessageDelegate.qml      |  10 +-
 resources/qml/delegates/PlayableMediaMessage.qml |   4 +-
 resources/qml/emoji/EmojiButton.qml              |   2 +-
 resources/qml/voip/ActiveCallBar.qml             |   2 +-
 resources/qml/voip/CallInviteBar.qml             |   2 +-
 resources/qml/voip/PlaceCall.qml                 |  12 +-
 resources/qml/voip/ScreenShare.qml               |   4 +-
 src/ChatPage.cpp                                 |   7 +-
 src/timeline/InputBar.cpp                        |  35 +++++-
 src/timeline/InputBar.h                          |   1 +
 src/timeline/RoomlistModel.cpp                   |  18 +++
 src/timeline/RoomlistModel.h                     |  16 ++-
 src/timeline/TimelineViewManager.cpp             | 125 ++++++-------------
 src/timeline/TimelineViewManager.h               |  26 +---
 src/ui/NhekoDropArea.cpp                         |   2 +-
 30 files changed, 350 insertions(+), 346 deletions(-)

(limited to 'src/timeline/TimelineViewManager.cpp')

diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml
index fc6137a6..966f169b 100644
--- a/resources/qml/ChatPage.qml
+++ b/resources/qml/ChatPage.qml
@@ -31,6 +31,7 @@ Rectangle {
 
         TimelineView {
             id: timeline
+            room: Rooms.currentRoom
 
             SplitView.fillWidth: true
             SplitView.minimumWidth: 400
diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml
index 2609371b..0cdd789d 100644
--- a/resources/qml/Completer.qml
+++ b/resources/qml/Completer.qml
@@ -70,7 +70,7 @@ Popup {
     onCompleterNameChanged: {
         if (completerName) {
             if (completerName == "user")
-                completer = TimelineManager.completerFor(completerName, TimelineManager.timeline.roomId());
+                completer = TimelineManager.completerFor(completerName, room.roomId());
             else
                 completer = TimelineManager.completerFor(completerName);
             completer.setSearchString("");
@@ -83,8 +83,8 @@ Popup {
     height: listView.contentHeight + 2 // + 2 for the padding on top and bottom
 
     Connections {
-        onTimelineChanged: completer = null
-        target: TimelineManager
+        onRoomChanged: completer = null
+        target: timelineView
     }
 
     ListView {
diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml
index 1ec18540..eee3879c 100644
--- a/resources/qml/ForwardCompleter.qml
+++ b/resources/qml/ForwardCompleter.qml
@@ -50,7 +50,7 @@ Popup {
         Reply {
             id: replyPreview
 
-            modelData: TimelineManager.timeline ? TimelineManager.timeline.getDump(mid, "") : {
+            modelData: room ? room.getDump(mid, "") : {
             }
             userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.window)
         }
@@ -95,7 +95,7 @@ Popup {
 
     Connections {
         onCompletionSelected: {
-            TimelineManager.timeline.forwardMessage(messageContextMenu.eventId, id);
+            room.forwardMessage(messageContextMenu.eventId, id);
             forwardMessagePopup.close();
         }
         onCountChanged: {
diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index f4e253ad..24f9b0e8 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -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: Nheko.colors.window
-                visible: TimelineManager.timeline && TimelineManager.timeline.input.uploading
+                visible: room && room.input.uploading
 
                 NhekoBusyIndicator {
                     anchors.fill: parent
@@ -123,16 +123,16 @@ Rectangle {
                 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,7 +355,7 @@ 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: Nheko.colors.text
     }
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 5af4e4de..176905db 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -4,6 +4,7 @@
 
 import "./delegates"
 import "./emoji"
+import Qt.labs.platform 1.1 as Platform
 import QtGraphicalEffects 1.0
 import QtQuick 2.12
 import QtQuick.Controls 2.3
@@ -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
@@ -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("React")
+            onTriggered: emojiPopup.show(null, function(emoji) {
+                room.input.reaction(messageContextMenu.eventId, emoji);
+            })
+        }
+
+        Platform.MenuItem {
+            visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
+            text: qsTr("Reply")
+            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 receipts")
+            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("Remove 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 event")
+            onTriggered: room.copyLinkToEvent(messageContextMenu.eventId)
+        }
+
+    }
+
+    Component {
+        id: forwardCompleterComponent
+
+        ForwardCompleter {
+        }
+
+    }
+
 }
diff --git a/resources/qml/QuickSwitcher.qml b/resources/qml/QuickSwitcher.qml
index a6373b1c..8c4f47ca 100644
--- a/resources/qml/QuickSwitcher.qml
+++ b/resources/qml/QuickSwitcher.qml
@@ -72,8 +72,7 @@ Popup {
 
     Connections {
         onCompletionSelected: {
-            TimelineManager.setHistoryView(id);
-            TimelineManager.highlightRoom(id);
+            Rooms.setCurrentRoom(id);
             quickSwitcher.close();
         }
         onCountChanged: {
diff --git a/resources/qml/Reactions.qml b/resources/qml/Reactions.qml
index 064df543..def87f75 100644
--- a/resources/qml/Reactions.qml
+++ b/resources/qml/Reactions.qml
@@ -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 {
diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml
index 1d85acb0..0de68fe8 100644
--- a/resources/qml/ReplyPopup.qml
+++ b/resources/qml/ReplyPopup.qml
@@ -11,8 +11,6 @@ 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
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index b184aef0..c5e07032 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -149,7 +149,7 @@ Page {
                 },
                 State {
                     name: "selected"
-                    when: TimelineManager.timeline && model.roomId == TimelineManager.timeline.roomId()
+                    when: Rooms.currentRoom && model.roomId == Rooms.currentRoom.roomId()
 
                     PropertyChanges {
                         target: roomItem
@@ -165,16 +165,25 @@ Page {
 
             TapHandler {
                 acceptedButtons: Qt.RightButton
-                onSingleTapped: roomContextMenu.show(model.roomId, model.tags)
+                onSingleTapped: {
+                    if (!TimelineManager.isInvite) {
+                        roomContextMenu.show(model.roomId, model.tags);
+                    }
+                }
                 gesturePolicy: TapHandler.ReleaseWithinBounds
             }
 
-            HoverHandler {
-                id: hovered
+            TapHandler {
+                onSingleTapped: Rooms.setCurrentRoom(model.roomId)
+                onLongPressed: {
+                    if (!TimelineManager.isInvite) {
+                        roomContextMenu.show(model.roomId, model.tags);
+                    }
+                }
             }
 
-            TapHandler {
-                onSingleTapped: TimelineManager.setHistoryView(model.roomId)
+            HoverHandler {
+                id: hovered
             }
 
             RowLayout {
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index 35b81a1f..a8b6fa52 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -63,14 +63,6 @@ Page {
 
     }
 
-    Component {
-        id: forwardCompleterComponent
-
-        ForwardCompleter {
-        }
-
-    }
-
     Shortcut {
         sequence: "Ctrl+K"
         onActivated: {
@@ -80,135 +72,6 @@ Page {
         }
     }
 
-    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("React")
-            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("Reply")
-            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 receipts")
-            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("Remove 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 event")
-            onTriggered: TimelineManager.timeline.copyLinkToEvent(messageContextMenu.eventId)
-        }
-
-    }
-
     Component {
         id: deviceVerificationDialog
 
@@ -233,16 +96,6 @@ Page {
         }
     }
 
-    Connections {
-        target: TimelineManager.timeline
-        onOpenRoomSettingsDialog: {
-            var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
-                "roomSettings": settings
-            });
-            roomSettings.show();
-        }
-    }
-
     Connections {
         target: CallManager
         onNewInviteState: {
diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml
index 3d2d8278..739cc007 100644
--- a/resources/qml/StatusIndicator.qml
+++ b/resources/qml/StatusIndicator.qml
@@ -31,7 +31,7 @@ ImageButton {
     }
     onClicked: {
         if (model.state == MtxEvent.Read)
-            TimelineManager.timeline.readReceiptsAction(model.id);
+            room.readReceiptsAction(model.id);
 
     }
     image: {
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 257d670d..747be61e 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -18,8 +18,10 @@ import im.nheko.EmojiModel 1.0
 Item {
     id: timelineView
 
+    property var room: null
+
     Label {
-        visible: !TimelineManager.timeline && !TimelineManager.isInitialSync
+        visible: !room && !TimelineManager.isInitialSync
         anchors.centerIn: parent
         text: qsTr("No room open")
         font.pointSize: 24
@@ -38,7 +40,7 @@ Item {
     ColumnLayout {
         id: timelineLayout
 
-        visible: TimelineManager.timeline != null
+        visible: room != null
         anchors.fill: parent
         spacing: 0
 
@@ -69,11 +71,11 @@ Item {
                     currentIndex: 0
 
                     Connections {
-                        function onActiveTimelineChanged() {
+                        function onRoomChanged() {
                             stackLayout.currentIndex = 0;
                         }
 
-                        target: TimelineManager
+                        target: timelineView
                     }
 
                     MessageView {
@@ -125,7 +127,17 @@ Item {
 
     NhekoDropArea {
         anchors.fill: parent
-        roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : ""
+        roomid: room ? room.roomId() : ""
+    }
+
+    Connections {
+        target: room
+        onOpenRoomSettingsDialog: {
+            var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
+                "roomSettings": settings
+            });
+            roomSettings.show();
+        }
     }
 
 }
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index bda5ce14..65e27939 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -11,8 +11,6 @@ import im.nheko 1.0
 Rectangle {
     id: topBar
 
-    property var room: TimelineManager.timeline
-
     Layout.fillWidth: true
     implicitHeight: topLayout.height + Nheko.paddingMedium * 2
     z: 3
@@ -20,7 +18,7 @@ Rectangle {
 
     TapHandler {
         onSingleTapped: {
-            TimelineManager.timeline.openRoomSettings();
+            room.openRoomSettings();
             eventPoint.accepted = true;
         }
         gesturePolicy: TapHandler.ReleaseWithinBounds
@@ -61,7 +59,7 @@ Rectangle {
             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 {
@@ -101,24 +99,24 @@ Rectangle {
                 id: roomOptionsMenu
 
                 Platform.MenuItem {
-                    visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canInvite() : false
+                    visible: room ? room.permissions.canInvite() : false
                     text: qsTr("Invite users")
                     onTriggered: TimelineManager.openInviteUsersDialog()
                 }
 
                 Platform.MenuItem {
                     text: qsTr("Members")
-                    onTriggered: TimelineManager.openMemberListDialog()
+                    onTriggered: TimelineManager.openMemberListDialog(room.roomId())
                 }
 
                 Platform.MenuItem {
                     text: qsTr("Leave room")
-                    onTriggered: TimelineManager.openLeaveRoomDialog()
+                    onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId())
                 }
 
                 Platform.MenuItem {
                     text: qsTr("Settings")
-                    onTriggered: TimelineManager.timeline.openRoomSettings()
+                    onTriggered: room.openRoomSettings()
                 }
 
             }
diff --git a/resources/qml/TypingIndicator.qml b/resources/qml/TypingIndicator.qml
index 783a9ebc..974d1840 100644
--- a/resources/qml/TypingIndicator.qml
+++ b/resources/qml/TypingIndicator.qml
@@ -8,8 +8,6 @@ import QtQuick.Layouts 1.2
 import im.nheko 1.0
 
 Item {
-    property var room: TimelineManager.timeline
-
     implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height)
     Layout.fillWidth: true
 
diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml
index 2e5f33c2..0392c73a 100644
--- a/resources/qml/delegates/FileMessage.qml
+++ b/resources/qml/delegates/FileMessage.qml
@@ -34,7 +34,7 @@ Item {
             }
 
             TapHandler {
-                onSingleTapped: TimelineManager.timeline.saveMedia(model.data.id)
+                onSingleTapped: room.saveMedia(model.data.id)
                 gesturePolicy: TapHandler.ReleaseWithinBounds
             }
 
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index 4e6a73fe..9e076a7a 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -207,7 +207,7 @@ Item {
             roleValue: MtxEvent.PowerLevels
 
             NoticeMessage {
-                text: TimelineManager.timeline.formatPowerLevelEvent(model.data.id)
+                text: room.formatPowerLevelEvent(model.data.id)
             }
 
         }
@@ -216,7 +216,7 @@ Item {
             roleValue: MtxEvent.RoomJoinRules
 
             NoticeMessage {
-                text: TimelineManager.timeline.formatJoinRuleEvent(model.data.id)
+                text: room.formatJoinRuleEvent(model.data.id)
             }
 
         }
@@ -225,7 +225,7 @@ Item {
             roleValue: MtxEvent.RoomHistoryVisibility
 
             NoticeMessage {
-                text: TimelineManager.timeline.formatHistoryVisibilityEvent(model.data.id)
+                text: room.formatHistoryVisibilityEvent(model.data.id)
             }
 
         }
@@ -234,7 +234,7 @@ Item {
             roleValue: MtxEvent.RoomGuestAccess
 
             NoticeMessage {
-                text: TimelineManager.timeline.formatGuestAccessEvent(model.data.id)
+                text: room.formatGuestAccessEvent(model.data.id)
             }
 
         }
@@ -243,7 +243,7 @@ Item {
             roleValue: MtxEvent.Member
 
             NoticeMessage {
-                text: TimelineManager.timeline.formatMemberEvent(model.data.id)
+                text: room.formatMemberEvent(model.data.id)
             }
 
         }
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index 0234495d..83864db9 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -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;
diff --git a/resources/qml/emoji/EmojiButton.qml b/resources/qml/emoji/EmojiButton.qml
index cec51d75..5f4d23d3 100644
--- a/resources/qml/emoji/EmojiButton.qml
+++ b/resources/qml/emoji/EmojiButton.qml
@@ -17,7 +17,7 @@ ImageButton {
 
     image: ":/icons/icons/ui/smile.png"
     onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) {
-        TimelineManager.queueReactionMessage(event_id, emoji);
+        room.input.reaction(event_id, emoji);
         TimelineManager.focusMessageInput();
     })
 }
diff --git a/resources/qml/voip/ActiveCallBar.qml b/resources/qml/voip/ActiveCallBar.qml
index 5798433a..3106c382 100644
--- a/resources/qml/voip/ActiveCallBar.qml
+++ b/resources/qml/voip/ActiveCallBar.qml
@@ -35,7 +35,7 @@ Rectangle {
             height: Nheko.avatarSize
             url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
             displayName: CallManager.callParty
-            onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id)
+            onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
         }
 
         Label {
diff --git a/resources/qml/voip/CallInviteBar.qml b/resources/qml/voip/CallInviteBar.qml
index a169aca9..2d8e3040 100644
--- a/resources/qml/voip/CallInviteBar.qml
+++ b/resources/qml/voip/CallInviteBar.qml
@@ -42,7 +42,7 @@ Rectangle {
             height: Nheko.avatarSize
             url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
             displayName: CallManager.callParty
-            onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id)
+            onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
         }
 
         Label {
diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml
index 7e2146cb..97e39e02 100644
--- a/resources/qml/voip/PlaceCall.qml
+++ b/resources/qml/voip/PlaceCall.qml
@@ -45,7 +45,7 @@ Popup {
             Layout.leftMargin: 8
 
             Label {
-                text: qsTr("Place a call to %1?").arg(TimelineManager.timeline.roomName)
+                text: qsTr("Place a call to %1?").arg(room.roomName)
                 color: Nheko.colors.windowText
             }
 
@@ -77,9 +77,9 @@ Popup {
                 Layout.rightMargin: cameraCombo.visible ? 16 : 64
                 width: Nheko.avatarSize
                 height: Nheko.avatarSize
-                url: TimelineManager.timeline.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
-                displayName: TimelineManager.timeline.roomName
-                onClicked: TimelineManager.openImageOverlay(TimelineManager.timeline.avatarUrl(userid), TimelineManager.timeline.data.id)
+                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();
                     }
                 }
diff --git a/resources/qml/voip/ScreenShare.qml b/resources/qml/voip/ScreenShare.qml
index 258ac9b0..a10057b2 100644
--- a/resources/qml/voip/ScreenShare.qml
+++ b/resources/qml/voip/ScreenShare.qml
@@ -27,7 +27,7 @@ Popup {
             Layout.leftMargin: 8
             Layout.rightMargin: 8
             Layout.alignment: Qt.AlignLeft
-            text: qsTr("Share desktop with %1?").arg(TimelineManager.timeline.roomName)
+            text: qsTr("Share desktop with %1?").arg(room.roomName)
             color: Nheko.colors.windowText
         }
 
@@ -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();
                     }
                 }
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 166c03ec..bee20d60 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -215,8 +215,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                 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);
@@ -982,7 +980,7 @@ ChatPage::leaveRoom(const QString &room_id)
 void
 ChatPage::changeRoom(const QString &room_id)
 {
-        view_manager_->setHistoryView(room_id);
+        view_manager_->rooms()->setCurrentRoom(room_id);
         room_list_->highlightSelectedRoom(room_id);
 }
 
@@ -1397,7 +1395,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);
                 }
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index cda38b75..a283d24e 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -508,8 +508,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 +714,35 @@ InputBar::stopTyping()
                 }
         });
 }
+
+void
+InputBar::reaction(const QString &reactedEvent, const QString &reactionKey)
+{
+        auto reactions = room->reactions(reactedEvent.toStdString());
+
+        QString selfReactedEvent;
+        for (const auto &reaction : reactions) {
+                if (reactionKey == reaction.key_) {
+                        selfReactedEvent = reaction.selfReactedEvent_;
+                        break;
+                }
+        }
+
+        if (selfReactedEvent.startsWith("m"))
+                return;
+
+        // If selfReactedEvent is empty, that means we haven't previously reacted
+        if (selfReactedEvent.isEmpty()) {
+                mtx::events::msg::Reaction reaction;
+                mtx::common::Relation rel;
+                rel.rel_type = mtx::common::RelationType::Annotation;
+                rel.event_id = reactedEvent.toStdString();
+                rel.key      = reactionKey.toStdString();
+                reaction.relations.relations.push_back(rel);
+
+                room->sendMessageEvent(reaction, mtx::events::EventType::Reaction);
+                // Otherwise, we have previously reacted and the reaction should be redacted
+        } else {
+                room->redactEvent(selfReactedEvent);
+        }
+}
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index 9db16bae..c9728379 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -56,6 +56,7 @@ public slots:
         void message(QString body,
                      MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED,
                      bool rainbowify              = false);
+        void reaction(const QString &reactedEvent, const QString &reactionKey);
 
 private slots:
         void startTyping();
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 63054aa9..ad4177a4 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -341,6 +341,8 @@ RoomlistModel::clear()
         models.clear();
         invites.clear();
         roomids.clear();
+        currentRoom_ = nullptr;
+        emit currentRoomChanged();
         endResetModel();
 }
 
@@ -390,6 +392,17 @@ RoomlistModel::leave(QString 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
 {
@@ -463,6 +476,11 @@ FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *pare
                                  invalidate();
                          });
 
+        connect(roomlistmodel,
+                &RoomlistModel::currentRoomChanged,
+                this,
+                &FilteredRoomlistModel::currentRoomChanged);
+
         sort(0);
 }
 
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index 2d1e5264..1c6fa833 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -14,12 +14,14 @@
 
 #include 
 
-class TimelineModel;
+#include "TimelineModel.h"
+
 class TimelineViewManager;
 
 class RoomlistModel : public QAbstractListModel
 {
         Q_OBJECT
+        Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged)
 public:
         enum Roles
         {
@@ -69,12 +71,15 @@ public slots:
         void acceptInvite(QString roomid);
         void declineInvite(QString roomid);
         void leave(QString roomid);
+        TimelineModel *currentRoom() const { return currentRoom_.get(); }
+        void setCurrentRoom(QString roomid);
 
 private slots:
         void updateReadStatus(const std::map roomReadStatus_);
 
 signals:
         void totalUnreadMessageCountUpdated(int unreadMessages);
+        void currentRoomChanged();
 
 private:
         void addRoom(const QString &room_id, bool suppressInsertNotification = false);
@@ -85,12 +90,15 @@ private:
         QHash> models;
         std::map roomReadStatus;
 
+        QSharedPointer currentRoom_;
+
         friend class FilteredRoomlistModel;
 };
 
 class FilteredRoomlistModel : public QSortFilterProxyModel
 {
         Q_OBJECT
+        Q_PROPERTY(TimelineModel *currentRoom READ currentRoom NOTIFY currentRoomChanged)
 public:
         FilteredRoomlistModel(RoomlistModel *model, QObject *parent = nullptr);
         bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
@@ -107,6 +115,12 @@ public slots:
         QStringList tags();
         void toggleTag(QString roomid, QString tag, bool on);
 
+        TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); }
+        void setCurrentRoom(QString roomid) { roomlistmodel->setCurrentRoom(std::move(roomid)); }
+
+signals:
+        void currentRoomChanged();
+
 private:
         short int calculateImportance(const QModelIndex &idx) const;
         RoomlistModel *roomlistmodel;
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 9fa7f8b6..3b3ea423 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -133,7 +133,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
   , colorImgProvider(new ColorImageProvider())
   , blurhashProvider(new BlurhashProvider())
   , callManager_(callManager)
-  , rooms(new RoomlistModel(this))
+  , rooms_(new RoomlistModel(this))
 {
         qRegisterMetaType();
         qRegisterMetaType();
@@ -193,7 +193,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
-                  return new FilteredRoomlistModel(self->rooms);
+                  return new FilteredRoomlistModel(self->rooms_);
           });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
@@ -320,9 +320,9 @@ TimelineViewManager::setVideoCallItem()
 }
 
 void
-TimelineViewManager::sync(const mtx::responses::Rooms &rooms_)
+TimelineViewManager::sync(const mtx::responses::Rooms &rooms_res)
 {
-        this->rooms->sync(rooms_);
+        this->rooms_->sync(rooms_res);
 
         if (isInitialSync_) {
                 this->isInitialSync_ = false;
@@ -330,37 +330,17 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms_)
         }
 }
 
-void
-TimelineViewManager::setHistoryView(const QString &room_id)
-{
-        nhlog::ui()->info("Trying to activate room {}", room_id.toStdString());
-
-        if (auto room = rooms->getRoomById(room_id)) {
-                timeline_ = room.get();
-                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)
 {
-        if (auto room = rooms->getRoomById(room_id)) {
-                if (timeline_ != room) {
-                        timeline_ = room.get();
-                        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);
         }
 }
 
@@ -395,17 +375,20 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img)
 
         auto imgDialog = new dialogs::ImageOverlay(pixmap);
         imgDialog->showFullScreen();
-        connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId, imgDialog]() {
-                // hide the overlay while presenting the save dialog for better
-                // cross platform support.
-                imgDialog->hide();
-
-                if (!timeline_->saveMedia(eventId)) {
-                        imgDialog->show();
-                } else {
-                        imgDialog->close();
-                }
-        });
+
+        auto room = rooms_->currentRoom();
+        connect(
+          imgDialog, &dialogs::ImageOverlay::saving, room, [this, eventId, imgDialog, room]() {
+                  // hide the overlay while presenting the save dialog for better
+                  // cross platform support.
+                  imgDialog->hide();
+
+                  if (!room->saveMedia(eventId)) {
+                          imgDialog->show();
+                  } else {
+                          imgDialog->close();
+                  }
+          });
 }
 
 void
@@ -415,14 +398,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
@@ -439,7 +422,7 @@ TimelineViewManager::verifyUser(QString userid)
                                       room_members.end(),
                                       (userid).toStdString()) != room_members.end()) {
                                 if (auto model =
-                                      rooms->getRoomById(QString::fromStdString(room_id))) {
+                                      rooms_->getRoomById(QString::fromStdString(room_id))) {
                                         auto flow =
                                           DeviceVerificationFlow::InitiateUserVerification(
                                             this, model.data(), userid);
@@ -485,7 +468,7 @@ void
 TimelineViewManager::updateReadReceipts(const QString &room_id,
                                         const std::vector &event_ids)
 {
-        if (auto room = rooms->getRoomById(room_id)) {
+        if (auto room = rooms_->getRoomById(room_id)) {
                 room->markEventsAsRead(event_ids);
         }
 }
@@ -493,7 +476,7 @@ TimelineViewManager::updateReadReceipts(const QString &room_id,
 void
 TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::string &session_id)
 {
-        if (auto room = rooms->getRoomById(QString::fromStdString(room_id))) {
+        if (auto room = rooms_->getRoomById(QString::fromStdString(room_id))) {
                 room->receivedSessionKey(session_id);
         }
 }
@@ -501,7 +484,7 @@ TimelineViewManager::receivedSessionKey(const std::string &room_id, const std::s
 void
 TimelineViewManager::initializeRoomlist()
 {
-        rooms->initializeRooms();
+        rooms_->initializeRooms();
 }
 
 void
@@ -509,51 +492,17 @@ TimelineViewManager::queueReply(const QString &roomid,
                                 const QString &repliedToEvent,
                                 const QString &replyBody)
 {
-        if (auto room = rooms->getRoomById(roomid)) {
+        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)
 {
-        if (auto room = rooms->getRoomById(roomid))
+        if (auto room = rooms_->getRoomById(roomid))
                 room->sendMessageEvent(callInvite, mtx::events::EventType::CallInvite);
 }
 
@@ -561,7 +510,7 @@ void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallCandidates &callCandidates)
 {
-        if (auto room = rooms->getRoomById(roomid))
+        if (auto room = rooms_->getRoomById(roomid))
                 room->sendMessageEvent(callCandidates, mtx::events::EventType::CallCandidates);
 }
 
@@ -569,7 +518,7 @@ void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallAnswer &callAnswer)
 {
-        if (auto room = rooms->getRoomById(roomid))
+        if (auto room = rooms_->getRoomById(roomid))
                 room->sendMessageEvent(callAnswer, mtx::events::EventType::CallAnswer);
 }
 
@@ -577,7 +526,7 @@ void
 TimelineViewManager::queueCallMessage(const QString &roomid,
                                       const mtx::events::msg::CallHangUp &callHangUp)
 {
-        if (auto room = rooms->getRoomById(roomid))
+        if (auto room = rooms_->getRoomById(roomid))
                 room->sendMessageEvent(callHangUp, mtx::events::EventType::CallHangUp);
 }
 
@@ -629,7 +578,7 @@ void
 TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEvents *e,
                                           QString roomId)
 {
-        auto room                                                = rooms->getRoomById(roomId);
+        auto room                                                = rooms_->getRoomById(roomId);
         auto content                                             = mtx::accessors::url(*e);
         std::optional encryptionInfo = mtx::accessors::file(*e);
 
@@ -672,7 +621,7 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven
                                                               ev.content.url = url;
                                                       }
 
-                                                      if (auto room = rooms->getRoomById(roomId)) {
+                                                      if (auto room = rooms_->getRoomById(roomId)) {
                                                               removeReplyFallback(ev);
                                                               ev.content.relations.relations
                                                                 .clear();
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 37e50804..c4707208 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -35,8 +35,6 @@ class TimelineViewManager : public QObject
 {
         Q_OBJECT
 
-        Q_PROPERTY(
-          TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged)
         Q_PROPERTY(
           bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged)
         Q_PROPERTY(
@@ -53,14 +51,8 @@ public:
         MxcImageProvider *imageProvider() { return imgProvider; }
         CallManager *callManager() { return callManager_; }
 
-        void clearAll()
-        {
-                timeline_ = nullptr;
-                emit activeTimelineChanged(nullptr);
-                rooms->clear();
-        }
+        void clearAll() { rooms_->clear(); }
 
-        Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
         Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
         bool isNarrowView() const { return isNarrowView_; }
         bool isWindowFocused() const { return isWindowFocused_; }
@@ -74,8 +66,8 @@ public:
 
         Q_INVOKABLE void focusMessageInput();
         Q_INVOKABLE void openInviteUsersDialog();
-        Q_INVOKABLE void openMemberListDialog() const;
-        Q_INVOKABLE void openLeaveRoomDialog() const;
+        Q_INVOKABLE void openMemberListDialog(QString roomid) const;
+        Q_INVOKABLE void openLeaveRoomDialog(QString roomid) const;
         Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
 
         void verifyUser(QString userid);
@@ -107,20 +99,13 @@ public slots:
                 emit focusChanged();
         }
 
-        void setHistoryView(const QString &room_id);
-        void highlightRoom(const QString &room_id);
         void showEvent(const QString &room_id, const QString &event_id);
         void focusTimeline();
-        TimelineModel *getHistoryView(const QString &room_id)
-        {
-                return rooms->getRoomById(room_id).get();
-        }
 
         void updateColorPalette();
         void queueReply(const QString &roomid,
                         const QString &repliedToEvent,
                         const QString &replyBody);
-        void queueReactionMessage(const QString &reactedEvent, const QString &reactionKey);
         void queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
         void queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
         void queueCallMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
@@ -147,6 +132,8 @@ public slots:
         QObject *completerFor(QString completerName, QString roomId = "");
         void forwardMessageToRoom(mtx::events::collections::TimelineEvents *e, QString roomId);
 
+        RoomlistModel *rooms() { return rooms_; }
+
 private slots:
         void openImageOverlayInternal(QString eventId, QImage img);
 
@@ -162,14 +149,13 @@ private:
         ColorImageProvider *colorImgProvider;
         BlurhashProvider *blurhashProvider;
 
-        TimelineModel *timeline_  = nullptr;
         CallManager *callManager_ = nullptr;
 
         bool isInitialSync_   = true;
         bool isNarrowView_    = false;
         bool isWindowFocused_ = false;
 
-        RoomlistModel *rooms = nullptr;
+        RoomlistModel *rooms_ = nullptr;
 
         QHash userColors;
 
diff --git a/src/ui/NhekoDropArea.cpp b/src/ui/NhekoDropArea.cpp
index 54f48d3c..bbcedd7e 100644
--- a/src/ui/NhekoDropArea.cpp
+++ b/src/ui/NhekoDropArea.cpp
@@ -35,7 +35,7 @@ void
 NhekoDropArea::dropEvent(QDropEvent *event)
 {
         if (event) {
-                auto model = ChatPage::instance()->timelineManager()->getHistoryView(roomid_);
+                auto model = ChatPage::instance()->timelineManager()->rooms()->getRoomById(roomid_);
                 if (model) {
                         model->input()->insertMimeData(event->mimeData());
                 }
-- 
cgit 1.5.1


From 18ff58edb3bc186e2114efad34de7ffca803be02 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sun, 30 May 2021 00:23:57 +0200
Subject: Fix use after free from Qml widget

---
 src/ChatPage.cpp                     | 8 ++++++++
 src/timeline/RoomlistModel.cpp       | 3 ++-
 src/timeline/TimelineViewManager.cpp | 5 +++--
 3 files changed, 13 insertions(+), 3 deletions(-)

(limited to 'src/timeline/TimelineViewManager.cpp')

diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 4ad7bd14..0f16f205 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -171,6 +171,14 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                         activateWindow();
                 });
 
+        connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
+                // ensure the qml context is shutdown before we destroy all other singletons
+                // Otherwise Qml will try to access the room list or settings, after they have been
+                // destroyed
+                topLayout_->removeWidget(view_manager_->getWidget());
+                delete view_manager_->getWidget();
+        });
+
         connect(
           this,
           &ChatPage::initializeViews,
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index d2ba0dc3..283224f1 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -13,7 +13,8 @@
 #include "UserSettingsPage.h"
 
 RoomlistModel::RoomlistModel(TimelineViewManager *parent)
-  : manager(parent)
+  : QAbstractListModel(parent)
+  , manager(parent)
 {
         connect(ChatPage::instance(), &ChatPage::decryptSidebarChanged, this, [this]() {
                 auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar();
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 3b3ea423..dd623f2f 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -129,7 +129,8 @@ 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)
@@ -230,7 +231,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);
-- 
cgit 1.5.1


From 2cd1a931c28d0fd8e8755e9622a7d8f56d1a24a0 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Wed, 9 Jun 2021 23:52:28 +0200
Subject: Basic community list model

---
 CMakeLists.txt                       |   2 +
 resources/qml/RoomList.qml           |   6 +-
 src/timeline/CommunitiesModel.cpp    | 158 +++++++++++++++++++++++++++++++++++
 src/timeline/CommunitiesModel.h      |  60 +++++++++++++
 src/timeline/RoomlistModel.cpp       |  23 -----
 src/timeline/RoomlistModel.h         |   1 -
 src/timeline/TimelineViewManager.cpp |   9 ++
 src/timeline/TimelineViewManager.h   |   4 +-
 8 files changed, 234 insertions(+), 29 deletions(-)
 create mode 100644 src/timeline/CommunitiesModel.cpp
 create mode 100644 src/timeline/CommunitiesModel.h

(limited to 'src/timeline/TimelineViewManager.cpp')

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5a5e3ba1..3d9d793c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -265,6 +265,7 @@ set(SRC_FILES
 
 
 	# Timeline
+	src/timeline/CommunitiesModel.cpp
 	src/timeline/EventStore.cpp
 	src/timeline/InputBar.cpp
 	src/timeline/Reaction.cpp
@@ -481,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
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 21973b77..a6637467 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -43,12 +43,10 @@ Page {
 
             property string roomid
             property var tags
-            property var allTags
 
             function show(roomid_, tags_) {
                 roomid = roomid_;
                 tags = tags_;
-                allTags = Rooms.tags();
                 open();
             }
 
@@ -72,7 +70,7 @@ Page {
             }
 
             Instantiator {
-                model: roomContextMenu.allTags
+                model: Communities.tags
                 onObjectAdded: roomContextMenu.insertItem(index + 2, object)
                 onObjectRemoved: roomContextMenu.removeItem(object)
 
@@ -92,7 +90,7 @@ Page {
                         }
                     }
                     checkable: true
-                    checked: roomContextMenu.tags.includes(t)
+                    checked: roomContextMenu.tags !== undefined && roomContextMenu.tags.includes(t)
                     onTriggered: Rooms.toggleTag(roomContextMenu.roomid, t, checked)
                 }
 
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
new file mode 100644
index 00000000..cedaacce
--- /dev/null
+++ b/src/timeline/CommunitiesModel.cpp
@@ -0,0 +1,158 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "CommunitiesModel.h"
+
+#include 
+
+#include "Cache.h"
+#include "UserSettingsPage.h"
+
+CommunitiesModel::CommunitiesModel(QObject *parent)
+  : QAbstractListModel(parent)
+{}
+
+QHash
+CommunitiesModel::roleNames() const
+{
+        return {
+          {AvatarUrl, "avatarUrl"},
+          {DisplayName, "displayName"},
+          {Tooltip, "tooltip"},
+          {ChildrenHidden, "childrenHidden"},
+        };
+}
+
+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::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.right(2);
+                        case CommunitiesModel::Roles::Tooltip:
+                                return tag.right(2);
+                        }
+                }
+
+                switch (role) {
+                case CommunitiesModel::Roles::ChildrenHidden:
+                        return UserSettings::instance()->hiddenTags().contains("tag:" + tag);
+                case CommunitiesModel::Roles::Id:
+                        return "tag:" + tag;
+                }
+        }
+        return QVariant();
+}
+
+void
+CommunitiesModel::initializeSidebar()
+{
+        std::set 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));
+        endResetModel();
+
+        emit tagsChanged();
+}
+
+void
+CommunitiesModel::clear()
+{
+        beginResetModel();
+        tags_.clear();
+        endResetModel();
+
+        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>(e)) {
+                                tagsUpdated = true;
+                        }
+        }
+
+        if (tagsUpdated)
+                initializeSidebar();
+}
+
+void
+CommunitiesModel::setCurrentTagId(QString tagId)
+{
+        if (tagId.startsWith("tag:")) {
+                auto tag = tagId.remove(0, 4);
+                for (const auto &t : tags_) {
+                        if (t == tag) {
+                                this->currentTagId_ = tagId;
+                                emit currentTagIdChanged();
+                                return;
+                        }
+                }
+        }
+
+        this->currentTagId_ = "";
+        emit currentTagIdChanged();
+}
diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h
new file mode 100644
index 00000000..3f6a2a4c
--- /dev/null
+++ b/src/timeline/CommunitiesModel.h
@@ -0,0 +1,60 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+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,
+                Id,
+        };
+
+        CommunitiesModel(QObject *parent = nullptr);
+        QHash 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();
+        }
+        QStringList tags() const { return tags_; }
+
+signals:
+        void currentTagIdChanged();
+        void tagsChanged();
+
+private:
+        QStringList tags_;
+        QString currentTagId_;
+};
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 283224f1..4dd44b30 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -485,29 +485,6 @@ FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *pare
         sort(0);
 }
 
-QStringList
-FilteredRoomlistModel::tags()
-{
-        std::set ts;
-        for (const auto &e : cache::roomInfo()) {
-                for (const auto &t : e.tags) {
-                        if (t.find("u.") == 0) {
-                                ts.insert(t);
-                        }
-                }
-        }
-
-        QStringList ret{{
-          "m.favourite",
-          "m.lowpriority",
-        }};
-
-        for (const auto &t : ts)
-                ret.push_back(QString::fromStdString(t));
-
-        return ret;
-}
-
 void
 FilteredRoomlistModel::toggleTag(QString roomid, QString tag, bool on)
 {
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index fa991f6b..7ee0419f 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -119,7 +119,6 @@ public slots:
         void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); }
         void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); }
         void leave(QString roomid) { roomlistmodel->leave(roomid); }
-        QStringList tags();
         void toggleTag(QString roomid, QString tag, bool on);
 
         TimelineModel *currentRoom() const { return roomlistmodel->currentRoom(); }
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index dd623f2f..faf56b85 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -135,6 +135,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
   , blurhashProvider(new BlurhashProvider())
   , callManager_(callManager)
   , rooms_(new RoomlistModel(this))
+  , communities_(new CommunitiesModel(this))
 {
         qRegisterMetaType();
         qRegisterMetaType();
@@ -196,6 +197,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
                   return new FilteredRoomlistModel(self->rooms_);
           });
+        qmlRegisterSingletonType(
+          "im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * {
+                  auto ptr = self->communities_;
+                  QQmlEngine::setObjectOwnership(ptr, QQmlEngine::CppOwnership);
+                  return ptr;
+          });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
                   auto ptr = ChatPage::instance()->userSettings().data();
@@ -324,6 +331,7 @@ void
 TimelineViewManager::sync(const mtx::responses::Rooms &rooms_res)
 {
         this->rooms_->sync(rooms_res);
+        this->communities_->sync(rooms_res);
 
         if (isInitialSync_) {
                 this->isInitialSync_ = false;
@@ -486,6 +494,7 @@ void
 TimelineViewManager::initializeRoomlist()
 {
         rooms_->initializeRooms();
+        communities_->initializeSidebar();
 }
 
 void
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 68d9cd1b..556bcf4c 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -22,6 +22,7 @@
 #include "WebRTCSession.h"
 #include "emoji/EmojiModel.h"
 #include "emoji/Provider.h"
+#include "timeline/CommunitiesModel.h"
 #include "timeline/RoomlistModel.h"
 
 class MxcImageProvider;
@@ -131,7 +132,8 @@ private:
         bool isInitialSync_   = true;
         bool isWindowFocused_ = false;
 
-        RoomlistModel *rooms_ = nullptr;
+        RoomlistModel *rooms_          = nullptr;
+        CommunitiesModel *communities_ = nullptr;
 
         QHash userColors;
 
-- 
cgit 1.5.1


From 8d2d8dc26727a5b46613d83522490f568aef7cad Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 11 Jun 2021 14:51:29 +0200
Subject: Enable toggling tags

---
 resources/qml/Avatar.qml             |  1 +
 resources/qml/ChatPage.qml           |  5 +++--
 resources/qml/CommunitiesList.qml    | 25 +++++++++++++++++--------
 src/timeline/CommunitiesModel.cpp    |  5 +++--
 src/timeline/CommunitiesModel.h      |  4 ++--
 src/timeline/RoomlistModel.cpp       | 17 +++++++++++++++++
 src/timeline/RoomlistModel.h         | 23 +++++++++++++++++++++++
 src/timeline/TimelineViewManager.cpp |  8 +++++++-
 8 files changed, 73 insertions(+), 15 deletions(-)

(limited to 'src/timeline/TimelineViewManager.cpp')

diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml
index 9eb3380e..6c12952a 100644
--- a/resources/qml/Avatar.qml
+++ b/resources/qml/Avatar.qml
@@ -28,6 +28,7 @@ Rectangle {
 
     Label {
         id: label
+
         anchors.fill: parent
         text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "")
         textFormat: Text.RichText
diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml
index 5ccdd9f1..6cd48788 100644
--- a/resources/qml/ChatPage.qml
+++ b/resources/qml/ChatPage.qml
@@ -24,12 +24,13 @@ Rectangle {
             id: communityListC
 
             minimumWidth: communitiesList.avatarSize * 4 + Nheko.paddingMedium * 2
-            collapsedWidth: communitiesList.avatarSize + 2* Nheko.paddingMedium
+            collapsedWidth: communitiesList.avatarSize + 2 * Nheko.paddingMedium
             preferredWidth: collapsedWidth
-            maximumWidth: communitiesList.avatarSize * 10 + 2* Nheko.paddingMedium
+            maximumWidth: communitiesList.avatarSize * 10 + 2 * Nheko.paddingMedium
 
             CommunitiesList {
                 id: communitiesList
+
                 collapsed: parent.collapsed
             }
 
diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml
index 6ca619c4..0ccd7e82 100644
--- a/resources/qml/CommunitiesList.qml
+++ b/resources/qml/CommunitiesList.qml
@@ -10,7 +10,6 @@ import QtQuick.Controls 2.13
 import QtQuick.Layouts 1.3
 import im.nheko 1.0
 
-
 Page {
     //leftPadding: Nheko.paddingSmall
     //rightPadding: Nheko.paddingSmall
@@ -97,8 +96,7 @@ Page {
             TapHandler {
                 margin: -Nheko.paddingSmall
                 acceptedButtons: Qt.RightButton
-                onSingleTapped: communityContextMenu.show(model.id);
-
+                onSingleTapped: communityContextMenu.show(model.id)
                 gesturePolicy: TapHandler.ReleaseWithinBounds
             }
 
@@ -127,15 +125,26 @@ Page {
                     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
-                        }
+                        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
                 }
 
             }
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
index c8ebaa96..9b758e97 100644
--- a/src/timeline/CommunitiesModel.cpp
+++ b/src/timeline/CommunitiesModel.cpp
@@ -118,6 +118,7 @@ CommunitiesModel::clear()
         beginResetModel();
         tags_.clear();
         endResetModel();
+        resetCurrentTagId();
 
         emit tagsChanged();
 }
@@ -148,12 +149,12 @@ CommunitiesModel::setCurrentTagId(QString tagId)
                 for (const auto &t : tags_) {
                         if (t == tag) {
                                 this->currentTagId_ = tagId;
-                                emit currentTagIdChanged();
+                                emit currentTagIdChanged(currentTagId_);
                                 return;
                         }
                 }
         }
 
         this->currentTagId_ = "";
-        emit currentTagIdChanged();
+        emit currentTagIdChanged(currentTagId_);
 }
diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h
index 3f6a2a4c..038c253b 100644
--- a/src/timeline/CommunitiesModel.h
+++ b/src/timeline/CommunitiesModel.h
@@ -46,12 +46,12 @@ public slots:
         void resetCurrentTagId()
         {
                 currentTagId_.clear();
-                emit currentTagIdChanged();
+                emit currentTagIdChanged(currentTagId_);
         }
         QStringList tags() const { return tags_; }
 
 signals:
-        void currentTagIdChanged();
+        void currentTagIdChanged(QString tagId);
         void tagsChanged();
 
 private:
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 4dd44b30..c0fb74a4 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -324,6 +324,7 @@ RoomlistModel::initializeRooms()
         models.clear();
         roomids.clear();
         invites.clear();
+        currentRoom_ = nullptr;
 
         invites = cache::client()->invites();
         for (const auto &id : invites.keys())
@@ -461,6 +462,22 @@ FilteredRoomlistModel::lessThan(const QModelIndex &left, const QModelIndex &righ
                 return left.row() < right.row();
 }
 
+bool
+FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const
+{
+        if (filterType == FilterBy::Nothing)
+                return true;
+        else if (filterType == FilterBy::Tag) {
+                auto tags = sourceModel()
+                              ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
+                              .toStringList();
+
+                return tags.contains(filterStr);
+        } else {
+                return true;
+        }
+}
+
 FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *parent)
   : QSortFilterProxyModel(parent)
   , roomlistmodel(model)
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index 7ee0419f..b89c9a54 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -109,6 +109,7 @@ class FilteredRoomlistModel : public QSortFilterProxyModel
 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)
@@ -128,6 +129,19 @@ public slots:
         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();
+        }
+
 signals:
         void currentRoomChanged();
 
@@ -135,4 +149,13 @@ 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;
 };
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index faf56b85..2ee79d4f 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -195,7 +195,13 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Rooms", [](QQmlEngine *, QJSEngine *) -> QObject * {
-                  return new FilteredRoomlistModel(self->rooms_);
+                  auto ptr = new FilteredRoomlistModel(self->rooms_);
+
+                  connect(self->communities_,
+                          &CommunitiesModel::currentTagIdChanged,
+                          ptr,
+                          &FilteredRoomlistModel::updateFilterTag);
+                  return ptr;
           });
         qmlRegisterSingletonType(
           "im.nheko", 1, 0, "Communities", [](QQmlEngine *, QJSEngine *) -> QObject * {
-- 
cgit 1.5.1


From a5291605a9912a411100edf8ee88e59857d8b9aa Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 11 Jun 2021 17:54:05 +0200
Subject: Reenable tag hiding

---
 resources/qml/CommunitiesList.qml    | 10 +++---
 src/timeline/CommunitiesModel.cpp    | 30 ++++++++++++++++-
 src/timeline/CommunitiesModel.h      |  4 +++
 src/timeline/RoomlistModel.cpp       | 65 +++++++++++++++++++++++++++---------
 src/timeline/RoomlistModel.h         |  3 ++
 src/timeline/TimelineViewManager.cpp |  4 +++
 6 files changed, 94 insertions(+), 22 deletions(-)

(limited to 'src/timeline/TimelineViewManager.cpp')

diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml
index 0ccd7e82..6aab949c 100644
--- a/resources/qml/CommunitiesList.qml
+++ b/resources/qml/CommunitiesList.qml
@@ -33,16 +33,16 @@ Page {
         Platform.Menu {
             id: communityContextMenu
 
-            property string id
+            property string tagId
 
             function show(id_, tags_) {
-                id = id_;
+                tagId = id_;
                 open();
             }
 
             Platform.MenuItem {
-                text: qsTr("Leave room")
-                onTriggered: Rooms.leave(roomContextMenu.roomid)
+                text: qsTr("Hide rooms with this tag or from this space by default.")
+                onTriggered: Communities.toggleTagId(communityContextMenu.tagId)
             }
 
         }
@@ -65,7 +65,7 @@ Page {
             states: [
                 State {
                     name: "highlight"
-                    when: hovered.hovered && !(Communities.currentTagId == model.id)
+                    when: (hovered.hovered || model.hidden) && !(Communities.currentTagId == model.id)
 
                     PropertyChanges {
                         target: communityItem
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
index 9b758e97..96a450ea 100644
--- a/src/timeline/CommunitiesModel.cpp
+++ b/src/timeline/CommunitiesModel.cpp
@@ -21,6 +21,7 @@ CommunitiesModel::roleNames() const
           {DisplayName, "displayName"},
           {Tooltip, "tooltip"},
           {ChildrenHidden, "childrenHidden"},
+          {Hidden, "hidden"},
           {Id, "id"},
         };
 }
@@ -38,6 +39,8 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
                         return tr("Shows all rooms without filtering.");
                 case CommunitiesModel::Roles::ChildrenHidden:
                         return false;
+                case CommunitiesModel::Roles::Hidden:
+                        return false;
                 case CommunitiesModel::Roles::Id:
                         return "";
                 }
@@ -82,8 +85,10 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
                 }
 
                 switch (role) {
+                case CommunitiesModel::Roles::Hidden:
+                        return hiddentTagIds_.contains("tag:" + tag);
                 case CommunitiesModel::Roles::ChildrenHidden:
-                        return UserSettings::instance()->hiddenTags().contains("tag:" + tag);
+                        return true;
                 case CommunitiesModel::Roles::Id:
                         return "tag:" + tag;
                 }
@@ -107,9 +112,12 @@ CommunitiesModel::initializeSidebar()
         tags_.clear();
         for (const auto &t : ts)
                 tags_.push_back(QString::fromStdString(t));
+
+        hiddentTagIds_ = UserSettings::instance()->hiddenTags();
         endResetModel();
 
         emit tagsChanged();
+        emit hiddenTagsChanged();
 }
 
 void
@@ -158,3 +166,23 @@ CommunitiesModel::setCurrentTagId(QString tagId)
         this->currentTagId_ = "";
         emit currentTagIdChanged(currentTagId_);
 }
+
+void
+CommunitiesModel::toggleTagId(QString tagId)
+{
+        if (hiddentTagIds_.contains(tagId)) {
+                hiddentTagIds_.removeOne(tagId);
+                UserSettings::instance()->setHiddenTags(hiddentTagIds_);
+        } else {
+                hiddentTagIds_.push_back(tagId);
+                UserSettings::instance()->setHiddenTags(hiddentTagIds_);
+        }
+
+        if (tagId.startsWith("tag:")) {
+                auto idx = tags_.indexOf(tagId.mid(4));
+                if (idx != -1)
+                        emit dataChanged(index(idx), index(idx), {Hidden});
+        }
+
+        emit hiddenTagsChanged();
+}
diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h
index 038c253b..c98b5955 100644
--- a/src/timeline/CommunitiesModel.h
+++ b/src/timeline/CommunitiesModel.h
@@ -25,6 +25,7 @@ public:
                 DisplayName,
                 Tooltip,
                 ChildrenHidden,
+                Hidden,
                 Id,
         };
 
@@ -49,12 +50,15 @@ public slots:
                 emit currentTagIdChanged(currentTagId_);
         }
         QStringList tags() const { return tags_; }
+        void toggleTagId(QString tagId);
 
 signals:
         void currentTagIdChanged(QString tagId);
+        void hiddenTagsChanged();
         void tagsChanged();
 
 private:
         QStringList tags_;
         QString currentTagId_;
+        QStringList hiddentTagIds_;
 };
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index c0fb74a4..0f980c6c 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -462,22 +462,6 @@ FilteredRoomlistModel::lessThan(const QModelIndex &left, const QModelIndex &righ
                 return left.row() < right.row();
 }
 
-bool
-FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const
-{
-        if (filterType == FilterBy::Nothing)
-                return true;
-        else if (filterType == FilterBy::Tag) {
-                auto tags = sourceModel()
-                              ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
-                              .toStringList();
-
-                return tags.contains(filterStr);
-        } else {
-                return true;
-        }
-}
-
 FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *parent)
   : QSortFilterProxyModel(parent)
   , roomlistmodel(model)
@@ -502,6 +486,55 @@ FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *pare
         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)
 {
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index b89c9a54..b0244886 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -142,6 +142,8 @@ public slots:
                 invalidateFilter();
         }
 
+        void updateHiddenTagsAndSpaces();
+
 signals:
         void currentRoomChanged();
 
@@ -158,4 +160,5 @@ private:
         };
         QString filterStr   = "";
         FilterBy filterType = FilterBy::Nothing;
+        QStringList hiddenTags, hiddenSpaces;
 };
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 2ee79d4f..c109d38e 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -201,6 +201,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
                           &CommunitiesModel::currentTagIdChanged,
                           ptr,
                           &FilteredRoomlistModel::updateFilterTag);
+                  connect(self->communities_,
+                          &CommunitiesModel::hiddenTagsChanged,
+                          ptr,
+                          &FilteredRoomlistModel::updateHiddenTagsAndSpaces);
                   return ptr;
           });
         qmlRegisterSingletonType(
-- 
cgit 1.5.1


From 1d80f5d0b4f353d135a5d7b348416db503197a16 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 11 Jun 2021 21:25:06 +0200
Subject: Remove useless capture

---
 src/timeline/TimelineViewManager.cpp | 23 +++++++++++------------
 1 file changed, 11 insertions(+), 12 deletions(-)

(limited to 'src/timeline/TimelineViewManager.cpp')

diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index c109d38e..a6947f99 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -396,18 +396,17 @@ TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img)
         imgDialog->showFullScreen();
 
         auto room = rooms_->currentRoom();
-        connect(
-          imgDialog, &dialogs::ImageOverlay::saving, room, [this, eventId, imgDialog, room]() {
-                  // hide the overlay while presenting the save dialog for better
-                  // cross platform support.
-                  imgDialog->hide();
-
-                  if (!room->saveMedia(eventId)) {
-                          imgDialog->show();
-                  } else {
-                          imgDialog->close();
-                  }
-          });
+        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 (!room->saveMedia(eventId)) {
+                        imgDialog->show();
+                } else {
+                        imgDialog->close();
+                }
+        });
 }
 
 void
-- 
cgit 1.5.1