diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml
index 61287789..ec9ef940 100644
--- a/resources/qml/CommunitiesList.qml
+++ b/resources/qml/CommunitiesList.qml
@@ -3,6 +3,7 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
+import "./components"
import "./dialogs"
import Qt.labs.platform 1.1 as Platform
import QtQml 2.12
@@ -36,14 +37,27 @@ Page {
id: communityContextMenu
property string tagId
+ property bool hidden
+ property bool muted
- function show(id_, tags_) {
+ function show(id_, hidden_, muted_) {
tagId = id_;
+ hidden = hidden_;
+ muted = muted_;
open();
}
+ Platform.MenuItem {
+ text: qsTr("Do not show notification counts for this space or tag.")
+ checkable: true
+ checked: communityContextMenu.muted
+ onTriggered: Communities.toggleTagMute(communityContextMenu.tagId)
+ }
+
Platform.MenuItem {
text: qsTr("Hide rooms with this tag or from this space by default.")
+ checkable: true
+ checked: communityContextMenu.hidden
onTriggered: Communities.toggleTagId(communityContextMenu.tagId)
}
@@ -57,19 +71,30 @@ Page {
property color unimportantText: Nheko.colors.buttonText
property color bubbleBackground: Nheko.colors.highlight
property color bubbleText: Nheko.colors.highlightedText
+ required property string avatarUrl
+ required property string displayName
+ required property string tooltip
+ required property bool collapsed
+ required property bool collapsible
+ required property bool hidden
+ required property int depth
+ required property string id
+ required property int unreadMessages
+ required property bool hasLoudNotification
+ required property bool muted
height: avatarSize + 2 * Nheko.paddingMedium
width: ListView.view.width
state: "normal"
ToolTip.visible: hovered && collapsed
- ToolTip.text: model.tooltip
+ ToolTip.text: communityItem.tooltip
ToolTip.delay: Nheko.tooltipDelay
- onClicked: Communities.setCurrentTagId(model.id)
- onPressAndHold: communityContextMenu.show(model.id)
+ onClicked: Communities.setCurrentTagId(communityItem.id)
+ onPressAndHold: communityContextMenu.show(communityItem.id, communityItem.hidden, communityItem.muted)
states: [
State {
name: "highlight"
- when: (communityItem.hovered || model.hidden) && !(Communities.currentTagId == model.id)
+ when: (communityItem.hovered || communityItem.hidden) && !(Communities.currentTagId === communityItem.id)
PropertyChanges {
target: communityItem
@@ -83,7 +108,7 @@ Page {
},
State {
name: "selected"
- when: Communities.currentTagId == model.id
+ when: Communities.currentTagId == communityItem.id
PropertyChanges {
target: communityItem
@@ -102,7 +127,7 @@ Page {
TapHandler {
acceptedButtons: Qt.RightButton
- onSingleTapped: communityContextMenu.show(model.id)
+ onSingleTapped: communityContextMenu.show(communityItem.id, communityItem.hidden, communityItem.muted)
gesturePolicy: TapHandler.ReleaseWithinBounds
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
}
@@ -114,27 +139,27 @@ Page {
spacing: Nheko.paddingMedium
anchors.fill: parent
anchors.margins: Nheko.paddingMedium
- anchors.leftMargin: Nheko.paddingMedium + (communitySidebar.collapsed ? 0 : (fontMetrics.lineSpacing * model.depth))
+ anchors.leftMargin: Nheko.paddingMedium + (communitySidebar.collapsed ? 0 : (fontMetrics.lineSpacing * communityItem.depth))
ImageButton {
- visible: !communitySidebar.collapsed && model.collapsible
+ visible: !communitySidebar.collapsed && communityItem.collapsible
Layout.preferredHeight: fontMetrics.lineSpacing
Layout.preferredWidth: fontMetrics.lineSpacing
Layout.alignment: Qt.AlignVCenter
height: fontMetrics.lineSpacing
width: fontMetrics.lineSpacing
- image: model.collapsed ? ":/icons/icons/ui/collapsed.svg" : ":/icons/icons/ui/expanded.svg"
+ image: communityItem.collapsed ? ":/icons/icons/ui/collapsed.svg" : ":/icons/icons/ui/expanded.svg"
ToolTip.visible: hovered
ToolTip.delay: Nheko.tooltipDelay
- ToolTip.text: model.collapsed ? qsTr("Expand") : qsTr("Collapse")
+ ToolTip.text: communityItem.collapsed ? qsTr("Expand") : qsTr("Collapse")
hoverEnabled: true
- onClicked: model.collapsed = !model.collapsed
+ onClicked: communityItem.collapsed = !communityItem.collapsed
}
Item {
Layout.preferredWidth: fontMetrics.lineSpacing
- visible: !communitySidebar.collapsed && !model.collapsible && Communities.containsSubspaces
+ visible: !communitySidebar.collapsed && !communityItem.collapsible && Communities.containsSubspaces
}
Avatar {
@@ -145,14 +170,27 @@ Page {
height: avatarSize
width: avatarSize
url: {
- if (model.avatarUrl.startsWith("mxc://"))
- return model.avatarUrl.replace("mxc://", "image://MxcImage/");
+ if (communityItem.avatarUrl.startsWith("mxc://"))
+ return communityItem.avatarUrl.replace("mxc://", "image://MxcImage/");
else
- return "image://colorimage/" + model.avatarUrl + "?" + communityItem.unimportantText;
+ return "image://colorimage/" + communityItem.avatarUrl + "?" + communityItem.unimportantText;
}
- roomid: model.id
- displayName: model.displayName
+ roomid: communityItem.id
+ displayName: communityItem.displayName
color: communityItem.backgroundColor
+
+ NotificationBubble {
+ notificationCount: communityItem.unreadMessages
+ hasLoudNotification: communityItem.hasLoudNotification
+ bubbleBackgroundColor: communityItem.bubbleBackground
+ bubbleTextColor: communityItem.bubbleText
+ font.pixelSize: fontMetrics.font.pixelSize * 0.6
+ mayBeVisible: communitySidebar.collapsed && !communityItem.muted && Settings.spaceNotifications
+ anchors.right: avatar.right
+ anchors.bottom: avatar.bottom
+ anchors.margins: -Nheko.paddingSmall
+ }
+
}
ElidedLabel {
@@ -161,7 +199,7 @@ Page {
color: communityItem.importantText
Layout.fillWidth: true
elideWidth: width
- fullText: model.displayName
+ fullText: communityItem.displayName
textFormat: Text.PlainText
}
@@ -169,10 +207,20 @@ Page {
Layout.fillWidth: true
}
+ NotificationBubble {
+ notificationCount: communityItem.unreadMessages
+ hasLoudNotification: communityItem.hasLoudNotification
+ bubbleBackgroundColor: communityItem.bubbleBackground
+ bubbleTextColor: communityItem.bubbleText
+ mayBeVisible: !communitySidebar.collapsed && !communityItem.muted && Settings.spaceNotifications
+ Layout.alignment: Qt.AlignRight
+ Layout.leftMargin: Nheko.paddingSmall
+ }
+
}
background: Rectangle {
- color: backgroundColor
+ color: communityItem.backgroundColor
}
}
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index a86ca725..1e61b68b 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -3,6 +3,7 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
+import "./components"
import "./dialogs"
import "./ui"
import Qt.labs.platform 1.1 as Platform
@@ -294,9 +295,6 @@ Page {
anchors.margins: Nheko.paddingMedium
Avatar {
- // In the future we could show an online indicator by setting the userid for the avatar
- //userid: Nheko.currentUser.userid
-
id: avatar
enabled: false
@@ -308,33 +306,17 @@ Page {
userid: isDirect ? directChatOtherUserId : ""
roomid: roomId
- Rectangle {
+ NotificationBubble {
id: collapsedNotificationBubble
+ notificationCount: roomItem.notificationCount
+ hasLoudNotification: roomItem.hasLoudNotification
+ bubbleBackgroundColor: roomItem.bubbleBackground
+ bubbleTextColor: roomItem.bubbleText
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: -Nheko.paddingSmall
- visible: collapsed && notificationCount > 0
- enabled: false
- Layout.alignment: Qt.AlignRight
- height: fontMetrics.averageCharacterWidth * 3
- width: Math.max(collapsedBubbleText.width, height)
- radius: height / 2
- color: hasLoudNotification ? Nheko.theme.red : roomItem.bubbleBackground
-
- Label {
- id: collapsedBubbleText
-
- anchors.centerIn: parent
- horizontalAlignment: Text.AlignHCenter
- verticalAlignment: Text.AlignVCenter
- width: Math.max(implicitWidth + Nheko.paddingMedium, parent.height)
- font.bold: true
- font.pixelSize: fontMetrics.font.pixelSize * 0.8
- color: hasLoudNotification ? "white" : roomItem.bubbleText
- text: notificationCount > 9999 ? "9999+" : notificationCount
- }
-
+ mayBeVisible: collapsed && (isSpace ? Settings.spaceNotifications : true)
}
}
@@ -351,7 +333,24 @@ Page {
height: avatar.height
spacing: Nheko.paddingSmall
+ NotificationBubble {
+ id: notificationBubble
+
+ parent: isSpace ? titleRow : subtextRow
+ notificationCount: roomItem.notificationCount
+ hasLoudNotification: roomItem.hasLoudNotification
+ bubbleBackgroundColor: roomItem.bubbleBackground
+ bubbleTextColor: roomItem.bubbleText
+ Layout.alignment: Qt.AlignRight
+ Layout.leftMargin: Nheko.paddingSmall
+ Layout.preferredWidth: implicitWidth
+ Layout.preferredHeight: implicitHeight
+ mayBeVisible: !collapsed && (isSpace ? Settings.spaceNotifications : true)
+ }
+
RowLayout {
+ id: titleRow
+
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
spacing: Nheko.paddingSmall
@@ -380,6 +379,8 @@ Page {
}
RowLayout {
+ id: subtextRow
+
Layout.fillWidth: true
spacing: 0
visible: !isSpace
@@ -395,40 +396,6 @@ Page {
Layout.fillWidth: true
}
- Rectangle {
- id: notificationBubble
-
- visible: notificationCount > 0
- Layout.alignment: Qt.AlignRight
- Layout.leftMargin: Nheko.paddingSmall
- height: notificationBubbleText.height + Nheko.paddingMedium
- Layout.preferredWidth: Math.max(notificationBubbleText.width, height)
- radius: height / 2
- color: hasLoudNotification ? Nheko.theme.red : roomItem.bubbleBackground
- ToolTip.text: notificationCount
- ToolTip.delay: Nheko.tooltipDelay
- ToolTip.visible: notificationBubbleHover.hovered && (notificationCount > 9999)
-
- Label {
- id: notificationBubbleText
-
- anchors.centerIn: parent
- horizontalAlignment: Text.AlignHCenter
- verticalAlignment: Text.AlignVCenter
- width: Math.max(implicitWidth + Nheko.paddingMedium, parent.height)
- font.bold: true
- font.pixelSize: fontMetrics.font.pixelSize * 0.8
- color: hasLoudNotification ? "white" : roomItem.bubbleText
- text: notificationCount > 9999 ? "9999+" : notificationCount
-
- HoverHandler {
- id: notificationBubbleHover
- }
-
- }
-
- }
-
}
}
diff --git a/resources/qml/components/NotificationBubble.qml b/resources/qml/components/NotificationBubble.qml
new file mode 100644
index 00000000..ca0ae6cb
--- /dev/null
+++ b/resources/qml/components/NotificationBubble.qml
@@ -0,0 +1,46 @@
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import im.nheko 1.0
+
+Rectangle {
+ id: bubbleRoot
+
+ required property int notificationCount
+ required property bool hasLoudNotification
+ required property color bubbleBackgroundColor
+ required property color bubbleTextColor
+ property bool mayBeVisible: true
+ property alias font: notificationBubbleText.font
+
+ visible: mayBeVisible && notificationCount > 0
+ implicitHeight: notificationBubbleText.height + Nheko.paddingMedium
+ implicitWidth: Math.max(notificationBubbleText.width, height)
+ radius: height / 2
+ color: hasLoudNotification ? Nheko.theme.red : bubbleBackgroundColor
+ ToolTip.text: notificationCount
+ ToolTip.delay: Nheko.tooltipDelay
+ ToolTip.visible: notificationBubbleHover.hovered && (notificationCount > 9999)
+
+ Label {
+ id: notificationBubbleText
+
+ anchors.centerIn: bubbleRoot
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ width: Math.max(implicitWidth + Nheko.paddingMedium, bubbleRoot.height)
+ font.bold: true
+ font.pixelSize: fontMetrics.font.pixelSize * 0.8
+ color: bubbleRoot.hasLoudNotification ? "white" : bubbleRoot.bubbleTextColor
+ text: bubbleRoot.notificationCount > 9999 ? "9999+" : bubbleRoot.notificationCount
+
+ HoverHandler {
+ id: notificationBubbleHover
+ }
+
+ }
+
+}
diff --git a/resources/res.qrc b/resources/res.qrc
index 3ec24238..7f08c29d 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -185,6 +185,7 @@
qml/voip/PlaceCall.qml
qml/voip/ScreenShare.qml
qml/voip/VideoCall.qml
+ qml/components/NotificationBubble.qml
media/ring.ogg
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 666a03b4..b850d2e5 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -88,7 +88,8 @@ UserSettings::load(std::optional profile)
openImageExternal_ = settings.value(QStringLiteral("user/open_image_external"), false).toBool();
openVideoExternal_ = settings.value(QStringLiteral("user/open_video_external"), false).toBool();
decryptSidebar_ = settings.value(QStringLiteral("user/decrypt_sidebar"), true).toBool();
- privacyScreen_ = settings.value(QStringLiteral("user/privacy_screen"), false).toBool();
+ spaceNotifications_ = settings.value(QStringLiteral("user/space_notifications"), true).toBool();
+ privacyScreen_ = settings.value(QStringLiteral("user/privacy_screen"), false).toBool();
privacyScreenTimeout_ =
settings.value(QStringLiteral("user/privacy_screen_timeout"), 0).toInt();
exposeDBusApi_ = settings.value(QStringLiteral("user/expose_dbus_api"), false).toBool();
@@ -132,7 +133,8 @@ UserSettings::load(std::optional profile)
userId_ = settings.value(prefix + "auth/user_id", "").toString();
deviceId_ = settings.value(prefix + "auth/device_id", "").toString();
hiddenTags_ = settings.value(prefix + "user/hidden_tags", QStringList{}).toStringList();
- hiddenPins_ = settings.value(prefix + "user/hidden_pins", QStringList{}).toStringList();
+ mutedTags_ = settings.value(prefix + "user/muted_tags", QStringList{"global"}).toStringList();
+ hiddenPins_ = settings.value(prefix + "user/hidden_pins", QStringList{}).toStringList();
hiddenWidgets_ = settings.value(prefix + "user/hidden_widgets", QStringList{}).toStringList();
recentReactions_ =
settings.value(prefix + "user/recent_reactions", QStringList{}).toStringList();
@@ -220,14 +222,21 @@ UserSettings::setGroupView(bool state)
}
void
-UserSettings::setHiddenTags(QStringList hiddenTags)
+UserSettings::setHiddenTags(const QStringList &hiddenTags)
{
hiddenTags_ = hiddenTags;
save();
}
void
-UserSettings::setHiddenPins(QStringList hiddenTags)
+UserSettings::setMutedTags(const QStringList &mutedTags)
+{
+ mutedTags_ = mutedTags;
+ save();
+}
+
+void
+UserSettings::setHiddenPins(const QStringList &hiddenTags)
{
hiddenPins_ = hiddenTags;
save();
@@ -235,7 +244,7 @@ UserSettings::setHiddenPins(QStringList hiddenTags)
}
void
-UserSettings::setHiddenWidgets(QStringList hiddenTags)
+UserSettings::setHiddenWidgets(const QStringList &hiddenTags)
{
hiddenWidgets_ = hiddenTags;
save();
@@ -416,6 +425,16 @@ UserSettings::setDecryptSidebar(bool state)
save();
}
+void
+UserSettings::setSpaceNotifications(bool state)
+{
+ if (state == spaceNotifications_)
+ return;
+ spaceNotifications_ = state;
+ emit spaceNotificationsChanged(state);
+ save();
+}
+
void
UserSettings::setPrivacyScreen(bool state)
{
@@ -777,6 +796,7 @@ UserSettings::save()
settings.setValue(QStringLiteral("avatar_circles"), avatarCircles_);
settings.setValue(QStringLiteral("decrypt_sidebar"), decryptSidebar_);
+ settings.setValue(QStringLiteral("space_notifications"), spaceNotifications_);
settings.setValue(QStringLiteral("privacy_screen"), privacyScreen_);
settings.setValue(QStringLiteral("privacy_screen_timeout"), privacyScreenTimeout_);
settings.setValue(QStringLiteral("mobile_mode"), mobileMode_);
@@ -830,6 +850,7 @@ UserSettings::save()
onlyShareKeysWithVerifiedUsers_);
settings.setValue(prefix + "user/online_key_backup", useOnlineKeyBackup_);
settings.setValue(prefix + "user/hidden_tags", hiddenTags_);
+ settings.setValue(prefix + "user/muted_tags", mutedTags_);
settings.setValue(prefix + "user/hidden_pins", hiddenPins_);
settings.setValue(prefix + "user/hidden_widgets", hiddenWidgets_);
settings.setValue(prefix + "user/recent_reactions", recentReactions_);
@@ -923,6 +944,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
return tr("Open videos with external program");
case DecryptSidebar:
return tr("Decrypt messages in sidebar");
+ case SpaceNotifications:
+ return tr("Show message counts for spaces");
case PrivacyScreen:
return tr("Privacy Screen");
case PrivacyScreenTimeout:
@@ -1053,6 +1076,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
return i->openVideoExternal();
case DecryptSidebar:
return i->decryptSidebar();
+ case SpaceNotifications:
+ return i->spaceNotifications();
case PrivacyScreen:
return i->privacyScreen();
case PrivacyScreenTimeout:
@@ -1208,6 +1233,9 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
case DecryptSidebar:
return tr("Decrypt the messages shown in the sidebar.\nOnly affects messages in "
"encrypted chats.");
+ case SpaceNotifications:
+ return tr(
+ "Choose where to show the total number of notifications contained within a space.");
case PrivacyScreen:
return tr("When the window loses focus, the timeline will\nbe blurred.");
case MobileMode:
@@ -1317,6 +1345,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
case ShareKeysWithTrustedUsers:
case UseOnlineKeyBackup:
case ExposeDBusApi:
+ case SpaceNotifications:
return Toggle;
case Profile:
case UserId:
@@ -1409,7 +1438,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
return fontDb.families();
case EmojiFont:
return fontDb.families(QFontDatabase::WritingSystem::Symbol);
- case Ringtone:
+ case Ringtone: {
QStringList l{
QStringLiteral("Mute"),
QStringLiteral("Default"),
@@ -1419,6 +1448,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
l.push_back(i->ringtone());
return l;
}
+ }
} else if (role == Good) {
switch (index.row()) {
case OnlineBackupKey:
@@ -1624,6 +1654,13 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int
return false;
}
return i->decryptSidebar();
+ case SpaceNotifications: {
+ if (value.userType() == QMetaType::Bool) {
+ i->setSpaceNotifications(value.toBool());
+ return true;
+ } else
+ return false;
+ }
case PrivacyScreen: {
if (value.userType() == QMetaType::Bool) {
i->setPrivacyScreen(value.toBool());
@@ -1936,7 +1973,9 @@ UserSettingsModel::UserSettingsModel(QObject *p)
connect(s.get(), &UserSettings::decryptSidebarChanged, this, [this]() {
emit dataChanged(index(DecryptSidebar), index(DecryptSidebar), {Value});
});
-
+ connect(s.get(), &UserSettings::spaceNotificationsChanged, this, [this] {
+ emit dataChanged(index(SpaceNotifications), index(SpaceNotifications), {Value});
+ });
connect(s.get(), &UserSettings::trayChanged, this, [this]() {
emit dataChanged(index(Tray), index(Tray), {Value});
emit dataChanged(index(StartInTray), index(StartInTray), {Enabled});
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index 1fb3ddcf..34a792eb 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -58,6 +58,8 @@ class UserSettings : public QObject
bool avatarCircles READ avatarCircles WRITE setAvatarCircles NOTIFY avatarCirclesChanged)
Q_PROPERTY(
bool decryptSidebar READ decryptSidebar WRITE setDecryptSidebar NOTIFY decryptSidebarChanged)
+ Q_PROPERTY(bool spaceNotifications READ spaceNotifications WRITE setSpaceNotifications NOTIFY
+ spaceNotificationsChanged)
Q_PROPERTY(
bool privacyScreen READ privacyScreen WRITE setPrivacyScreen NOTIFY privacyScreenChanged)
Q_PROPERTY(int privacyScreenTimeout READ privacyScreenTimeout WRITE setPrivacyScreenTimeout
@@ -162,6 +164,7 @@ public:
void setAlertOnNotification(bool state);
void setAvatarCircles(bool state);
void setDecryptSidebar(bool state);
+ void setSpaceNotifications(bool state);
void setPrivacyScreen(bool state);
void setPrivacyScreenTimeout(int state);
void setPresence(Presence state);
@@ -184,9 +187,10 @@ public:
void setDeviceId(QString deviceId);
void setHomeserver(QString homeserver);
void setDisableCertificateValidation(bool disabled);
- void setHiddenTags(QStringList hiddenTags);
- void setHiddenPins(QStringList hiddenTags);
- void setHiddenWidgets(QStringList hiddenTags);
+ void setHiddenTags(const QStringList &hiddenTags);
+ void setMutedTags(const QStringList &mutedTags);
+ void setHiddenPins(const QStringList &hiddenTags);
+ void setHiddenWidgets(const QStringList &hiddenTags);
void setRecentReactions(QStringList recent);
void setUseIdenticon(bool state);
void setOpenImageExternal(bool state);
@@ -202,6 +206,7 @@ public:
bool groupView() const { return groupView_; }
bool avatarCircles() const { return avatarCircles_; }
bool decryptSidebar() const { return decryptSidebar_; }
+ bool spaceNotifications() const { return spaceNotifications_; }
bool privacyScreen() const { return privacyScreen_; }
int privacyScreenTimeout() const { return privacyScreenTimeout_; }
bool markdown() const { return markdown_; }
@@ -250,6 +255,7 @@ public:
QString homeserver() const { return homeserver_; }
bool disableCertificateValidation() const { return disableCertificateValidation_; }
QStringList hiddenTags() const { return hiddenTags_; }
+ QStringList mutedTags() const { return mutedTags_; }
QStringList hiddenPins() const { return hiddenPins_; }
QStringList hiddenWidgets() const { return hiddenWidgets_; }
QStringList recentReactions() const { return recentReactions_; }
@@ -278,6 +284,7 @@ signals:
void alertOnNotificationChanged(bool state);
void avatarCirclesChanged(bool state);
void decryptSidebarChanged(bool state);
+ void spaceNotificationsChanged(bool state);
void privacyScreenChanged(bool state);
void privacyScreenTimeoutChanged(int state);
void timelineMaxWidthChanged(int state);
@@ -340,6 +347,7 @@ private:
bool hasAlertOnNotification_;
bool avatarCircles_;
bool decryptSidebar_;
+ bool spaceNotifications_;
bool privacyScreen_;
int privacyScreenTimeout_;
bool shareKeysWithTrustedUsers_;
@@ -370,6 +378,7 @@ private:
QString deviceId_;
QString homeserver_;
QStringList hiddenTags_;
+ QStringList mutedTags_;
QStringList hiddenPins_;
QStringList hiddenWidgets_;
QStringList recentReactions_;
@@ -424,6 +433,7 @@ class UserSettingsModel : public QAbstractListModel
GroupView,
SortByImportance,
DecryptSidebar,
+ SpaceNotifications,
TraySection,
Tray,
diff --git a/src/Utils.cpp b/src/Utils.cpp
index 0ac37d8e..3a90bd50 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -27,6 +27,7 @@
#include
#include "Cache.h"
+#include "Cache_p.h"
#include "Config.h"
#include "EventAccessors.h"
#include "Logging.h"
@@ -880,3 +881,21 @@ utils::markRoomAsDirect(QString roomid, std::vector members)
});
});
}
+
+QPair
+utils::getChildNotificationsForSpace(const QString &spaceId)
+{
+ auto children = cache::getRoomInfo(cache::client()->getChildRoomIds(spaceId.toStdString()));
+ QPair retVal;
+ for (const auto &[childId, child] : children) {
+ if (child.is_space) {
+ auto temp{utils::getChildNotificationsForSpace(childId)};
+ retVal.first += temp.first;
+ retVal.second += temp.second;
+ } else {
+ retVal.first += child.notification_count;
+ retVal.second += child.highlight_count;
+ }
+ }
+ return retVal;
+}
diff --git a/src/Utils.h b/src/Utils.h
index 0b6034ac..bdd56d55 100644
--- a/src/Utils.h
+++ b/src/Utils.h
@@ -311,4 +311,9 @@ removeDirectFromRoom(QString roomid);
void
markRoomAsDirect(QString roomid, std::vector members);
+
+//! Returns a pair of integers representing the unread notifications in a space and how many of them
+//! are loud notifications, respectively.
+QPair
+getChildNotificationsForSpace(const QString &spaceId);
}
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
index 4f650f49..91f7d555 100644
--- a/src/timeline/CommunitiesModel.cpp
+++ b/src/timeline/CommunitiesModel.cpp
@@ -9,12 +9,24 @@
#include "Cache.h"
#include "Cache_p.h"
+#include "ChatPage.h"
#include "Logging.h"
#include "UserSettingsPage.h"
+#include "Utils.h"
CommunitiesModel::CommunitiesModel(QObject *parent)
: QAbstractListModel(parent)
-{}
+ , hiddenTagIds_{UserSettings::instance()->hiddenTags()}
+ , mutedTagIds_{UserSettings::instance()->mutedTags()}
+{
+ connect(ChatPage::instance(), &ChatPage::unreadMessages, this, [this](int) {
+ // Simply updating every space is easier than tracking which ones need updated.
+ if (!spaces_.empty())
+ emit dataChanged(index(0, 0),
+ index(spaces_.size() + tags_.size() + 1, 0),
+ {Roles::UnreadMessages, Roles::HasLoudNotification});
+ });
+}
QHash
CommunitiesModel::roleNames() const
@@ -28,6 +40,9 @@ CommunitiesModel::roleNames() const
{Hidden, "hidden"},
{Depth, "depth"},
{Id, "id"},
+ {UnreadMessages, "unreadMessages"},
+ {HasLoudNotification, "hasLoudNotification"},
+ {Muted, "muted"},
};
}
@@ -50,6 +65,13 @@ CommunitiesModel::setData(const QModelIndex &index, const QVariant &value, int r
QVariant
CommunitiesModel::data(const QModelIndex &index, int role) const
{
+ if (role == CommunitiesModel::Roles::Muted) {
+ if (index.row() == 0)
+ return mutedTagIds_.contains(QStringLiteral("global"));
+ else
+ return mutedTagIds_.contains(data(index, CommunitiesModel::Roles::Id).toString());
+ }
+
if (index.row() == 0) {
switch (role) {
case CommunitiesModel::Roles::AvatarUrl:
@@ -70,6 +92,17 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
return 0;
case CommunitiesModel::Roles::Id:
return "";
+ case CommunitiesModel::Roles::UnreadMessages: {
+ int total{0};
+ for (const auto &[id, info] : cache::getRoomInfo(cache::joinedRooms()))
+ total += info.notification_count;
+ return total;
+ }
+ case CommunitiesModel::Roles::HasLoudNotification:
+ for (const auto &[id, info] : cache::getRoomInfo(cache::joinedRooms()))
+ if (info.highlight_count > 0)
+ return true;
+ return false;
}
} else if (index.row() == 1) {
switch (role) {
@@ -84,16 +117,27 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
case CommunitiesModel::Roles::Collapsible:
return false;
case CommunitiesModel::Roles::Hidden:
- return hiddentTagIds_.contains(QStringLiteral("dm"));
+ return hiddenTagIds_.contains(QStringLiteral("dm"));
case CommunitiesModel::Roles::Parent:
return "";
case CommunitiesModel::Roles::Depth:
return 0;
case CommunitiesModel::Roles::Id:
return "dm";
+ case CommunitiesModel::Roles::UnreadMessages: {
+ int total{0};
+ for (const auto &[id, info] : cache::getRoomInfo(directMessages_))
+ total += info.notification_count;
+ return total;
+ }
+ case CommunitiesModel::Roles::HasLoudNotification:
+ for (const auto &[id, info] : cache::getRoomInfo(directMessages_))
+ if (info.highlight_count > 0)
+ return true;
+ return false;
}
} else if (index.row() - 2 < spaceOrder_.size()) {
- auto id = spaceOrder_.tree.at(index.row() - 2).name;
+ auto id = spaceOrder_.tree.at(index.row() - 2).id;
switch (role) {
case CommunitiesModel::Roles::AvatarUrl:
return QString::fromStdString(spaces_.at(id).avatar_url);
@@ -107,10 +151,10 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
return idx != spaceOrder_.lastChild(idx);
}
case CommunitiesModel::Roles::Hidden:
- return hiddentTagIds_.contains("space:" + id);
+ return hiddenTagIds_.contains("space:" + id);
case CommunitiesModel::Roles::Parent: {
if (auto p = spaceOrder_.parent(index.row() - 2); p >= 0)
- return spaceOrder_.tree[p].name;
+ return spaceOrder_.tree[p].id;
return "";
}
@@ -118,6 +162,10 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
return spaceOrder_.tree.at(index.row() - 2).depth;
case CommunitiesModel::Roles::Id:
return "space:" + id;
+ case CommunitiesModel::Roles::UnreadMessages:
+ return utils::getChildNotificationsForSpace(id).first;
+ case CommunitiesModel::Roles::HasLoudNotification:
+ return utils::getChildNotificationsForSpace(id).second > 0;
}
} else if (index.row() - 2 < tags_.size() + spaceOrder_.size()) {
auto tag = tags_.at(index.row() - 2 - spaceOrder_.size());
@@ -160,7 +208,7 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
switch (role) {
case CommunitiesModel::Roles::Hidden:
- return hiddentTagIds_.contains("tag:" + tag);
+ return hiddenTagIds_.contains("tag:" + tag);
case CommunitiesModel::Roles::Collapsed:
return true;
case CommunitiesModel::Roles::Collapsible:
@@ -171,6 +219,24 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
return 0;
case CommunitiesModel::Roles::Id:
return "tag:" + tag;
+ case CommunitiesModel::Roles::UnreadMessages: {
+ int total{0};
+ auto rooms{cache::joinedRooms()};
+ for (const auto &[roomid, info] : cache::getRoomInfo(rooms))
+ if (std::find(std::begin(info.tags), std::end(info.tags), tag.toStdString()) !=
+ std::end(info.tags))
+ total += info.notification_count;
+ return total;
+ }
+ case CommunitiesModel::Roles::HasLoudNotification: {
+ auto rooms{cache::joinedRooms()};
+ for (const auto &[roomid, info] : cache::getRoomInfo(rooms))
+ if (std::find(std::begin(info.tags), std::end(info.tags), tag.toStdString()) !=
+ std::end(info.tags))
+ if (info.highlight_count > 0)
+ return true;
+ return false;
+ }
}
}
return QVariant();
@@ -277,8 +343,8 @@ CommunitiesModel::initializeSidebar()
for (const auto &t : ts)
tags_.push_back(QString::fromStdString(t));
- hiddentTagIds_ = UserSettings::instance()->hiddenTags();
spaceOrder_.restoreCollapsed();
+
endResetModel();
emit tagsChanged();
@@ -298,12 +364,12 @@ CommunitiesModel::FlatTree::storeCollapsed()
for (const auto &e : tree) {
if (e.depth > depth) {
- current.push_back(e.name);
+ current.push_back(e.id);
} else if (e.depth == depth) {
- current.back() = e.name;
+ current.back() = e.id;
} else {
current.pop_back();
- current.back() = e.name;
+ current.back() = e.id;
}
if (e.collapsed)
@@ -323,12 +389,12 @@ CommunitiesModel::FlatTree::restoreCollapsed()
for (auto &e : tree) {
if (e.depth > depth) {
- current.push_back(e.name);
+ current.push_back(e.id);
} else if (e.depth == depth) {
- current.back() = e.name;
+ current.back() = e.id;
} else {
current.pop_back();
- current.back() = e.name;
+ current.back() = e.id;
}
if (elements.contains(current))
@@ -353,7 +419,6 @@ CommunitiesModel::sync(const mtx::responses::Sync &sync_)
bool tagsUpdated = false;
for (const auto &[roomid, room] : sync_.rooms.join) {
- (void)roomid;
for (const auto &e : room.account_data.events)
if (std::holds_alternative<
mtx::events::AccountDataEvent>(e)) {
@@ -380,8 +445,12 @@ CommunitiesModel::sync(const mtx::responses::Sync &sync_)
tagsUpdated = true;
}
for (const auto &e : sync_.account_data.events) {
- if (std::holds_alternative<
- mtx::events::AccountDataEvent>(e)) {
+ if (auto event =
+ std::get_if>(&e)) {
+ directMessages_.clear();
+ for (const auto &[userId, roomIds] : event->content.user_to_rooms)
+ for (const auto &roomId : roomIds)
+ directMessages_.push_back(roomId);
tagsUpdated = true;
break;
}
@@ -392,7 +461,7 @@ CommunitiesModel::sync(const mtx::responses::Sync &sync_)
}
void
-CommunitiesModel::setCurrentTagId(QString tagId)
+CommunitiesModel::setCurrentTagId(const QString &tagId)
{
if (tagId.startsWith(QLatin1String("tag:"))) {
auto tag = tagId.mid(4);
@@ -406,7 +475,7 @@ CommunitiesModel::setCurrentTagId(QString tagId)
} else if (tagId.startsWith(QLatin1String("space:"))) {
auto tag = tagId.mid(6);
for (const auto &t : spaceOrder_.tree) {
- if (t.name == tag) {
+ if (t.id == tag) {
this->currentTagId_ = tagId;
emit currentTagIdChanged(currentTagId_);
return;
@@ -425,13 +494,11 @@ CommunitiesModel::setCurrentTagId(QString tagId)
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 (hiddenTagIds_.contains(tagId))
+ hiddenTagIds_.removeOne(tagId);
+ else
+ hiddenTagIds_.push_back(tagId);
+ UserSettings::instance()->setHiddenTags(hiddenTagIds_);
if (tagId.startsWith(QLatin1String("tag:"))) {
auto idx = tags_.indexOf(tagId.mid(4));
@@ -449,6 +516,34 @@ CommunitiesModel::toggleTagId(QString tagId)
emit hiddenTagsChanged();
}
+void
+CommunitiesModel::toggleTagMute(QString tagId)
+{
+ if (tagId.isEmpty())
+ tagId = QStringLiteral("global");
+
+ if (mutedTagIds_.contains(tagId))
+ mutedTagIds_.removeOne(tagId);
+ else
+ mutedTagIds_.push_back(tagId);
+ UserSettings::instance()->setMutedTags(mutedTagIds_);
+
+ if (tagId.startsWith(QLatin1String("tag:"))) {
+ auto idx = tags_.indexOf(tagId.mid(4));
+ if (idx != -1)
+ emit dataChanged(index(idx + 1 + spaceOrder_.size()),
+ index(idx + 1 + spaceOrder_.size()));
+ } else if (tagId.startsWith(QLatin1String("space:"))) {
+ auto idx = spaceOrder_.indexOf(tagId.mid(6));
+ if (idx != -1)
+ emit dataChanged(index(idx + 1), index(idx + 1));
+ } else if (tagId == QLatin1String("dm")) {
+ emit dataChanged(index(1), index(1));
+ } else if (tagId == QLatin1String("global")) {
+ emit dataChanged(index(0), index(0));
+ }
+}
+
FilteredCommunitiesModel::FilteredCommunitiesModel(CommunitiesModel *model, QObject *parent)
: QSortFilterProxyModel(parent)
{
diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h
index 5da7d1bd..5a659751 100644
--- a/src/timeline/CommunitiesModel.h
+++ b/src/timeline/CommunitiesModel.h
@@ -48,13 +48,17 @@ public:
Parent,
Depth,
Id,
+ UnreadMessages,
+ HasLoudNotification,
+ Muted,
+ IsDirect,
};
struct FlatTree
{
struct Elem
{
- QString name;
+ QString id;
int depth = 0;
bool collapsed = false;
};
@@ -65,7 +69,7 @@ public:
int indexOf(const QString &s) const
{
for (int i = 0; i < size(); i++)
- if (tree[i].name == s)
+ if (tree[i].id == s)
return i;
return -1;
}
@@ -121,7 +125,7 @@ public slots:
void sync(const mtx::responses::Sync &sync_);
void clear();
QString currentTagId() const { return currentTagId_; }
- void setCurrentTagId(QString tagId);
+ void setCurrentTagId(const QString &tagId);
void resetCurrentTagId()
{
currentTagId_.clear();
@@ -138,6 +142,7 @@ public slots:
return tagsWD;
}
void toggleTagId(QString tagId);
+ void toggleTagMute(QString tagId);
FilteredCommunitiesModel *filtered() { return new FilteredCommunitiesModel(this, this); }
signals:
@@ -149,9 +154,11 @@ signals:
private:
QStringList tags_;
QString currentTagId_;
- QStringList hiddentTagIds_;
+ QStringList hiddenTagIds_;
+ QStringList mutedTagIds_;
FlatTree spaceOrder_;
std::map spaces_;
+ std::vector directMessages_;
friend class FilteredCommunitiesModel;
};
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 1cf16243..3b46c053 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -330,10 +330,13 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
Qt::DisplayRole,
});
+ if (getRoomById(room_id)->isSpace())
+ return; // no need to update space notifications
+
int total_unread_msgs = 0;
for (const auto &room : qAsConst(models)) {
- if (!room.isNull())
+ if (!room.isNull() && !room->isSpace())
total_unread_msgs += room->notificationCount();
}
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 767fdaa2..db56ac52 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -364,16 +364,26 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
{
this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString());
- auto roomInfo = cache::singleRoomInfo(room_id_.toStdString());
- this->isSpace_ = roomInfo.is_space;
- this->notification_count = roomInfo.notification_count;
- this->highlight_count = roomInfo.highlight_count;
- lastMessage_.timestamp = roomInfo.approximate_last_modification_ts;
+ auto roomInfo = cache::singleRoomInfo(room_id_.toStdString());
+ this->isSpace_ = roomInfo.is_space;
+ this->notification_count =
+ isSpace_ ? utils::getChildNotificationsForSpace(room_id_).first : roomInfo.notification_count;
+ this->highlight_count =
+ isSpace_ ? utils::getChildNotificationsForSpace(room_id_).second : roomInfo.highlight_count;
+ lastMessage_.timestamp = roomInfo.approximate_last_modification_ts;
// this connection will simplify adding the plainRoomNameChanged() signal everywhere that it
// needs to be
connect(this, &TimelineModel::roomNameChanged, this, &TimelineModel::plainRoomNameChanged);
+ if (isSpace_)
+ connect(ChatPage::instance(), &ChatPage::unreadMessages, this, [this](int) {
+ auto temp{utils::getChildNotificationsForSpace(room_id_)};
+ notification_count = temp.first;
+ highlight_count = temp.second;
+ emit notificationsChanged();
+ });
+
connect(
this,
&TimelineModel::redactionFailed,