diff --git a/src/events/pollevent.cpp b/src/events/pollevent.cpp index b8cbd61d9..0146a91c7 100644 --- a/src/events/pollevent.cpp +++ b/src/events/pollevent.cpp @@ -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 PollEndEvent::relatesTo() const { return contentPart>(RelatesToKey); diff --git a/src/events/pollevent.h b/src/events/pollevent.h index ac99fd5ed..0ee10a1a2 100644 --- a/src/events/pollevent.h +++ b/src/events/pollevent.h @@ -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. diff --git a/src/models/messagemodel.cpp b/src/models/messagemodel.cpp index 99202e222..7e0e76a7d 100644 --- a/src/models/messagemodel.cpp +++ b/src/models/messagemodel.cpp @@ -238,6 +238,10 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const return {}; } + if (role == IsPollRole) { + return event->get().is(); + } + if (role == ShowSectionRole) { for (auto r = row + 1; r < rowCount(); ++r) { auto i = index(r); @@ -316,6 +320,7 @@ QHash MessageModel::roleNames() const roles[ProgressInfoRole] = "progressInfo"; roles[IsThreadedRole] = "isThreaded"; roles[ThreadRootRole] = "threadRoot"; + roles[IsPollRole] = "isPoll"; roles[ShowSectionRole] = "showSection"; roles[ReadMarkersRole] = "readMarkers"; roles[ShowReadMarkersRole] = "showReadMarkers"; diff --git a/src/models/messagemodel.h b/src/models/messagemodel.h index dc411aa84..4df174e68 100644 --- a/src/models/messagemodel.h +++ b/src/models/messagemodel.h @@ -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. */ diff --git a/src/neochatroom.h b/src/neochatroom.h index 0b5475f39..484b1014b 100644 --- a/src/neochatroom.h +++ b/src/neochatroom.h @@ -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 &answers); diff --git a/src/pollhandler.cpp b/src/pollhandler.cpp index c80ee0b11..0374d8551 100644 --- a/src/pollhandler.cpp +++ b/src/pollhandler.cpp @@ -3,6 +3,8 @@ #include "pollhandler.h" +#include + #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()) { - const auto endEvent = eventCast(event); - if (endEvent->relatesTo()->eventId != m_pollStartId) { - continue; - } - - auto plEvent = room->currentState().get(); - 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()) { - handleResponse(eventCast(event)); - } - if (event->contentPart("m.relates_to"_L1).contains("rel_type"_L1) - && event->contentPart("m.relates_to"_L1)["rel_type"_L1].toString() == "m.replace"_L1 - && event->contentPart("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(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()) { - const auto endEvent = eventCast(event); - if (endEvent->relatesTo()->eventId != m_pollStartId) { - continue; - } - - auto plEvent = room->currentState().get(); - 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()) { - handleResponse(eventCast(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(parent()); + auto pollStartEvent = eventCast(room->getEvent(m_pollStartId).first); + if (pollStartEvent == nullptr) { + return; + } + + if (event->is()) { + const auto endEvent = eventCast(event); + if (endEvent->relatesTo()->eventId != m_pollStartId) { + return; + } + + auto plEvent = room->currentState().get(); + 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()) { + handleResponse(eventCast(event)); + } + if (event->contentPart("m.relates_to"_L1).contains("rel_type"_L1) + && event->contentPart("m.relates_to"_L1)["rel_type"_L1].toString() == "m.replace"_L1 + && event->contentPart("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(m_pollStartId, endText()); +} + +QString PollHandler::endText() const +{ + auto room = dynamic_cast(parent()); + if (room == nullptr) { + return {}; + } + auto pollStartEvent = eventCast(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" diff --git a/src/pollhandler.h b/src/pollhandler.h index 79f35df9d..1de60d76b 100644 --- a/src/pollhandler.h +++ b/src/pollhandler.h @@ -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 m_selectionTimestamps; QHash m_selections; bool m_hasEnded = false; QDateTime m_endedTimestamp; + QString endText() const; QPointer m_answerModel; }; diff --git a/src/qml/HoverActions.qml b/src/qml/HoverActions.qml index 856cbd3ec..cbcc313a5 100644 --- a/src/qml/HoverActions.qml +++ b/src/qml/HoverActions.qml @@ -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 diff --git a/src/timeline/MessageDelegate.qml b/src/timeline/MessageDelegate.qml index 5854ba135..d63a4c91a 100644 --- a/src/timeline/MessageDelegate.qml +++ b/src/timeline/MessageDelegate.qml @@ -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. */