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 std::optional<EventRelation> PollEndEvent::relatesTo() const
{ {
return contentPart<std::optional<EventRelation>>(RelatesToKey); return contentPart<std::optional<EventRelation>>(RelatesToKey);

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,8 @@
#include "pollhandler.h" #include "pollhandler.h"
#include <KLocalization>
#include "events/pollevent.h" #include "events/pollevent.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "pollanswermodel.h" #include "pollanswermodel.h"
@@ -24,6 +26,7 @@ PollHandler::PollHandler(NeoChatRoom *room, const QString &pollStartId)
if (room != nullptr) { if (room != nullptr) {
connect(room, &NeoChatRoom::aboutToAddNewMessages, this, &PollHandler::updatePoll); connect(room, &NeoChatRoom::aboutToAddNewMessages, this, &PollHandler::updatePoll);
connect(room, &NeoChatRoom::pendingEventAboutToAdd, this, &PollHandler::handleEvent);
checkLoadRelations(); checkLoadRelations();
} }
} }
@@ -37,34 +40,8 @@ void PollHandler::updatePoll(Quotient::RoomEventsRange events)
if (pollStartEvent == nullptr) { if (pollStartEvent == nullptr) {
return; return;
} }
for (const auto &event : events) { for (const auto &event : events) {
if (event->is<PollEndEvent>()) { handleEvent(event.get());
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();
}
} }
} }
@@ -79,32 +56,51 @@ void PollHandler::checkLoadRelations()
} }
auto job = room->connection()->callApi<GetRelatingEventsJob>(room->id(), pollStartEvent->id()); 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()) { for (const auto &event : job->chunk()) {
if (event->is<PollEndEvent>()) { handleEvent(event.get());
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));
}
} }
}); });
} }
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) void PollHandler::handleResponse(const Quotient::PollResponseEvent *event)
{ {
if (event == nullptr) { if (event == nullptr) {
@@ -292,4 +288,32 @@ bool PollHandler::hasEnded() const
return m_hasEnded; 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" #include "moc_pollhandler.cpp"

View File

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

View File

@@ -136,7 +136,7 @@ QQC2.Control {
} }
QQC2.Button { QQC2.Button {
visible: NeoChatConfig.threads && !root.currentRoom.readOnly visible: NeoChatConfig.threads && !root.currentRoom.readOnly && !root.delegate?.isPoll
text: i18n("Reply in Thread") text: i18n("Reply in Thread")
icon.name: "dialog-messages" icon.name: "dialog-messages"
display: QQC2.Button.IconOnly display: QQC2.Button.IconOnly
@@ -153,6 +153,18 @@ QQC2.Control {
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay 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 { EmojiDialog {
id: emojiDialog id: emojiDialog
currentRoom: root.currentRoom currentRoom: root.currentRoom

View File

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