Enable ending polls

This commit is contained in:
James Graham
2025-03-28 13:41:46 +00:00
parent fadb5725e0
commit 42fab806c6
9 changed files with 113 additions and 50 deletions

View File

@@ -58,6 +58,14 @@ PollEndEvent::PollEndEvent(const QJsonObject &obj)
{
}
PollEndEvent::PollEndEvent(const QString &pollStartEventId, const QString &endText)
: RoomEvent(basicJson(TypeId,
{{"org.matrix.msc1767.text"_L1, endText},
{"org.matrix.msc3381.poll.end"_L1, QJsonObject{}},
{"m.relates_to"_L1, QJsonObject{{"rel_type"_L1, "m.reference"_L1}, {"event_id"_L1, pollStartEventId}}}}))
{
}
std::optional<EventRelation> PollEndEvent::relatesTo() const
{
return contentPart<std::optional<EventRelation>>(RelatesToKey);

View File

@@ -225,6 +225,7 @@ class PollEndEvent : public RoomEvent
public:
QUO_EVENT(PollEndEvent, "org.matrix.msc3381.poll.end");
explicit PollEndEvent(const QJsonObject &obj);
explicit PollEndEvent(const QString &pollStartEventId, const QString &endText);
/**
* @brief The EventRelation pointing to the PollStartEvent.

View File

@@ -238,6 +238,10 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const
return {};
}
if (role == IsPollRole) {
return event->get().is<PollStartEvent>();
}
if (role == ShowSectionRole) {
for (auto r = row + 1; r < rowCount(); ++r) {
auto i = index(r);
@@ -316,6 +320,7 @@ QHash<int, QByteArray> MessageModel::roleNames() const
roles[ProgressInfoRole] = "progressInfo";
roles[IsThreadedRole] = "isThreaded";
roles[ThreadRootRole] = "threadRoot";
roles[IsPollRole] = "isPoll";
roles[ShowSectionRole] = "showSection";
roles[ReadMarkersRole] = "readMarkers";
roles[ShowReadMarkersRole] = "showReadMarkers";

View File

@@ -72,6 +72,7 @@ public:
IsThreadedRole, /**< Whether the message is in a thread. */
ThreadRootRole, /**< The Matrix ID of the thread root message, if any . */
IsPollRole, /**< Whether the message is a poll. */
ShowSectionRole, /**< Whether the section header should be shown. */

View File

@@ -502,7 +502,7 @@ public:
*
* @sa PollHandler
*/
PollHandler *poll(const QString &eventId);
Q_INVOKABLE PollHandler *poll(const QString &eventId);
Q_INVOKABLE void postPoll(PollKind::Kind kind, const QString &question, const QList<QString> &answers);

View File

@@ -3,6 +3,8 @@
#include "pollhandler.h"
#include <KLocalization>
#include "events/pollevent.h"
#include "neochatroom.h"
#include "pollanswermodel.h"
@@ -24,6 +26,7 @@ PollHandler::PollHandler(NeoChatRoom *room, const QString &pollStartId)
if (room != nullptr) {
connect(room, &NeoChatRoom::aboutToAddNewMessages, this, &PollHandler::updatePoll);
connect(room, &NeoChatRoom::pendingEventAboutToAdd, this, &PollHandler::handleEvent);
checkLoadRelations();
}
}
@@ -37,34 +40,8 @@ void PollHandler::updatePoll(Quotient::RoomEventsRange events)
if (pollStartEvent == nullptr) {
return;
}
for (const auto &event : events) {
if (event->is<PollEndEvent>()) {
const auto endEvent = eventCast<const PollEndEvent>(event);
if (endEvent->relatesTo()->eventId != m_pollStartId) {
continue;
}
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) {
continue;
}
auto userPl = plEvent->powerLevelForUser(event->senderId());
if (event->senderId() == pollStartEvent->senderId() || userPl >= plEvent->redact()) {
m_hasEnded = true;
m_endedTimestamp = event->originTimestamp();
Q_EMIT hasEndedChanged();
}
}
if (event->is<PollResponseEvent>()) {
handleResponse(eventCast<const PollResponseEvent>(event));
}
if (event->contentPart<QJsonObject>("m.relates_to"_L1).contains("rel_type"_L1)
&& event->contentPart<QJsonObject>("m.relates_to"_L1)["rel_type"_L1].toString() == "m.replace"_L1
&& event->contentPart<QJsonObject>("m.relates_to"_L1)["event_id"_L1].toString() == pollStartEvent->id()) {
Q_EMIT questionChanged();
Q_EMIT answersChanged();
}
handleEvent(event.get());
}
}
@@ -79,32 +56,51 @@ void PollHandler::checkLoadRelations()
}
auto job = room->connection()->callApi<GetRelatingEventsJob>(room->id(), pollStartEvent->id());
connect(job, &BaseJob::success, this, [this, job, room, pollStartEvent]() {
connect(job, &BaseJob::success, this, [this, job]() {
for (const auto &event : job->chunk()) {
if (event->is<PollEndEvent>()) {
const auto endEvent = eventCast<const PollEndEvent>(event);
if (endEvent->relatesTo()->eventId != m_pollStartId) {
continue;
}
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) {
continue;
}
auto userPl = plEvent->powerLevelForUser(event->senderId());
if (event->senderId() == pollStartEvent->senderId() || userPl >= plEvent->redact()) {
m_hasEnded = true;
m_endedTimestamp = event->originTimestamp();
Q_EMIT hasEndedChanged();
}
}
if (event->is<PollResponseEvent>()) {
handleResponse(eventCast<const PollResponseEvent>(event));
}
handleEvent(event.get());
}
});
}
void PollHandler::handleEvent(Quotient::RoomEvent *event)
{
// This function will never be called if the PollHandler was not initialized with
// a NeoChatRoom as parent and a PollStartEvent so no need to null check.
const auto room = dynamic_cast<NeoChatRoom *>(parent());
auto pollStartEvent = eventCast<const PollStartEvent>(room->getEvent(m_pollStartId).first);
if (pollStartEvent == nullptr) {
return;
}
if (event->is<PollEndEvent>()) {
const auto endEvent = eventCast<const PollEndEvent>(event);
if (endEvent->relatesTo()->eventId != m_pollStartId) {
return;
}
auto plEvent = room->currentState().get<RoomPowerLevelsEvent>();
if (!plEvent) {
return;
}
auto userPl = plEvent->powerLevelForUser(event->senderId());
if (event->senderId() == pollStartEvent->senderId() || userPl >= plEvent->redact()) {
m_hasEnded = true;
m_endedTimestamp = event->originTimestamp();
Q_EMIT hasEndedChanged();
}
}
if (event->is<PollResponseEvent>()) {
handleResponse(eventCast<const PollResponseEvent>(event));
}
if (event->contentPart<QJsonObject>("m.relates_to"_L1).contains("rel_type"_L1)
&& event->contentPart<QJsonObject>("m.relates_to"_L1)["rel_type"_L1].toString() == "m.replace"_L1
&& event->contentPart<QJsonObject>("m.relates_to"_L1)["event_id"_L1].toString() == pollStartEvent->id()) {
Q_EMIT questionChanged();
Q_EMIT answersChanged();
}
}
void PollHandler::handleResponse(const Quotient::PollResponseEvent *event)
{
if (event == nullptr) {
@@ -292,4 +288,32 @@ bool PollHandler::hasEnded() const
return m_hasEnded;
}
void PollHandler::endPoll() const
{
room()->post<PollEndEvent>(m_pollStartId, endText());
}
QString PollHandler::endText() const
{
auto room = dynamic_cast<NeoChatRoom *>(parent());
if (room == nullptr) {
return {};
}
auto pollStartEvent = eventCast<const PollStartEvent>(room->getEvent(m_pollStartId).first);
if (pollStartEvent == nullptr) {
return {};
}
int maxCount = 0;
QString answerText = {};
for (const auto &answer : pollStartEvent->answers()) {
const auto currentCount = answerCountAtId(answer.id);
if (currentCount > maxCount) {
maxCount = currentCount;
answerText = answer.text;
}
}
return i18nc("%1 is the poll answer that had the most votes", "The poll has ended. Top answer: %1", answerText);
}
#include "moc_pollhandler.cpp"

View File

@@ -106,6 +106,11 @@ public:
*/
Q_INVOKABLE void sendPollAnswer(const QString &eventId, const QString &answerId);
/**
* @brief Send the PollEndEvent.
*/
Q_INVOKABLE void endPoll() const;
Q_SIGNALS:
void questionChanged();
void hasEndedChanged();
@@ -123,12 +128,14 @@ private:
void updatePoll(Quotient::RoomEventsRange events);
void checkLoadRelations();
void handleEvent(Quotient::RoomEvent *event);
void handleResponse(const Quotient::PollResponseEvent *event);
QHash<QString, QDateTime> m_selectionTimestamps;
QHash<QString, QStringList> m_selections;
bool m_hasEnded = false;
QDateTime m_endedTimestamp;
QString endText() const;
QPointer<PollAnswerModel> m_answerModel;
};

View File

@@ -136,7 +136,7 @@ QQC2.Control {
}
QQC2.Button {
visible: NeoChatConfig.threads && !root.currentRoom.readOnly
visible: NeoChatConfig.threads && !root.currentRoom.readOnly && !root.delegate?.isPoll
text: i18n("Reply in Thread")
icon.name: "dialog-messages"
display: QQC2.Button.IconOnly
@@ -153,6 +153,18 @@ QQC2.Control {
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.Button {
visible: (root.delegate?.isPoll ?? false) && !root.currentRoom.poll(root.delegate.eventId).hasEnded
text: i18n("End Poll")
icon.name: "gtk-stop"
display: QQC2.ToolButton.IconOnly
onClicked: root.currentRoom.poll(root.delegate.eventId).endPoll()
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
EmojiDialog {
id: emojiDialog
currentRoom: root.currentRoom

View File

@@ -92,6 +92,11 @@ TimelineDelegate {
*/
required property string threadRoot
/**
* @brief Whether the message in a poll.
*/
required property bool isPoll
/**
* @brief Whether this message has a local user mention.
*/