Poll Updates and Send Polls
Fix showing polls and update the events and PollHandler to make them easier to work with. Add a PollAnswerModel to visualise poll answers. Enable sending polls.
This commit is contained in:
@@ -8,14 +8,14 @@
|
||||
"answers": [
|
||||
{
|
||||
"id": "option1",
|
||||
"org.matrix.msc1767.text": "option1"
|
||||
"org.matrix.msc1767.text": "option1text"
|
||||
},
|
||||
{
|
||||
"id": "option2",
|
||||
"org.matrix.msc1767.text": "option2"
|
||||
"org.matrix.msc1767.text": "option2text"
|
||||
}
|
||||
],
|
||||
"kind": "org.matrix.msc3381.poll.disclosed",
|
||||
"kind": "org.matrix.msc3381.poll.undisclosed",
|
||||
"max_selections": 1,
|
||||
"question": {
|
||||
"body": "test",
|
||||
|
||||
@@ -41,29 +41,32 @@ void PollHandlerTest::nullObject()
|
||||
auto pollHandler = PollHandler();
|
||||
|
||||
QCOMPARE(pollHandler.hasEnded(), false);
|
||||
QCOMPARE(pollHandler.answerCount(), 0);
|
||||
QCOMPARE(pollHandler.numAnswers(), 0);
|
||||
QCOMPARE(pollHandler.question(), QString());
|
||||
QCOMPARE(pollHandler.options(), QJsonArray());
|
||||
QCOMPARE(pollHandler.answers(), QJsonObject());
|
||||
QCOMPARE(pollHandler.counts(), QJsonObject());
|
||||
QCOMPARE(pollHandler.kind(), QString());
|
||||
QCOMPARE(pollHandler.kind(), PollKind::Disclosed);
|
||||
}
|
||||
|
||||
void PollHandlerTest::poll()
|
||||
{
|
||||
auto startEvent = eventCast<const PollStartEvent>(room->messageEvents().at(0).get());
|
||||
auto pollHandler = PollHandler(room, startEvent);
|
||||
auto pollHandler = PollHandler(room, startEvent->id());
|
||||
|
||||
auto options = QJsonArray{QJsonObject{{"id"_L1, "option1"_L1}, {"org.matrix.msc1767.text"_L1, "option1"_L1}},
|
||||
QJsonObject{{"id"_L1, "option2"_L1}, {"org.matrix.msc1767.text"_L1, "option2"_L1}}};
|
||||
QList<Quotient::EventContent::Answer> options = {EventContent::Answer{"option1"_L1, "option1"_L1}, EventContent::Answer{"option2"_L1, "option2"_L1}};
|
||||
|
||||
const auto answer0 = pollHandler.answerAtRow(0);
|
||||
const auto answer1 = pollHandler.answerAtRow(1);
|
||||
QCOMPARE(pollHandler.hasEnded(), false);
|
||||
QCOMPARE(pollHandler.answerCount(), 0);
|
||||
QCOMPARE(pollHandler.numAnswers(), 2);
|
||||
QCOMPARE(pollHandler.question(), u"test"_s);
|
||||
QCOMPARE(pollHandler.options(), options);
|
||||
QCOMPARE(pollHandler.answers(), QJsonObject());
|
||||
QCOMPARE(pollHandler.counts(), QJsonObject());
|
||||
QCOMPARE(pollHandler.kind(), u"org.matrix.msc3381.poll.disclosed"_s);
|
||||
QCOMPARE(answer0.id, "option1"_L1);
|
||||
QCOMPARE(answer1.id, "option2"_L1);
|
||||
QCOMPARE(answer0.text, "option1text"_L1);
|
||||
QCOMPARE(answer1.text, "option2text"_L1);
|
||||
QCOMPARE(pollHandler.answerCountAtId(answer0.id), 0);
|
||||
QCOMPARE(pollHandler.answerCountAtId(answer1.id), 0);
|
||||
QCOMPARE(pollHandler.checkMemberSelectedId(connection->userId(), answer0.id), false);
|
||||
QCOMPARE(pollHandler.checkMemberSelectedId(connection->userId(), answer1.id), false);
|
||||
QCOMPARE(pollHandler.kind(), PollKind::Undisclosed);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(PollHandlerTest)
|
||||
|
||||
@@ -196,6 +196,8 @@ add_library(neochat STATIC
|
||||
models/pinnedmessagemodel.h
|
||||
models/commonroomsmodel.cpp
|
||||
models/commonroomsmodel.h
|
||||
models/pollanswermodel.cpp
|
||||
models/pollanswermodel.h
|
||||
)
|
||||
|
||||
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
|
||||
@@ -296,6 +298,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
||||
qml/HoverLinkIndicator.qml
|
||||
qml/AvatarNotification.qml
|
||||
qml/ReasonDialog.qml
|
||||
qml/NewPollDialog.qml
|
||||
SOURCES
|
||||
messageattached.cpp
|
||||
messageattached.h
|
||||
|
||||
@@ -113,6 +113,20 @@ QQC2.Control {
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
Kirigami.Action {
|
||||
id: pollButton
|
||||
icon.name: "amarok_playcount"
|
||||
property bool isBusy: false
|
||||
text: i18nc("@action:button", "Create a Poll")
|
||||
displayHint: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onTriggered: {
|
||||
newPollDialog.createObject(QQC2.Overlay.overlay, {
|
||||
room: root.currentRoom
|
||||
}).open();
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
Kirigami.Action {
|
||||
id: sendAction
|
||||
|
||||
@@ -492,6 +506,11 @@ QQC2.Control {
|
||||
LocationChooser {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: newPollDialog
|
||||
NewPollDialog {}
|
||||
}
|
||||
|
||||
CompletionMenu {
|
||||
id: completionMenu
|
||||
chatDocumentHandler: documentHandler
|
||||
|
||||
@@ -2,22 +2,28 @@
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "pollevent.h"
|
||||
#include <Quotient/converters.h>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
PollStartEvent::PollStartEvent(const QJsonObject &obj)
|
||||
: RoomEvent(obj)
|
||||
PollKind::Kind PollStartEvent::kind() const
|
||||
{
|
||||
return content().kind;
|
||||
}
|
||||
|
||||
int PollStartEvent::maxSelections() const
|
||||
{
|
||||
return contentJson()["org.matrix.msc3381.poll.start"_L1]["max_selections"_L1].toInt();
|
||||
return content().maxSelection > 0 ? content().maxSelection : 1;
|
||||
}
|
||||
|
||||
QString PollStartEvent::question() const
|
||||
{
|
||||
return contentJson()["org.matrix.msc3381.poll.start"_L1]["question"_L1]["body"_L1].toString();
|
||||
return content().question;
|
||||
}
|
||||
|
||||
QList<EventContent::Answer> PollStartEvent::answers() const
|
||||
{
|
||||
return content().answers;
|
||||
}
|
||||
|
||||
PollResponseEvent::PollResponseEvent(const QJsonObject &obj)
|
||||
@@ -25,14 +31,34 @@ PollResponseEvent::PollResponseEvent(const QJsonObject &obj)
|
||||
{
|
||||
}
|
||||
|
||||
PollEndEvent::PollEndEvent(const QJsonObject &obj)
|
||||
: RoomEvent(obj)
|
||||
{
|
||||
}
|
||||
|
||||
PollResponseEvent::PollResponseEvent(const QString &pollStartEventId, QStringList responses)
|
||||
: RoomEvent(basicJson(TypeId,
|
||||
{{"org.matrix.msc3381.poll.response"_L1, QJsonObject{{"answers"_L1, QJsonArray::fromStringList(responses)}}},
|
||||
{"m.relates_to"_L1, QJsonObject{{"rel_type"_L1, "m.reference"_L1}, {"event_id"_L1, pollStartEventId}}}}))
|
||||
{
|
||||
}
|
||||
|
||||
QStringList PollResponseEvent::selections() const
|
||||
{
|
||||
const auto jsonSelections = contentPart<QJsonObject>("org.matrix.msc3381.poll.response"_L1)["answers"_L1].toArray();
|
||||
QStringList selections;
|
||||
for (const auto &selection : jsonSelections) {
|
||||
selections += selection.toString();
|
||||
}
|
||||
return selections;
|
||||
}
|
||||
|
||||
std::optional<EventRelation> PollResponseEvent::relatesTo() const
|
||||
{
|
||||
return contentPart<std::optional<EventRelation>>(RelatesToKey);
|
||||
}
|
||||
|
||||
PollEndEvent::PollEndEvent(const QJsonObject &obj)
|
||||
: RoomEvent(obj)
|
||||
{
|
||||
}
|
||||
|
||||
std::optional<EventRelation> PollEndEvent::relatesTo() const
|
||||
{
|
||||
return contentPart<std::optional<EventRelation>>(RelatesToKey);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,148 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include <Quotient/converters.h>
|
||||
#include <Quotient/events/eventrelation.h>
|
||||
#include <Quotient/events/roomevent.h>
|
||||
#include <Quotient/quotient_common.h>
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
/**
|
||||
* @class PollKind
|
||||
*
|
||||
* This class is designed to define the PollKind enumeration.
|
||||
*/
|
||||
class PollKind : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Enum representing the available poll kinds.
|
||||
*/
|
||||
enum Kind {
|
||||
Disclosed, /**< The poll results can been seen after the user votes. */
|
||||
Undisclosed, /**< The poll results can only been seen after the poll ends. */
|
||||
};
|
||||
Q_ENUM(Kind);
|
||||
|
||||
/**
|
||||
* @brief Return the string for the given Kind.
|
||||
*
|
||||
* @sa Kind
|
||||
*/
|
||||
static QString stringForKind(Kind kind)
|
||||
{
|
||||
switch (kind) {
|
||||
case Undisclosed:
|
||||
return "org.matrix.msc3381.poll.undisclosed"_L1;
|
||||
default:
|
||||
return "org.matrix.msc3381.poll.disclosed"_L1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Return the Kind for the given string.
|
||||
*
|
||||
* @sa Kind
|
||||
*/
|
||||
static Kind kindForString(const QString &kindString)
|
||||
{
|
||||
if (kindString == "org.matrix.msc3381.poll.undisclosed"_L1) {
|
||||
return Undisclosed;
|
||||
}
|
||||
return Disclosed;
|
||||
}
|
||||
};
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
namespace EventContent
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief An answer to the poll.
|
||||
*/
|
||||
struct Answer {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString id MEMBER id CONSTANT)
|
||||
Q_PROPERTY(QString text MEMBER text CONSTANT)
|
||||
|
||||
public:
|
||||
QString id;
|
||||
QString text;
|
||||
|
||||
int operator==(const Answer &right) const
|
||||
{
|
||||
return id == right.id && text == right.text;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Struct representing the content of a poll event.
|
||||
*/
|
||||
struct PollStartContent {
|
||||
PollKind::Kind kind;
|
||||
int maxSelection;
|
||||
QString question;
|
||||
QList<EventContent::Answer> answers;
|
||||
};
|
||||
|
||||
} // namespace EventContent
|
||||
|
||||
template<>
|
||||
inline EventContent::Answer fromJson(const QJsonObject &jo)
|
||||
{
|
||||
return EventContent::Answer{fromJson<QString>(jo["id"_L1]), fromJson<QString>(jo["org.matrix.msc1767.text"_L1])};
|
||||
}
|
||||
|
||||
template<>
|
||||
inline auto toJson(const EventContent::Answer &c)
|
||||
{
|
||||
QJsonObject jo;
|
||||
addParam<IfNotEmpty>(jo, "id"_L1, c.id);
|
||||
addParam<IfNotEmpty>(jo, "org.matrix.msc1767.text"_L1, c.text);
|
||||
return jo;
|
||||
}
|
||||
|
||||
template<>
|
||||
inline EventContent::PollStartContent fromJson(const QJsonObject &jo)
|
||||
{
|
||||
return EventContent::PollStartContent{
|
||||
PollKind::kindForString(jo["org.matrix.msc3381.poll.start"_L1]["kind"_L1].toString()),
|
||||
fromJson<int>(jo["org.matrix.msc3381.poll.start"_L1]["max_selections"_L1]),
|
||||
fromJson<QString>(jo["org.matrix.msc3381.poll.start"_L1]["question"_L1]["org.matrix.msc1767.text"_L1]),
|
||||
fromJson<QList<EventContent::Answer>>(jo["org.matrix.msc3381.poll.start"_L1]["answers"_L1]),
|
||||
};
|
||||
}
|
||||
|
||||
template<>
|
||||
inline auto toJson(const EventContent::PollStartContent &c)
|
||||
{
|
||||
QJsonObject innerJo;
|
||||
addParam<IfNotEmpty>(innerJo, "kind"_L1, PollKind::stringForKind(c.kind));
|
||||
addParam(innerJo, "max_selections"_L1, c.maxSelection);
|
||||
if (innerJo["max_selections"_L1].toInt() < 1) {
|
||||
innerJo["max_selections"_L1] = 1;
|
||||
}
|
||||
innerJo.insert("question"_L1, QJsonObject{{"org.matrix.msc1767.text"_L1, c.question}});
|
||||
addParam<IfNotEmpty>(innerJo, "answers"_L1, c.answers);
|
||||
|
||||
QJsonObject jo;
|
||||
auto textString = c.question;
|
||||
for (int i = 0; i < c.answers.length(); ++i) {
|
||||
textString.append("\n%1. %2"_L1.arg(QString::number(i + 1), c.answers.at(i).text));
|
||||
}
|
||||
addParam<IfNotEmpty>(jo, "org.matrix.msc1767.text"_L1, textString);
|
||||
jo.insert("org.matrix.msc3381.poll.start"_L1, innerJo);
|
||||
return jo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class PollStartEvent
|
||||
*
|
||||
@@ -17,11 +155,16 @@ namespace Quotient
|
||||
*
|
||||
* @sa Quotient::RoomEvent
|
||||
*/
|
||||
class PollStartEvent : public RoomEvent
|
||||
class PollStartEvent : public EventTemplate<PollStartEvent, RoomEvent, EventContent::PollStartContent>
|
||||
{
|
||||
public:
|
||||
QUO_EVENT(PollStartEvent, "org.matrix.msc3381.poll.start");
|
||||
explicit PollStartEvent(const QJsonObject &obj);
|
||||
using EventTemplate::EventTemplate;
|
||||
|
||||
/**
|
||||
* @brief The poll kind.
|
||||
*/
|
||||
PollKind::Kind kind() const;
|
||||
|
||||
/**
|
||||
* @brief The maximum number of options a user can select in a poll.
|
||||
@@ -32,6 +175,11 @@ public:
|
||||
* @brief The question being asked in the poll.
|
||||
*/
|
||||
QString question() const;
|
||||
|
||||
/**
|
||||
* @brief The list of answers to the poll.
|
||||
*/
|
||||
QList<EventContent::Answer> answers() const;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -50,6 +198,16 @@ public:
|
||||
QUO_EVENT(PollResponseEvent, "org.matrix.msc3381.poll.response");
|
||||
explicit PollResponseEvent(const QJsonObject &obj);
|
||||
explicit PollResponseEvent(const QString &pollStartEventId, QStringList responses);
|
||||
|
||||
/**
|
||||
* @brief The selected answers to the poll.
|
||||
*/
|
||||
QStringList selections() const;
|
||||
|
||||
/**
|
||||
* @brief The EventRelation pointing to the PollStartEvent.
|
||||
*/
|
||||
std::optional<EventRelation> relatesTo() const;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -67,5 +225,10 @@ class PollEndEvent : public RoomEvent
|
||||
public:
|
||||
QUO_EVENT(PollEndEvent, "org.matrix.msc3381.poll.end");
|
||||
explicit PollEndEvent(const QJsonObject &obj);
|
||||
|
||||
/**
|
||||
* @brief The EventRelation pointing to the PollStartEvent.
|
||||
*/
|
||||
std::optional<EventRelation> relatesTo() const;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ QVariant MessageModel::data(const QModelIndex &idx, int role) const
|
||||
}
|
||||
|
||||
if (role == ContentModelRole) {
|
||||
if (event->get().is<EncryptedEvent>()) {
|
||||
if (event->get().is<EncryptedEvent>() || event->get().is<PollStartEvent>()) {
|
||||
return QVariant::fromValue<MessageContentModel *>(m_room->contentModelForEvent(event->get().id()));
|
||||
}
|
||||
|
||||
@@ -401,18 +401,12 @@ void MessageModel::refreshLastUserEvents(int baseTimelineRow)
|
||||
}
|
||||
}
|
||||
|
||||
void MessageModel::createEventObjects(const Quotient::RoomEvent *event, bool isPending)
|
||||
void MessageModel::createEventObjects(const Quotient::RoomEvent *event)
|
||||
{
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We only create the poll handler for event acknowledged by the server as we need
|
||||
// an ID
|
||||
if (!event->id().isEmpty() && event->is<PollStartEvent>()) {
|
||||
m_room->createPollHandler(eventCast<const PollStartEvent>(event));
|
||||
}
|
||||
|
||||
auto eventId = event->id();
|
||||
auto senderId = event->senderId();
|
||||
if (eventId.isEmpty()) {
|
||||
|
||||
@@ -129,7 +129,7 @@ Q_SIGNALS:
|
||||
* Any model inheriting from MessageModel needs to emit this signal for every
|
||||
* new event it adds.
|
||||
*/
|
||||
void newEventAdded(const Quotient::RoomEvent *event, bool isPending = false);
|
||||
void newEventAdded(const Quotient::RoomEvent *event);
|
||||
|
||||
protected:
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
@@ -154,5 +154,5 @@ private:
|
||||
|
||||
QMap<QString, QSharedPointer<ReadMarkerModel>> m_readMarkerModels;
|
||||
|
||||
void createEventObjects(const Quotient::RoomEvent *event, bool isPending = false);
|
||||
void createEventObjects(const Quotient::RoomEvent *event);
|
||||
};
|
||||
|
||||
@@ -58,7 +58,7 @@ void PinnedMessageModel::fill()
|
||||
connect(job, &BaseJob::success, this, [this, job] {
|
||||
beginInsertRows({}, m_pinnedEvents.size(), m_pinnedEvents.size());
|
||||
m_pinnedEvents.push_back(std::move(fromJson<event_ptr_tt<RoomEvent>>(job->jsonData())));
|
||||
Q_EMIT newEventAdded(m_pinnedEvents.back().get(), false);
|
||||
Q_EMIT newEventAdded(m_pinnedEvents.back().get());
|
||||
endInsertRows();
|
||||
});
|
||||
}
|
||||
|
||||
76
src/models/pollanswermodel.cpp
Normal file
76
src/models/pollanswermodel.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "pollanswermodel.h"
|
||||
|
||||
#include "neochatroom.h"
|
||||
#include "pollhandler.h"
|
||||
|
||||
PollAnswerModel::PollAnswerModel(PollHandler *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
Q_ASSERT(parent != nullptr);
|
||||
|
||||
connect(parent, &PollHandler::selectionsChanged, this, [this]() {
|
||||
dataChanged(index(0), index(rowCount() - 1), {CountRole, LocalChoiceRole});
|
||||
});
|
||||
connect(parent, &PollHandler::answersChanged, this, [this]() {
|
||||
dataChanged(index(0), index(rowCount() - 1), {TextRole});
|
||||
});
|
||||
}
|
||||
|
||||
QVariant PollAnswerModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid));
|
||||
|
||||
const auto row = index.row();
|
||||
if (row < 0 || row >= rowCount()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto pollHandler = dynamic_cast<PollHandler *>(this->parent());
|
||||
if (pollHandler == nullptr) {
|
||||
qWarning() << "PollAnswerModel created with nullptr parent.";
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (role == IdRole) {
|
||||
return pollHandler->answerAtRow(row).id;
|
||||
}
|
||||
if (role == TextRole) {
|
||||
return pollHandler->answerAtRow(row).text;
|
||||
}
|
||||
if (role == CountRole) {
|
||||
return pollHandler->answerCountAtId(pollHandler->answerAtRow(row).id);
|
||||
}
|
||||
if (role == LocalChoiceRole) {
|
||||
const auto room = pollHandler->room();
|
||||
if (room == nullptr) {
|
||||
return {};
|
||||
}
|
||||
return pollHandler->checkMemberSelectedId(room->localMember().id(), pollHandler->answerAtRow(row).id);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
int PollAnswerModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
const auto pollHandler = dynamic_cast<PollHandler *>(this->parent());
|
||||
if (pollHandler == nullptr) {
|
||||
qWarning() << "PollAnswerModel created with nullptr parent.";
|
||||
return 0;
|
||||
}
|
||||
|
||||
return pollHandler->numAnswers();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> PollAnswerModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{IdRole, "id"},
|
||||
{TextRole, "answerText"},
|
||||
{CountRole, "count"},
|
||||
{LocalChoiceRole, "localChoice"},
|
||||
};
|
||||
}
|
||||
56
src/models/pollanswermodel.h
Normal file
56
src/models/pollanswermodel.h
Normal file
@@ -0,0 +1,56 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlEngine>
|
||||
|
||||
class PollHandler;
|
||||
|
||||
/**
|
||||
* @class PollAnswerModel
|
||||
*
|
||||
* This class defines the model for visualising a list of answer to a poll.
|
||||
*/
|
||||
class PollAnswerModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
IdRole, /**< The ID of the answer. */
|
||||
TextRole, /**< The answer text. */
|
||||
CountRole, /**< The number of people who gave this answer. */
|
||||
LocalChoiceRole, /**< Whether this option was selected by the local user */
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
explicit PollAnswerModel(PollHandler *parent);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
int rowCount(const QModelIndex &parent = {}) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "timelinemessagemodel.h"
|
||||
#include "events/pollevent.h"
|
||||
#include "messagemodel_logging.h"
|
||||
|
||||
using namespace Quotient;
|
||||
@@ -36,7 +37,7 @@ void TimelineMessageModel::connectNewRoom()
|
||||
});
|
||||
connect(m_room, &Room::addedMessages, this, [this](int lowest, int biggest) {
|
||||
if (m_initialized) {
|
||||
for (int i = lowest; i == biggest; ++i) {
|
||||
for (int i = lowest; i <= biggest; ++i) {
|
||||
const auto event = m_room->findInTimeline(i)->event();
|
||||
Q_EMIT newEventAdded(event);
|
||||
}
|
||||
@@ -58,14 +59,14 @@ void TimelineMessageModel::connectNewRoom()
|
||||
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 0)
|
||||
connect(m_room, &Room::pendingEventAdded, this, [this](const Quotient::RoomEvent *event) {
|
||||
m_initialized = true;
|
||||
Q_EMIT newEventAdded(event, true);
|
||||
Q_EMIT newEventAdded(event);
|
||||
beginInsertRows({}, 0, 0);
|
||||
endInsertRows();
|
||||
});
|
||||
#else
|
||||
connect(m_room, &Room::pendingEventAboutToAdd, this, [this](Quotient::RoomEvent *event) {
|
||||
m_initialized = true;
|
||||
Q_EMIT newEventAdded(event, true);
|
||||
Q_EMIT newEventAdded(event);
|
||||
beginInsertRows({}, 0, 0);
|
||||
});
|
||||
connect(m_room, &Room::pendingEventAdded, this, &TimelineMessageModel::endInsertRows);
|
||||
@@ -111,9 +112,6 @@ void TimelineMessageModel::connectNewRoom()
|
||||
const auto eventIt = m_room->findInTimeline(eventId);
|
||||
if (eventIt != m_room->historyEdge()) {
|
||||
Q_EMIT newEventAdded(eventIt->event());
|
||||
if (eventIt->event()->is<PollStartEvent>()) {
|
||||
m_room->createPollHandler(eventCast<const PollStartEvent>(eventIt->event()));
|
||||
}
|
||||
}
|
||||
refreshEventRoles(eventId, {Qt::DisplayRole});
|
||||
});
|
||||
|
||||
@@ -1438,24 +1438,37 @@ bool NeoChatRoom::canEncryptRoom() const
|
||||
|
||||
static PollHandler *emptyPollHandler = new PollHandler;
|
||||
|
||||
PollHandler *NeoChatRoom::poll(const QString &eventId) const
|
||||
PollHandler *NeoChatRoom::poll(const QString &eventId)
|
||||
{
|
||||
if (auto pollHandler = m_polls[eventId]) {
|
||||
return pollHandler;
|
||||
const auto event = getEvent(eventId);
|
||||
if (event.first == nullptr || event.second) {
|
||||
return emptyPollHandler;
|
||||
}
|
||||
return emptyPollHandler;
|
||||
|
||||
if (m_polls.contains(eventId)) {
|
||||
return m_polls[eventId];
|
||||
}
|
||||
auto handler = new PollHandler(this, eventId);
|
||||
m_polls.insert(eventId, handler);
|
||||
return handler;
|
||||
}
|
||||
|
||||
void NeoChatRoom::createPollHandler(const Quotient::PollStartEvent *event)
|
||||
void NeoChatRoom::postPoll(PollKind::Kind kind, const QString &question, const QList<QString> &answers)
|
||||
{
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto eventId = event->id();
|
||||
if (!m_polls.contains(eventId)) {
|
||||
auto handler = new PollHandler(this, event);
|
||||
m_polls.insert(eventId, handler);
|
||||
QList<EventContent::Answer> answerStructs;
|
||||
for (const auto &answer : answers) {
|
||||
answerStructs += EventContent::Answer{
|
||||
QUuid::createUuid().toString().remove(QRegularExpression(u"{|}|-"_s)),
|
||||
answer,
|
||||
};
|
||||
}
|
||||
const auto content = EventContent::PollStartContent{
|
||||
.kind = kind,
|
||||
.maxSelection = 1,
|
||||
.question = question,
|
||||
.answers = answerStructs,
|
||||
};
|
||||
post<PollStartEvent>(content);
|
||||
}
|
||||
|
||||
bool NeoChatRoom::downloadTempFile(const QString &eventId)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "enums/messagetype.h"
|
||||
#include "enums/pushrule.h"
|
||||
#include "events/pollevent.h"
|
||||
#include "models/messagecontentmodel.h"
|
||||
#include "models/threadmodel.h"
|
||||
#include "neochatroommember.h"
|
||||
@@ -501,14 +502,9 @@ public:
|
||||
*
|
||||
* @sa PollHandler
|
||||
*/
|
||||
PollHandler *poll(const QString &eventId) const;
|
||||
PollHandler *poll(const QString &eventId);
|
||||
|
||||
/**
|
||||
* @brief Create a PollHandler object for the given event.
|
||||
*
|
||||
* @sa PollHandler
|
||||
*/
|
||||
void createPollHandler(const Quotient::PollStartEvent *event);
|
||||
Q_INVOKABLE void postPoll(PollKind::Kind kind, const QString &question, const QList<QString> &answers);
|
||||
|
||||
/**
|
||||
* @brief Get the full Json data for a given room account data event.
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#include "pollhandler.h"
|
||||
|
||||
#include "events/pollevent.h"
|
||||
#include "neochatroom.h"
|
||||
#include "pollanswermodel.h"
|
||||
|
||||
#include <Quotient/csapi/relations.h>
|
||||
#include <Quotient/events/roompowerlevelsevent.h>
|
||||
@@ -12,11 +14,14 @@
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
PollHandler::PollHandler(NeoChatRoom *room, const Quotient::PollStartEvent *pollStartEvent)
|
||||
PollHandler::PollHandler(NeoChatRoom *room, const QString &pollStartId)
|
||||
: QObject(room)
|
||||
, m_pollStartEvent(pollStartEvent)
|
||||
, m_pollStartId(pollStartId)
|
||||
{
|
||||
if (room != nullptr && m_pollStartEvent != nullptr) {
|
||||
Q_ASSERT(room != nullptr);
|
||||
Q_ASSERT(!pollStartId.isEmpty());
|
||||
|
||||
if (room != nullptr) {
|
||||
connect(room, &NeoChatRoom::aboutToAddNewMessages, this, &PollHandler::updatePoll);
|
||||
checkLoadRelations();
|
||||
}
|
||||
@@ -26,28 +31,38 @@ void PollHandler::updatePoll(Quotient::RoomEventsRange events)
|
||||
{
|
||||
// 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.
|
||||
auto room = dynamic_cast<NeoChatRoom *>(parent());
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(parent());
|
||||
auto pollStartEvent = eventCast<const PollStartEvent>(room->getEvent(m_pollStartId).first);
|
||||
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() == m_pollStartEvent->senderId() || userPl >= plEvent->redact()) {
|
||||
if (event->senderId() == pollStartEvent->senderId() || userPl >= plEvent->redact()) {
|
||||
m_hasEnded = true;
|
||||
m_endedTimestamp = event->originTimestamp();
|
||||
Q_EMIT hasEndedChanged();
|
||||
}
|
||||
}
|
||||
if (event->is<PollResponseEvent>()) {
|
||||
handleAnswer(event->contentJson(), event->senderId(), event->originTimestamp());
|
||||
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() == m_pollStartEvent->id()) {
|
||||
&& event->contentPart<QJsonObject>("m.relates_to"_L1)["event_id"_L1].toString() == pollStartEvent->id()) {
|
||||
Q_EMIT questionChanged();
|
||||
Q_EMIT optionsChanged();
|
||||
Q_EMIT answersChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,91 +72,149 @@ void PollHandler::checkLoadRelations()
|
||||
// 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.
|
||||
auto room = dynamic_cast<NeoChatRoom *>(parent());
|
||||
m_maxVotes = m_pollStartEvent->maxSelections();
|
||||
auto job = room->connection()->callApi<GetRelatingEventsJob>(room->id(), m_pollStartEvent->id());
|
||||
connect(job, &BaseJob::success, this, [this, job, room]() {
|
||||
const auto pollStartEvent = room->getEvent(m_pollStartId).first;
|
||||
if (pollStartEvent == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto job = room->connection()->callApi<GetRelatingEventsJob>(room->id(), pollStartEvent->id());
|
||||
connect(job, &BaseJob::success, this, [this, job, room, pollStartEvent]() {
|
||||
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() == m_pollStartEvent->senderId() || userPl >= plEvent->redact()) {
|
||||
if (event->senderId() == pollStartEvent->senderId() || userPl >= plEvent->redact()) {
|
||||
m_hasEnded = true;
|
||||
m_endedTimestamp = event->originTimestamp();
|
||||
Q_EMIT hasEndedChanged();
|
||||
}
|
||||
}
|
||||
if (event->is<PollResponseEvent>()) {
|
||||
handleAnswer(event->contentJson(), event->senderId(), event->originTimestamp());
|
||||
handleResponse(eventCast<const PollResponseEvent>(event));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void PollHandler::handleAnswer(const QJsonObject &content, const QString &sender, QDateTime timestamp)
|
||||
void PollHandler::handleResponse(const Quotient::PollResponseEvent *event)
|
||||
{
|
||||
if (timestamp > m_answerTimestamps[sender] && (!m_hasEnded || timestamp < m_endedTimestamp)) {
|
||||
m_answerTimestamps[sender] = timestamp;
|
||||
m_answers[sender] = {};
|
||||
int i = 0;
|
||||
for (const auto &answer : content["org.matrix.msc3381.poll.response"_L1]["answers"_L1].toArray()) {
|
||||
auto array = m_answers[sender].toArray();
|
||||
array.insert(0, answer);
|
||||
m_answers[sender] = array;
|
||||
i++;
|
||||
if (i == m_maxVotes) {
|
||||
break;
|
||||
}
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->relatesTo()->eventId != m_pollStartId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is no origin timestamp it's pending and therefore must be newer.
|
||||
if ((event->originTimestamp() > m_selectionTimestamps[event->senderId()] || event->id().isEmpty())
|
||||
&& (!m_hasEnded || event->originTimestamp() < m_endedTimestamp)) {
|
||||
m_selectionTimestamps[event->senderId()] = event->originTimestamp();
|
||||
|
||||
// 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.
|
||||
auto room = dynamic_cast<NeoChatRoom *>(parent());
|
||||
const auto pollStartEvent = eventCast<const PollStartEvent>(room->getEvent(m_pollStartId).first);
|
||||
if (pollStartEvent == nullptr) {
|
||||
return;
|
||||
}
|
||||
for (const auto &key : m_answers.keys()) {
|
||||
if (m_answers[key].toArray().isEmpty()) {
|
||||
m_answers.remove(key);
|
||||
}
|
||||
|
||||
m_selections[event->senderId()] = event->selections().size() > 0 ? event->selections().first(pollStartEvent->maxSelections()) : event->selections();
|
||||
if (m_selections.contains(event->senderId()) && m_selections[event->senderId()].isEmpty()) {
|
||||
m_selections.remove(event->senderId());
|
||||
}
|
||||
}
|
||||
Q_EMIT answersChanged();
|
||||
|
||||
Q_EMIT selectionsChanged();
|
||||
}
|
||||
|
||||
NeoChatRoom *PollHandler::room() const
|
||||
{
|
||||
return dynamic_cast<NeoChatRoom *>(parent());
|
||||
}
|
||||
|
||||
QString PollHandler::question() const
|
||||
{
|
||||
if (m_pollStartEvent == nullptr) {
|
||||
auto room = dynamic_cast<NeoChatRoom *>(parent());
|
||||
if (room == nullptr) {
|
||||
return {};
|
||||
}
|
||||
return m_pollStartEvent->contentPart<QJsonObject>("org.matrix.msc3381.poll.start"_L1)["question"_L1].toObject()["body"_L1].toString();
|
||||
}
|
||||
|
||||
QJsonArray PollHandler::options() const
|
||||
{
|
||||
if (m_pollStartEvent == nullptr) {
|
||||
auto pollStartEvent = eventCast<const PollStartEvent>(room->getEvent(m_pollStartId).first);
|
||||
if (pollStartEvent == nullptr) {
|
||||
return {};
|
||||
}
|
||||
return m_pollStartEvent->contentPart<QJsonObject>("org.matrix.msc3381.poll.start"_L1)["answers"_L1].toArray();
|
||||
return pollStartEvent->question();
|
||||
}
|
||||
|
||||
QJsonObject PollHandler::answers() const
|
||||
int PollHandler::numAnswers() const
|
||||
{
|
||||
return m_answers;
|
||||
auto room = dynamic_cast<NeoChatRoom *>(parent());
|
||||
if (room == nullptr) {
|
||||
return {};
|
||||
}
|
||||
auto pollStartEvent = eventCast<const PollStartEvent>(room->getEvent(m_pollStartId).first);
|
||||
if (pollStartEvent == nullptr) {
|
||||
return {};
|
||||
}
|
||||
return pollStartEvent->answers().length();
|
||||
}
|
||||
|
||||
QJsonObject PollHandler::counts() const
|
||||
Quotient::EventContent::Answer PollHandler::answerAtRow(int row) const
|
||||
{
|
||||
QJsonObject counts;
|
||||
for (const auto &answer : m_answers) {
|
||||
for (const auto &id : answer.toArray()) {
|
||||
counts[id.toString()] = counts[id.toString()].toInt() + 1;
|
||||
auto room = dynamic_cast<NeoChatRoom *>(parent());
|
||||
if (room == nullptr) {
|
||||
return {};
|
||||
}
|
||||
auto pollStartEvent = eventCast<const PollStartEvent>(room->getEvent(m_pollStartId).first);
|
||||
if (pollStartEvent == nullptr) {
|
||||
return {};
|
||||
}
|
||||
return pollStartEvent->answers()[row];
|
||||
}
|
||||
|
||||
int PollHandler::answerCountAtId(const QString &id) const
|
||||
{
|
||||
int count = 0;
|
||||
for (const auto &selection : m_selections) {
|
||||
if (selection.contains(id)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
return count;
|
||||
}
|
||||
|
||||
QString PollHandler::kind() const
|
||||
bool PollHandler::checkMemberSelectedId(const QString &memberId, const QString &id) const
|
||||
{
|
||||
if (m_pollStartEvent == nullptr) {
|
||||
return m_selections[memberId].contains(id);
|
||||
}
|
||||
|
||||
PollKind::Kind PollHandler::kind() const
|
||||
{
|
||||
auto room = dynamic_cast<NeoChatRoom *>(parent());
|
||||
if (room == nullptr) {
|
||||
return {};
|
||||
}
|
||||
return m_pollStartEvent->contentPart<QJsonObject>("org.matrix.msc3381.poll.start"_L1)["kind"_L1].toString();
|
||||
auto pollStartEvent = eventCast<const PollStartEvent>(room->getEvent(m_pollStartId).first);
|
||||
if (pollStartEvent == nullptr) {
|
||||
return {};
|
||||
}
|
||||
return pollStartEvent->kind();
|
||||
}
|
||||
|
||||
PollAnswerModel *PollHandler::answerModel()
|
||||
{
|
||||
if (m_answerModel == nullptr) {
|
||||
m_answerModel = new PollAnswerModel(this);
|
||||
}
|
||||
return m_answerModel;
|
||||
}
|
||||
|
||||
void PollHandler::sendPollAnswer(const QString &eventId, const QString &answerId)
|
||||
@@ -153,23 +226,25 @@ void PollHandler::sendPollAnswer(const QString &eventId, const QString &answerId
|
||||
qWarning() << "PollHandler is empty, cannot send an answer.";
|
||||
return;
|
||||
}
|
||||
QStringList ownAnswers;
|
||||
for (const auto &answer : m_answers[room->localMember().id()].toArray()) {
|
||||
ownAnswers += answer.toString();
|
||||
auto pollStartEvent = eventCast<const PollStartEvent>(room->getEvent(m_pollStartId).first);
|
||||
if (pollStartEvent == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList ownAnswers = m_selections[room->localMember().id()];
|
||||
if (ownAnswers.contains(answerId)) {
|
||||
ownAnswers.erase(std::remove_if(ownAnswers.begin(), ownAnswers.end(), [answerId](const auto &it) {
|
||||
return answerId == it;
|
||||
}));
|
||||
} else {
|
||||
while (ownAnswers.size() >= m_maxVotes) {
|
||||
while (ownAnswers.size() >= pollStartEvent->maxSelections() && ownAnswers.size() > 0) {
|
||||
ownAnswers.pop_front();
|
||||
}
|
||||
ownAnswers.insert(0, answerId);
|
||||
}
|
||||
|
||||
const auto &response = room->post<PollResponseEvent>(eventId, ownAnswers);
|
||||
handleAnswer(response->contentJson(), room->localMember().id(), QDateTime::currentDateTime());
|
||||
handleResponse(eventCast<const PollResponseEvent>(response.event()));
|
||||
}
|
||||
|
||||
bool PollHandler::hasEnded() const
|
||||
@@ -177,9 +252,4 @@ bool PollHandler::hasEnded() const
|
||||
return m_hasEnded;
|
||||
}
|
||||
|
||||
int PollHandler::answerCount() const
|
||||
{
|
||||
return m_answers.size();
|
||||
}
|
||||
|
||||
#include "moc_pollhandler.cpp"
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
#include <Quotient/events/roomevent.h>
|
||||
|
||||
#include "events/pollevent.h"
|
||||
#include "models/pollanswermodel.h"
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
class PollResponseEvent;
|
||||
}
|
||||
|
||||
class NeoChatRoom;
|
||||
|
||||
@@ -32,53 +38,56 @@ class PollHandler : public QObject
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("Use NeoChatRoom::poll")
|
||||
|
||||
/**
|
||||
* @brief The kind of the poll.
|
||||
*/
|
||||
Q_PROPERTY(PollKind::Kind kind READ kind CONSTANT)
|
||||
|
||||
/**
|
||||
* @brief The question for the poll.
|
||||
*/
|
||||
Q_PROPERTY(QString question READ question NOTIFY questionChanged)
|
||||
|
||||
/**
|
||||
* @brief The list of possible answers to the poll.
|
||||
*/
|
||||
Q_PROPERTY(QJsonArray options READ options NOTIFY optionsChanged)
|
||||
|
||||
/**
|
||||
* @brief The list of answer responses to the poll from users in the room.
|
||||
*/
|
||||
Q_PROPERTY(QJsonObject answers READ answers NOTIFY answersChanged)
|
||||
|
||||
/**
|
||||
* @brief The list number of votes for each answer in the poll.
|
||||
*/
|
||||
Q_PROPERTY(QJsonObject counts READ counts NOTIFY answersChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the poll has ended.
|
||||
*/
|
||||
Q_PROPERTY(bool hasEnded READ hasEnded NOTIFY hasEndedChanged)
|
||||
|
||||
/**
|
||||
* @brief The total number of answers to the poll.
|
||||
* @brief The model to visualize the answers to this poll.
|
||||
*/
|
||||
Q_PROPERTY(int answerCount READ answerCount NOTIFY answersChanged)
|
||||
|
||||
/**
|
||||
* @brief The kind of the poll.
|
||||
*/
|
||||
Q_PROPERTY(QString kind READ kind CONSTANT)
|
||||
Q_PROPERTY(PollAnswerModel *answerModel READ answerModel CONSTANT)
|
||||
|
||||
public:
|
||||
PollHandler() = default;
|
||||
PollHandler(NeoChatRoom *room, const Quotient::PollStartEvent *pollStartEvent);
|
||||
PollHandler(NeoChatRoom *room, const QString &pollStartId);
|
||||
|
||||
bool hasEnded() const;
|
||||
int answerCount() const;
|
||||
NeoChatRoom *room() const;
|
||||
|
||||
PollKind::Kind kind() const;
|
||||
QString question() const;
|
||||
QJsonArray options() const;
|
||||
QJsonObject answers() const;
|
||||
QJsonObject counts() const;
|
||||
QString kind() const;
|
||||
bool hasEnded() const;
|
||||
PollAnswerModel *answerModel();
|
||||
|
||||
/**
|
||||
* @brief The total number of answer options.
|
||||
*/
|
||||
int numAnswers() const;
|
||||
|
||||
/**
|
||||
* @brief The answer at the given row.
|
||||
*/
|
||||
Quotient::EventContent::Answer answerAtRow(int row) const;
|
||||
|
||||
/**
|
||||
* @brief The number of responders who gave the answer ID.
|
||||
*/
|
||||
int answerCountAtId(const QString &id) const;
|
||||
|
||||
/**
|
||||
* @brief Check whether the given member has selected the given ID in their response.
|
||||
*/
|
||||
bool checkMemberSelectedId(const QString &memberId, const QString &id) const;
|
||||
|
||||
/**
|
||||
* @brief Send an answer to the poll.
|
||||
@@ -87,20 +96,27 @@ public:
|
||||
|
||||
Q_SIGNALS:
|
||||
void questionChanged();
|
||||
void optionsChanged();
|
||||
void answersChanged();
|
||||
void hasEndedChanged();
|
||||
|
||||
void answersChanged();
|
||||
|
||||
/**
|
||||
* @brief Emitted when the selected answers to the poll change.
|
||||
*/
|
||||
void selectionsChanged();
|
||||
|
||||
private:
|
||||
const Quotient::PollStartEvent *m_pollStartEvent = nullptr;
|
||||
QString m_pollStartId;
|
||||
|
||||
void updatePoll(Quotient::RoomEventsRange events);
|
||||
|
||||
void checkLoadRelations();
|
||||
void handleAnswer(const QJsonObject &object, const QString &sender, QDateTime timestamp);
|
||||
QMap<QString, QDateTime> m_answerTimestamps;
|
||||
QJsonObject m_answers;
|
||||
int m_maxVotes = 1;
|
||||
void handleResponse(const Quotient::PollResponseEvent *event);
|
||||
QHash<QString, QDateTime> m_selectionTimestamps;
|
||||
QHash<QString, QList<QString>> m_selections;
|
||||
|
||||
bool m_hasEnded = false;
|
||||
QDateTime m_endedTimestamp;
|
||||
|
||||
QPointer<PollAnswerModel> m_answerModel;
|
||||
};
|
||||
|
||||
156
src/qml/NewPollDialog.qml
Normal file
156
src/qml/NewPollDialog.qml
Normal file
@@ -0,0 +1,156 @@
|
||||
// SPDX-FileCopyrightText: 2025 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
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
|
||||
import Quotient
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom room
|
||||
|
||||
standardButtons: Kirigami.Dialog.Cancel
|
||||
|
||||
customFooterActions: [
|
||||
Kirigami.Action {
|
||||
enabled: optionModel.allValuesSet && questionTextField.text.length > 0
|
||||
text: i18nc("@action:button", "Send")
|
||||
icon.name: "document-send"
|
||||
onTriggered: {
|
||||
root.room.postPoll(pollTypeCombo.currentValue, questionTextField.text, optionModel.values())
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
width: Math.min(applicationWindow().width, Kirigami.Units.gridUnit * 24)
|
||||
title: i18nc("@title: create new poll in the room", "Create Poll")
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
FormCard.FormComboBoxDelegate {
|
||||
id: pollTypeCombo
|
||||
|
||||
text: i18n("Poll type:")
|
||||
currentIndex: 0
|
||||
textRole: "text"
|
||||
valueRole: "value"
|
||||
model: [
|
||||
{ value: PollKind.Disclosed, text: i18n("Open poll") },
|
||||
{ value: PollKind.Undisclosed, text: i18n("Closed poll") }
|
||||
]
|
||||
}
|
||||
FormCard.FormTextDelegate {
|
||||
verticalPadding: 0
|
||||
text: pollTypeCombo.currentValue == 0 ? i18n("Voters can see the result as soon as they have voted") : i18n("Results are revealed only after the poll has closed")
|
||||
}
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: questionTextField
|
||||
label: i18n("Question:")
|
||||
}
|
||||
Repeater {
|
||||
id: optionRepeater
|
||||
|
||||
model: ListModel {
|
||||
id: optionModel
|
||||
|
||||
readonly property bool allValuesSet: {
|
||||
for( var i = 0; i < optionModel.rowCount(); i++ ) {
|
||||
if (optionModel.get(i).optionText.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ListElement {
|
||||
optionText: ""
|
||||
}
|
||||
ListElement {
|
||||
optionText: ""
|
||||
}
|
||||
|
||||
function values() {
|
||||
let textValues = []
|
||||
for( var i = 0; i < optionModel.rowCount(); i++ ) {
|
||||
textValues.push(optionModel.get(i).optionText);
|
||||
}
|
||||
return textValues;
|
||||
}
|
||||
}
|
||||
delegate: FormCard.AbstractFormDelegate {
|
||||
id: optionDelegate
|
||||
|
||||
required property int index
|
||||
required property string optionText
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
QQC2.Label {
|
||||
id: optionLabel
|
||||
|
||||
Layout.fillWidth: true
|
||||
text: i18nc("As in first answer option to the poll", "Option %1:", optionDelegate.index + 1)
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.Wrap
|
||||
Accessible.ignored: true
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
QQC2.TextField {
|
||||
id: textField
|
||||
Layout.fillWidth: true
|
||||
Accessible.name: optionLabel.text
|
||||
onTextChanged: {
|
||||
optionModel.set(optionDelegate.index, {optionText: text})
|
||||
optionModel.allValuesSetChanged()
|
||||
}
|
||||
placeholderText: i18n("Enter option")
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
action: Kirigami.Action {
|
||||
id: removeOptionAction
|
||||
text: i18nc("@action:button", "Remove option")
|
||||
icon.name: "edit-delete-remove"
|
||||
onTriggered: optionModel.remove(optionDelegate.index)
|
||||
}
|
||||
QQC2.ToolTip {
|
||||
text: removeOptionAction.text
|
||||
delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: null
|
||||
}
|
||||
}
|
||||
Delegates.RoundedItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
|
||||
horizontalPadding: Kirigami.Units.largeSpacing * 2
|
||||
leftInset: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
|
||||
rightInset: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
|
||||
|
||||
highlighted: true
|
||||
|
||||
icon.name: "list-add"
|
||||
text: i18nc("@action:button", "Add option")
|
||||
|
||||
onClicked: optionModel.append({optionText: ""})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@ import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
import Quotient
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
@@ -43,24 +46,31 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.pollHandler.options
|
||||
model: root.pollHandler.answerModel
|
||||
delegate: FormCard.FormCheckDelegate {
|
||||
id: answerDelegate
|
||||
|
||||
required property string id
|
||||
required property string answerText
|
||||
required property int count
|
||||
required property bool localChoice
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: -Kirigami.Units.largeSpacing - Kirigami.Units.smallSpacing
|
||||
Layout.rightMargin: -Kirigami.Units.largeSpacing - Kirigami.Units.smallSpacing
|
||||
|
||||
checked: root.pollHandler.answers[root.Message.room.localMember.id] ? root.pollHandler.answers[root.Message.room.localMember.id].includes(modelData["id"]) : false
|
||||
onClicked: root.pollHandler.sendPollAnswer(root.eventId, modelData["id"])
|
||||
checked: answerDelegate.localChoice
|
||||
onClicked: root.pollHandler.sendPollAnswer(root.eventId, answerDelegate.id)
|
||||
enabled: !root.pollHandler.hasEnded
|
||||
text: modelData["org.matrix.msc1767.text"]
|
||||
text: answerDelegate.answerText
|
||||
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
|
||||
trailing: Label {
|
||||
visible: root.pollHandler.kind == "org.matrix.msc3381.poll.disclosed" || pollHandler.hasEnded
|
||||
visible: root.pollHandler.kind == PollKind.Disclosed || pollHandler.hasEnded
|
||||
Layout.preferredWidth: contentWidth
|
||||
text: root.pollHandler.counts[modelData["id"]] ?? "0"
|
||||
text: answerDelegate.count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user