diff options
Diffstat (limited to 'eventsview-plugins/eventsview-plugin-mastodon')
14 files changed, 1199 insertions, 0 deletions
diff --git a/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml b/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml new file mode 100644 index 0000000..231b814 --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml @@ -0,0 +1,143 @@ +/**************************************************************************** + ** + ** Copyright (C) 2026 Open Mobile Platform LLC. + ** + ****************************************************************************/ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.TextLinking 1.0 +import "shared" + +SocialMediaFeedItem { + id: item + + property variant imageList + property int likeCount + property int commentCount + property int boostCount + + property string _booster: item.stringValue("boostedBy", "rebloggedBy", "retweeter") + property string _displayName: item.stringValue("name", "displayName", "display_name") + property string _accountName: item.stringValue("accountName", "acct", "screenName", "username") + property string _bodyText: item.stringValue("body", "content", "text") + + timestamp: item.stringValue("timestamp", "createdAt", "created_at") + + avatar.y: item._booster.length > 0 + ? topMargin + boosterIcon.height + Theme.paddingSmall + : topMargin + contentHeight: Math.max(content.y + content.height, avatar.y + avatar.height) + bottomMargin + topMargin: item._booster.length > 0 ? Theme.paddingMedium : Theme.paddingLarge + userRemovable: false + + Image { + id: boosterIcon + + anchors { + right: avatar.right + top: parent.top + topMargin: item.topMargin + } + visible: item._booster.length > 0 + source: "image://theme/icon-s-repost" + (item.highlighted ? "?" + Theme.highlightColor : "") + } + + Text { + anchors { + left: content.left + right: content.right + verticalCenter: boosterIcon.verticalCenter + } + elide: Text.ElideRight + font.pixelSize: Theme.fontSizeExtraSmall + color: item.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + textFormat: Text.PlainText + visible: text.length > 0 + + text: item._booster.length > 0 + ? //: Shown above a post that is boosted by another user. %1 = name of user who boosted + //% "%1 boosted" + qsTrId("lipstick-jolla-home-la-boosted_by").arg(item._booster) + : "" + } + + Column { + id: content + + anchors { + left: avatar.right + leftMargin: Theme.paddingMedium + top: avatar.top + } + width: parent.width - x + + Label { + width: parent.width + truncationMode: TruncationMode.Fade + text: item._displayName + color: item.highlighted ? Theme.highlightColor : Theme.primaryColor + textFormat: Text.PlainText + } + + Label { + width: parent.width + truncationMode: TruncationMode.Fade + text: item._accountName.length > 0 && item._accountName.charAt(0) !== "@" + ? "@" + item._accountName + : item._accountName + font.pixelSize: Theme.fontSizeSmall + color: item.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + textFormat: Text.PlainText + } + + LinkedText { + width: parent.width + elide: Text.ElideRight + wrapMode: Text.Wrap + font.pixelSize: Theme.fontSizeSmall + shortenUrl: true + color: item.highlighted ? Theme.highlightColor : Theme.primaryColor + linkColor: Theme.highlightColor + plainText: item._bodyText + } + + Text { + width: parent.width + height: previewRow.visible ? implicitHeight + Theme.paddingMedium : implicitHeight // add padding below + maximumLineCount: 1 + elide: Text.ElideRight + wrapMode: Text.Wrap + color: item.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + font.pixelSize: Theme.fontSizeExtraSmall + text: item.formattedTime + textFormat: Text.PlainText + } + + SocialMediaPreviewRow { + id: previewRow + + width: parent.width + Theme.horizontalPageMargin // extend to right edge of notification area + imageList: item.imageList + downloader: item.downloader + accountId: item.accountId + connectedToNetwork: item.connectedToNetwork + highlighted: item.highlighted + eventsColumnMaxWidth: item.eventsColumnMaxWidth - item.avatar.width + } + } + + function stringValue() { + for (var i = 0; i < arguments.length; ++i) { + var value = model[arguments[i]] + if (typeof value === "undefined" || value === null) { + continue + } + value = String(value) + if (value.length > 0) { + return value + } + } + return "" + } +} diff --git a/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel.cpp b/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel.cpp new file mode 100644 index 0000000..6d33d48 --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel.cpp @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2013 Jolla Ltd. + * Contact: Lucien Xu <lucien.xu@jollamobile.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "abstractsocialcachemodel.h" +#include "abstractsocialcachemodel_p.h" + +#include <synchronizelists_p.h> + +#include <QtCore/QDebug> +#include <QtCore/QMutexLocker> + +template <> bool compareIdentity<SocialCacheModelRow>( + const SocialCacheModelRow &item, const SocialCacheModelRow &reference) +{ + return item.value(0) == reference.value(0); +} + +template <> +int updateRange<AbstractSocialCacheModelPrivate, SocialCacheModelData>( + AbstractSocialCacheModelPrivate *d, + int index, + int count, + const SocialCacheModelData &source, + int sourceIndex) +{ + d->updateRange(index, count, source, sourceIndex); + + return count; +} + +AbstractSocialCacheModelPrivate::AbstractSocialCacheModelPrivate(AbstractSocialCacheModel *q) + : q_ptr(q) +{ +} + +AbstractSocialCacheModelPrivate::~AbstractSocialCacheModelPrivate() +{ +} + +void AbstractSocialCacheModelPrivate::clearData() +{ + Q_Q(AbstractSocialCacheModel); + if (m_data.count() > 0) { + q->beginRemoveRows(QModelIndex(), 0, m_data.count() - 1); + m_data.clear(); + q->endRemoveRows(); + emit q->countChanged(); + } +} + +void AbstractSocialCacheModelPrivate::updateData(const SocialCacheModelData &data) +{ + Q_Q(AbstractSocialCacheModel); + q->updateData(data); +} + +void AbstractSocialCacheModelPrivate::updateRow(int row, const SocialCacheModelRow &data) +{ + Q_Q(AbstractSocialCacheModel); + q->updateRow(row, data); +} + +void AbstractSocialCacheModelPrivate::insertRange( + int index, int count, const SocialCacheModelData &source, int sourceIndex) +{ + Q_Q(AbstractSocialCacheModel); + + if (count > 0 && index >= 0) { + q->beginInsertRows(QModelIndex(), index, index + count - 1); + m_data = m_data.mid(0, index) + source.mid(sourceIndex, count) + m_data.mid(index); + q->endInsertRows(); + emit q->countChanged(); + } +} + +void AbstractSocialCacheModelPrivate::removeRange(int index, int count) +{ + Q_Q(AbstractSocialCacheModel); + + if (count > 0 && index >= 0) { + q->beginRemoveRows(QModelIndex(), index, index + count - 1); + m_data = m_data.mid(0, index) + m_data.mid(index + count); + q->endRemoveRows(); + emit q->countChanged(); + } +} + +void AbstractSocialCacheModelPrivate::updateRange( + int index, int count, const SocialCacheModelData &source, int sourceIndex) +{ + Q_Q(AbstractSocialCacheModel); + + for (int i = 0; i < count; ++i) { + m_data[index + i] = source[sourceIndex + i]; + } + + emit q->dataChanged(q->createIndex(index, 0), q->createIndex(index + count - 1, 0)); +} + +AbstractSocialCacheModel::AbstractSocialCacheModel(AbstractSocialCacheModelPrivate &dd, + QObject *parent) + : QAbstractListModel(parent), d_ptr(&dd) +{ +} + +AbstractSocialCacheModel::~AbstractSocialCacheModel() +{ +} + +int AbstractSocialCacheModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + Q_D(const AbstractSocialCacheModel); + return d->m_data.count(); +} + +QVariant AbstractSocialCacheModel::data(const QModelIndex &index, int role) const +{ + int row = index.row(); + return getField(row, role); +} + +QVariant AbstractSocialCacheModel::getField(int row, int role) const +{ + Q_D(const AbstractSocialCacheModel); + if (row < 0 || row >= d->m_data.count()) { + return QVariant(); + } + + return d->m_data.at(row).value(role); +} + +QString AbstractSocialCacheModel::nodeIdentifier() const +{ + Q_D(const AbstractSocialCacheModel); + return d->nodeIdentifier; +} + +void AbstractSocialCacheModel::setNodeIdentifier(const QString &nodeIdentifier) +{ + Q_D(AbstractSocialCacheModel); + if (d->nodeIdentifier != nodeIdentifier) { + d->nodeIdentifier = nodeIdentifier; + emit nodeIdentifierChanged(); + d->nodeIdentifierChanged(); + } +} + +int AbstractSocialCacheModel::count() const +{ + return rowCount(); +} + +void AbstractSocialCacheModel::updateData(const SocialCacheModelData &data) +{ + Q_D(AbstractSocialCacheModel); + + const int count = d->m_data.count(); + synchronizeList(d, d->m_data, data); + + if (d->m_data.count() != count) { + emit countChanged(); + } + emit modelUpdated(); +} + +void AbstractSocialCacheModel::updateRow(int row, const SocialCacheModelRow &data) +{ + Q_D(AbstractSocialCacheModel); + foreach (int key, data.keys()) { + d->m_data[row].insert(key, data.value(key)); + } + emit dataChanged(index(row), index(row)); +} diff --git a/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel.h b/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel.h new file mode 100644 index 0000000..1e6394f --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2013 Jolla Ltd. + * Contact: Lucien Xu <lucien.xu@jollamobile.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef ABSTRACTSOCIALCACHEMODEL_H +#define ABSTRACTSOCIALCACHEMODEL_H + +#include <QtCore/QAbstractListModel> + +typedef QMap<int, QVariant> SocialCacheModelRow; +typedef QList<SocialCacheModelRow> SocialCacheModelData; + +class AbstractSocialCacheModelPrivate; + +class AbstractSocialCacheModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QString nodeIdentifier READ nodeIdentifier WRITE setNodeIdentifier NOTIFY nodeIdentifierChanged) + Q_PROPERTY(int count READ count NOTIFY countChanged) + +public: + virtual ~AbstractSocialCacheModel(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role) const; + Q_INVOKABLE QVariant getField(int row, int role) const; + + // properties + QString nodeIdentifier() const; + void setNodeIdentifier(const QString &nodeIdentifier); + int count() const; + + +public Q_SLOTS: + virtual void refresh() = 0; + +Q_SIGNALS: + void nodeIdentifierChanged(); + void countChanged(); + void modelUpdated(); + +protected: + // Methods used to update the model in the C++ side + void updateData(const SocialCacheModelData &data); + void updateRow(int row, const SocialCacheModelRow &data); + + explicit AbstractSocialCacheModel(AbstractSocialCacheModelPrivate &dd, QObject *parent = 0); + QScopedPointer<AbstractSocialCacheModelPrivate> d_ptr; + +private: + Q_DECLARE_PRIVATE(AbstractSocialCacheModel) +}; + +Q_DECLARE_METATYPE(SocialCacheModelRow) +Q_DECLARE_METATYPE(SocialCacheModelData) + +#endif // ABSTRACTSOCIALCACHEMODEL_H diff --git a/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel_p.h b/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel_p.h new file mode 100644 index 0000000..6c92655 --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel_p.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2013 Jolla Ltd. + * Contact: Lucien Xu <lucien.xu@jollamobile.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef ABSTRACTSOCIALCACHEMODEL_P_H +#define ABSTRACTSOCIALCACHEMODEL_P_H + +#include "abstractsocialcachemodel.h" + +#include <QtCore/QMap> + +class AbstractSocialCacheModelPrivate +{ +public: + virtual ~AbstractSocialCacheModelPrivate(); + QString nodeIdentifier; + + void insertRange(int index, int count, const SocialCacheModelData &source, int sourceIndex); + void updateRange(int index, int count, const SocialCacheModelData &source, int sourceIndex); + void removeRange(int index, int count); + + void clearData(); + void updateData(const SocialCacheModelData &data); + void updateRow(int row, const SocialCacheModelRow &data); + + QList<QMap<int, QVariant> > m_data; + +protected: + explicit AbstractSocialCacheModelPrivate(AbstractSocialCacheModel *q); + + virtual void nodeIdentifierChanged() {} + + AbstractSocialCacheModel * const q_ptr; +private: + Q_DECLARE_PUBLIC(AbstractSocialCacheModel) +}; + +#endif // ABSTRACTSOCIALCACHEMODEL_P_H diff --git a/eventsview-plugins/eventsview-plugin-mastodon/eventsview-plugin-mastodon.pro b/eventsview-plugins/eventsview-plugin-mastodon/eventsview-plugin-mastodon.pro new file mode 100644 index 0000000..229f38a --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-mastodon/eventsview-plugin-mastodon.pro @@ -0,0 +1,61 @@ +TEMPLATE = lib +TARGET = jollaeventsviewmastodonplugin +TARGET = $$qtLibraryTarget($$TARGET) + +MODULENAME = com/jolla/eventsview/mastodon +TARGETPATH = $$[QT_INSTALL_QML]/$$MODULENAME + +QT += qml +CONFIG += plugin link_pkgconfig +PKGCONFIG += socialcache + +include($$PWD/../../common/common.pri) + +TS_FILE = $$OUT_PWD/lipstick-jolla-home-mastodon.ts +EE_QM = $$OUT_PWD/lipstick-jolla-home-mastodon_eng_en.qm + +ts.commands += lupdate $$PWD -ts $$TS_FILE +ts.CONFIG += no_check_exist no_link +ts.output = $$TS_FILE +ts.input = . + +ts_install.files = $$TS_FILE +ts_install.path = /usr/share/translations/source +ts_install.CONFIG += no_check_exist + +# should add -markuntranslated "-" when proper translations are in place (or for testing) +engineering_english.commands += lrelease -idbased $$TS_FILE -qm $$EE_QM +engineering_english.CONFIG += no_check_exist no_link +engineering_english.depends = ts +engineering_english.input = $$TS_FILE +engineering_english.output = $$EE_QM + +engineering_english_install.path = /usr/share/translations +engineering_english_install.files = $$EE_QM +engineering_english_install.CONFIG += no_check_exist + +QMAKE_EXTRA_TARGETS += ts engineering_english +PRE_TARGETDEPS += ts engineering_english + +INSTALLS += ts_install engineering_english_install + +HEADERS += \ + abstractsocialcachemodel.h \ + abstractsocialcachemodel_p.h \ + mastodonpostsmodel.h + +SOURCES += \ + abstractsocialcachemodel.cpp \ + mastodonpostsmodel.cpp \ + plugin.cpp + +qml.files = mastodon-delegate.qml MastodonFeedItem.qml +qml.path = /usr/share/lipstick/eventfeed/ + +import.files = qmldir +import.path = $$TARGETPATH +target.path = $$TARGETPATH + +OTHER_FILES += $$qml.files $$import.files + +INSTALLS += target import qml diff --git a/eventsview-plugins/eventsview-plugin-mastodon/lipstick-jolla-home-mastodon.ts b/eventsview-plugins/eventsview-plugin-mastodon/lipstick-jolla-home-mastodon.ts new file mode 100644 index 0000000..60f39fa --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-mastodon/lipstick-jolla-home-mastodon.ts @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1"> +<context> + <name></name> + <message id="lipstick-jolla-home-la-mastodon_posts"> + <location filename="mastodon-delegate.qml" line="19"/> + <source>Posts</source> + <extracomment>Mastodon posts</extracomment> + <translation type="unfinished"></translation> + </message> + <message id="lipstick-jolla-home-la-show-more-in-mastodon"> + <location filename="mastodon-delegate.qml" line="56"/> + <source>Show more in Mastodon</source> + <translation type="unfinished"></translation> + </message> + <message id="lipstick-jolla-home-la-boosted_by"> + <location filename="MastodonFeedItem.qml" line="61"/> + <source>%1 boosted</source> + <extracomment>Shown above a post that is boosted by another user. %1 = name of user who boosted</extracomment> + <translation type="unfinished"></translation> + </message> +</context> +</TS> diff --git a/eventsview-plugins/eventsview-plugin-mastodon/lipstick-jolla-home-mastodon_eng_en.qm b/eventsview-plugins/eventsview-plugin-mastodon/lipstick-jolla-home-mastodon_eng_en.qm Binary files differnew file mode 100644 index 0000000..30da83b --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-mastodon/lipstick-jolla-home-mastodon_eng_en.qm diff --git a/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml b/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml new file mode 100644 index 0000000..ed79fdb --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml @@ -0,0 +1,167 @@ +/**************************************************************************** + ** + ** Copyright (C) 2026 Open Mobile Platform LLC. + ** + ****************************************************************************/ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.socialcache 1.0 +import com.jolla.eventsview.mastodon 1.0 +import QtQml.Models 2.1 +import "shared" + +SocialMediaAccountDelegate { + id: delegateItem + + //: Mastodon posts + //% "Posts" + headerText: qsTrId("lipstick-jolla-home-la-mastodon_posts") + headerIcon: "image://theme/graphic-service-mastodon" + showRemainingCount: false + + services: ["Posts", "Notifications"] + socialNetwork: 9 + dataType: SocialSync.Posts + providerName: "mastodon" + + model: MastodonPostsModel { + onCountChanged: { + if (count > 0) { + if (!updateTimer.running) { + shortUpdateTimer.start() + } + } else { + shortUpdateTimer.stop() + } + } + } + + delegate: MastodonFeedItem { + downloader: delegateItem.downloader + imageList: delegateItem.variantRole(model, ["images", "mediaAttachments", "media"]) + avatarSource: delegateItem.convertUrl(delegateItem.stringRole(model, ["icon", "avatar", "avatarUrl"])) + fallbackAvatarSource: delegateItem.stringRole(model, ["icon", "avatar", "avatarUrl"]) + accountId: model.accounts[0] + + onTriggered: Qt.openUrlExternally(delegateItem.statusUrl(model)) + + Component.onCompleted: { + refreshTimeCount = Qt.binding(function() { return delegateItem.refreshTimeCount }) + connectedToNetwork = Qt.binding(function() { return delegateItem.connectedToNetwork }) + eventsColumnMaxWidth = Qt.binding(function() { return delegateItem.eventsColumnMaxWidth }) + } + } + //% "Show more in Mastodon" + expandedLabel: qsTrId("lipstick-jolla-home-la-show-more-in-mastodon") + + onHeaderClicked: Qt.openUrlExternally("https://mastodon.social/explore") + onExpandedClicked: Qt.openUrlExternally("https://mastodon.social/explore") + + onViewVisibleChanged: { + if (viewVisible) { + delegateItem.resetHasSyncableAccounts() + delegateItem.model.refresh() + if (delegateItem.hasSyncableAccounts && !updateTimer.running) { + shortUpdateTimer.start() + } + } else { + shortUpdateTimer.stop() + } + } + + onConnectedToNetworkChanged: { + if (viewVisible) { + if (!updateTimer.running) { + shortUpdateTimer.start() + } + } + } + + // The Mastodon feed is updated 3 seconds after the feed view becomes visible, + // unless it has been updated during last 60 seconds. After that it will be updated + // periodically in every 60 seconds as long as the feed view is visible. + + Timer { + id: shortUpdateTimer + + interval: 3000 + onTriggered: { + delegateItem.sync() + updateTimer.start() + } + } + + Timer { + id: updateTimer + + interval: 60000 + repeat: true + onTriggered: { + if (delegateItem.viewVisible) { + delegateItem.sync() + } else { + stop() + } + } + } + + function variantRole(modelData, roleNames) { + for (var i = 0; i < roleNames.length; ++i) { + var value = modelData[roleNames[i]] + if (typeof value !== "undefined" && value !== null) { + return value + } + } + return undefined + } + + function stringRole(modelData, roleNames) { + for (var i = 0; i < roleNames.length; ++i) { + var value = modelData[roleNames[i]] + if (typeof value === "undefined" || value === null) { + continue + } + value = String(value) + if (value.length > 0) { + return value + } + } + return "" + } + + function statusUrl(modelData) { + var directUrl = stringRole(modelData, ["url", "link", "uri"]) + if (directUrl.length > 0) { + return directUrl + } + + var instanceUrl = stringRole(modelData, ["instanceUrl", "serverUrl", "baseUrl"]) + if (instanceUrl.length === 0) { + instanceUrl = "https://mastodon.social" + } + while (instanceUrl.length > 0 && instanceUrl.charAt(instanceUrl.length - 1) === "/") { + instanceUrl = instanceUrl.slice(0, instanceUrl.length - 1) + } + + var accountName = stringRole(modelData, ["accountName", "acct", "screenName", "username"]) + var statusId = stringRole(modelData, ["mastodonId", "statusId", "id", "twitterId"]) + if (accountName.length > 0 && statusId.length > 0) { + while (accountName.length > 0 && accountName.charAt(0) === "@") { + accountName = accountName.substring(1) + } + return instanceUrl + "/@" + accountName + "/" + statusId + } + + return instanceUrl + "/explore" + } + + function convertUrl(source) { + if (source.indexOf("_normal.") !== -1) { + return source.replace("_normal.", "_bigger.") + } else if (source.indexOf("_mini.") !== -1) { + return source.replace("_mini.", "_bigger.") + } + return source + } +} diff --git a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.cpp b/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.cpp new file mode 100644 index 0000000..3e54b8b --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.cpp @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2026 Open Mobile Platform LLC. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "mastodonpostsmodel.h" +#include "abstractsocialcachemodel_p.h" +#include "mastodonpostsdatabase.h" +#include "postimagehelper_p.h" + +class MastodonPostsModelPrivate: public AbstractSocialCacheModelPrivate +{ +public: + explicit MastodonPostsModelPrivate(MastodonPostsModel *q); + + MastodonPostsDatabase database; + +private: + Q_DECLARE_PUBLIC(MastodonPostsModel) +}; + +MastodonPostsModelPrivate::MastodonPostsModelPrivate(MastodonPostsModel *q) + : AbstractSocialCacheModelPrivate(q) +{ +} + +MastodonPostsModel::MastodonPostsModel(QObject *parent) + : AbstractSocialCacheModel(*(new MastodonPostsModelPrivate(this)), parent) +{ + Q_D(MastodonPostsModel); + + connect(&d->database, &AbstractSocialPostCacheDatabase::postsChanged, + this, &MastodonPostsModel::postsChanged); + connect(&d->database, SIGNAL(accountIdFilterChanged()), + this, SIGNAL(accountIdFilterChanged())); +} + +QHash<int, QByteArray> MastodonPostsModel::roleNames() const +{ + QHash<int, QByteArray> roleNames; + roleNames.insert(MastodonId, "mastodonId"); + roleNames.insert(Name, "name"); + roleNames.insert(AccountName, "accountName"); + roleNames.insert(Acct, "acct"); + roleNames.insert(Body, "body"); + roleNames.insert(Timestamp, "timestamp"); + roleNames.insert(Icon, "icon"); + roleNames.insert(Images, "images"); + roleNames.insert(Url, "url"); + roleNames.insert(Link, "link"); + roleNames.insert(BoostedBy, "boostedBy"); + roleNames.insert(RebloggedBy, "rebloggedBy"); + roleNames.insert(InstanceUrl, "instanceUrl"); + roleNames.insert(Accounts, "accounts"); + return roleNames; +} + +QVariantList MastodonPostsModel::accountIdFilter() const +{ + Q_D(const MastodonPostsModel); + + return d->database.accountIdFilter(); +} + +void MastodonPostsModel::setAccountIdFilter(const QVariantList &accountIds) +{ + Q_D(MastodonPostsModel); + + d->database.setAccountIdFilter(accountIds); +} + +void MastodonPostsModel::refresh() +{ + Q_D(MastodonPostsModel); + + d->database.refresh(); +} + +void MastodonPostsModel::postsChanged() +{ + Q_D(MastodonPostsModel); + + SocialCacheModelData data; + QList<SocialPost::ConstPtr> postsData = d->database.posts(); + Q_FOREACH (const SocialPost::ConstPtr &post, postsData) { + QMap<int, QVariant> eventMap; + const QString accountName = d->database.accountName(post); + const QString postUrl = d->database.url(post); + const QString boostedBy = d->database.boostedBy(post); + + eventMap.insert(MastodonPostsModel::MastodonId, post->identifier()); + eventMap.insert(MastodonPostsModel::Name, post->name()); + eventMap.insert(MastodonPostsModel::AccountName, accountName); + eventMap.insert(MastodonPostsModel::Acct, accountName); + eventMap.insert(MastodonPostsModel::Body, post->body()); + eventMap.insert(MastodonPostsModel::Timestamp, post->timestamp()); + eventMap.insert(MastodonPostsModel::Icon, post->icon()); + eventMap.insert(MastodonPostsModel::Url, postUrl); + eventMap.insert(MastodonPostsModel::Link, postUrl); + eventMap.insert(MastodonPostsModel::BoostedBy, boostedBy); + eventMap.insert(MastodonPostsModel::RebloggedBy, boostedBy); + eventMap.insert(MastodonPostsModel::InstanceUrl, d->database.instanceUrl(post)); + + QVariantList images; + Q_FOREACH (const SocialPostImage::ConstPtr &image, post->images()) { + images.append(createImageData(image)); + } + eventMap.insert(MastodonPostsModel::Images, images); + + QVariantList accountsVariant; + Q_FOREACH (int account, post->accounts()) { + accountsVariant.append(account); + } + eventMap.insert(MastodonPostsModel::Accounts, accountsVariant); + data.append(eventMap); + } + + updateData(data); +} diff --git a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.h b/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.h new file mode 100644 index 0000000..9692729 --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2026 Open Mobile Platform LLC. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef MASTODONPOSTSMODEL_H +#define MASTODONPOSTSMODEL_H + +#include "abstractsocialcachemodel.h" + +class MastodonPostsModelPrivate; + +class MastodonPostsModel: public AbstractSocialCacheModel +{ + Q_OBJECT + Q_PROPERTY(QVariantList accountIdFilter READ accountIdFilter WRITE setAccountIdFilter NOTIFY accountIdFilterChanged) + +public: + enum MastodonPostsRole { + MastodonId = 0, + Name, + AccountName, + Acct, + Body, + Timestamp, + Icon, + Images, + Url, + Link, + BoostedBy, + RebloggedBy, + InstanceUrl, + Accounts + }; + + explicit MastodonPostsModel(QObject *parent = 0); + QHash<int, QByteArray> roleNames() const; + + QVariantList accountIdFilter() const; + void setAccountIdFilter(const QVariantList &accountIds); + + void refresh(); + +signals: + void accountIdFilterChanged(); + +private slots: + void postsChanged(); + +private: + Q_DECLARE_PRIVATE(MastodonPostsModel) +}; + +#endif // MASTODONPOSTSMODEL_H diff --git a/eventsview-plugins/eventsview-plugin-mastodon/plugin.cpp b/eventsview-plugins/eventsview-plugin-mastodon/plugin.cpp new file mode 100644 index 0000000..35d95ca --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-mastodon/plugin.cpp @@ -0,0 +1,19 @@ +#include <QQmlExtensionPlugin> +#include <QtQml> + +#include "mastodonpostsmodel.h" + +class JollaEventsviewMastodonPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "com.jolla.eventsview.mastodon") + +public: + void registerTypes(const char *uri) override + { + Q_ASSERT(QLatin1String(uri) == QLatin1String("com.jolla.eventsview.mastodon")); + qmlRegisterType<MastodonPostsModel>(uri, 1, 0, "MastodonPostsModel"); + } +}; + +#include "plugin.moc" diff --git a/eventsview-plugins/eventsview-plugin-mastodon/postimagehelper_p.h b/eventsview-plugins/eventsview-plugin-mastodon/postimagehelper_p.h new file mode 100644 index 0000000..fe61212 --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-mastodon/postimagehelper_p.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2013 Jolla Ltd. + * Contact: Lucien Xu <lucien.xu@jollamobile.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef POSTIMAGEHELPER_P_H +#define POSTIMAGEHELPER_P_H + +#include <QtCore/QVariantMap> + +static const char *URL_KEY = "url"; +static const char *TYPE_KEY = "type"; +static const char *TYPE_PHOTO = "photo"; +static const char *TYPE_VIDEO = "video"; + +inline static QVariantMap createImageData(const SocialPostImage::ConstPtr &image) +{ + QVariantMap imageData; + imageData.insert(QLatin1String(URL_KEY), image->url()); + switch (image->type()) { + case SocialPostImage::Video: + imageData.insert(QLatin1String(TYPE_KEY), QLatin1String(TYPE_VIDEO)); + break; + default: + imageData.insert(QLatin1String(TYPE_KEY), QLatin1String(TYPE_PHOTO)); + break; + } + return imageData; +} + +#endif // POSTIMAGEHELPER_P_H diff --git a/eventsview-plugins/eventsview-plugin-mastodon/qmldir b/eventsview-plugins/eventsview-plugin-mastodon/qmldir new file mode 100644 index 0000000..515a0a0 --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-mastodon/qmldir @@ -0,0 +1,2 @@ +module com.jolla.eventsview.mastodon +plugin jollaeventsviewmastodonplugin diff --git a/eventsview-plugins/eventsview-plugin-mastodon/synchronizelists_p.h b/eventsview-plugins/eventsview-plugin-mastodon/synchronizelists_p.h new file mode 100644 index 0000000..1e09e86 --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-mastodon/synchronizelists_p.h @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2013 Jolla Mobile <andrew.den.exter@jollamobile.com> + * + * You may use this file under the terms of the BSD license as follows: + * + * "Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Nemo Mobile nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + */ + +#ifndef SYNCHRONIZELISTS_P_H +#define SYNCHRONIZELISTS_P_H + +template <typename T> +bool compareIdentity(const T &item, const T &reference) +{ + return item == reference; +} + +template <typename Agent, typename ReferenceList> +int insertRange(Agent *agent, int index, int count, const ReferenceList &source, int sourceIndex) +{ + agent->insertRange(index, count, source, sourceIndex); + return count; +} + +template <typename Agent> +int removeRange(Agent *agent, int index, int count) +{ + agent->removeRange(index, count); + return 0; +} + +template <typename Agent, typename ReferenceList> +int updateRange(Agent *agent, int index, int count, const ReferenceList &source, int sourceIndex) +{ + Q_UNUSED(agent); + Q_UNUSED(index); + Q_UNUSED(source); + Q_UNUSED(sourceIndex); + return count; +} + +template <typename Agent, typename CacheList, typename ReferenceList> +class SynchronizeList +{ +public: + SynchronizeList( + Agent *agent, + const CacheList &cache, + int &c, + const ReferenceList &reference, + int &r) + : agent(agent), cache(cache), c(c), reference(reference), r(r) + { + int lastEqualC = c; + int lastEqualR = r; + for (; c < cache.count() && r < reference.count(); ++c, ++r) { + if (compareIdentity(cache.at(c), reference.at(r))) { + continue; + } + + if (c > lastEqualC) { + lastEqualC += updateRange(agent, lastEqualC, c - lastEqualC, reference, lastEqualR); + c = lastEqualC; + lastEqualR = r; + } + + bool match = false; + + // Iterate through both the reference and cache lists in parallel looking for first + // point of commonality, when that is found resolve the differences and continue + // looking. + int count = 1; + for (; !match && c + count < cache.count() && r + count < reference.count(); ++count) { + typename CacheList::const_reference cacheItem = cache.at(c + count); + typename ReferenceList::const_reference referenceItem = reference.at(r + count); + + for (int i = 0; i <= count; ++i) { + if (cacheMatch(i, count, referenceItem) || referenceMatch(i, count, cacheItem)) { + match = true; + break; + } + } + } + + // Continue scanning the reference list if the cache has been exhausted. + for (int re = r + count; !match && re < reference.count(); ++re) { + typename ReferenceList::const_reference referenceItem = reference.at(re); + for (int i = 0; i < count; ++i) { + if (cacheMatch(i, re - r, referenceItem)) { + match = true; + break; + } + } + } + + // Continue scanning the cache if the reference list has been exhausted. + for (int ce = c + count; !match && ce < cache.count(); ++ce) { + typename CacheList::const_reference cacheItem = cache.at(ce); + for (int i = 0; i < count; ++i) { + if (referenceMatch(i, ce - c, cacheItem)) { + match = true; + break; + } + } + } + + if (!match) + return; + + lastEqualC = c; + lastEqualR = r; + } + + if (c > lastEqualC) { + updateRange(agent, lastEqualC, c - lastEqualC, reference, lastEqualR); + } + } + +private: + // Tests if the cached contact id at i matches a referenceId. + // If there is a match removes all items traversed in the cache since the previous match + // and inserts any items in the reference set found to to not be in the cache. + bool cacheMatch(int i, int count, typename ReferenceList::const_reference referenceItem) + { + if (compareIdentity(cache.at(c + i), referenceItem)) { + if (i > 0) + c += removeRange(agent, c, i); + c += insertRange(agent, c, count, reference, r); + r += count; + return true; + } else { + return false; + } + } + + // Tests if the reference contact id at i matches a cacheId. + // If there is a match inserts all items traversed in the reference set since the + // previous match and removes any items from the cache that were not found in the + // reference list. + bool referenceMatch(int i, int count, typename ReferenceList::const_reference cacheItem) + { + if (compareIdentity(reference.at(r + i), cacheItem)) { + c += removeRange(agent, c, count); + if (i > 0) + c += insertRange(agent, c, i, reference, r); + r += i; + return true; + } else { + return false; + } + } + + Agent * const agent; + const CacheList &cache; + int &c; + const ReferenceList &reference; + int &r; +}; + +template <typename Agent, typename CacheList, typename ReferenceList> +void completeSynchronizeList( + Agent *agent, + const CacheList &cache, + int &cacheIndex, + const ReferenceList &reference, + int &referenceIndex) +{ + if (cacheIndex < cache.count()) { + agent->removeRange(cacheIndex, cache.count() - cacheIndex); + } + if (referenceIndex < reference.count()) { + agent->insertRange(cache.count(), reference.count() - referenceIndex, reference, referenceIndex); + } + + cacheIndex = 0; + referenceIndex = 0; +} + +template <typename Agent, typename CacheList, typename ReferenceList> +void synchronizeList( + Agent *agent, + const CacheList &cache, + int &cacheIndex, + const ReferenceList &reference, + int &referenceIndex) +{ + SynchronizeList<Agent, CacheList, ReferenceList>( + agent, cache, cacheIndex, reference, referenceIndex); +} + +template <typename Agent, typename CacheList, typename ReferenceList> +void synchronizeList(Agent *agent, const CacheList &cache, const ReferenceList &reference) +{ + int cacheIndex = 0; + int referenceIndex = 0; + SynchronizeList<Agent, CacheList, ReferenceList>( + agent, cache, cacheIndex, reference, referenceIndex); + completeSynchronizeList(agent, cache, cacheIndex, reference, referenceIndex); +} + +#endif |
