319 lines
9.8 KiB
C++
319 lines
9.8 KiB
C++
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
|
// SPDX-License-Identifier: LGPL-2.0-or-later
|
|
|
|
#include "pollhandler.h"
|
|
|
|
#include <KLocalization>
|
|
|
|
#include "events/pollevent.h"
|
|
#include "neochatroom.h"
|
|
|
|
#include <Quotient/csapi/relations.h>
|
|
#include <Quotient/events/roompowerlevelsevent.h>
|
|
|
|
#include <algorithm>
|
|
#include <qcontainerfwd.h>
|
|
|
|
using namespace Quotient;
|
|
|
|
PollHandler::PollHandler(NeoChatRoom *room, const QString &pollStartId)
|
|
: QObject(room)
|
|
, m_pollStartId(pollStartId)
|
|
{
|
|
Q_ASSERT(room != nullptr);
|
|
Q_ASSERT(!pollStartId.isEmpty());
|
|
|
|
if (room != nullptr) {
|
|
connect(room, &NeoChatRoom::aboutToAddNewMessages, this, &PollHandler::updatePoll);
|
|
connect(room, &NeoChatRoom::pendingEventAboutToAdd, this, &PollHandler::handleEvent);
|
|
checkLoadRelations();
|
|
}
|
|
}
|
|
|
|
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.
|
|
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) {
|
|
handleEvent(event.get());
|
|
}
|
|
}
|
|
|
|
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());
|
|
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]() {
|
|
for (const auto &event : job->chunk()) {
|
|
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) {
|
|
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;
|
|
}
|
|
|
|
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 totalCountChanged();
|
|
Q_EMIT selectionsChanged();
|
|
}
|
|
|
|
NeoChatRoom *PollHandler::room() const
|
|
{
|
|
return dynamic_cast<NeoChatRoom *>(parent());
|
|
}
|
|
|
|
QString PollHandler::question() 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 {};
|
|
}
|
|
return pollStartEvent->question();
|
|
}
|
|
|
|
int PollHandler::numAnswers() 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 {};
|
|
}
|
|
return pollStartEvent->answers().length();
|
|
}
|
|
|
|
Quotient::EventContent::Answer PollHandler::answerAtRow(int row) 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 {};
|
|
}
|
|
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 count;
|
|
}
|
|
|
|
bool PollHandler::checkMemberSelectedId(const QString &memberId, const QString &id) const
|
|
{
|
|
return m_selections[memberId].contains(id);
|
|
}
|
|
|
|
PollKind::Kind PollHandler::kind() 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 {};
|
|
}
|
|
return pollStartEvent->kind();
|
|
}
|
|
|
|
PollAnswerModel *PollHandler::answerModel()
|
|
{
|
|
if (m_answerModel == nullptr) {
|
|
m_answerModel = new PollAnswerModel(this);
|
|
}
|
|
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<NeoChatRoom *>(parent());
|
|
if (room == nullptr) {
|
|
return {};
|
|
}
|
|
auto pollStartEvent = eventCast<const PollStartEvent>(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);
|
|
Q_ASSERT(answerId.length() > 0);
|
|
auto room = dynamic_cast<NeoChatRoom *>(parent());
|
|
if (room == nullptr) {
|
|
qWarning() << "PollHandler is empty, cannot send an answer.";
|
|
return;
|
|
}
|
|
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() >= pollStartEvent->maxSelections() && ownAnswers.size() > 0) {
|
|
ownAnswers.pop_front();
|
|
}
|
|
ownAnswers.insert(0, answerId);
|
|
}
|
|
|
|
const auto &response = room->post<PollResponseEvent>(eventId, ownAnswers);
|
|
handleResponse(eventCast<const PollResponseEvent>(response.event()));
|
|
}
|
|
|
|
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"
|