diff options
Diffstat (limited to 'eventsview-plugins/eventsview-plugin-mastodon')
9 files changed, 0 insertions, 1440 deletions
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/eventsview-plugin-mastodon.pro b/eventsview-plugins/eventsview-plugin-mastodon/eventsview-plugin-mastodon.pro deleted file mode 100644 index 04be215..0000000 --- a/eventsview-plugins/eventsview-plugin-mastodon/eventsview-plugin-mastodon.pro +++ /dev/null @@ -1,64 +0,0 @@ -# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -# -# SPDX-License-Identifier: BSD-3-Clause - -TEMPLATE = lib -TARGET = jollaeventsviewmastodonplugin -TARGET = $$qtLibraryTarget($$TARGET) - -MODULENAME = com/jolla/eventsview/mastodon -TARGETPATH = $$[QT_INSTALL_QML]/$$MODULENAME - -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.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 += \ - mastodonpostactions.h \ - mastodonpostsmodel.h - -SOURCES += \ - mastodonpostactions.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/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/mastodonpostactions.cpp b/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostactions.cpp deleted file mode 100644 index 371b7dd..0000000 --- a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostactions.cpp +++ /dev/null @@ -1,317 +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 "mastodonpostactions.h" - -#include "mastodonauthutils.h" - -#include <Accounts/Account> -#include <Accounts/AccountService> -#include <Accounts/Manager> -#include <Accounts/Service> - -#include <SignOn/AuthSession> -#include <SignOn/Error> -#include <SignOn/Identity> -#include <SignOn/SessionData> - -#include <QtCore/QJsonDocument> -#include <QtCore/QJsonObject> -#include <QtCore/QVariantMap> -#include <QtCore/QUrl> -#include <QtNetwork/QNetworkReply> -#include <QtNetwork/QNetworkRequest> -#include <QtDebug> - -namespace { - const char *const MicroblogServiceName = "mastodon-microblog"; -} - -MastodonPostActions::MastodonPostActions(QObject *parent) - : QObject(parent) - , m_accountManager(new Accounts::Manager(this)) -{ -} - -void MastodonPostActions::favourite(int accountId, const QString &statusId) -{ - performAction(accountId, statusId, QStringLiteral("favourite")); -} - -void MastodonPostActions::unfavourite(int accountId, const QString &statusId) -{ - performAction(accountId, statusId, QStringLiteral("unfavourite")); -} - -void MastodonPostActions::boost(int accountId, const QString &statusId) -{ - performAction(accountId, statusId, QStringLiteral("reblog")); -} - -void MastodonPostActions::unboost(int accountId, const QString &statusId) -{ - performAction(accountId, statusId, QStringLiteral("unreblog")); -} - -void MastodonPostActions::performAction(int accountId, const QString &statusId, const QString &action) -{ - const QString trimmedStatusId = statusId.trimmed(); - if (accountId <= 0 || trimmedStatusId.isEmpty() || action.isEmpty()) { - emit actionFailed(accountId, trimmedStatusId, action, QStringLiteral("Invalid action request")); - return; - } - - const QString key = actionKey(accountId, trimmedStatusId, action); - if (m_pendingActions.contains(key)) { - return; - } - - Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); - if (!account) { - emit actionFailed(accountId, trimmedStatusId, action, QStringLiteral("Unable to load account")); - return; - } - - const Accounts::Service service(m_accountManager->service(QString::fromLatin1(MicroblogServiceName))); - if (!service.isValid()) { - account->deleteLater(); - emit actionFailed(accountId, trimmedStatusId, action, QStringLiteral("Invalid account service")); - return; - } - - account->selectService(service); - SignOn::Identity *identity = account->credentialsId() > 0 - ? SignOn::Identity::existingIdentity(account->credentialsId()) - : 0; - if (!identity) { - account->deleteLater(); - emit actionFailed(accountId, trimmedStatusId, action, QStringLiteral("Missing account credentials")); - return; - } - - Accounts::AccountService accountService(account, service); - const QString method = accountService.authData().method(); - const QString mechanism = accountService.authData().mechanism(); - SignOn::AuthSession *session = identity->createSession(method); - if (!session) { - identity->deleteLater(); - account->deleteLater(); - emit actionFailed(accountId, trimmedStatusId, action, QStringLiteral("Unable to create auth session")); - return; - } - - QVariantMap signonSessionData = accountService.authData().parameters(); - MastodonAuthUtils::addSignOnSessionParameters(account, &signonSessionData); - - connect(session, SIGNAL(response(SignOn::SessionData)), - this, SLOT(signOnResponse(SignOn::SessionData)), - Qt::UniqueConnection); - connect(session, SIGNAL(error(SignOn::Error)), - this, SLOT(signOnError(SignOn::Error)), - Qt::UniqueConnection); - - session->setProperty("account", QVariant::fromValue<Accounts::Account *>(account)); - session->setProperty("identity", QVariant::fromValue<SignOn::Identity *>(identity)); - session->setProperty("action", action); - session->setProperty("statusId", trimmedStatusId); - session->setProperty("accountId", accountId); - - m_pendingActions.insert(key); - session->process(SignOn::SessionData(signonSessionData), mechanism); -} - -void MastodonPostActions::signOnResponse(const SignOn::SessionData &responseData) -{ - QObject *sessionObject = sender(); - SignOn::AuthSession *session = qobject_cast<SignOn::AuthSession *>(sessionObject); - if (!session) { - return; - } - - const int accountId = session->property("accountId").toInt(); - const QString statusId = session->property("statusId").toString(); - 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); - - Accounts::Account *account = session->property("account").value<Accounts::Account *>(); - const QString apiHost = account - ? MastodonAuthUtils::normalizeApiHost(account->value(QStringLiteral("api/Host")).toString()) - : QString(); - - if (accessToken.isEmpty() || apiHost.isEmpty()) { - m_pendingActions.remove(key); - emit actionFailed(accountId, statusId, action, QStringLiteral("Missing access token")); - releaseSignOnObjects(sessionObject); - return; - } - - releaseSignOnObjects(sessionObject); - executeActionRequest(accountId, statusId, action, apiHost, accessToken); -} - -void MastodonPostActions::signOnError(const SignOn::Error &error) -{ - QObject *sessionObject = sender(); - SignOn::AuthSession *session = qobject_cast<SignOn::AuthSession *>(sessionObject); - if (!session) { - return; - } - - const int accountId = session->property("accountId").toInt(); - const QString statusId = session->property("statusId").toString(); - const QString action = session->property("action").toString(); - const QString key = actionKey(accountId, statusId, action); - m_pendingActions.remove(key); - - emit actionFailed(accountId, statusId, action, error.message()); - releaseSignOnObjects(sessionObject); -} - -void MastodonPostActions::executeActionRequest(int accountId, - const QString &statusId, - const QString &action, - const QString &apiHost, - const QString &accessToken) -{ - const QString encodedStatusId = QString::fromLatin1(QUrl::toPercentEncoding(statusId)); - QUrl url(apiHost + QStringLiteral("/api/v1/statuses/") - + encodedStatusId + QStringLiteral("/") + action); - - QNetworkRequest request(url); - request.setRawHeader("Authorization", QStringLiteral("Bearer %1").arg(accessToken).toUtf8()); - - QNetworkReply *reply = m_networkAccessManager.post(request, QByteArray()); - if (!reply) { - const QString key = actionKey(accountId, statusId, action); - m_pendingActions.remove(key); - emit actionFailed(accountId, statusId, action, QStringLiteral("Failed to start request")); - return; - } - - reply->setProperty("accountId", accountId); - reply->setProperty("statusId", statusId); - reply->setProperty("action", action); - connect(reply, SIGNAL(finished()), this, SLOT(actionFinishedHandler())); -} - -void MastodonPostActions::actionFinishedHandler() -{ - QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender()); - if (!reply) { - return; - } - - const int accountId = reply->property("accountId").toInt(); - const QString statusId = reply->property("statusId").toString(); - const QString action = reply->property("action").toString(); - const QString key = actionKey(accountId, statusId, action); - - const QByteArray data = reply->readAll(); - const bool hasError = reply->error() != QNetworkReply::NoError; - const QString errorString = reply->errorString(); - reply->deleteLater(); - - m_pendingActions.remove(key); - - if (hasError) { - emit actionFailed(accountId, statusId, action, errorString); - return; - } - - int favouritesCount = -1; - int reblogsCount = -1; - bool favourited = false; - bool reblogged = false; - - QJsonParseError parseError; - const QJsonDocument document = QJsonDocument::fromJson(data, &parseError); - if (parseError.error == QJsonParseError::NoError && document.isObject()) { - const QJsonObject object = document.object(); - QJsonObject metricsObject = object; - - const auto jsonObjectId = [](const QJsonObject &obj) -> QString { - return obj.value(QStringLiteral("id")).toVariant().toString().trimmed(); - }; - const QString requestedStatusId = statusId.trimmed(); - - const QJsonValue reblogValue = object.value(QStringLiteral("reblog")); - if (reblogValue.isObject() && !reblogValue.isNull()) { - const QJsonObject reblogObject = reblogValue.toObject(); - const QString nestedReblogId = jsonObjectId(reblogObject); - - if (nestedReblogId == requestedStatusId) { - metricsObject = reblogObject; - } - } - - favouritesCount = metricsObject.value(QStringLiteral("favourites_count")).toInt(-1); - if (favouritesCount < 0) { - favouritesCount = object.value(QStringLiteral("favourites_count")).toInt(-1); - } - - reblogsCount = metricsObject.value(QStringLiteral("reblogs_count")).toInt(-1); - if (reblogsCount < 0) { - reblogsCount = object.value(QStringLiteral("reblogs_count")).toInt(-1); - } - - if (metricsObject.contains(QStringLiteral("favourited"))) { - favourited = metricsObject.value(QStringLiteral("favourited")).toBool(false); - } else { - favourited = object.value(QStringLiteral("favourited")).toBool(false); - } - - if (metricsObject.contains(QStringLiteral("reblogged"))) { - reblogged = metricsObject.value(QStringLiteral("reblogged")).toBool(false); - } else { - reblogged = object.value(QStringLiteral("reblogged")).toBool(false); - } - - } - - emit actionSucceeded(accountId, statusId, action, - favouritesCount, reblogsCount, - favourited, reblogged); -} - -void MastodonPostActions::releaseSignOnObjects(QObject *sessionObject) -{ - SignOn::AuthSession *session = qobject_cast<SignOn::AuthSession *>(sessionObject); - if (!session) { - return; - } - - Accounts::Account *account = session->property("account").value<Accounts::Account *>(); - SignOn::Identity *identity = session->property("identity").value<SignOn::Identity *>(); - - session->disconnect(this); - if (identity) { - identity->destroySession(session); - identity->deleteLater(); - } - if (account) { - account->deleteLater(); - } -} - -QString MastodonPostActions::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-mastodon/mastodonpostactions.h deleted file mode 100644 index cfe0c2a..0000000 --- a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostactions.h +++ /dev/null @@ -1,82 +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 - */ - -#ifndef MASTODONPOSTACTIONS_H -#define MASTODONPOSTACTIONS_H - -#include <QtCore/QObject> -#include <QtCore/QSet> -#include <QtCore/QString> -#include <QtNetwork/QNetworkAccessManager> - -namespace Accounts { -class Account; -class Manager; -} - -namespace SignOn { -class Error; -class SessionData; -} - -class MastodonPostActions : public QObject -{ - Q_OBJECT - -public: - explicit MastodonPostActions(QObject *parent = 0); - - Q_INVOKABLE void favourite(int accountId, const QString &statusId); - Q_INVOKABLE void unfavourite(int accountId, const QString &statusId); - Q_INVOKABLE void boost(int accountId, const QString &statusId); - Q_INVOKABLE void unboost(int accountId, const QString &statusId); - -Q_SIGNALS: - void actionSucceeded(int accountId, - const QString &statusId, - const QString &action, - int favouritesCount, - int reblogsCount, - bool favourited, - bool reblogged); - void actionFailed(int accountId, - const QString &statusId, - const QString &action, - const QString &errorMessage); - -private Q_SLOTS: - void signOnResponse(const SignOn::SessionData &responseData); - void signOnError(const SignOn::Error &error); - void actionFinishedHandler(); - -private: - void performAction(int accountId, const QString &statusId, const QString &action); - void executeActionRequest(int accountId, - const QString &statusId, - const QString &action, - const QString &apiHost, - const QString &accessToken); - void releaseSignOnObjects(QObject *sessionObject); - QString actionKey(int accountId, const QString &statusId, const QString &action) const; - - Accounts::Manager *m_accountManager; - QNetworkAccessManager m_networkAccessManager; - QSet<QString> m_pendingActions; -}; - -#endif // MASTODONPOSTACTIONS_H 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/mastodonpostsmodel.h b/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.h deleted file mode 100644 index e30437d..0000000 --- a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.h +++ /dev/null @@ -1,79 +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 - */ - -#ifndef MASTODONPOSTSMODEL_H -#define MASTODONPOSTSMODEL_H - -#include "mastodonpostsdatabase.h" -#include <QtCore/QAbstractListModel> -#include <QtCore/QMap> - -class MastodonPostsModel: 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, - Name, - AccountName, - Acct, - Body, - Timestamp, - Icon, - Images, - Url, - Link, - BoostedBy, - RebloggedBy, - RepliesCount, - FavouritesCount, - ReblogsCount, - Favourited, - Reblogged, - InstanceUrl, - Accounts - }; - - explicit MastodonPostsModel(QObject *parent = 0); - - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role) const override; - QHash<int, QByteArray> roleNames() const override; - - QVariantList accountIdFilter() const; - void setAccountIdFilter(const QVariantList &accountIds); - - Q_INVOKABLE void refresh(); - -signals: - void accountIdFilterChanged(); - void countChanged(); - -private slots: - void postsChanged(); - -private: - typedef QMap<int, QVariant> RowData; - QList<RowData> m_data; - MastodonPostsDatabase m_database; -}; - -#endif // MASTODONPOSTSMODEL_H 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 |
