diff options
| author | Andrew Branson <andrew.branson@jolla.com> | 2026-04-03 22:55:30 +0200 |
|---|---|---|
| committer | Andrew Branson <andrew.branson@jolla.com> | 2026-04-04 11:55:25 +0200 |
| commit | a35c9fa159173388d88ef77e1d31f53488aad094 (patch) | |
| tree | e4691b5bbf054ca13e35d98d9df653bf9cdc0054 /eventsview-plugins | |
| parent | 5f999f7a4712c4a4d1c89054b544064cfd4b769e (diff) | |
Generalize for all fediverse accounts
Diffstat (limited to 'eventsview-plugins')
15 files changed, 823 insertions, 941 deletions
diff --git a/eventsview-plugins/eventsview-plugin-fediverse/FediverseFeedItem.qml b/eventsview-plugins/eventsview-plugin-fediverse/FediverseFeedItem.qml new file mode 100644 index 0000000..16dc191 --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-fediverse/FediverseFeedItem.qml @@ -0,0 +1,362 @@ +/* + * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Share 1.0 +import Sailfish.TextLinking 1.0 +import org.nemomobile.lipstick 0.1 +import "shared" + +SocialMediaFeedItem { + id: item + + property variant imageList + property string resolvedStatusUrl: model && model.url ? model.url.toString() : "" + property string postId + property QtObject postActions + property int likeCount: model && model.favouritesCount ? model.favouritesCount : 0 + property int commentCount: model && model.repliesCount ? model.repliesCount : 0 + property int boostCount: model && model.reblogsCount ? model.reblogsCount : 0 + property bool favourited: model ? !!model.favourited : false + property bool reblogged: model ? !!model.reblogged : false + property int _likeCountOverride: -1 + property int _boostCountOverride: -1 + property int _favouritedOverride: -1 + property int _rebloggedOverride: -1 + property bool isFavourited: _favouritedOverride >= 0 ? _favouritedOverride === 1 : favourited + property bool isReblogged: _rebloggedOverride >= 0 ? _rebloggedOverride === 1 : reblogged + readonly property bool housekeeping: Lipstick.compositor.eventsLayer.housekeeping + readonly property bool lockScreenActive: Lipstick.compositor.lockScreenLayer.deviceIsLocked + property bool _pendingOpenActionMenu: false + property bool _contextMenuOpen: false + property var _actionMenu + property real _contextMenuHeight: (_contextMenuOpen && _actionMenu) ? _actionMenu.height : 0 + + property string _booster: model && model.boostedBy ? model.boostedBy.toString() : "" + property string _displayName: model && model.name ? model.name.toString() : "" + property string _accountName: model && model.accountName ? model.accountName.toString() : "" + property string _bodyText: model && model.body ? model.body.toString() : "" + //: Action label shown in Fediverse interaction menu. + //% "Favourite" + readonly property string _favouriteActionText: qsTrId("lipstick-jolla-home-la-fediverse_favourite") + //: Action label shown in Fediverse interaction menu when the post is already favourited. + //% "Unfavourite" + readonly property string _unfavouriteActionText: qsTrId("lipstick-jolla-home-la-fediverse_unfavourite") + //: Action label shown in Fediverse interaction menu. + //% "Boost" + readonly property string _boostActionText: qsTrId("lipstick-jolla-home-la-fediverse_boost") + //: Action label shown in Fediverse interaction menu when the post is already boosted. + //% "Undo boost" + readonly property string _unboostActionText: qsTrId("lipstick-jolla-home-la-fediverse_unboost") + //: Action label shown in Fediverse interaction menu. + //% "Share" + readonly property string _shareActionText: qsTrId("lipstick-jolla-home-la-fediverse_share") + //: Link title used when sharing a Fediverse post. + //% "Post from Fediverse" + readonly property string _shareLinkTitle: qsTrId("lipstick-jolla-home-la-fediverse_share_link_title") + property var _shareAction: ShareAction { + title: item._shareActionText + } + + timestamp: model.timestamp + onRefreshTimeCountChanged: formattedTime = Format.formatDate(model.timestamp, Format.TimeElapsed) + onLockScreenActiveChanged: { + if (lockScreenActive && _actionMenu) { + _actionMenu.close() + } + } + onPressAndHold: function(mouse) { + if (mouse) { + mouse.accepted = true + } + _pendingOpenActionMenu = !lockScreenActive + && postActions + && actionPostId().length > 0 + && actionAccountId() >= 0 + openActionMenuTimer.restart() + } + onHousekeepingChanged: { + if (housekeeping && _pendingOpenActionMenu) { + Lipstick.compositor.eventsLayer.setHousekeeping(false) + } + } + Component.onDestruction: { + if (_actionMenu) { + _actionMenu.destroy() + _actionMenu = null + } + } + + avatar.y: item._booster.length > 0 + ? topMargin + boosterIcon.height + Theme.paddingSmall + : topMargin + contentHeight: Math.max(content.y + content.height, avatar.y + avatar.height) + bottomMargin + _contextMenuHeight + topMargin: item._booster.length > 0 ? Theme.paddingMedium : Theme.paddingLarge + userRemovable: false + + SocialReshareIcon { + id: boosterIcon + + anchors { + right: avatar.right + top: parent.top + topMargin: item.topMargin + } + visible: item._booster.length > 0 + highlighted: item.highlighted + iconSource: "image://theme/icon-s-repost" + } + + SocialReshareText { + anchors { + left: content.left + right: content.right + verticalCenter: boosterIcon.verticalCenter + } + highlighted: item.highlighted + 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 + } + + SocialPostMetadataRow { + id: metadataRow + + width: parent.width + highlighted: item.highlighted + commentCount: item.commentCount + likeCount: item._likeCountOverride >= 0 ? item._likeCountOverride : item.likeCount + repostCount: item._boostCountOverride >= 0 ? item._boostCountOverride : item.boostCount + liked: item.isFavourited + reposted: item.isReblogged + timeText: item.formattedTime + addBottomPadding: previewRow.visible + } + + 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 actionPostId() { + if (item.postId.length > 0) { + return item.postId + } + return model && model.fediverseId ? model.fediverseId.toString() : "" + } + + function actionAccountId() { + var parsed = Number(item.accountId) + return isNaN(parsed) ? -1 : parsed + } + + function shareStatusUrl() { + return model && model.url ? model.url.toString() : "" + } + + function topLevelParent() { + var p = item + while (p && p.parent) { + p = p.parent + } + return p + } + + function openActionMenu() { + if (_actionMenu) { + _actionMenu.destroy() + _actionMenu = null + } + + var parentItem = topLevelParent() + _actionMenu = actionMenuComponent.createObject(parentItem) + if (_actionMenu) { + _actionMenu.open(item) + } + } + + Connections { + target: item.postActions ? item.postActions : null + + onActionSucceeded: { + if (accountId !== item.actionAccountId() || statusId !== item.actionPostId()) { + return + } + + if (favouritesCount >= 0) { + item._likeCountOverride = favouritesCount + } + if (reblogsCount >= 0) { + item._boostCountOverride = reblogsCount + } + item._favouritedOverride = favourited ? 1 : 0 + item._rebloggedOverride = reblogged ? 1 : 0 + item._contextMenuOpen = false + + if (item._accountDelegate) { + item._accountDelegate.sync() + } + } + + onActionFailed: { + if (accountId !== item.actionAccountId() || statusId !== item.actionPostId()) { + return + } + console.warn("Fediverse action failed:", action, errorMessage) + item._contextMenuOpen = false + } + } + + Component { + id: actionMenuComponent + + SocialInteractionContextMenu { + id: actionMenu + z: 10000 + mapSourceItem: _contentColumn + actionEnabled: item.postActions + && item.actionPostId().length > 0 + && item.actionAccountId() >= 0 + && !item.lockScreenActive + && !item.housekeeping + interactionItems: [ + { + name: "like", + // U+2605 BLACK STAR + symbol: "\u2605", + active: item.isFavourited, + inactiveText: item._favouriteActionText, + activeText: item._unfavouriteActionText + }, + { + name: "reblog", + // U+21BB CLOCKWISE OPEN CIRCLE ARROW + symbol: "\u21BB", + active: item.isReblogged, + inactiveText: item._boostActionText, + activeText: item._unboostActionText + }, + { + name: "share", + // U+260D OPPOSITION (ironic doncha think) + symbol: "\u260D", + active: false, + inactiveText: item._shareActionText, + activeText: item._shareActionText + } + ] + + onInteractionMenuOpened: item._contextMenuOpen = true + onInteractionMenuClosed: { + item._contextMenuOpen = false + destroy() + item._actionMenu = null + } + + onInteractionTriggered: function(actionName) { + if (!actionEnabled) { + return + } + var postId = item.actionPostId() + var accountId = item.actionAccountId() + if (actionName === "like") { + if (item.isFavourited) { + item.postActions.unfavourite(accountId, postId) + } else { + item.postActions.favourite(accountId, postId) + } + } else if (actionName === "reblog") { + if (item.isReblogged) { + item.postActions.unboost(accountId, postId) + } else { + item.postActions.boost(accountId, postId) + } + } else if (actionName === "share") { + var shareUrl = item.shareStatusUrl() + if (shareUrl.length === 0) { + return + } + item._shareAction.resources = [{ + "data": shareUrl, + "linkTitle": item._shareLinkTitle, + "type": "text/x-url" + }] + item._shareAction.trigger() + } + } + } + } + + Timer { + id: openActionMenuTimer + + interval: 0 + repeat: false + onTriggered: { + if (item.lockScreenActive) { + item._pendingOpenActionMenu = false + return + } + Lipstick.compositor.eventsLayer.setHousekeeping(false) + if (item._pendingOpenActionMenu) { + item._contextMenuOpen = false + item.openActionMenu() + } + item._pendingOpenActionMenu = false + } + } +} diff --git a/eventsview-plugins/eventsview-plugin-mastodon/eventsview-plugin-mastodon.pro b/eventsview-plugins/eventsview-plugin-fediverse/eventsview-plugin-fediverse.pro index 04be215..e546e9d 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/eventsview-plugin-mastodon.pro +++ b/eventsview-plugins/eventsview-plugin-fediverse/eventsview-plugin-fediverse.pro @@ -3,20 +3,21 @@ # SPDX-License-Identifier: BSD-3-Clause TEMPLATE = lib -TARGET = jollaeventsviewmastodonplugin +TARGET = jollaeventsviewfediverseplugin TARGET = $$qtLibraryTarget($$TARGET) -MODULENAME = com/jolla/eventsview/mastodon +MODULENAME = com/jolla/eventsview/fediverse TARGETPATH = $$[QT_INSTALL_QML]/$$MODULENAME +QT -= gui QT += qml network CONFIG += plugin link_pkgconfig PKGCONFIG += socialcache accounts-qt5 libsignon-qt5 sailfishaccounts 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_FILE = $$OUT_PWD/lipstick-jolla-home-fediverse.ts +EE_QM = $$OUT_PWD/lipstick-jolla-home-fediverse_eng_en.qm ts.commands += lupdate $$PWD -ts $$TS_FILE ts.CONFIG += no_check_exist no_link @@ -44,15 +45,15 @@ PRE_TARGETDEPS += ts engineering_english INSTALLS += ts_install engineering_english_install HEADERS += \ - mastodonpostactions.h \ - mastodonpostsmodel.h + fediversepostactions.h \ + fediversepostsmodel.h SOURCES += \ - mastodonpostactions.cpp \ - mastodonpostsmodel.cpp \ + fediversepostactions.cpp \ + fediversepostsmodel.cpp \ plugin.cpp -qml.files = mastodon-delegate.qml MastodonFeedItem.qml +qml.files = fediverse-delegate.qml FediverseFeedItem.qml qml.path = /usr/share/lipstick/eventfeed/ import.files = qmldir diff --git a/eventsview-plugins/eventsview-plugin-fediverse/fediverse-delegate.qml b/eventsview-plugins/eventsview-plugin-fediverse/fediverse-delegate.qml new file mode 100644 index 0000000..f954db4 --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-fediverse/fediverse-delegate.qml @@ -0,0 +1,185 @@ +/* + * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import org.nemomobile.socialcache 1.0 +import com.jolla.eventsview.fediverse 1.0 +import QtQml.Models 2.1 +import "shared" + +SocialMediaAccountDelegate { + id: delegateItem + property string instanceHomeUrl: "" + property string instanceIconPath: "" + + //: Fediverse posts + //% "Posts" + headerText: qsTrId("lipstick-jolla-home-la-fediverse_posts") + headerIcon: delegateItem.instanceIconPath.length > 0 ? delegateItem.instanceIconPath : "image://theme/icon-l-fediverse" + showRemainingCount: false + + services: ["Posts"] + socialNetwork: SocialSync.Fediverse + dataType: SocialSync.Posts + providerName: "fediverse" + periodicSyncLoopEnabled: true + + FediversePostActions { + id: fediversePostActions + } + + model: FediversePostsModel {} + + delegate: FediverseFeedItem { + downloader: delegateItem.downloader + imageList: model.images + avatarSource: model.icon + fallbackAvatarSource: model.icon + resolvedStatusUrl: delegateItem.authorizeInteractionUrl(model) + postId: model.fediverseId + postActions: fediversePostActions + accountId: delegateItem.firstAccountId(model, -1) + + onTriggered: { + if (resolvedStatusUrl.length > 0) { + Qt.openUrlExternally(resolvedStatusUrl) + } + } + + Component.onCompleted: { + delegateItem.instanceHomeUrl = statusUrl({instanceUrl: model.instanceUrl}) + if (model.instanceIconPath && model.instanceIconPath.length > 0) { + delegateItem.instanceIconPath = model.instanceIconPath + } + refreshTimeCount = Qt.binding(function() { return delegateItem.refreshTimeCount }) + connectedToNetwork = Qt.binding(function() { return delegateItem.connectedToNetwork }) + eventsColumnMaxWidth = Qt.binding(function() { return delegateItem.eventsColumnMaxWidth }) + } + } + //% "Show more in Fediverse" + expandedLabel: qsTrId("lipstick-jolla-home-la-show-more-in-fediverse") + + onHeaderClicked: { + if (delegateItem.instanceHomeUrl.length > 0) { + Qt.openUrlExternally(delegateItem.instanceHomeUrl) + } + } + onExpandedClicked: { + if (delegateItem.instanceHomeUrl.length > 0) { + Qt.openUrlExternally(delegateItem.instanceHomeUrl) + } + } + + onViewVisibleChanged: { + if (viewVisible) { + delegateItem.resetHasSyncableAccounts() + delegateItem.model.refresh() + if (delegateItem.hasSyncableAccounts) { + delegateItem.startPeriodicSyncLoop() + } + } else { + delegateItem.stopPeriodicSyncLoop() + } + } + + onConnectedToNetworkChanged: { + if (viewVisible) { + delegateItem.startPeriodicSyncLoop() + } + } + + Connections { + target: delegateItem.model + + onCountChanged: { + if (target.count === 0) { + delegateItem.instanceHomeUrl = "" + delegateItem.instanceIconPath = "" + } + } + } + + function statusUrl(modelData) { + var directUrl = modelData && modelData.url ? modelData.url.toString() : "" + if (directUrl.length > 0) { + return directUrl + } + + var instanceUrl = modelData && modelData.instanceUrl ? modelData.instanceUrl.toString() : "" + instanceUrl = stripTrailingSlashes(instanceUrl) + if (instanceUrl.length === 0) { + return "" + } + + var accountName = modelData && modelData.accountName ? modelData.accountName.toString() : "" + var statusId = modelData && modelData.fediverseId ? modelData.fediverseId.toString() : "" + if (accountName.length > 0 && statusId.length > 0) { + accountName = trimLeadingCharacter(accountName, "@") + return instanceUrl + "/@" + accountName + "/" + statusId + } + + return instanceUrl + "/explore" + } + + function authorizeInteractionUrl(modelData) { + var targetUrl = statusUrl(modelData) + if (targetUrl.length === 0) { + return targetUrl + } + + var instanceUrl = modelData && modelData.instanceUrl ? modelData.instanceUrl.toString() : "" + if (instanceUrl.length === 0) { + return targetUrl + } + instanceUrl = stripTrailingSlashes(instanceUrl) + + // Links on the user's own instance should open directly. + var sameServer = /^([a-z][a-z0-9+.-]*):\/\/([^\/?#]+)/i + var targetMatch = targetUrl.match(sameServer) + var instanceMatch = instanceUrl.match(sameServer) + if (targetMatch && instanceMatch + && targetMatch.length > 2 + && instanceMatch.length > 2 + && targetMatch[1].toLowerCase() === instanceMatch[1].toLowerCase() + && targetMatch[2].toLowerCase() === instanceMatch[2].toLowerCase()) { + return targetUrl + } + + return instanceUrl + "/authorize_interaction?uri=" + encodeURIComponent(targetUrl) + } + + function firstAccountId(modelData, defaultValue) { + var fallback = typeof defaultValue === "undefined" ? -1 : Number(defaultValue) + var accounts = modelData ? modelData.accounts : undefined + if (!accounts || accounts.length <= 0) { + return fallback + } + + var accountId = Number(accounts[0]) + return isNaN(accountId) ? fallback : accountId + } + + function stripTrailingSlashes(value) { + value = String(value || "") + while (value.length > 0 && value.charAt(value.length - 1) === "/") { + value = value.slice(0, value.length - 1) + } + return value + } + + function trimLeadingCharacter(value, character) { + value = String(value || "") + if (!character || character.length === 0) { + return value + } + + while (value.length > 0 && value.charAt(0) === character) { + value = value.substring(1) + } + return value + } +} diff --git a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostactions.cpp b/eventsview-plugins/eventsview-plugin-fediverse/fediversepostactions.cpp index 371b7dd..2b370f2 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostactions.cpp +++ b/eventsview-plugins/eventsview-plugin-fediverse/fediversepostactions.cpp @@ -16,9 +16,9 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#include "mastodonpostactions.h" +#include "fediversepostactions.h" -#include "mastodonauthutils.h" +#include "fediverseauthutils.h" #include <Accounts/Account> #include <Accounts/AccountService> @@ -39,36 +39,36 @@ #include <QtDebug> namespace { - const char *const MicroblogServiceName = "mastodon-microblog"; + const char *const MicroblogServiceName = "fediverse-microblog"; } -MastodonPostActions::MastodonPostActions(QObject *parent) +FediversePostActions::FediversePostActions(QObject *parent) : QObject(parent) , m_accountManager(new Accounts::Manager(this)) { } -void MastodonPostActions::favourite(int accountId, const QString &statusId) +void FediversePostActions::favourite(int accountId, const QString &statusId) { performAction(accountId, statusId, QStringLiteral("favourite")); } -void MastodonPostActions::unfavourite(int accountId, const QString &statusId) +void FediversePostActions::unfavourite(int accountId, const QString &statusId) { performAction(accountId, statusId, QStringLiteral("unfavourite")); } -void MastodonPostActions::boost(int accountId, const QString &statusId) +void FediversePostActions::boost(int accountId, const QString &statusId) { performAction(accountId, statusId, QStringLiteral("reblog")); } -void MastodonPostActions::unboost(int accountId, const QString &statusId) +void FediversePostActions::unboost(int accountId, const QString &statusId) { performAction(accountId, statusId, QStringLiteral("unreblog")); } -void MastodonPostActions::performAction(int accountId, const QString &statusId, const QString &action) +void FediversePostActions::performAction(int accountId, const QString &statusId, const QString &action) { const QString trimmedStatusId = statusId.trimmed(); if (accountId <= 0 || trimmedStatusId.isEmpty() || action.isEmpty()) { @@ -116,7 +116,7 @@ void MastodonPostActions::performAction(int accountId, const QString &statusId, } QVariantMap signonSessionData = accountService.authData().parameters(); - MastodonAuthUtils::addSignOnSessionParameters(account, &signonSessionData); + FediverseAuthUtils::addSignOnSessionParameters(account, &signonSessionData); connect(session, SIGNAL(response(SignOn::SessionData)), this, SLOT(signOnResponse(SignOn::SessionData)), @@ -135,7 +135,7 @@ void MastodonPostActions::performAction(int accountId, const QString &statusId, session->process(SignOn::SessionData(signonSessionData), mechanism); } -void MastodonPostActions::signOnResponse(const SignOn::SessionData &responseData) +void FediversePostActions::signOnResponse(const SignOn::SessionData &responseData) { QObject *sessionObject = sender(); SignOn::AuthSession *session = qobject_cast<SignOn::AuthSession *>(sessionObject); @@ -148,12 +148,12 @@ void MastodonPostActions::signOnResponse(const SignOn::SessionData &responseData const QString action = session->property("action").toString(); const QString key = actionKey(accountId, statusId, action); - const QVariantMap data = MastodonAuthUtils::responseDataToMap(responseData); - const QString accessToken = MastodonAuthUtils::accessToken(data); + const QVariantMap data = FediverseAuthUtils::responseDataToMap(responseData); + const QString accessToken = FediverseAuthUtils::accessToken(data); Accounts::Account *account = session->property("account").value<Accounts::Account *>(); const QString apiHost = account - ? MastodonAuthUtils::normalizeApiHost(account->value(QStringLiteral("api/Host")).toString()) + ? FediverseAuthUtils::normalizeApiHost(account->value(QStringLiteral("api/Host")).toString()) : QString(); if (accessToken.isEmpty() || apiHost.isEmpty()) { @@ -167,7 +167,7 @@ void MastodonPostActions::signOnResponse(const SignOn::SessionData &responseData executeActionRequest(accountId, statusId, action, apiHost, accessToken); } -void MastodonPostActions::signOnError(const SignOn::Error &error) +void FediversePostActions::signOnError(const SignOn::Error &error) { QObject *sessionObject = sender(); SignOn::AuthSession *session = qobject_cast<SignOn::AuthSession *>(sessionObject); @@ -185,7 +185,7 @@ void MastodonPostActions::signOnError(const SignOn::Error &error) releaseSignOnObjects(sessionObject); } -void MastodonPostActions::executeActionRequest(int accountId, +void FediversePostActions::executeActionRequest(int accountId, const QString &statusId, const QString &action, const QString &apiHost, @@ -212,7 +212,7 @@ void MastodonPostActions::executeActionRequest(int accountId, connect(reply, SIGNAL(finished()), this, SLOT(actionFinishedHandler())); } -void MastodonPostActions::actionFinishedHandler() +void FediversePostActions::actionFinishedHandler() { QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender()); if (!reply) { @@ -291,7 +291,7 @@ void MastodonPostActions::actionFinishedHandler() favourited, reblogged); } -void MastodonPostActions::releaseSignOnObjects(QObject *sessionObject) +void FediversePostActions::releaseSignOnObjects(QObject *sessionObject) { SignOn::AuthSession *session = qobject_cast<SignOn::AuthSession *>(sessionObject); if (!session) { @@ -311,7 +311,7 @@ void MastodonPostActions::releaseSignOnObjects(QObject *sessionObject) } } -QString MastodonPostActions::actionKey(int accountId, const QString &statusId, const QString &action) const +QString FediversePostActions::actionKey(int accountId, const QString &statusId, const QString &action) const { return QString::number(accountId) + QLatin1Char(':') + statusId + QLatin1Char(':') + action; } diff --git a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostactions.h b/eventsview-plugins/eventsview-plugin-fediverse/fediversepostactions.h index cfe0c2a..536d339 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostactions.h +++ b/eventsview-plugins/eventsview-plugin-fediverse/fediversepostactions.h @@ -16,8 +16,8 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#ifndef MASTODONPOSTACTIONS_H -#define MASTODONPOSTACTIONS_H +#ifndef FEDIVERSEPOSTACTIONS_H +#define FEDIVERSEPOSTACTIONS_H #include <QtCore/QObject> #include <QtCore/QSet> @@ -34,12 +34,12 @@ class Error; class SessionData; } -class MastodonPostActions : public QObject +class FediversePostActions : public QObject { Q_OBJECT public: - explicit MastodonPostActions(QObject *parent = 0); + explicit FediversePostActions(QObject *parent = 0); Q_INVOKABLE void favourite(int accountId, const QString &statusId); Q_INVOKABLE void unfavourite(int accountId, const QString &statusId); @@ -79,4 +79,4 @@ private: QSet<QString> m_pendingActions; }; -#endif // MASTODONPOSTACTIONS_H +#endif // FEDIVERSEPOSTACTIONS_H diff --git a/eventsview-plugins/eventsview-plugin-fediverse/fediversepostsmodel.cpp b/eventsview-plugins/eventsview-plugin-fediverse/fediversepostsmodel.cpp new file mode 100644 index 0000000..48b3446 --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-fediverse/fediversepostsmodel.cpp @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2013-2026 Jolla Ltd. + * + * 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 "fediversepostsmodel.h" + +#include <QtCore/QVariantList> + +namespace { + +QVariantList imageListForPost(const SocialPost::ConstPtr &post) +{ + QVariantList images; + if (!post) { + return images; + } + + const QList<SocialPostImage::ConstPtr> postImages = post->images(); + for (const SocialPostImage::ConstPtr &image : postImages) { + if (!image) { + continue; + } + + QVariantMap imageMap; + imageMap.insert(QStringLiteral("url"), image->url()); + imageMap.insert(QStringLiteral("type"), image->type() == SocialPostImage::Video + ? QStringLiteral("video") + : QStringLiteral("image")); + images.append(imageMap); + } + + return images; +} + +QVariantList accountListForPost(const SocialPost::ConstPtr &post) +{ + QVariantList accounts; + if (!post) { + return accounts; + } + + const QList<int> postAccounts = post->accounts(); + for (int accountId : postAccounts) { + accounts.append(accountId); + } + return accounts; +} + +void appendCommonPostFields(QMap<int, QVariant> *eventMap, + const SocialPost::ConstPtr &post, + int idRole, + int nameRole, + int bodyRole, + int timestampRole, + int iconRole, + int imagesRole, + int accountsRole) +{ + if (!eventMap || !post) { + return; + } + + eventMap->insert(idRole, post->identifier()); + eventMap->insert(nameRole, post->name()); + eventMap->insert(bodyRole, post->body()); + eventMap->insert(timestampRole, post->timestamp()); + eventMap->insert(iconRole, post->icon()); + eventMap->insert(imagesRole, imageListForPost(post)); + eventMap->insert(accountsRole, accountListForPost(post)); +} + +} + +FediversePostsModel::FediversePostsModel(QObject *parent) + : QAbstractListModel(parent) +{ + connect(&m_database, &AbstractSocialPostCacheDatabase::postsChanged, + this, &FediversePostsModel::postsChanged); + connect(&m_database, SIGNAL(accountIdFilterChanged()), + this, SIGNAL(accountIdFilterChanged())); +} + +int FediversePostsModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_data.count(); +} + +QVariant FediversePostsModel::data(const QModelIndex &index, int role) const +{ + const int row = index.row(); + if (!index.isValid() || row < 0 || row >= m_data.count()) { + return QVariant(); + } + + return m_data.at(row).value(role); +} + +QHash<int, QByteArray> FediversePostsModel::roleNames() const +{ + QHash<int, QByteArray> roleNames; + roleNames.insert(FediverseId, "fediverseId"); + 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(RepliesCount, "repliesCount"); + roleNames.insert(FavouritesCount, "favouritesCount"); + roleNames.insert(ReblogsCount, "reblogsCount"); + roleNames.insert(Favourited, "favourited"); + roleNames.insert(Reblogged, "reblogged"); + roleNames.insert(InstanceUrl, "instanceUrl"); + roleNames.insert(InstanceIconPath, "instanceIconPath"); + roleNames.insert(Accounts, "accounts"); + return roleNames; +} + +QVariantList FediversePostsModel::accountIdFilter() const +{ + return m_database.accountIdFilter(); +} + +void FediversePostsModel::setAccountIdFilter(const QVariantList &accountIds) +{ + m_database.setAccountIdFilter(accountIds); +} + +void FediversePostsModel::refresh() +{ + m_database.refresh(); +} + +void FediversePostsModel::postsChanged() +{ + QList<RowData> data; + QList<SocialPost::ConstPtr> postsData = m_database.posts(); + Q_FOREACH (const SocialPost::ConstPtr &post, postsData) { + RowData eventMap; + const QString accountName = m_database.accountName(post); + const QString postUrl = m_database.url(post); + const QString boostedBy = m_database.boostedBy(post); + const int repliesCount = m_database.repliesCount(post); + const int favouritesCount = m_database.favouritesCount(post); + const int reblogsCount = m_database.reblogsCount(post); + const bool favourited = m_database.favourited(post); + const bool reblogged = m_database.reblogged(post); + + appendCommonPostFields(&eventMap, post, + FediversePostsModel::FediverseId, + FediversePostsModel::Name, + FediversePostsModel::Body, + FediversePostsModel::Timestamp, + FediversePostsModel::Icon, + FediversePostsModel::Images, + FediversePostsModel::Accounts); + eventMap.insert(FediversePostsModel::AccountName, accountName); + eventMap.insert(FediversePostsModel::Acct, accountName); + eventMap.insert(FediversePostsModel::Url, postUrl); + eventMap.insert(FediversePostsModel::Link, postUrl); + eventMap.insert(FediversePostsModel::BoostedBy, boostedBy); + eventMap.insert(FediversePostsModel::RebloggedBy, boostedBy); + eventMap.insert(FediversePostsModel::RepliesCount, repliesCount); + eventMap.insert(FediversePostsModel::FavouritesCount, favouritesCount); + eventMap.insert(FediversePostsModel::ReblogsCount, reblogsCount); + eventMap.insert(FediversePostsModel::Favourited, favourited); + eventMap.insert(FediversePostsModel::Reblogged, reblogged); + eventMap.insert(FediversePostsModel::InstanceUrl, m_database.instanceUrl(post)); + eventMap.insert(FediversePostsModel::InstanceIconPath, m_database.instanceIconPath(post)); + data.append(eventMap); + } + + const int oldCount = m_data.count(); + beginResetModel(); + m_data = data; + endResetModel(); + if (oldCount != m_data.count()) { + emit countChanged(); + } +} diff --git a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.h b/eventsview-plugins/eventsview-plugin-fediverse/fediversepostsmodel.h index e30437d..96acae3 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.h +++ b/eventsview-plugins/eventsview-plugin-fediverse/fediversepostsmodel.h @@ -16,22 +16,22 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#ifndef MASTODONPOSTSMODEL_H -#define MASTODONPOSTSMODEL_H +#ifndef FEDIVERSEPOSTSMODEL_H +#define FEDIVERSEPOSTSMODEL_H -#include "mastodonpostsdatabase.h" +#include "fediversepostsdatabase.h" #include <QtCore/QAbstractListModel> #include <QtCore/QMap> -class MastodonPostsModel: public QAbstractListModel +class FediversePostsModel: public QAbstractListModel { Q_OBJECT Q_PROPERTY(QVariantList accountIdFilter READ accountIdFilter WRITE setAccountIdFilter NOTIFY accountIdFilterChanged) Q_PROPERTY(int count READ rowCount NOTIFY countChanged) public: - enum MastodonPostsRole { - MastodonId = 0, + enum FediversePostsRole { + FediverseId = 0, Name, AccountName, Acct, @@ -49,10 +49,11 @@ public: Favourited, Reblogged, InstanceUrl, + InstanceIconPath, Accounts }; - explicit MastodonPostsModel(QObject *parent = 0); + explicit FediversePostsModel(QObject *parent = 0); int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; @@ -73,7 +74,7 @@ private slots: private: typedef QMap<int, QVariant> RowData; QList<RowData> m_data; - MastodonPostsDatabase m_database; + FediversePostsDatabase m_database; }; -#endif // MASTODONPOSTSMODEL_H +#endif // FEDIVERSEPOSTSMODEL_H diff --git a/eventsview-plugins/eventsview-plugin-fediverse/plugin.cpp b/eventsview-plugins/eventsview-plugin-fediverse/plugin.cpp new file mode 100644 index 0000000..509b602 --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-fediverse/plugin.cpp @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include <QQmlExtensionPlugin> +#include <QtQml> + +#include "fediversepostactions.h" +#include "fediversepostsmodel.h" + +class JollaEventsviewFediversePlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "com.jolla.eventsview.fediverse") + +public: + void registerTypes(const char *uri) override + { + Q_ASSERT(QLatin1String(uri) == QLatin1String("com.jolla.eventsview.fediverse")); + qmlRegisterType<FediversePostsModel>(uri, 1, 0, "FediversePostsModel"); + qmlRegisterType<FediversePostActions>(uri, 1, 0, "FediversePostActions"); + } +}; + +#include "plugin.moc" diff --git a/eventsview-plugins/eventsview-plugin-fediverse/qmldir b/eventsview-plugins/eventsview-plugin-fediverse/qmldir new file mode 100644 index 0000000..8b25e83 --- /dev/null +++ b/eventsview-plugins/eventsview-plugin-fediverse/qmldir @@ -0,0 +1,4 @@ +# Copyright (C) 2013-2026 Jolla Ltd. + +module com.jolla.eventsview.fediverse +plugin jollaeventsviewfediverseplugin diff --git a/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml b/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml deleted file mode 100644 index 63b9556..0000000 --- a/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml +++ /dev/null @@ -1,488 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -import QtQuick 2.0 -import Sailfish.Silica 1.0 -import Sailfish.Share 1.0 -import Sailfish.TextLinking 1.0 -import org.nemomobile.lipstick 0.1 -import "shared" - -SocialMediaFeedItem { - id: item - - property variant imageList - property string resolvedStatusUrl: item.stringValue("url", "link", "uri") - property string postId - property QtObject postActions - property int likeCount: item.intValue("favouritesCount", "likeCount", "favoriteCount") - property int commentCount: item.intValue("repliesCount", "commentCount") - property int boostCount: item.intValue("reblogsCount", "boostCount", "repostsCount") - property bool favourited: !!model.favourited - property bool reblogged: !!model.reblogged - property int _likeCountOverride: -1 - property int _boostCountOverride: -1 - property int _favouritedOverride: -1 - property int _rebloggedOverride: -1 - property bool isFavourited: _favouritedOverride >= 0 ? _favouritedOverride === 1 : favourited - property bool isReblogged: _rebloggedOverride >= 0 ? _rebloggedOverride === 1 : reblogged - readonly property bool housekeeping: Lipstick.compositor.eventsLayer.housekeeping - readonly property bool lockScreenActive: Lipstick.compositor.lockScreenLayer.deviceIsLocked - property bool _pendingOpenActionMenu: false - property bool _contextMenuOpen: false - property var _actionMenu - property real _contextMenuHeight: (_contextMenuOpen && _actionMenu) ? _actionMenu.height : 0 - - property string _booster: model && model.boostedBy ? model.boostedBy.toString() : "" - property string _displayName: model && model.name ? model.name.toString() : "" - property string _accountName: model && model.accountName ? model.accountName.toString() : "" - property string _bodyText: model && model.body ? model.body.toString() : "" - //: Action label shown in Mastodon interaction menu. - //% "Share" - readonly property string _shareActionText: qsTrId("lipstick-jolla-home-la-mastodon_share") - //: Link title used when sharing a Mastodon post. - //% "Post from Mastodon" - readonly property string _shareLinkTitle: qsTrId("lipstick-jolla-home-la-mastodon_share_link_title") - property var _shareAction: ShareAction { - title: item._shareActionText - } - - timestamp: model.timestamp - onRefreshTimeCountChanged: formattedTime = Format.formatDate(model.timestamp, Format.TimeElapsed) - onLockScreenActiveChanged: { - if (lockScreenActive && _actionMenu) { - _actionMenu.close() - } - } - onPressAndHold: function(mouse) { - if (mouse) { - mouse.accepted = true - } - _pendingOpenActionMenu = !lockScreenActive - && postActions - && actionPostId().length > 0 - && actionAccountId() >= 0 - openActionMenuTimer.restart() - } - onHousekeepingChanged: { - if (housekeeping && _pendingOpenActionMenu) { - Lipstick.compositor.eventsLayer.setHousekeeping(false) - } - } - Component.onDestruction: { - if (_actionMenu) { - _actionMenu.destroy() - _actionMenu = null - } - } - - avatar.y: item._booster.length > 0 - ? topMargin + boosterIcon.height + Theme.paddingSmall - : topMargin - contentHeight: Math.max(content.y + content.height, avatar.y + avatar.height) + bottomMargin + _contextMenuHeight - 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 - } - - Row { - id: metadataRow - - width: parent.width - height: previewRow.visible ? implicitHeight + Theme.paddingMedium : implicitHeight // add padding below - spacing: Theme.paddingSmall - - readonly property color passiveColor: item.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor - readonly property color activeColor: Theme.highlightColor - - Label { - font.pixelSize: Theme.fontSizeExtraSmall - text: "↩ " + item.commentCount - color: metadataRow.passiveColor - } - - Label { - font.pixelSize: Theme.fontSizeExtraSmall - text: "|" - color: metadataRow.passiveColor - } - - Label { - font.pixelSize: Theme.fontSizeExtraSmall - text: "★ " + (item._likeCountOverride >= 0 ? item._likeCountOverride : item.likeCount) - color: item.isFavourited ? metadataRow.activeColor : metadataRow.passiveColor - } - - Label { - font.pixelSize: Theme.fontSizeExtraSmall - text: "|" - color: metadataRow.passiveColor - } - - Label { - font.pixelSize: Theme.fontSizeExtraSmall - text: "↻ " + (item._boostCountOverride >= 0 ? item._boostCountOverride : item.boostCount) - color: item.isReblogged ? metadataRow.activeColor : metadataRow.passiveColor - } - - Label { - visible: item.formattedTime.length > 0 - font.pixelSize: Theme.fontSizeExtraSmall - text: "|" - color: metadataRow.passiveColor - } - - Label { - visible: item.formattedTime.length > 0 - width: Math.max(0, metadataRow.width - x) - truncationMode: TruncationMode.Fade - font.pixelSize: Theme.fontSizeExtraSmall - text: item.formattedTime - color: metadataRow.passiveColor - } - } - - 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 "" - } - - function intValue() { - for (var i = 0; i < arguments.length; ++i) { - var value = model[arguments[i]] - if (typeof value === "undefined" || value === null) { - continue - } - var number = Number(value) - if (!isNaN(number)) { - return Math.max(0, Math.floor(number)) - } - } - return 0 - } - - function actionPostId() { - if (item.postId.length > 0) { - return item.postId - } - return item.stringValue("mastodonId", "statusId", "id", "twitterId") - } - - function actionAccountId() { - var parsed = Number(item.accountId) - return isNaN(parsed) ? -1 : parsed - } - - function shareStatusUrl() { - return item.stringValue("url", "link", "uri") - } - - function topLevelParent() { - var p = item - while (p && p.parent) { - p = p.parent - } - return p - } - - function openActionMenu() { - if (_actionMenu) { - _actionMenu.destroy() - _actionMenu = null - } - - var parentItem = topLevelParent() - _actionMenu = actionMenuComponent.createObject(parentItem) - if (_actionMenu) { - _actionMenu.open(item) - } - } - - Connections { - target: item.postActions ? item.postActions : null - - onActionSucceeded: { - if (accountId !== item.actionAccountId() || statusId !== item.actionPostId()) { - return - } - - if (favouritesCount >= 0) { - item._likeCountOverride = favouritesCount - } - if (reblogsCount >= 0) { - item._boostCountOverride = reblogsCount - } - item._favouritedOverride = favourited ? 1 : 0 - item._rebloggedOverride = reblogged ? 1 : 0 - item._contextMenuOpen = false - - if (item._accountDelegate) { - item._accountDelegate.sync() - } - } - - onActionFailed: { - if (accountId !== item.actionAccountId() || statusId !== item.actionPostId()) { - return - } - console.warn("Mastodon action failed:", action, errorMessage) - item._contextMenuOpen = false - } - } - - Component { - id: actionMenuComponent - - ContextMenu { - id: actionMenu - property bool menuOpen: height > 0 - property bool wasOpened: false - z: 10000 - - onPositionChanged: { - horizontalActions.xPos = _contentColumn.mapFromItem(actionMenu, mouse.x, mouse.y).x - } - - onMenuOpenChanged: { - if (menuOpen) { - wasOpened = true - item._contextMenuOpen = true - } else if (wasOpened) { - item._contextMenuOpen = false - destroy() - item._actionMenu = null - } - } - - Item { - id: horizontalActions - - // Makes Silica treat this custom row as a context-menu item. - property int __silica_menuitem - property bool down - property bool highlighted - signal clicked - - property real xPos: 0 - property int hoveredIndex: -1 - readonly property bool actionEnabled: item.postActions - && item.actionPostId().length > 0 - && item.actionAccountId() >= 0 - && !item.lockScreenActive - && !item.housekeeping - - width: parent.width - height: Theme.itemSizeMedium - - onXPosChanged: hoveredIndex = Math.max(0, Math.min(2, Math.floor((xPos * 3) / Math.max(1, width)))) - onDownChanged: if (!down) hoveredIndex = -1 - - onClicked: { - xPos = _contentColumn.mapFromItem(actionMenu, actionMenu.mouseX, actionMenu.mouseY).x - var index = hoveredIndex >= 0 ? hoveredIndex : Math.max(0, Math.min(2, Math.floor((xPos * 3) / Math.max(1, width)))) - if (!actionEnabled) { - return - } - var postId = item.actionPostId() - var accountId = item.actionAccountId() - if (index === 0) { - if (item.isFavourited) { - item.postActions.unfavourite(accountId, postId) - } else { - item.postActions.favourite(accountId, postId) - } - } else if (index === 1) { - if (item.isReblogged) { - item.postActions.unboost(accountId, postId) - } else { - item.postActions.boost(accountId, postId) - } - } else { - var shareUrl = item.shareStatusUrl() - if (shareUrl.length === 0) { - return - } - item._shareAction.resources = [{ - "data": shareUrl, - "linkTitle": item._shareLinkTitle, - "type": "text/x-url" - }] - item._shareAction.trigger() - } - } - - Rectangle { - anchors.verticalCenter: parent.verticalCenter - x: (horizontalActions.hoveredIndex >= 0 ? horizontalActions.hoveredIndex : 0) * (parent.width / 3) - width: parent.width / 3 - height: parent.height - visible: horizontalActions.down && horizontalActions.hoveredIndex >= 0 - color: Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) - } - - Row { - anchors.fill: parent - - Label { - width: parent.width / 3 - height: parent.height - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: Theme.fontSizeExtraLarge - text: "★" - color: horizontalActions.actionEnabled - ? (item.isFavourited - ? Theme.highlightColor - : ((horizontalActions.down && horizontalActions.hoveredIndex === 0) - || (horizontalActions.highlighted && horizontalActions.hoveredIndex === 0) - ? Theme.secondaryHighlightColor : Theme.primaryColor)) - : Theme.rgba(Theme.secondaryColor, 0.4) - } - - Label { - width: parent.width / 3 - height: parent.height - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: Theme.fontSizeExtraLarge - text: "↻" - color: horizontalActions.actionEnabled - ? (item.isReblogged - ? Theme.highlightColor - : ((horizontalActions.down && horizontalActions.hoveredIndex === 1) - || (horizontalActions.highlighted && horizontalActions.hoveredIndex === 1) - ? Theme.secondaryHighlightColor : Theme.primaryColor)) - : Theme.rgba(Theme.secondaryColor, 0.4) - } - - Label { - width: parent.width / 3 - height: parent.height - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: Theme.fontSizeExtraLarge - text: "\u260D" - color: horizontalActions.actionEnabled - ? (((horizontalActions.down && horizontalActions.hoveredIndex === 2) - || (horizontalActions.highlighted && horizontalActions.hoveredIndex === 2)) - ? Theme.secondaryHighlightColor : Theme.primaryColor) - : Theme.rgba(Theme.secondaryColor, 0.4) - } - } - } - } - } - - Timer { - id: openActionMenuTimer - - interval: 0 - repeat: false - onTriggered: { - if (item.lockScreenActive) { - item._pendingOpenActionMenu = false - return - } - Lipstick.compositor.eventsLayer.setHousekeeping(false) - if (item._pendingOpenActionMenu) { - item._contextMenuOpen = false - item.openActionMenu() - } - item._pendingOpenActionMenu = false - } - } -} diff --git a/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml b/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml deleted file mode 100644 index fac0b89..0000000 --- a/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml +++ /dev/null @@ -1,214 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -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/icon-l-mastodon" - showRemainingCount: false - - services: ["Posts"] - socialNetwork: 9 - dataType: SocialSync.Posts - providerName: "mastodon" - - MastodonPostActions { - id: mastodonPostActions - } - - 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"]) - resolvedStatusUrl: delegateItem.authorizeInteractionUrl(model) - postId: delegateItem.stringRole(model, ["mastodonId", "statusId", "id", "twitterId"]) - postActions: mastodonPostActions - accountId: delegateItem.firstAccountId(model) - - onTriggered: Qt.openUrlExternally(resolvedStatusUrl) - - 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 authorizeInteractionUrl(modelData) { - var targetUrl = statusUrl(modelData) - if (targetUrl.length === 0) { - return targetUrl - } - - var instanceUrl = stringRole(modelData, ["instanceUrl", "serverUrl", "baseUrl"]) - if (instanceUrl.length === 0) { - return targetUrl - } - while (instanceUrl.length > 0 && instanceUrl.charAt(instanceUrl.length - 1) === "/") { - instanceUrl = instanceUrl.slice(0, instanceUrl.length - 1) - } - - // Links on the user's own instance should open directly. - var sameServer = /^([a-z][a-z0-9+.-]*):\/\/([^\/?#]+)/i - var targetMatch = targetUrl.match(sameServer) - var instanceMatch = instanceUrl.match(sameServer) - if (targetMatch && instanceMatch - && targetMatch.length > 2 - && instanceMatch.length > 2 - && targetMatch[1].toLowerCase() === instanceMatch[1].toLowerCase() - && targetMatch[2].toLowerCase() === instanceMatch[2].toLowerCase()) { - return targetUrl - } - - return instanceUrl + "/authorize_interaction?uri=" + encodeURIComponent(targetUrl) - } - - 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 - } - - function firstAccountId(modelData) { - var accounts = modelData.accounts - if (accounts && accounts.length > 0) { - var accountId = Number(accounts[0]) - if (!isNaN(accountId)) { - return accountId - } - } - return -1 - } -} diff --git a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.cpp b/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.cpp deleted file mode 100644 index aa98a95..0000000 --- a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.cpp +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (C) 2013-2026 Jolla Ltd. - * - * 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 <QtCore/QVariantMap> - -namespace { - -static const char *URL_KEY = "url"; -static const char *TYPE_KEY = "type"; -static const char *TYPE_PHOTO = "photo"; -static const char *TYPE_VIDEO = "video"; - -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; -} - -} - -MastodonPostsModel::MastodonPostsModel(QObject *parent) - : QAbstractListModel(parent) -{ - connect(&m_database, &AbstractSocialPostCacheDatabase::postsChanged, - this, &MastodonPostsModel::postsChanged); - connect(&m_database, SIGNAL(accountIdFilterChanged()), - this, SIGNAL(accountIdFilterChanged())); -} - -int MastodonPostsModel::rowCount(const QModelIndex &parent) const -{ - Q_UNUSED(parent) - return m_data.count(); -} - -QVariant MastodonPostsModel::data(const QModelIndex &index, int role) const -{ - const int row = index.row(); - if (!index.isValid() || row < 0 || row >= m_data.count()) { - return QVariant(); - } - - return m_data.at(row).value(role); -} - -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(RepliesCount, "repliesCount"); - roleNames.insert(FavouritesCount, "favouritesCount"); - roleNames.insert(ReblogsCount, "reblogsCount"); - roleNames.insert(Favourited, "favourited"); - roleNames.insert(Reblogged, "reblogged"); - roleNames.insert(InstanceUrl, "instanceUrl"); - roleNames.insert(Accounts, "accounts"); - return roleNames; -} - -QVariantList MastodonPostsModel::accountIdFilter() const -{ - return m_database.accountIdFilter(); -} - -void MastodonPostsModel::setAccountIdFilter(const QVariantList &accountIds) -{ - m_database.setAccountIdFilter(accountIds); -} - -void MastodonPostsModel::refresh() -{ - m_database.refresh(); -} - -void MastodonPostsModel::postsChanged() -{ - QList<RowData> data; - QList<SocialPost::ConstPtr> postsData = m_database.posts(); - Q_FOREACH (const SocialPost::ConstPtr &post, postsData) { - RowData eventMap; - const QString accountName = m_database.accountName(post); - const QString postUrl = m_database.url(post); - const QString boostedBy = m_database.boostedBy(post); - const int repliesCount = m_database.repliesCount(post); - const int favouritesCount = m_database.favouritesCount(post); - const int reblogsCount = m_database.reblogsCount(post); - const bool favourited = m_database.favourited(post); - const bool reblogged = m_database.reblogged(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::RepliesCount, repliesCount); - eventMap.insert(MastodonPostsModel::FavouritesCount, favouritesCount); - eventMap.insert(MastodonPostsModel::ReblogsCount, reblogsCount); - eventMap.insert(MastodonPostsModel::Favourited, favourited); - eventMap.insert(MastodonPostsModel::Reblogged, reblogged); - eventMap.insert(MastodonPostsModel::InstanceUrl, m_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); - } - - const int oldCount = m_data.count(); - beginResetModel(); - m_data = data; - endResetModel(); - if (oldCount != m_data.count()) { - emit countChanged(); - } -} diff --git a/eventsview-plugins/eventsview-plugin-mastodon/plugin.cpp b/eventsview-plugins/eventsview-plugin-mastodon/plugin.cpp deleted file mode 100644 index 9ade6e2..0000000 --- a/eventsview-plugins/eventsview-plugin-mastodon/plugin.cpp +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -#include <QQmlExtensionPlugin> -#include <QtQml> - -#include "mastodonpostactions.h" -#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"); - qmlRegisterType<MastodonPostActions>(uri, 1, 0, "MastodonPostActions"); - } -}; - -#include "plugin.moc" diff --git a/eventsview-plugins/eventsview-plugin-mastodon/qmldir b/eventsview-plugins/eventsview-plugin-mastodon/qmldir deleted file mode 100644 index 74461ce..0000000 --- a/eventsview-plugins/eventsview-plugin-mastodon/qmldir +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2013-2026 Jolla Ltd. - -module com.jolla.eventsview.mastodon -plugin jollaeventsviewmastodonplugin diff --git a/eventsview-plugins/eventsview-plugins.pro b/eventsview-plugins/eventsview-plugins.pro index 095fd02..bc96c5f 100644 --- a/eventsview-plugins/eventsview-plugins.pro +++ b/eventsview-plugins/eventsview-plugins.pro @@ -3,4 +3,4 @@ # SPDX-License-Identifier: BSD-3-Clause TEMPLATE = subdirs -SUBDIRS += eventsview-plugin-mastodon +SUBDIRS += eventsview-plugin-fediverse |
