Foldable State Events
Implement folding sections for multiple consecutive state events. This also reworks some of the aggregated text output: - Only new days cause a new section - The aggregated text starts with either a single username or n users - If the same user did the same action mutltiple times it will be in the aggregated text a user did x n times - When there are multiple authors in a block with multiple state event types it will use or rather than and before the last event. e.g 3 user left the room or joined the room. Folded  Unfolded  Implements network/neochat#90
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
|
||||||
#include "collapsestateproxymodel.h"
|
#include "collapsestateproxymodel.h"
|
||||||
|
#include "messageeventmodel.h"
|
||||||
|
|
||||||
#include <KLocalizedString>
|
#include <KLocalizedString>
|
||||||
|
|
||||||
@@ -12,18 +13,17 @@ bool CollapseStateProxyModel::filterAcceptsRow(int source_row, const QModelIndex
|
|||||||
!= MessageEventModel::DelegateType::State // If this is not a state, show it
|
!= MessageEventModel::DelegateType::State // If this is not a state, show it
|
||||||
|| sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::EventTypeRole)
|
|| sourceModel()->data(sourceModel()->index(source_row + 1, 0), MessageEventModel::EventTypeRole)
|
||||||
!= MessageEventModel::DelegateType::State // If this is the first state in a block, show it. TODO hidden events?
|
!= MessageEventModel::DelegateType::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::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
|
QVariant CollapseStateProxyModel::data(const QModelIndex &index, int role) const
|
||||||
{
|
{
|
||||||
if (role == AggregateDisplayRole) {
|
if (role == AggregateDisplayRole) {
|
||||||
return aggregateEventToString(mapToSource(index).row());
|
return aggregateEventToString(mapToSource(index).row());
|
||||||
|
} else if (role == StateEventsRole) {
|
||||||
|
return stateEventsList(mapToSource(index).row());
|
||||||
|
} else if (role == AuthorListRole) {
|
||||||
|
return authorList(mapToSource(index).row());
|
||||||
}
|
}
|
||||||
return sourceModel()->data(mapToSource(index), role);
|
return sourceModel()->data(mapToSource(index), role);
|
||||||
}
|
}
|
||||||
@@ -32,27 +32,29 @@ QHash<int, QByteArray> CollapseStateProxyModel::roleNames() const
|
|||||||
{
|
{
|
||||||
auto roles = sourceModel()->roleNames();
|
auto roles = sourceModel()->roleNames();
|
||||||
roles[AggregateDisplayRole] = "aggregateDisplay";
|
roles[AggregateDisplayRole] = "aggregateDisplay";
|
||||||
|
roles[StateEventsRole] = "stateEvents";
|
||||||
|
roles[AuthorListRole] = "authorList";
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
|
QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
|
||||||
{
|
{
|
||||||
QStringList parts;
|
QStringList parts;
|
||||||
|
QVariantList uniqueAuthors;
|
||||||
for (int i = sourceRow; i >= 0; i--) {
|
for (int i = sourceRow; i >= 0; i--) {
|
||||||
parts += sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString();
|
parts += sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::GenericDisplayRole).toString();
|
||||||
if (sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::EventTypeRole)
|
QVariant nextAuthor = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
|
||||||
|
if (!uniqueAuthors.contains(nextAuthor)) {
|
||||||
|
uniqueAuthors.append(nextAuthor);
|
||||||
|
}
|
||||||
|
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole)
|
||||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
!= MessageEventModel::DelegateType::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
|
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||||
) {
|
) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
parts.sort(); // Sort them so that all identical events can be collected.
|
||||||
if (!parts.isEmpty()) {
|
if (!parts.isEmpty()) {
|
||||||
QStringList chunks;
|
QStringList chunks;
|
||||||
while (!parts.isEmpty()) {
|
while (!parts.isEmpty()) {
|
||||||
@@ -64,18 +66,27 @@ QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
|
|||||||
parts.removeFirst();
|
parts.removeFirst();
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
if (count > 1) {
|
if (count > 1 && uniqueAuthors.length() == 1) {
|
||||||
chunks.last() += i18ncp("[user did something] n times", " %1 time", " %1 times", count);
|
chunks.last() += i18ncp("n times", " %1 time ", " %1 times ", count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QString text = chunks.takeFirst();
|
chunks.removeDuplicates();
|
||||||
|
QString text = "<style>a {text-decoration: none;}</style>"; // There can be links in the event text so make sure all are styled.
|
||||||
|
// The author text is either "n users" if > 1 user or the matrix.to link to a single user.
|
||||||
|
QString userText = uniqueAuthors.length() > 1 ? i18ncp("n users", " %1 user ", " %1 users ", uniqueAuthors.length())
|
||||||
|
: QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a> ")
|
||||||
|
.arg(uniqueAuthors[0].toMap()["id"].toString(),
|
||||||
|
uniqueAuthors[0].toMap()["color"].toString(),
|
||||||
|
uniqueAuthors[0].toMap()["displayName"].toString());
|
||||||
|
text += userText;
|
||||||
|
text += chunks.takeFirst();
|
||||||
|
|
||||||
if (chunks.size() > 0) {
|
if (chunks.size() > 0) {
|
||||||
while (chunks.size() > 1) {
|
while (chunks.size() > 1) {
|
||||||
text += i18nc("[action 1], [action 2 and action 3]", ", ");
|
text += i18nc("[action 1], [action 2 and/or action 3]", ", ");
|
||||||
text += chunks.takeFirst();
|
text += chunks.takeFirst();
|
||||||
}
|
}
|
||||||
text += i18nc("[action 1, action 2] and [action 3]", " and ");
|
text += uniqueAuthors.length() > 1 ? i18nc("[action 1, action 2] or [action 3]", " or ") : i18nc("[action 1, action 2] and [action 3]", " and ");
|
||||||
text += chunks.takeFirst();
|
text += chunks.takeFirst();
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
@@ -83,3 +94,41 @@ QString CollapseStateProxyModel::aggregateEventToString(int sourceRow) const
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVariantList CollapseStateProxyModel::stateEventsList(int sourceRow) const
|
||||||
|
{
|
||||||
|
QVariantList stateEvents;
|
||||||
|
for (int i = sourceRow; i >= 0; i--) {
|
||||||
|
auto nextState = QVariantMap{
|
||||||
|
{"author", sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole)},
|
||||||
|
{"authorDisplayName", sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorDisplayNameRole).toString()},
|
||||||
|
{"text", sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()},
|
||||||
|
};
|
||||||
|
stateEvents.append(nextState);
|
||||||
|
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole)
|
||||||
|
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||||
|
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stateEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList CollapseStateProxyModel::authorList(int sourceRow) const
|
||||||
|
{
|
||||||
|
QVariantList uniqueAuthors;
|
||||||
|
for (int i = sourceRow; i >= 0; i--) {
|
||||||
|
QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole);
|
||||||
|
if (!uniqueAuthors.contains(nextAvatar)) {
|
||||||
|
uniqueAuthors.append(nextAvatar);
|
||||||
|
}
|
||||||
|
if (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::EventTypeRole)
|
||||||
|
!= MessageEventModel::DelegateType::State // If it's not a state event
|
||||||
|
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueAuthors;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,10 +12,29 @@ class CollapseStateProxyModel : public QSortFilterProxyModel
|
|||||||
public:
|
public:
|
||||||
enum Roles {
|
enum Roles {
|
||||||
AggregateDisplayRole = MessageEventModel::LastRole + 1,
|
AggregateDisplayRole = MessageEventModel::LastRole + 1,
|
||||||
|
StateEventsRole,
|
||||||
|
AuthorListRole,
|
||||||
};
|
};
|
||||||
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
|
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
|
||||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||||
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief QString aggregating the text of consecutive state events starting at row.
|
||||||
|
*
|
||||||
|
* If state events happen on different days they will be split into two aggregate
|
||||||
|
* events.
|
||||||
|
*/
|
||||||
[[nodiscard]] QString aggregateEventToString(int row) const;
|
[[nodiscard]] QString aggregateEventToString(int row) const;
|
||||||
|
/**
|
||||||
|
* @brief Return a list of consecutive state events starting at row.
|
||||||
|
*
|
||||||
|
* If state events happen on different days they will be split into two aggregate
|
||||||
|
* events.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QVariantList stateEventsList(int row) const;
|
||||||
|
/**
|
||||||
|
* @brief List of unique authors for the aggregate state events starting at row.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QVariantList authorList(int row) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
|||||||
roles[IsNameChangeRole] = "isNameChange";
|
roles[IsNameChangeRole] = "isNameChange";
|
||||||
roles[IsAvatarChangeRole] = "isAvatarChange";
|
roles[IsAvatarChangeRole] = "isAvatarChange";
|
||||||
roles[IsRedactedRole] = "isRedacted";
|
roles[IsRedactedRole] = "isRedacted";
|
||||||
|
roles[GenericDisplayRole] = "genericDisplay";
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,6 +463,14 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
return m_currentRoom->eventToString(evt, Qt::RichText);
|
return m_currentRoom->eventToString(evt, Qt::RichText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (role == GenericDisplayRole) {
|
||||||
|
if (evt.isRedacted()) {
|
||||||
|
return i18n("<i>[This message was deleted]</i>");
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_currentRoom->eventToGenericString(evt);
|
||||||
|
}
|
||||||
|
|
||||||
if (role == FormattedBodyRole) {
|
if (role == FormattedBodyRole) {
|
||||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
||||||
if (e->hasTextContent() && e->mimeType().name() != "text/plain") {
|
if (e->hasTextContent() && e->mimeType().name() != "text/plain") {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public:
|
|||||||
AnnotationRole,
|
AnnotationRole,
|
||||||
UserMarkerRole,
|
UserMarkerRole,
|
||||||
FormattedBodyRole,
|
FormattedBodyRole,
|
||||||
|
GenericDisplayRole,
|
||||||
|
|
||||||
MimeTypeRole,
|
MimeTypeRole,
|
||||||
FileMimetypeIcon,
|
FileMimetypeIcon,
|
||||||
|
|||||||
@@ -658,6 +658,135 @@ QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format,
|
|||||||
i18n("Unknown event"));
|
i18n("Unknown event"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
|
||||||
|
{
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
return switchOnType(
|
||||||
|
#else
|
||||||
|
return visit(
|
||||||
|
#endif
|
||||||
|
evt,
|
||||||
|
[](const RoomMessageEvent &e) {
|
||||||
|
Q_UNUSED(e)
|
||||||
|
return i18n("sent a message");
|
||||||
|
},
|
||||||
|
[](const StickerEvent &e) {
|
||||||
|
Q_UNUSED(e)
|
||||||
|
return i18n("sent a sticker");
|
||||||
|
},
|
||||||
|
[](const RoomMemberEvent &e) {
|
||||||
|
switch (e.membership()) {
|
||||||
|
case MembershipType::Invite:
|
||||||
|
if (e.repeatsState()) {
|
||||||
|
return i18n("reinvited someone to the room");
|
||||||
|
}
|
||||||
|
Q_FALLTHROUGH();
|
||||||
|
case MembershipType::Join: {
|
||||||
|
QString text{};
|
||||||
|
// Part 1: invites and joins
|
||||||
|
if (e.repeatsState()) {
|
||||||
|
text = i18n("joined the room (repeated)");
|
||||||
|
} else if (e.changesMembership()) {
|
||||||
|
text = e.membership() == MembershipType::Invite ? i18n("invited someone to the room") : i18n("joined the room");
|
||||||
|
}
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
// Part 2: profile changes of joined members
|
||||||
|
if (e.isRename()) {
|
||||||
|
if (e.displayName().isEmpty()) {
|
||||||
|
text = i18nc("their refers to a singular user", "cleared their display name");
|
||||||
|
} else {
|
||||||
|
text = i18nc("their refers to a singular user", "changed their display name");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.isAvatarUpdate()) {
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
text += i18n(" and ");
|
||||||
|
}
|
||||||
|
if (e.avatarUrl().isEmpty()) {
|
||||||
|
text += i18nc("their refers to a singular user", "cleared their avatar");
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
} else if (!e.prevContent()->avatarUrl) {
|
||||||
|
#else
|
||||||
|
} else if (e.prevContent()->avatarUrl.isEmpty()) {
|
||||||
|
#endif
|
||||||
|
text += i18n("set an avatar");
|
||||||
|
} else {
|
||||||
|
text += i18nc("their refers to a singular user", "updated their avatar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
text = i18nc("<user> changed nothing", "changed nothing");
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
case MembershipType::Leave:
|
||||||
|
if (e.prevContent() && e.prevContent()->membership == MembershipType::Invite) {
|
||||||
|
return (e.senderId() != e.userId()) ? i18n("withdrew a user's invitation") : i18n("rejected the invitation");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.prevContent() && e.prevContent()->membership == MembershipType::Ban) {
|
||||||
|
return (e.senderId() != e.userId()) ? i18n("unbanned a user") : i18n("self-unbanned");
|
||||||
|
}
|
||||||
|
return (e.senderId() != e.userId()) ? i18n("put a user out of the room") : i18n("left the room");
|
||||||
|
case MembershipType::Ban:
|
||||||
|
if (e.senderId() != e.userId()) {
|
||||||
|
return i18n("banned a user from the room");
|
||||||
|
} else {
|
||||||
|
return i18n("self-banned from the room");
|
||||||
|
}
|
||||||
|
case MembershipType::Knock: {
|
||||||
|
return i18n("requested an invite");
|
||||||
|
}
|
||||||
|
default:;
|
||||||
|
}
|
||||||
|
return i18n("made something unknown");
|
||||||
|
},
|
||||||
|
[](const RoomCanonicalAliasEvent &e) {
|
||||||
|
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias");
|
||||||
|
},
|
||||||
|
[](const RoomNameEvent &e) {
|
||||||
|
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name");
|
||||||
|
},
|
||||||
|
[](const RoomTopicEvent &e) {
|
||||||
|
return (e.topic().isEmpty()) ? i18n("cleared the topic") : i18n("set the topic");
|
||||||
|
},
|
||||||
|
[](const RoomAvatarEvent &) {
|
||||||
|
return i18n("changed the room avatar");
|
||||||
|
},
|
||||||
|
[](const EncryptionEvent &) {
|
||||||
|
return i18n("activated End-to-End Encryption");
|
||||||
|
},
|
||||||
|
[](const RoomCreateEvent &e) {
|
||||||
|
return e.isUpgrade() ? i18n("upgraded the room version") : i18n("created the room");
|
||||||
|
},
|
||||||
|
[](const RoomPowerLevelsEvent &) {
|
||||||
|
return i18nc("'power level' means permission level", "changed the power levels for this room");
|
||||||
|
},
|
||||||
|
[](const StateEventBase &e) {
|
||||||
|
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
||||||
|
return i18n("changed the server access control lists for this room");
|
||||||
|
}
|
||||||
|
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
|
||||||
|
if (e.fullJson()["unsigned"]["prev_content"].toObject().isEmpty()) {
|
||||||
|
return i18n("added a widget");
|
||||||
|
}
|
||||||
|
if (e.contentJson().isEmpty()) {
|
||||||
|
return i18n("removed a widget");
|
||||||
|
}
|
||||||
|
return i18n("configured a widget");
|
||||||
|
}
|
||||||
|
return i18n("updated the state");
|
||||||
|
},
|
||||||
|
#ifdef QUOTIENT_07
|
||||||
|
[](const PollStartEvent &e) {
|
||||||
|
return i18n("started a poll");
|
||||||
|
},
|
||||||
|
#endif
|
||||||
|
i18n("Unknown event"));
|
||||||
|
}
|
||||||
|
|
||||||
void NeoChatRoom::changeAvatar(const QUrl &localFile)
|
void NeoChatRoom::changeAvatar(const QUrl &localFile)
|
||||||
{
|
{
|
||||||
const auto job = connection()->uploadFile(localFile.toLocalFile());
|
const auto job = connection()->uploadFile(localFile.toLocalFile());
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ public:
|
|||||||
[[nodiscard]] QString avatarMediaId() const;
|
[[nodiscard]] QString avatarMediaId() const;
|
||||||
|
|
||||||
[[nodiscard]] QString eventToString(const Quotient::RoomEvent &evt, Qt::TextFormat format = Qt::PlainText, bool removeReply = true) const;
|
[[nodiscard]] QString eventToString(const Quotient::RoomEvent &evt, Qt::TextFormat format = Qt::PlainText, bool removeReply = true) const;
|
||||||
|
[[nodiscard]] QString eventToGenericString(const Quotient::RoomEvent &evt) const;
|
||||||
|
|
||||||
Q_INVOKABLE [[nodiscard]] bool containsUser(const QString &userID) const;
|
Q_INVOKABLE [[nodiscard]] bool containsUser(const QString &userID) const;
|
||||||
Q_INVOKABLE [[nodiscard]] bool isUserBanned(const QString &user) const;
|
Q_INVOKABLE [[nodiscard]] bool isUserBanned(const QString &user) const;
|
||||||
|
|||||||
47
src/qml/Component/Timeline/StateComponent.qml
Normal file
47
src/qml/Component/Timeline/StateComponent.qml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15 as QQC2
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
import org.kde.kirigami 2.15 as Kirigami
|
||||||
|
|
||||||
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: root
|
||||||
|
property var name
|
||||||
|
property alias avatar: stateAvatar.source
|
||||||
|
property var color
|
||||||
|
property alias text: label.text
|
||||||
|
|
||||||
|
signal avatarClicked()
|
||||||
|
signal linkClicked(string link)
|
||||||
|
|
||||||
|
implicitHeight: Math.max(label.contentHeight, stateAvatar.implicitHeight)
|
||||||
|
|
||||||
|
Kirigami.Avatar {
|
||||||
|
id: stateAvatar
|
||||||
|
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||||
|
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||||
|
|
||||||
|
name: root.name
|
||||||
|
color: root.color
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: avatarClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
id: label
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
textFormat: Text.RichText
|
||||||
|
onLinkActivated: linkClicked(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,9 @@ QQC2.Control {
|
|||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: columnLayout
|
id: columnLayout
|
||||||
|
|
||||||
|
property bool folded: true
|
||||||
|
|
||||||
spacing: sectionVisible ? Kirigami.Units.largeSpacing : 0
|
spacing: sectionVisible ? Kirigami.Units.largeSpacing : 0
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.topMargin: sectionVisible ? 0 : Kirigami.Units.largeSpacing
|
anchors.topMargin: sectionVisible ? 0 : Kirigami.Units.largeSpacing
|
||||||
@@ -63,45 +66,73 @@ QQC2.Control {
|
|||||||
visible: sectionVisible
|
visible: sectionVisible
|
||||||
labelText: sectionVisible ? section : ""
|
labelText: sectionVisible ? section : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: rowLayout
|
|
||||||
implicitHeight: label.contentHeight
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing * 1.5 + (Config.compactLayout ? Kirigami.Units.largeSpacing * 1.25 : 0)
|
Layout.leftMargin: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing * 1.5 + (Config.compactLayout ? Kirigami.Units.largeSpacing * 1.25 : 0)
|
||||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||||
|
visible: stateEventRepeater.count !== 1
|
||||||
|
|
||||||
Kirigami.Avatar {
|
Flow {
|
||||||
id: icon
|
visible: columnLayout.folded
|
||||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
spacing: -Kirigami.Units.iconSizes.small / 2
|
||||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
Repeater {
|
||||||
|
model: authorList
|
||||||
|
delegate: Kirigami.Avatar {
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.small
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.small
|
||||||
|
|
||||||
name: author.displayName
|
name: modelData.displayName
|
||||||
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
|
source: modelData.avatarMediaId ? ("image://mxc/" + modelData.avatarMediaId) : ""
|
||||||
color: author.color
|
color: modelData.color
|
||||||
|
}
|
||||||
Component {
|
|
||||||
id: userDetailDialog
|
|
||||||
|
|
||||||
UserDetailDialog {}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {room: currentRoom, user: author.object, displayName: author.displayName, avatarMediaId: author.avatarMediaId, avatarUrl: author.avatarUrl}).open()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
id: label
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
wrapMode: Text.WordWrap
|
visible: columnLayout.folded
|
||||||
|
|
||||||
|
text: aggregateDisplay
|
||||||
|
elide: Qt.ElideRight
|
||||||
textFormat: Text.RichText
|
textFormat: Text.RichText
|
||||||
text: `<style>a {text-decoration: none;}</style><a href="https://matrix.to/#/${author.id}" style="color: ${author.color}">${model.authorDisplayName}</a> ${aggregateDisplay}`
|
wrapMode: Text.WordWrap
|
||||||
onLinkActivated: RoomManager.openResource(link)
|
onLinkActivated: RoomManager.openResource(link)
|
||||||
}
|
}
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: !columnLayout.folded
|
||||||
|
}
|
||||||
|
QQC2.ToolButton {
|
||||||
|
icon.name: (!columnLayout.folded ? "go-up" : "go-down")
|
||||||
|
icon.width: Kirigami.Units.iconSizes.small
|
||||||
|
icon.height: Kirigami.Units.iconSizes.small
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
columnLayout.toggleFolded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Repeater {
|
||||||
|
id: stateEventRepeater
|
||||||
|
model: stateEvents
|
||||||
|
delegate: StateComponent {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing * 1.5 + (Config.compactLayout ? Kirigami.Units.largeSpacing * 1.25 : 0)
|
||||||
|
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||||
|
visible: !columnLayout.folded || stateEventRepeater.count === 1
|
||||||
|
|
||||||
|
name: modelData.author.displayName
|
||||||
|
avatar: modelData.author.avatarMediaId ? ("image://mxc/" + modelData.author.avatarMediaId) : ""
|
||||||
|
color: modelData.author.color
|
||||||
|
text: `<style>a {text-decoration: none;}</style><a href="https://matrix.to/#/${modelData.author.id}" style="color: ${modelData.author.color}">${modelData.authorDisplayName}</a> ${modelData.text}`
|
||||||
|
|
||||||
|
onAvatarClicked: RoomManager.openResource("https://matrix.to/#/" + modelData.author.id)
|
||||||
|
onLinkClicked: RoomManager.openResource(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFolded() {
|
||||||
|
folded = !folded
|
||||||
|
foldedChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
<file alias="ReadMarkerDelegate.qml">qml/Component/Timeline/ReadMarkerDelegate.qml</file>
|
<file alias="ReadMarkerDelegate.qml">qml/Component/Timeline/ReadMarkerDelegate.qml</file>
|
||||||
<file alias="PollDelegate.qml">qml/Component/Timeline/PollDelegate.qml</file>
|
<file alias="PollDelegate.qml">qml/Component/Timeline/PollDelegate.qml</file>
|
||||||
<file alias="MimeComponent.qml">qml/Component/Timeline/MimeComponent.qml</file>
|
<file alias="MimeComponent.qml">qml/Component/Timeline/MimeComponent.qml</file>
|
||||||
|
<file alias="StateComponent.qml">qml/Component/Timeline/StateComponent.qml</file>
|
||||||
<file alias="LoginStep.qml">qml/Component/Login/LoginStep.qml</file>
|
<file alias="LoginStep.qml">qml/Component/Login/LoginStep.qml</file>
|
||||||
<file alias="Login.qml">qml/Component/Login/Login.qml</file>
|
<file alias="Login.qml">qml/Component/Login/Login.qml</file>
|
||||||
<file alias="Password.qml">qml/Component/Login/Password.qml</file>
|
<file alias="Password.qml">qml/Component/Login/Password.qml</file>
|
||||||
|
|||||||
Reference in New Issue
Block a user