From fadb5725e0291782e046f9cc249875341501e143 Mon Sep 17 00:00:00 2001 From: James Graham Date: Fri, 28 Mar 2025 09:05:39 +0000 Subject: [PATCH] Rework the appearance of poll delegate ![image](/uploads/510e995e15d76ce0566b126a6917a963/image.png){width=541 height=269} --- src/models/pollanswermodel.cpp | 6 ++- src/models/pollanswermodel.h | 1 + src/pollhandler.cpp | 40 ++++++++++++++++ src/pollhandler.h | 16 ++++++- src/timeline/PollComponent.qml | 87 +++++++++++++++++++++++++++------- 5 files changed, 129 insertions(+), 21 deletions(-) diff --git a/src/models/pollanswermodel.cpp b/src/models/pollanswermodel.cpp index 4aa42aa32..977fb4494 100644 --- a/src/models/pollanswermodel.cpp +++ b/src/models/pollanswermodel.cpp @@ -12,7 +12,7 @@ PollAnswerModel::PollAnswerModel(PollHandler *parent) Q_ASSERT(parent != nullptr); connect(parent, &PollHandler::selectionsChanged, this, [this]() { - dataChanged(index(0), index(rowCount() - 1), {CountRole, LocalChoiceRole}); + dataChanged(index(0), index(rowCount() - 1), {CountRole, LocalChoiceRole, IsWinnerRole}); }); connect(parent, &PollHandler::answersChanged, this, [this]() { dataChanged(index(0), index(rowCount() - 1), {TextRole}); @@ -50,6 +50,9 @@ QVariant PollAnswerModel::data(const QModelIndex &index, int role) const } return pollHandler->checkMemberSelectedId(room->localMember().id(), pollHandler->answerAtRow(row).id); } + if (role == IsWinnerRole) { + return pollHandler->winningAnswerIds().contains(pollHandler->answerAtRow(row).id) && pollHandler->hasEnded(); + } return {}; } @@ -72,5 +75,6 @@ QHash PollAnswerModel::roleNames() const {TextRole, "answerText"}, {CountRole, "count"}, {LocalChoiceRole, "localChoice"}, + {IsWinnerRole, "isWinner"}, }; } diff --git a/src/models/pollanswermodel.h b/src/models/pollanswermodel.h index 50d3db015..2452c292c 100644 --- a/src/models/pollanswermodel.h +++ b/src/models/pollanswermodel.h @@ -28,6 +28,7 @@ public: TextRole, /**< The answer text. */ CountRole, /**< The number of people who gave this answer. */ LocalChoiceRole, /**< Whether this option was selected by the local user */ + IsWinnerRole, /**< Whether this option was selected by the local user */ }; Q_ENUM(Roles) diff --git a/src/pollhandler.cpp b/src/pollhandler.cpp index b8f5b378b..c80ee0b11 100644 --- a/src/pollhandler.cpp +++ b/src/pollhandler.cpp @@ -11,6 +11,7 @@ #include #include +#include using namespace Quotient; @@ -133,6 +134,7 @@ void PollHandler::handleResponse(const Quotient::PollResponseEvent *event) } } + Q_EMIT totalCountChanged(); Q_EMIT selectionsChanged(); } @@ -217,6 +219,44 @@ PollAnswerModel *PollHandler::answerModel() return m_answerModel; } +int PollHandler::totalCount() const +{ + int votes = 0; + for (const auto &selection : m_selections) { + votes += selection.size(); + } + return votes; +} + +QStringList PollHandler::winningAnswerIds() const +{ + auto room = dynamic_cast(parent()); + if (room == nullptr) { + return {}; + } + auto pollStartEvent = eventCast(room->getEvent(m_pollStartId).first); + if (pollStartEvent == nullptr) { + return {}; + } + + QStringList currentWinners; + for (const auto &answer : pollStartEvent->answers()) { + if (currentWinners.isEmpty()) { + currentWinners += answer.id; + continue; + } + if (answerCountAtId(currentWinners.first()) < answerCountAtId(answer.id)) { + currentWinners.clear(); + currentWinners += answer.id; + continue; + } + if (answerCountAtId(currentWinners.first()) == answerCountAtId(answer.id)) { + currentWinners += answer.id; + } + } + return currentWinners; +} + void PollHandler::sendPollAnswer(const QString &eventId, const QString &answerId) { Q_ASSERT(eventId.length() > 0); diff --git a/src/pollhandler.h b/src/pollhandler.h index d580e2078..79f35df9d 100644 --- a/src/pollhandler.h +++ b/src/pollhandler.h @@ -58,6 +58,11 @@ class PollHandler : public QObject */ Q_PROPERTY(PollAnswerModel *answerModel READ answerModel CONSTANT) + /** + * @brief The total number of vote responses to the poll. + */ + Q_PROPERTY(int totalCount READ totalCount NOTIFY totalCountChanged) + public: PollHandler() = default; PollHandler(NeoChatRoom *room, const QString &pollStartId); @@ -89,6 +94,13 @@ public: */ bool checkMemberSelectedId(const QString &memberId, const QString &id) const; + int totalCount() const; + + /** + * @brief The current answer IDs with the most votes. + */ + QStringList winningAnswerIds() const; + /** * @brief Send an answer to the poll. */ @@ -97,8 +109,8 @@ public: Q_SIGNALS: void questionChanged(); void hasEndedChanged(); - void answersChanged(); + void totalCountChanged(); /** * @brief Emitted when the selected answers to the poll change. @@ -113,7 +125,7 @@ private: void checkLoadRelations(); void handleResponse(const Quotient::PollResponseEvent *event); QHash m_selectionTimestamps; - QHash> m_selections; + QHash m_selections; bool m_hasEnded = false; QDateTime m_endedTimestamp; diff --git a/src/timeline/PollComponent.qml b/src/timeline/PollComponent.qml index c3665ba7f..f5218aa03 100644 --- a/src/timeline/PollComponent.qml +++ b/src/timeline/PollComponent.qml @@ -3,10 +3,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later import QtQuick -import QtQuick.Controls +import QtQuick.Controls as QQC2 import QtQuick.Layouts import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.delegates as Delegates import org.kde.kirigamiaddons.formcard as FormCard import Quotient @@ -34,50 +35,100 @@ ColumnLayout { Layout.fillWidth: true Layout.maximumWidth: Message.maxContentWidth + Layout.minimumWidth: Message.maxContentWidth spacing: 0 - Label { - id: questionLabel - text: root.pollHandler.question - wrapMode: Text.Wrap + RowLayout { Layout.fillWidth: true - visible: text.length > 0 - } + Kirigami.Icon { + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: implicitWidth + source: "amarok_playcount" + } + QQC2.Label { + id: questionLabel + Layout.fillWidth: true + topPadding: Kirigami.Units.largeSpacing + bottomPadding: Kirigami.Units.largeSpacing + + text: root.pollHandler.question + wrapMode: Text.Wrap + visible: text.length > 0 + } + } Repeater { model: root.pollHandler.answerModel - delegate: FormCard.FormCheckDelegate { + delegate: Delegates.RoundedItemDelegate { id: answerDelegate required property string id required property string answerText required property int count required property bool localChoice + required property bool isWinner Layout.fillWidth: true Layout.leftMargin: -Kirigami.Units.largeSpacing - Kirigami.Units.smallSpacing Layout.rightMargin: -Kirigami.Units.largeSpacing - Kirigami.Units.smallSpacing - checked: answerDelegate.localChoice - onClicked: root.pollHandler.sendPollAnswer(root.eventId, answerDelegate.id) - enabled: !root.pollHandler.hasEnded + highlighted: false + + onClicked: { + if (root.pollHandler.hasEnded) { + return; + } + root.pollHandler.sendPollAnswer(root.eventId, answerDelegate.id); + } text: answerDelegate.answerText topPadding: Kirigami.Units.smallSpacing bottomPadding: Kirigami.Units.smallSpacing - trailing: Label { - visible: root.pollHandler.kind == PollKind.Disclosed || pollHandler.hasEnded - Layout.preferredWidth: contentWidth - text: answerDelegate.count + contentItem: ColumnLayout { + Layout.fillWidth: true + + RowLayout { + Layout.fillWidth: true + + QQC2.CheckBox { + enabled: !root.pollHandler.hasEnded + checked: answerDelegate.localChoice + + onClicked: answerDelegate.clicked() + } + QQC2.Label { + Layout.fillWidth: true + + text: answerDelegate.answerText + } + Kirigami.Icon { + implicitWidth: Kirigami.Units.iconSizes.small + implicitHeight: implicitWidth + visible: answerDelegate.isWinner + source: "favorite-favorited" + } + QQC2.Label { + visible: root.pollHandler.kind == PollKind.Disclosed || pollHandler.hasEnded + horizontalAlignment: Text.AlignRight + text: i18np("%1 Vote", "%1 Votes", answerDelegate.count) + } + } + QQC2.ProgressBar { + id: voteProgress + + Layout.fillWidth: true + to: root.pollHandler.totalCount + value: root.pollHandler.kind == PollKind.Disclosed || pollHandler.hasEnded ? answerDelegate.count : 0 + } } } } - Label { - visible: root.pollHandler.kind == "org.matrix.msc3381.poll.disclosed" || root.pollHandler.hasEnded - text: i18np("Based on votes by %1 user", "Based on votes by %1 users", root.pollHandler.answerCount) + (root.pollHandler.hasEnded ? (" " + i18nc("as in 'this vote has ended'", "(Ended)")) : "") + QQC2.Label { + visible: root.pollHandler.kind == PollKind.Disclosed || root.pollHandler.hasEnded + text: i18np("Based on votes by %1 user", "Based on votes by %1 users", root.pollHandler.totalCount) + (root.pollHandler.hasEnded ? (" " + i18nc("as in 'this vote has ended'", "(Ended)")) : "") font.pointSize: questionLabel.font.pointSize * 0.8 } }