Aggregate similar state events
This commit is contained in:
@@ -10,41 +10,57 @@ import org.kde.kirigami 2.15 as Kirigami
|
|||||||
import NeoChat.Component 1.0
|
import NeoChat.Component 1.0
|
||||||
import NeoChat.Dialog 1.0
|
import NeoChat.Dialog 1.0
|
||||||
|
|
||||||
RowLayout {
|
Control {
|
||||||
x: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing
|
x: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing
|
||||||
height: label.contentHeight
|
|
||||||
width: ListView.view.width - Kirigami.Units.largeSpacing - x
|
width: ListView.view.width - Kirigami.Units.largeSpacing - x
|
||||||
|
|
||||||
Kirigami.Avatar {
|
height: sectionDelegate.height + rowLayout.height
|
||||||
id: icon
|
SectionDelegate {
|
||||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
id: sectionDelegate
|
||||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
width: parent.width
|
||||||
Layout.alignment: Qt.AlignTop
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Kirigami.Units.smallSpacing
|
||||||
name: author.displayName
|
visible: model.showSection
|
||||||
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
|
height: visible ? implicitHeight : 0
|
||||||
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 {
|
RowLayout {
|
||||||
id: label
|
id: rowLayout
|
||||||
Layout.alignment: Qt.AlignVCenter
|
height: label.contentHeight
|
||||||
Layout.fillWidth: true
|
width: parent.width
|
||||||
Layout.preferredHeight: icon.height
|
anchors.bottom: parent.bottom
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
textFormat: Text.RichText
|
Kirigami.Avatar {
|
||||||
text: "<style>a {text-decoration: none;}</style><a href=\"https://matrix.to/#/" + author.id + "\" style='color: " + author.color + "'>" + currentRoom.htmlSafeMemberName(author.id) + "</a> " + display
|
id: icon
|
||||||
onLinkActivated: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open()
|
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: `<style>a {text-decoration: none;}</style><a href="https://matrix.to/#/${author.id}" style="color: ${author.color}">${currentRoom.htmlSafeMemberName(author.id)}</a> ${aggregateDisplay}`
|
||||||
|
onLinkActivated: userDetailDialog.createObject(ApplicationWindow.overlay, {room: currentRoom, user: author.object, displayName: author.displayName, avatarMediaId: author.avatarMediaId, avatarUrl: author.avatarUrl}).open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,6 +239,11 @@ Kirigami.ScrollablePage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CollapseStateProxyModel {
|
||||||
|
id: collapseStateProxyModel
|
||||||
|
sourceModel: sortedMessageEventModel
|
||||||
|
}
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
id: messageListView
|
id: messageListView
|
||||||
visible: !invitation.visible
|
visible: !invitation.visible
|
||||||
@@ -251,7 +256,7 @@ Kirigami.ScrollablePage {
|
|||||||
verticalLayoutDirection: ListView.BottomToTop
|
verticalLayoutDirection: ListView.BottomToTop
|
||||||
highlightMoveDuration: 500
|
highlightMoveDuration: 500
|
||||||
|
|
||||||
model: !isLoaded ? undefined : sortedMessageEventModel
|
model: !isLoaded ? undefined : collapseStateProxyModel
|
||||||
|
|
||||||
MessageEventModel {
|
MessageEventModel {
|
||||||
id: messageEventModel
|
id: messageEventModel
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ add_executable(neochat
|
|||||||
blurhash.cpp
|
blurhash.cpp
|
||||||
blurhashimageprovider.cpp
|
blurhashimageprovider.cpp
|
||||||
joinrulesevent.cpp
|
joinrulesevent.cpp
|
||||||
|
collapsestateproxymodel.cpp
|
||||||
../res.qrc
|
../res.qrc
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
84
src/collapsestateproxymodel.cpp
Normal file
84
src/collapsestateproxymodel.cpp
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "collapsestateproxymodel.h"
|
||||||
|
#include "messageeventmodel.h"
|
||||||
|
|
||||||
|
#include <KLocalizedString>
|
||||||
|
|
||||||
|
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<int, QByteArray> 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 {};
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/collapsestateproxymodel.h
Normal file
23
src/collapsestateproxymodel.h
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||||
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QPair>
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
|
||||||
|
#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<int, QByteArray> roleNames() const override;
|
||||||
|
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
[[nodiscard]] QString aggregateEventToString(int row) const;
|
||||||
|
};
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
#include "chatboxhelper.h"
|
#include "chatboxhelper.h"
|
||||||
#include "chatdocumenthandler.h"
|
#include "chatdocumenthandler.h"
|
||||||
#include "clipboard.h"
|
#include "clipboard.h"
|
||||||
|
#include "collapsestateproxymodel.h"
|
||||||
#include "commandmodel.h"
|
#include "commandmodel.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "csapi/joining.h"
|
#include "csapi/joining.h"
|
||||||
@@ -198,6 +199,7 @@ int main(int argc, char *argv[])
|
|||||||
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
|
qmlRegisterType<UserListModel>("org.kde.neochat", 1, 0, "UserListModel");
|
||||||
qmlRegisterType<CustomEmojiModel>("org.kde.neochat", 1, 0, "CustomEmojiModel");
|
qmlRegisterType<CustomEmojiModel>("org.kde.neochat", 1, 0, "CustomEmojiModel");
|
||||||
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
|
qmlRegisterType<MessageEventModel>("org.kde.neochat", 1, 0, "MessageEventModel");
|
||||||
|
qmlRegisterType<CollapseStateProxyModel>("org.kde.neochat", 1, 0, "CollapseStateProxyModel");
|
||||||
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
|
qmlRegisterType<MessageFilterModel>("org.kde.neochat", 1, 0, "MessageFilterModel");
|
||||||
qmlRegisterType<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel");
|
qmlRegisterType<PublicRoomListModel>("org.kde.neochat", 1, 0, "PublicRoomListModel");
|
||||||
qmlRegisterType<UserDirectoryListModel>("org.kde.neochat", 1, 0, "UserDirectoryListModel");
|
qmlRegisterType<UserDirectoryListModel>("org.kde.neochat", 1, 0, "UserDirectoryListModel");
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
|||||||
roles[SourceRole] = "source";
|
roles[SourceRole] = "source";
|
||||||
roles[MimeTypeRole] = "mimeType";
|
roles[MimeTypeRole] = "mimeType";
|
||||||
roles[FormattedBodyRole] = "formattedBody";
|
roles[FormattedBodyRole] = "formattedBody";
|
||||||
|
roles[AuthorIdRole] = "authorId";
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,6 +768,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
if (role == AuthorIdRole) {
|
||||||
|
return evt.senderId();
|
||||||
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ public:
|
|||||||
|
|
||||||
// For debugging
|
// For debugging
|
||||||
EventResolvedTypeRole,
|
EventResolvedTypeRole,
|
||||||
|
AuthorIdRole,
|
||||||
|
LastRole, // Keep this last
|
||||||
};
|
};
|
||||||
Q_ENUM(EventRoles)
|
Q_ENUM(EventRoles)
|
||||||
|
|
||||||
|
|||||||
@@ -396,6 +396,18 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
|||||||
[this](const RoomMemberEvent &e) {
|
[this](const RoomMemberEvent &e) {
|
||||||
// FIXME: Rewind to the name that was at the time of this event
|
// FIXME: Rewind to the name that was at the time of this event
|
||||||
auto subjectName = this->htmlSafeMemberName(e.userId());
|
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
|
// The below code assumes senderName output in AuthorRole
|
||||||
switch (e.membership()) {
|
switch (e.membership()) {
|
||||||
case MembershipType::Invite:
|
case MembershipType::Invite:
|
||||||
|
|||||||
Reference in New Issue
Block a user