From 5aac009e969cc3bd15c484ba3437348cb7a4d186 Mon Sep 17 00:00:00 2001 From: ConfuSomu Date: Sat, 20 Jan 2024 21:57:39 -0500 Subject: Implemement Actor information tab This class still has improvements to be made, but it works and I am satisfied with it! --- src/activitypub/apactor.cpp | 25 ++++++++++- src/activitypub/apactor.h | 42 ++++++++++++++++++- src/activitypub/apattachment.cpp | 26 ++++++++++++ src/activitypub/apattachment.h | 4 ++ src/archive/base_archive.cpp | 4 ++ src/archive/base_archive.h | 9 ++-- src/archive/mastodon.cpp | 90 ++++++++++++++++++++++++++++++++++++++++ src/archive/mastodon.h | 3 ++ src/mainwindow.cpp | 8 ++++ src/mainwindow.h | 3 +- src/widgets/tab_actor_info.cpp | 49 ++++++++++++++++++++++ src/widgets/tab_actor_info.h | 27 ++++++++++++ src/widgets/tab_actor_info.ui | 79 +++++++++++++++++++++++++++++++++++ 13 files changed, 362 insertions(+), 7 deletions(-) create mode 100644 src/widgets/tab_actor_info.cpp create mode 100644 src/widgets/tab_actor_info.h create mode 100644 src/widgets/tab_actor_info.ui (limited to 'src') diff --git a/src/activitypub/apactor.cpp b/src/activitypub/apactor.cpp index 6731dd3..78ed9a3 100644 --- a/src/activitypub/apactor.cpp +++ b/src/activitypub/apactor.cpp @@ -1,11 +1,34 @@ #include "apactor.h" +#include APActor::APActor() {} -APActor::APActor(const QString url) { +APActor::APActor(const QString& url) { object_url = url; } +APActor::APActor(APActorFields& fields) { + object_url = fields.url; + username = fields.username; + name = fields.display_name; + summary = fields.summary; + manuallyApprovesFollowers = fields.manuallyApprovesFollowers; + discoverable = fields.discoverable; + joined = QDateTime::fromString(fields.joined_date, Qt::ISODate); + avatar = fields.avatar; + header = fields.header; + + if (fields.keys.length() == fields.values.length()) + for (int i = 0; i < fields.keys.length(); ++i) + table.push_back({fields.keys[i], fields.values[i]}); + else qDebug() << "key and value vectors don't have the same lenght!"; +} + +APActor::~APActor() { + if (avatar) delete avatar; + if (header) delete header; +} + const QString APActor::get_url() { return object_url; } diff --git a/src/activitypub/apactor.h b/src/activitypub/apactor.h index 60ef0bf..ad8d689 100644 --- a/src/activitypub/apactor.h +++ b/src/activitypub/apactor.h @@ -1,22 +1,60 @@ #pragma once #include "apbase.h" +#include "apattachment.h" +#include #include +struct APPropertyValue { + QString key; + QString value; +}; + +struct APActorFields { + QString url; + QString username; // without the leading '@' + QString display_name; + QString summary; // Profile biography/description (bio) + QStringList keys; + QStringList values; + APAttachment* avatar = nullptr; + APAttachment* header = nullptr; + bool manuallyApprovesFollowers = false; + bool discoverable = true; // is discoverable or indexable + QString joined_date; + QStringList also_known_as; // other Actors representing the same user +}; + // APActors will have an avatar, display name, username and more that will be fetched using the remote instance +// Currently missing: featured tags and featured posts class APActor : APBase { public: // Empty actor APActor(); - APActor(const QString url); - ~APActor() {}; + APActor(const QString& url); + APActor(APActorFields& fields); + ~APActor(); const QString get_url(); QString get_html_render(HtmlRenderDetails render_info); const QString get_plain_render(); private: + friend class TabActorInfo; + QString object_url; + QString username; + QString name; // Display name + QString summary; // Profile biography/description (bio) + typedef std::vector APPropertyValueList; // Key/value table in profile (example: "web site | http://example.com/") + APPropertyValueList table; + APAttachment* avatar = nullptr; + APAttachment* header = nullptr; + bool manuallyApprovesFollowers = false; + bool discoverable = true; // is discoverable or indexable + QDateTime joined; + // TODO: parse the also_known_as field + // APActorList known_as; }; class APActorList : public std::vector, APBase { diff --git a/src/activitypub/apattachment.cpp b/src/activitypub/apattachment.cpp index 0589291..342ea21 100644 --- a/src/activitypub/apattachment.cpp +++ b/src/activitypub/apattachment.cpp @@ -1,5 +1,6 @@ #include "apattachment.h" #include +#include APAttachment::APAttachment() {} @@ -28,6 +29,31 @@ QString APAttachment::get_html_render(HtmlRenderDetails info) { return html; } +const QPixmap& APAttachment::get_pixmap(int width, int height) { + if (pixmap) return *pixmap; + + if (width > 0 or height > 0) { + QPixmap image(path_url); + + /* proportionality rule: + * width image width + * ----- = ----------- + * height image height + */ + if (height == 0) + height = (width * image.height()) / image.width(); + else if (width == 0) + width = (height * image.width()) / image.height(); + + pixmap = new QPixmap(image.scaled(QSize(width, height))); + } else { + pixmap = new QPixmap; + if (not pixmap->load(path_url)) + qDebug() << "failed to load" << path_url; + } + return *pixmap; +} + QString APAttachmentList::get_html_render(HtmlRenderDetails render_info) { QString html; diff --git a/src/activitypub/apattachment.h b/src/activitypub/apattachment.h index ff40dc3..f4e620b 100644 --- a/src/activitypub/apattachment.h +++ b/src/activitypub/apattachment.h @@ -2,6 +2,7 @@ #include "apbase.h" #include "fields.h" +#include #include #include @@ -11,6 +12,7 @@ public: APAttachment(APAttachmentFields fields); ~APAttachment() {}; QString get_html_render(HtmlRenderDetails render_info); + const QPixmap& get_pixmap(int width = 0, int height = 0); private: QString blurhash; @@ -19,6 +21,8 @@ private: QString description; // alt text, maps to "name" QString path_url; // attachment URL, might be file on filesystem (make sure that the attachment dir has been found and that the path is correct) QString filename; // nicer descriptor of the attachment's path + + QPixmap* pixmap = nullptr; }; class APAttachmentList : public std::vector, APBase { diff --git a/src/archive/base_archive.cpp b/src/archive/base_archive.cpp index 47cadf2..472abb1 100644 --- a/src/archive/base_archive.cpp +++ b/src/archive/base_archive.cpp @@ -15,3 +15,7 @@ Archive::Archive(const QString& filename) : main_filename(filename) {} const QString Archive::get_instance_address() { return ""; } + +APActor* Archive::get_main_actor() { + return nullptr; +} diff --git a/src/archive/base_archive.h b/src/archive/base_archive.h index bf43306..27259b3 100644 --- a/src/archive/base_archive.h +++ b/src/archive/base_archive.h @@ -1,10 +1,10 @@ #pragma once - +#include "src/activitypub/apactivity.h" +#include "src/activitypub/apactor.h" +#include "src/types.h" #include #include #include -#include "src/activitypub/apactivity.h" -#include "src/types.h" enum ArchiveType { MASTODON @@ -35,6 +35,9 @@ public: virtual void update_status_list(ViewStatusTypes allowed_types, QListWidget *parent) = 0; virtual APActivity* get_activity(ArchiveItemRef index, Hinting_t hinting) = 0; + // Get the Actor that represents the data export, the user who's account data has been exported. + // Return nullptr if the Actor cannot be retrieved + virtual APActor* get_main_actor(); virtual const QString get_html_status_text(ArchiveItemRef status_index) = 0; virtual const QString get_instance_address(); diff --git a/src/archive/mastodon.cpp b/src/archive/mastodon.cpp index 781f928..121c366 100644 --- a/src/archive/mastodon.cpp +++ b/src/archive/mastodon.cpp @@ -355,6 +355,96 @@ const QString MastodonArchive::get_html_status_text(ArchiveItemRef index) { return text; } +APActor* MastodonArchive::get_main_actor() { + // Avoid recreating the Actor each time by caching it + if (actor) return actor; + + // First, get the file + QString actor_filepath = archive_root_dir.filePath("actor.json"); + if (not QFile::exists(actor_filepath)) return nullptr; + QFile actor_file(actor_filepath); + + if (!actor_file.open(QIODevice::ReadOnly | QIODevice::Text)) + return nullptr; + + QJsonParseError json_error; + QJsonDocument actor_json_document = QJsonDocument::fromJson(actor_file.readAll(), &json_error); + actor_file.close(); + + if (json_error.error != QJsonParseError::NoError or actor_json_document.isNull() or actor_json_document.isEmpty() or not actor_json_document.isObject()) + return nullptr; + + // Second, now check the JSON + QJsonObject actor_json(actor_json_document.object()); + + // Do some more throughful checks to make sure that the JSON is actually valid + // Please tell me if actor is not "actor.json" (also, i should instead first read actor.json to know the outbox file location, not the other way around as i've been doing since the beginning). Indeed, the checks are also quite random + if (not json_check_item(actor_json.value("@context"), "https://www.w3.org/ns/activitystreams") or actor_json.value("outbox").toString() != "outbox.json") + return nullptr; + + // Third, now parse the JSON :-D + { + APActorFields obj_fields; + // If the key doesn't exist then toString() will return a null QString + obj_fields.url = actor_json.value("id").toString(); + obj_fields.username = actor_json.value("preferredUsername").toString(); + obj_fields.display_name = actor_json.value("name").toString(); + obj_fields.summary = actor_json.value("summary").toString(); + obj_fields.manuallyApprovesFollowers = actor_json.value("manuallyApprovesFollowers").toBool(); + obj_fields.discoverable = actor_json.value("discoverable").toBool(); + obj_fields.joined_date = actor_json.value("published").toString(); + + if (actor_json.value("attachment").isArray()) { + QJsonArray table = actor_json.value("attachment").toArray(); + for (auto pair_obj : table) { + QJsonObject pair = pair_obj.toObject(); + + // Not a PropertyValue, so better not continue with this pair (just in case it represents something else) + if (pair.value("type").toString() != "PropertyValue") { + qDebug() << "not a PropertyValue!"; + continue; + } + + if (pair.contains("name") && pair.contains("value")) { + obj_fields.keys.append(pair.value("name").toString()); + obj_fields.values.append(pair.value("value").toString()); + } + } + } + + if (actor_json.value("alsoKnownAs").isArray()) { + QJsonArray list = actor_json.value("alsoKnownAs").toArray(); + for (auto id_obj : list) + obj_fields.also_known_as.append(id_obj.toString()); + } + + if (actor_json.value("icon").isObject()) { + APAttachmentFields att_fields; + QJsonObject attachment = actor_json.value("icon").toObject(); + + att_fields.media_type = attachment["mediaType"].toString(); + att_fields.path = archive_root_dir.absoluteFilePath(attachment["url"].toString()); + + obj_fields.avatar = new APAttachment(att_fields); + } + + if (actor_json.value("image").isObject()) { + APAttachmentFields att_fields; + QJsonObject attachment = actor_json.value("image").toObject(); + att_fields.media_type = attachment["mediaType"].toString(); + att_fields.path = archive_root_dir.absoluteFilePath(attachment["url"].toString()); + obj_fields.header = new APAttachment(att_fields); + } + + actor = new APActor(obj_fields); + + // TODO: query type. it seems to be always "Person", but maybe if the account is a bot, it changes? + // The code also currently ignores possible featured tags and users + } + + return actor; +} + void MastodonArchive::find_attachment_dir(QString example_attachment) { // Find the root directory name of the attachment QString root_name = example_attachment.split('/', Qt::SkipEmptyParts)[0]; diff --git a/src/archive/mastodon.h b/src/archive/mastodon.h index 75bc7ec..f148047 100644 --- a/src/archive/mastodon.h +++ b/src/archive/mastodon.h @@ -3,6 +3,7 @@ #include "src/archive/base_archive.h" #include "src/activitypub/fields.h" #include "src/activitypub/apactivity.h" +#include "src/activitypub/apactor.h" #include "src/types.h" #include @@ -19,6 +20,7 @@ public: void update_status_list(ViewStatusTypes allowed_types, QListWidget *parent); APActivity* get_activity(ArchiveItemRef index, Hinting_t hinting = {}); + APActor* get_main_actor(); const QString get_html_status_text(ArchiveItemRef status_index); const QString get_instance_address(); private: @@ -28,6 +30,7 @@ private: QJsonObject *outbox_json = nullptr; QJsonArray *outbox_items = nullptr; + APActor* actor = nullptr; bool is_status_type_allowed(StatusType status_type, ViewStatusTypes allowed_types); StatusType get_status_type(QJsonObject obj); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index c2a9820..1432116 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5,6 +5,7 @@ #include "src/settingsdialog.h" #include "src/aboutdialog.h" #include "src/widgets/tab_activity_list.h" +#include "src/widgets/tab_actor_info.h" #include #include @@ -53,6 +54,10 @@ void MainWindow::create_initial_tabs() { activity_list_tab->relist_statuses(true); }); + // TODO: maybe have one Actor info tab that is constantly updated with the new opened archive? + // actor_info_tab = new TabActorInfo(data_archive); + // ui->tabWidget->addTab(actor_info_tab, tr("Actor Info")); + // TODO: Add the "+" tab for opening new tabs } @@ -151,6 +156,9 @@ void MainWindow::finish_open_file(const Archive::InitError& parse_error) { if (parse_error == Archive::NoError) { emit new_archive_opened(); } + actor_info_tab = new TabActorInfo(data_archive); + ui->tabWidget->addTab(actor_info_tab, tr("Actor Info")); + connect(this, &MainWindow::new_archive_opened, actor_info_tab, &TabActorInfo::deleteLater); // The cursor is restored in TabActivityList::relist_statuses() } diff --git a/src/mainwindow.h b/src/mainwindow.h index e77e2a9..aadd439 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -4,12 +4,12 @@ #include #include #include -#include #include "archive/base_archive.h" #include "src/list_item.h" #include "src/settingsdialog.h" #include "src/widgets/tab_activity_list.h" +#include "src/widgets/tab_actor_info.h" #include "command_line.h" QT_BEGIN_NAMESPACE @@ -53,6 +53,7 @@ private: SettingsDialog* settings_dialog = nullptr; TabActivityList* activity_list_tab = nullptr; + TabActorInfo* actor_info_tab = nullptr; QString open_file_filename; diff --git a/src/widgets/tab_actor_info.cpp b/src/widgets/tab_actor_info.cpp new file mode 100644 index 0000000..79630df --- /dev/null +++ b/src/widgets/tab_actor_info.cpp @@ -0,0 +1,49 @@ +#include "tab_actor_info.h" +#include "ui_tab_actor_info.h" +#include "src/activitypub/apactor.h" +#include "src/archive/base_archive.h" +#include +#include + +TabActorInfo::TabActorInfo(APActor* actor, QWidget* parent) + : QWidget(parent), ui(new Ui::TabActorInfo), actor(actor) +{ + ui->setupUi(this); + ui->gridLayout->setContentsMargins(0, 0, 0, 0); + + update_ui(); +} + +TabActorInfo::TabActorInfo(Archive* archive, QWidget* parent) + : QWidget(parent), ui(new Ui::TabActorInfo) +{ + ui->setupUi(this); + ui->gridLayout->setContentsMargins(0, 0, 0, 0); + + actor = archive->get_main_actor(); + qDebug() << actor; + update_ui(); +} + +TabActorInfo::~TabActorInfo() { + if (actor) delete actor; + delete ui; +} + +// TODO: do this in another thread, like for status_info +void TabActorInfo::update_ui() { + if (not actor) return; + + ui->displayNameText->setText(ui->displayNameText->text().arg(actor->name).arg(actor->username)); + ui->summaryText->setHtml(actor->summary); + ui->avatarImage->setPixmap(actor->avatar->get_pixmap(ui->avatarImage->size().width())); + // TODO: set header as top of background of widget + // Affiche le nom et la description fonctionne sans problèmes + + int row = 0; + for (APPropertyValue prop : actor->table) { + ui->attachments->insertRow(row); + ui->attachments->setItem(row, 0, new QTableWidgetItem(prop.key)); + ui->attachments->setItem(row++, 1, new QTableWidgetItem(prop.value)); + } +} diff --git a/src/widgets/tab_actor_info.h b/src/widgets/tab_actor_info.h new file mode 100644 index 0000000..a1dd84f --- /dev/null +++ b/src/widgets/tab_actor_info.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include "src/activitypub/apactor.h" +#include "src/archive/base_archive.h" + +QT_BEGIN_NAMESPACE +namespace Ui { class TabActorInfo; } +QT_END_NAMESPACE + +class TabActorInfo : public QWidget { + Q_OBJECT + +public: + TabActorInfo(APActor* actor, QWidget *parent = nullptr); + TabActorInfo(Archive* archive, QWidget *parent = nullptr); + ~TabActorInfo(); + // TODO: when clicking/activating avatar image, open it with the user's default image viewer + +public slots: + // Update displayed information to match Actor + void update_ui(); + +private: + Ui::TabActorInfo* ui; + + APActor* actor = nullptr; +}; diff --git a/src/widgets/tab_actor_info.ui b/src/widgets/tab_actor_info.ui new file mode 100644 index 0000000..8fb7e21 --- /dev/null +++ b/src/widgets/tab_actor_info.ui @@ -0,0 +1,79 @@ + + + TabActorInfo + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + <html><head/><body><p><span style=" font-size:12pt; font-weight:600;">%1</span> <span style=" font-size:10pt;">(@%2)</span></p></body></html> + + + + + + + + + true + + + true + + + + + + + QAbstractItemView::ScrollPerPixel + + + false + + + 2 + + + false + + + true + + + false + + + + + + + + + + + + 0 + 0 + + + + aaaaa + + + + + + + + -- cgit v1.2.3-54-g00ecf