diff options
-rw-r--r-- | CMakeLists.txt | 3 | ||||
-rw-r--r-- | src/activitypub/appost.h | 22 | ||||
-rw-r--r-- | src/activitypub/apquestion.cpp | 47 | ||||
-rw-r--r-- | src/activitypub/apquestion.h | 34 | ||||
-rw-r--r-- | src/activitypub/fields.h | 38 | ||||
-rw-r--r-- | src/archive_parser.cpp | 35 | ||||
-rw-r--r-- | src/templates/appoll.html | 9 | ||||
-rw-r--r-- | src/templates/appoll_item.html | 1 |
8 files changed, 166 insertions, 23 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bcbe6e..5bab400 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,7 @@ set(PROJECT_SOURCES src/list_item.h src/command_line.cpp src/command_line.h + src/activitypub/fields.h src/activitypub/apactivity.h src/activitypub/apactor.h src/activitypub/apattachment.h @@ -35,6 +36,7 @@ set(PROJECT_SOURCES src/activitypub/apobject.h src/activitypub/appost.h src/activitypub/apreblog.h + src/activitypub/apquestion.h src/activitypub/apactivity.cpp src/activitypub/apactor.cpp src/activitypub/apattachment.cpp @@ -42,6 +44,7 @@ set(PROJECT_SOURCES src/activitypub/apobject.cpp src/activitypub/appost.cpp src/activitypub/apreblog.cpp + src/activitypub/apquestion.cpp ) if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) diff --git a/src/activitypub/appost.h b/src/activitypub/appost.h index a8b99c5..ccbedd9 100644 --- a/src/activitypub/appost.h +++ b/src/activitypub/appost.h @@ -1,28 +1,10 @@ #pragma once #include "apobject.h" -#include "src/activitypub/apbase.h" -#include "src/types.h" +#include "apbase.h" +#include "fields.h" #include <vector> -// All these fields (including APAttachmentFields, etc. in other files) are made to map to the fields present in the ActivityStream JSON's "object" for very easy construction. -// There are additional fields that we don't use so they are not included in the objects (including parent and APActivity). - -struct APObjectFields { - QStringList to_actors; // Start APObject - QStringList cc_actors; - QString by_actor; - QString object_url; - QString web_url; - QString reply_to_url; - QString published; - std::vector<APAttachmentFields> attachments; // End APObject - QStringList languages; // This is taken from "content_map" - QString content; - QString summary; - StatusType visibility; // status type discovery is better left to the archive/API parser as this can very between fedi implementations and archive formats -}; - // APPost represents an ActivityPub Note Object class APPost : public APObject { public: diff --git a/src/activitypub/apquestion.cpp b/src/activitypub/apquestion.cpp new file mode 100644 index 0000000..d519947 --- /dev/null +++ b/src/activitypub/apquestion.cpp @@ -0,0 +1,47 @@ +#include "apquestion.h" +#include <QDebug> + +APQuestion::APQuestion(APObjectFields fields) : APPost(fields) { + end_time = QDateTime::fromString(fields.question.end_time, Qt::ISODate); + closed_time = QDateTime::fromString(fields.question.closed_time, Qt::ISODate); + total_votes = fields.question.total_votes; + + for (APQuestionFields::Option elem : fields.question.poll_options) { + options.push_back({elem.name, elem.votes}); + } + + qDebug() << options.size(); +} + +QString APQuestion::get_html_render(HtmlRenderDetails render_info) { + QString html(APPost::get_html_render(render_info)); + html.append(get_html_template("appoll")); + + if (end_time.isValid()) { + // TODO: add a UI setting for configuring the display of local time or UTC time. + html.replace("{{end-time}}", render_info.locale->toString(end_time.toLocalTime())); + } + if (closed_time.isValid()) { + // TODO: add a UI setting for configuring the display of local time or UTC time. + html.replace("{{closed-time}}", render_info.locale->toString(closed_time.toLocalTime())); + } + + html.replace("{{total-votes}}", render_info.locale->toString(total_votes)); + html.replace("{{options}}", get_html_poll_options(render_info)); + + return html; +} + +QString APQuestion::get_html_poll_options(HtmlRenderDetails render_info) { + QString full; + + for (PollOption option : options) { + QString html(get_html_template("appoll_item")); + html.replace("{{name}}", option.name); + html.replace("{{votes}}", render_info.locale->toString(option.votes)); + html.replace("{{votes-percent}}", render_info.locale->toString((int)((float)option.votes/(float)total_votes * 100))); + full.append(html); + } + + return full; +} diff --git a/src/activitypub/apquestion.h b/src/activitypub/apquestion.h new file mode 100644 index 0000000..350ceec --- /dev/null +++ b/src/activitypub/apquestion.h @@ -0,0 +1,34 @@ +#pragma once + +#include "appost.h" +#include "apbase.h" +#include "fields.h" +#include <vector> +#include <list> + +// APQuestion represents an ActivityPub Question Object +class APQuestion : public APPost { +public: + APQuestion(); + // A post that will be built from strings, including attachments + APQuestion(APObjectFields fields); + ~APQuestion() {}; + + QString get_html_render(HtmlRenderDetails render_info); + +protected: + struct PollOption { + QString name; + int votes; + }; + typedef std::vector<PollOption> PollOptionList; + + QDateTime end_time; + QDateTime closed_time; + + int total_votes = 0; + + PollOptionList options; + + QString get_html_poll_options(HtmlRenderDetails render_info); +}; diff --git a/src/activitypub/fields.h b/src/activitypub/fields.h new file mode 100644 index 0000000..25ed246 --- /dev/null +++ b/src/activitypub/fields.h @@ -0,0 +1,38 @@ +#pragma once + +#include "src/types.h" +#include <QString> +#include <vector> + +enum struct APObjectType {NOTE, QUESTION, UNKNOWN}; + +// All these fields (including APAttachmentFields, etc. in other files) are made to map to the fields present in the ActivityStream JSON's "object" for very easy construction. +// There are additional fields that we don't use so they are not included in the objects (including parent and APActivity). + +struct APQuestionFields { + QString end_time; + QString closed_time; + int total_votes; + struct Option { + QString name; + int votes; + }; + std::vector<Option> poll_options; +}; + +struct APObjectFields { + QStringList to_actors; // Start APObject + QStringList cc_actors; + QString by_actor; + QString object_url; + QString web_url; + QString reply_to_url; + QString published; + std::vector<APAttachmentFields> attachments; // End APObject + QStringList languages; // This is taken from "content_map" + QString content; + QString summary; + StatusType visibility; // status type discovery is better left to the archive/API parser as this can very between fedi implementations and archive formats + + APQuestionFields question = {}; // only fill if it's of type Question. +}; diff --git a/src/archive_parser.cpp b/src/archive_parser.cpp index 25d1b76..8b2289e 100644 --- a/src/archive_parser.cpp +++ b/src/archive_parser.cpp @@ -6,6 +6,8 @@ #include "activitypub/apobject.h" #include "activitypub/apreblog.h" #include "activitypub/appost.h" +#include "activitypub/apquestion.h" +#include "activitypub/fields.h" #include <QFile> #include <QJsonParseError> @@ -245,6 +247,7 @@ const QString Archive::get_html_status_info(int status_index, int text_zone_widt // The post APObjectFields obj_fields; + APObjectType obj_type = APObjectType::UNKNOWN; if (activity.contains("type") and activity["type"].isString()) { QString type = activity["type"].toString(); @@ -269,6 +272,14 @@ const QString Archive::get_html_status_info(int status_index, int text_zone_widt // the JSON AP Object QJsonObject object = activity["object"].toObject(); + { + QString type = object["type"].toString(); + if (type == "Note") + obj_type = APObjectType::NOTE; + else if (type == "Question") + obj_type = APObjectType::QUESTION; + } + obj_fields.visibility = get_status_type(object); if (object.contains("summary")) @@ -290,13 +301,31 @@ const QString Archive::get_html_status_info(int status_index, int text_zone_widt obj_fields.by_actor = object["attributedTo"].toString(); obj_fields.published = object["published"].toString(); + if (obj_type == APObjectType::QUESTION) { + obj_fields.question = { + .end_time = object["endTime"].toString(), + .closed_time = object["closed"].toString(), + .total_votes = object["votersCount"].toInt() + }; + for (QJsonValue elem : object["oneOf"].toArray()) + obj_fields.question.poll_options.push_back({ + (elem["type"].toString() == "Note") ? elem["name"].toString() : "?", + elem["replies"].toObject()["totalItems"].toInt() + }); + } + } else if (activity["object"].isString()) obj_url = activity["object"].toString(); - if (obj_url.isNull()) - act_fields.object = new APPost(obj_fields); - else + if (not obj_url.isNull()) act_fields.object = new APReblog({obj_url, act_fields.visibility}); + else switch(obj_type) { + case APObjectType::UNKNOWN: + case APObjectType::NOTE: + act_fields.object = new APPost(obj_fields); break; + case APObjectType::QUESTION: + act_fields.object = new APQuestion(obj_fields); break; + } // TODO: it is currently a waste to create this APActivity object that will be immediately destroyed but it allows us to extend archive parsing to something that will become abstract (and an abstract base class) and separate from display (the final goal) which will allow us to add other sources that feed us with posts, reblogs and Actor information. furthermore, these objects can be cached for reuse in a session. return APActivity(act_fields).get_html_render({text_zone_width, locale}); diff --git a/src/templates/appoll.html b/src/templates/appoll.html new file mode 100644 index 0000000..7d5bb87 --- /dev/null +++ b/src/templates/appoll.html @@ -0,0 +1,9 @@ +<h2>Poll</h2> +<ul> + <li id="poll-end-time"><b>End time:</b> {{end-time}}</li> + <li id="poll-closed-time"><b>Closed:</b> {{closed-time}}</li> + <li id="poll-votes"><b>Total votes:</b> {{total-votes}}</li> +</ul> + +<h3>Poll options</h3> +<ul id="poll-choices">{{options}}</ul> diff --git a/src/templates/appoll_item.html b/src/templates/appoll_item.html new file mode 100644 index 0000000..2ce6967 --- /dev/null +++ b/src/templates/appoll_item.html @@ -0,0 +1 @@ +<li><b>{{name}}</b> {{votes}} votes ({{votes-percent}}%)</li> |