diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index e9bb351f..b184aef0 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -2,6 +2,7 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
+import "./dialogs"
import Qt.labs.platform 1.1 as Platform
import QtQuick 2.13
import QtQuick.Controls 2.13
@@ -31,6 +32,93 @@ Page {
target: TimelineManager
}
+ Platform.Menu {
+ id: roomContextMenu
+
+ property string roomid
+ property var tags
+
+ function show(roomid_, tags_) {
+ roomid = roomid_;
+ tags = tags_;
+ roomContextMenu.clear();
+ roomContextMenu.addItem(leaveOpt.createObject(roomContextMenu));
+ roomContextMenu.addItem(separatorOpt.createObject(roomContextMenu));
+ for (let tag of Rooms.tags()) {
+ roomContextMenu.addItem(tagDelegate.createObject(roomContextMenu, {
+ "t": tag
+ }));
+ }
+ roomContextMenu.addItem(newTagOpt.createObject(roomContextMenu));
+ open();
+ }
+
+ InputDialog {
+ id: newTag
+
+ title: qsTr("New tag")
+ prompt: qsTr("Enter the tag you want to use:")
+ onAccepted: function(text) {
+ Rooms.toggleTag(roomContextMenu.roomid, "u." + text, true);
+ }
+ }
+
+ Component {
+ id: leaveOpt
+
+ Platform.MenuItem {
+ text: qsTr("Leave room")
+ onTriggered: Rooms.leave(roomContextMenu.roomid)
+ }
+
+ }
+
+ Component {
+ id: separatorOpt
+
+ Platform.MenuSeparator {
+ text: qsTr("Tag room as:")
+ }
+
+ }
+
+ Component {
+ id: tagDelegate
+
+ Platform.MenuItem {
+ property string t
+
+ text: {
+ switch (t) {
+ case "m.favourite":
+ return qsTr("Favourite");
+ case "m.lowpriority":
+ return qsTr("Low priority");
+ case "m.server_notice":
+ return qsTr("Server notice");
+ default:
+ return t.substring(2);
+ }
+ }
+ checkable: true
+ checked: roomContextMenu.tags.includes(t)
+ onTriggered: Rooms.toggleTag(roomContextMenu.roomid, t, checked)
+ }
+
+ }
+
+ Component {
+ id: newTagOpt
+
+ Platform.MenuItem {
+ text: qsTr("Create new tag...")
+ onTriggered: newTag.show()
+ }
+
+ }
+
+ }
+
delegate: Rectangle {
id: roomItem
@@ -75,6 +163,12 @@ Page {
}
]
+ TapHandler {
+ acceptedButtons: Qt.RightButton
+ onSingleTapped: roomContextMenu.show(model.roomId, model.tags)
+ gesturePolicy: TapHandler.ReleaseWithinBounds
+ }
+
HoverHandler {
id: hovered
}
@@ -94,6 +188,7 @@ Page {
id: avatar
+ enabled: false
Layout.alignment: Qt.AlignVCenter
height: Math.ceil(fontMetrics.lineSpacing * 2.3)
width: Math.ceil(fontMetrics.lineSpacing * 2.3)
@@ -279,43 +374,14 @@ Page {
Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium
Layout.minimumHeight: 40
- ApplicationWindow {
+ InputDialog {
id: statusDialog
- modality: Qt.NonModal
- flags: Qt.Dialog
title: qsTr("Status Message")
- width: 350
- height: fontMetrics.lineSpacing * 7
-
- ColumnLayout {
- anchors.margins: Nheko.paddingLarge
- anchors.fill: parent
-
- Label {
- color: Nheko.colors.text
- text: qsTr("Enter your status message:")
- }
-
- MatrixTextField {
- id: statusInput
-
- Layout.fillWidth: true
- }
-
- }
-
- footer: DialogButtonBox {
- standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel
- onAccepted: {
- Nheko.setStatusMessage(statusInput.text);
- statusDialog.close();
- }
- onRejected: {
- statusDialog.close();
- }
+ prompt: qsTr("Enter your status message:")
+ onAccepted: function(text) {
+ Nheko.setStatusMessage(text);
}
-
}
Platform.Menu {
diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml
index ae622480..f65eda79 100644
--- a/resources/qml/delegates/TextMessage.qml
+++ b/resources/qml/delegates/TextMessage.qml
@@ -9,7 +9,7 @@ MatrixText {
property string formatted: model.data.formattedBody
property string copyText: selectedText ? getText(selectionStart, selectionEnd) : model.data.body
- text: "<style type=\"text/css\">a { color:" + Nheko.colors.link + ";}\ncode { background-color: " + Nheko.colors.alternateBase + ";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap; background-color: " + Nheko.colors.alternateBase + "'>")
+ text: "<style type=\"text/css\">a { color:" + Nheko.colors.link + ";}\ncode { background-color: " + Nheko.colors.alternateBase + ";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap; background-color: " + Nheko.colors.alternateBase + "'>").replace("<del>", "<s>").replace("</del>", "</s>").replace("<strike>", "<s>").replace("</strike>", "</s>")
width: parent ? parent.width : undefined
height: isReply ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : undefined
clip: isReply
diff --git a/resources/qml/dialogs/InputDialog.qml b/resources/qml/dialogs/InputDialog.qml
new file mode 100644
index 00000000..0cd6be1c
--- /dev/null
+++ b/resources/qml/dialogs/InputDialog.qml
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import ".."
+import QtQuick 2.13
+import QtQuick.Controls 2.13
+import QtQuick.Layouts 1.3
+import im.nheko 1.0
+
+ApplicationWindow {
+ id: inputDialog
+
+ property alias prompt: promptLabel.text
+ property var onAccepted: undefined
+
+ modality: Qt.NonModal
+ flags: Qt.Dialog
+ width: 350
+ height: fontMetrics.lineSpacing * 7
+
+ ColumnLayout {
+ anchors.margins: Nheko.paddingLarge
+ anchors.fill: parent
+
+ Label {
+ id: promptLabel
+
+ color: Nheko.colors.text
+ }
+
+ MatrixTextField {
+ id: statusInput
+
+ Layout.fillWidth: true
+ }
+
+ }
+
+ footer: DialogButtonBox {
+ standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel
+ onAccepted: {
+ if (inputDialog.onAccepted)
+ inputDialog.onAccepted(statusInput.text);
+
+ inputDialog.close();
+ }
+ onRejected: {
+ inputDialog.close();
+ }
+ }
+
+}
diff --git a/resources/res.qrc b/resources/res.qrc
index 79e63810..183cf394 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -168,6 +168,7 @@
<file>qml/device-verification/NewVerificationRequest.qml</file>
<file>qml/device-verification/Failed.qml</file>
<file>qml/device-verification/Success.qml</file>
+ <file>qml/dialogs/InputDialog.qml</file>
<file>qml/ui/Ripple.qml</file>
<file>qml/voip/ActiveCallBar.qml</file>
<file>qml/voip/CallDevices.qml</file>
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index f3d4dad7..63054aa9 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -49,6 +49,7 @@ RoomlistModel::roleNames() const
{NotificationCount, "notificationCount"},
{IsInvite, "isInvite"},
{IsSpace, "isSpace"},
+ {Tags, "tags"},
};
}
@@ -84,6 +85,13 @@ RoomlistModel::data(const QModelIndex &index, int role) const
case Roles::IsInvite:
case Roles::IsSpace:
return false;
+ case Roles::Tags: {
+ auto info = cache::singleRoomInfo(roomid.toStdString());
+ QStringList list;
+ for (const auto &t : info.tags)
+ list.push_back(QString::fromStdString(t));
+ return list;
+ }
default:
return {};
}
@@ -111,6 +119,8 @@ RoomlistModel::data(const QModelIndex &index, int role) const
return true;
case Roles::IsSpace:
return false;
+ case Roles::Tags:
+ return QStringList();
default:
return {};
}
@@ -364,6 +374,21 @@ RoomlistModel::declineInvite(QString roomid)
}
}
}
+void
+RoomlistModel::leave(QString roomid)
+{
+ if (models.contains(roomid)) {
+ auto idx = roomidToIndex(roomid);
+
+ if (idx != -1) {
+ beginRemoveRows(QModelIndex(), idx, idx);
+ roomids.erase(roomids.begin() + idx);
+ models.remove(roomid);
+ endRemoveRows();
+ ChatPage::instance()->leaveRoom(roomid);
+ }
+ }
+}
namespace {
enum NotificationImportance : short
@@ -440,3 +465,50 @@ FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *pare
sort(0);
}
+
+QStringList
+FilteredRoomlistModel::tags()
+{
+ std::set<std::string> 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)
+{
+ if (on) {
+ http::client()->put_tag(
+ roomid.toStdString(), tag.toStdString(), {}, [tag](mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::ui()->error("Failed to add tag: {}, {}",
+ tag.toStdString(),
+ err->matrix_error.error);
+ }
+ });
+ } else {
+ http::client()->delete_tag(
+ roomid.toStdString(), tag.toStdString(), [tag](mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::ui()->error("Failed to delete tag: {}, {}",
+ tag.toStdString(),
+ err->matrix_error.error);
+ }
+ });
+ }
+}
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index ff85614c..2d1e5264 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -10,6 +10,7 @@
#include <QSharedPointer>
#include <QSortFilterProxyModel>
#include <QString>
+#include <set>
#include <mtx/responses/sync.hpp>
@@ -33,6 +34,7 @@ public:
NotificationCount,
IsInvite,
IsSpace,
+ Tags,
};
RoomlistModel(TimelineViewManager *parent = nullptr);
@@ -66,6 +68,7 @@ public slots:
}
void acceptInvite(QString roomid);
void declineInvite(QString roomid);
+ void leave(QString roomid);
private slots:
void updateReadStatus(const std::map<QString, bool> roomReadStatus_);
@@ -100,6 +103,9 @@ 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);
private:
short int calculateImportance(const QModelIndex &idx) const;
|