diff --git a/imports/NeoChat/Component/Timeline/StateDelegate.qml b/imports/NeoChat/Component/Timeline/StateDelegate.qml
index 43b149057..3e94805f0 100644
--- a/imports/NeoChat/Component/Timeline/StateDelegate.qml
+++ b/imports/NeoChat/Component/Timeline/StateDelegate.qml
@@ -10,41 +10,57 @@ import org.kde.kirigami 2.15 as Kirigami
import NeoChat.Component 1.0
import NeoChat.Dialog 1.0
-RowLayout {
+Control {
x: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing
- height: label.contentHeight
width: ListView.view.width - Kirigami.Units.largeSpacing - x
- Kirigami.Avatar {
- id: icon
- Layout.preferredWidth: Kirigami.Units.iconSizes.small
- Layout.preferredHeight: Kirigami.Units.iconSizes.small
- Layout.alignment: Qt.AlignTop
-
- name: author.displayName
- source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
- color: author.color
-
- Component {
- id: userDetailDialog
-
- UserDetailDialog {}
- }
-
- MouseArea {
- anchors.fill: parent
- onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
- }
+ height: sectionDelegate.height + rowLayout.height
+ SectionDelegate {
+ id: sectionDelegate
+ width: parent.width
+ anchors.top: parent.top
+ anchors.leftMargin: Kirigami.Units.smallSpacing
+ visible: model.showSection
+ height: visible ? implicitHeight : 0
}
- Label {
- id: label
- Layout.alignment: Qt.AlignVCenter
- Layout.fillWidth: true
- Layout.preferredHeight: icon.height
- wrapMode: Text.WordWrap
- textFormat: Text.RichText
- text: "" + currentRoom.htmlSafeMemberName(author.id) + " " + display
- onLinkActivated: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
+ RowLayout {
+ id: rowLayout
+ height: label.contentHeight
+ width: parent.width
+ anchors.bottom: parent.bottom
+
+ Kirigami.Avatar {
+ id: icon
+ Layout.preferredWidth: Kirigami.Units.iconSizes.small
+ Layout.preferredHeight: Kirigami.Units.iconSizes.small
+ Layout.alignment: Qt.AlignTop
+
+ name: author.displayName
+ source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
+ color: author.color
+
+ Component {
+ id: userDetailDialog
+
+ UserDetailDialog {}
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {room: currentRoom, user: author.object, displayName: author.displayName, avatarMediaId: author.avatarMediaId, avatarUrl: author.avatarUrl}).open()
+ }
+ }
+
+ Label {
+ id: label
+ Layout.alignment: Qt.AlignVCenter
+ Layout.fillWidth: true
+ Layout.preferredHeight: icon.height
+ wrapMode: Text.WordWrap
+ textFormat: Text.RichText
+ text: `${currentRoom.htmlSafeMemberName(author.id)} ${aggregateDisplay}`
+ onLinkActivated: userDetailDialog.createObject(ApplicationWindow.overlay, {room: currentRoom, user: author.object, displayName: author.displayName, avatarMediaId: author.avatarMediaId, avatarUrl: author.avatarUrl}).open()
+ }
}
}
diff --git a/imports/NeoChat/Page/RoomPage.qml b/imports/NeoChat/Page/RoomPage.qml
index ce4340ebf..ea4463a13 100644
--- a/imports/NeoChat/Page/RoomPage.qml
+++ b/imports/NeoChat/Page/RoomPage.qml
@@ -239,6 +239,11 @@ Kirigami.ScrollablePage {
}
}
+ CollapseStateProxyModel {
+ id: collapseStateProxyModel
+ sourceModel: sortedMessageEventModel
+ }
+
ListView {
id: messageListView
visible: !invitation.visible
@@ -251,7 +256,7 @@ Kirigami.ScrollablePage {
verticalLayoutDirection: ListView.BottomToTop
highlightMoveDuration: 500
- model: !isLoaded ? undefined : sortedMessageEventModel
+ model: !isLoaded ? undefined : collapseStateProxyModel
MessageEventModel {
id: messageEventModel
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 9e7f7c37b..8100099a2 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -36,6 +36,7 @@ add_executable(neochat
blurhash.cpp
blurhashimageprovider.cpp
joinrulesevent.cpp
+ collapsestateproxymodel.cpp
../res.qrc
)
diff --git a/src/collapsestateproxymodel.cpp b/src/collapsestateproxymodel.cpp
new file mode 100644
index 000000000..5b1f066a9
--- /dev/null
+++ b/src/collapsestateproxymodel.cpp
@@ -0,0 +1,84 @@
+// SPDX-FileCopyrightText: 2022 Tobias Fella
+// SPDX-License-Identifier: LGPL-2.0-or-later
+
+#include "collapsestateproxymodel.h"
+#include "messageeventmodel.h"
+
+#include
+
+bool CollapseStateProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
+{
+ return sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::EventTypeRole)
+ != QLatin1String("state") // If this is not a state, show it
+ || sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::EventTypeRole)
+ != QLatin1String("state") // If this is the first state in a block, show it. TODO hidden events?
+ || sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::ShowSectionRole).toBool() // If it's a new day, show it
+ || sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::EventResolvedTypeRole)
+ != sourceModel()->data(sourceModel()->index(source_row + 1, 0),
+ MessageEventModel::EventResolvedTypeRole) // Also show it if it's of a different type than the one before TODO improve in
+ || sourceModel()->data(sourceModel()->index(source_row, 0), MessageEventModel::AuthorIdRole)
+ != sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::AuthorIdRole); // Also show it if it's a different author
+}
+
+QVariant CollapseStateProxyModel::data(const QModelIndex &index, int role) const
+{
+ if (role == AggregateDisplayRole) {
+ return aggregateEventToString(mapToSource(index).row());
+ }
+ return sourceModel()->data(mapToSource(index), role);
+}
+
+QHash CollapseStateProxyModel::roleNames() const
+{
+ auto roles = sourceModel()->roleNames();
+ roles[AggregateDisplayRole] = "aggregateDisplay";
+ return roles;
+}
+
+QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
+{
+ QStringList parts;
+ for (int i = sourceRow; i >= 0; i--) {
+ parts += sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString();
+ if (sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::EventTypeRole) != QLatin1String("state") // If it's not a state event
+ || (i > 0
+ && sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::EventResolvedTypeRole)
+ != sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventResolvedTypeRole)) // or of a different type
+ || (i > 0
+ && sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorIdRole)
+ != sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::AuthorIdRole)) // or by a different author
+ || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
+ ) {
+ break;
+ }
+ }
+ if (!parts.isEmpty()) {
+ QStringList chunks;
+ while (!parts.isEmpty()) {
+ chunks += QString();
+ int count = 1;
+ auto part = parts.takeFirst();
+ chunks.last() += part;
+ while (!parts.isEmpty() && parts.first() == part) {
+ parts.removeFirst();
+ count++;
+ }
+ if (count > 1) {
+ chunks.last() += i18ncp("[user did something] n times", " %1 time", " %1 times", count);
+ }
+ }
+ QString text = chunks.takeFirst();
+
+ if (chunks.size() > 0) {
+ while (chunks.size() > 1) {
+ text += i18nc("[action 1], [action 2 and action 3]", ", ");
+ text += chunks.takeFirst();
+ }
+ text += i18nc("[action 1, action 2] and [action 3]", " and ");
+ text += chunks.takeFirst();
+ }
+ return text;
+ } else {
+ return {};
+ }
+}
diff --git a/src/collapsestateproxymodel.h b/src/collapsestateproxymodel.h
new file mode 100644
index 000000000..c3af1056b
--- /dev/null
+++ b/src/collapsestateproxymodel.h
@@ -0,0 +1,23 @@
+// SPDX-FileCopyrightText: 2022 Tobias Fella
+// SPDX-License-Identifier: LGPL-2.0-or-later
+
+#pragma once
+
+#include
+#include
+
+#include "messageeventmodel.h"
+
+class CollapseStateProxyModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+public:
+ enum Roles {
+ AggregateDisplayRole = MessageEventModel::LastRole + 1,
+ };
+ [[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
+ [[nodiscard]] QHash roleNames() const override;
+ [[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
+
+ [[nodiscard]] QString aggregateEventToString(int row) const;
+};
diff --git a/src/main.cpp b/src/main.cpp
index f990d61fa..98ab0903d 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -46,6 +46,7 @@
#include "chatboxhelper.h"
#include "chatdocumenthandler.h"
#include "clipboard.h"
+#include "collapsestateproxymodel.h"
#include "commandmodel.h"
#include "controller.h"
#include "csapi/joining.h"
@@ -198,6 +199,7 @@ int main(int argc, char *argv[])
qmlRegisterType("org.kde.neochat", 1, 0, "UserListModel");
qmlRegisterType("org.kde.neochat", 1, 0, "CustomEmojiModel");
qmlRegisterType("org.kde.neochat", 1, 0, "MessageEventModel");
+ qmlRegisterType("org.kde.neochat", 1, 0, "CollapseStateProxyModel");
qmlRegisterType("org.kde.neochat", 1, 0, "MessageFilterModel");
qmlRegisterType("org.kde.neochat", 1, 0, "PublicRoomListModel");
qmlRegisterType("org.kde.neochat", 1, 0, "UserDirectoryListModel");
diff --git a/src/messageeventmodel.cpp b/src/messageeventmodel.cpp
index 06e2c9b12..cf2ce1ea0 100644
--- a/src/messageeventmodel.cpp
+++ b/src/messageeventmodel.cpp
@@ -53,6 +53,7 @@ QHash MessageEventModel::roleNames() const
roles[SourceRole] = "source";
roles[MimeTypeRole] = "mimeType";
roles[FormattedBodyRole] = "formattedBody";
+ roles[AuthorIdRole] = "authorId";
return roles;
}
@@ -767,6 +768,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return res;
}
+ if (role == AuthorIdRole) {
+ return evt.senderId();
+ }
return {};
}
diff --git a/src/messageeventmodel.h b/src/messageeventmodel.h
index efd9ff566..cc8388fd1 100644
--- a/src/messageeventmodel.h
+++ b/src/messageeventmodel.h
@@ -47,6 +47,8 @@ public:
// For debugging
EventResolvedTypeRole,
+ AuthorIdRole,
+ LastRole, // Keep this last
};
Q_ENUM(EventRoles)
diff --git a/src/neochatroom.cpp b/src/neochatroom.cpp
index 8a3df1cb6..d22617228 100644
--- a/src/neochatroom.cpp
+++ b/src/neochatroom.cpp
@@ -396,6 +396,18 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
[this](const RoomMemberEvent &e) {
// FIXME: Rewind to the name that was at the time of this event
auto subjectName = this->htmlSafeMemberName(e.userId());
+ if (e.membership() == MembershipType::Leave) {
+ auto displayName = e.prevContent()->displayName;
+#ifdef QUOTIENT_07
+ if (displayName) {
+ subjectName = sanitized(*displayName).toHtmlEscaped();
+#else
+ if (displayName.isEmpty()) {
+ subjectName = sanitized(displayName).toHtmlEscaped();
+#endif
+ }
+ }
+
// The below code assumes senderName output in AuthorRole
switch (e.membership()) {
case MembershipType::Invite: