From 60d8504a8b5b0d5e5fa40c4ab7df8e5f98c88e32 Mon Sep 17 00:00:00 2001 From: Andrew Branson Date: Wed, 18 Feb 2026 21:37:05 +0100 Subject: Refactor - Move common pieces to eventsview-extensions e.g. interaction menu - Expose and add useful API to libsocialcache --- .../MastodonFeedItem.qml | 281 ++++++--------------- .../mastodon-delegate.qml | 128 ++-------- .../mastodonpostsmodel.cpp | 51 +--- 3 files changed, 109 insertions(+), 351 deletions(-) (limited to 'eventsview-plugins/eventsview-plugin-mastodon') diff --git a/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml b/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml index c003950..9d04dfe 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml +++ b/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml @@ -9,19 +9,20 @@ import Sailfish.Silica 1.0 import Sailfish.TextLinking 1.0 import org.nemomobile.lipstick 0.1 import "shared" +import "shared/SocialFeedUtils.js" as FeedUtils SocialMediaFeedItem { id: item property variant imageList - property string resolvedStatusUrl: item.stringValue("url", "link", "uri") + property string resolvedStatusUrl: FeedUtils.stringRole(model, ["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 likeCount: FeedUtils.intRole(model, ["favouritesCount", "likeCount", "favoriteCount"], 0) + property int commentCount: FeedUtils.intRole(model, ["repliesCount", "commentCount"], 0) + property int boostCount: FeedUtils.intRole(model, ["reblogsCount", "boostCount", "repostsCount"], 0) + property bool favourited: FeedUtils.boolRole(model, ["favourited"], false) + property bool reblogged: FeedUtils.boolRole(model, ["reblogged"], false) property int _likeCountOverride: -1 property int _boostCountOverride: -1 property int _favouritedOverride: -1 @@ -35,10 +36,22 @@ SocialMediaFeedItem { property var _actionMenu property real _contextMenuHeight: (_contextMenuOpen && _actionMenu) ? _actionMenu.height : 0 - property string _booster: item.stringValue("boostedBy", "rebloggedBy", "retweeter") - property string _displayName: item.stringValue("name", "displayName", "display_name") - property string _accountName: item.stringValue("accountName", "acct", "screenName", "username") - property string _bodyText: item.stringValue("body", "content", "text") + property string _booster: FeedUtils.stringRole(model, ["boostedBy", "rebloggedBy", "retweeter"], "") + property string _displayName: FeedUtils.stringRole(model, ["name", "displayName", "display_name"], "") + property string _accountName: FeedUtils.stringRole(model, ["accountName", "acct", "screenName", "username"], "") + property string _bodyText: FeedUtils.stringRole(model, ["body", "content", "text"], "") + //: Action label shown in Mastodon interaction menu. + //% "Favourite" + readonly property string _favouriteActionText: qsTrId("lipstick-jolla-home-la-mastodon_favourite") + //: Action label shown in Mastodon interaction menu when the post is already favourited. + //% "Unfavourite" + readonly property string _unfavouriteActionText: qsTrId("lipstick-jolla-home-la-mastodon_unfavourite") + //: Action label shown in Mastodon interaction menu. + //% "Boost" + readonly property string _boostActionText: qsTrId("lipstick-jolla-home-la-mastodon_boost") + //: Action label shown in Mastodon interaction menu when the post is already boosted. + //% "Undo boost" + readonly property string _unboostActionText: qsTrId("lipstick-jolla-home-la-mastodon_unboost") timestamp: model.timestamp onRefreshTimeCountChanged: formattedTime = Format.formatDate(model.timestamp, Format.TimeElapsed) @@ -76,7 +89,7 @@ SocialMediaFeedItem { topMargin: item._booster.length > 0 ? Theme.paddingMedium : Theme.paddingLarge userRemovable: false - Image { + SocialReshareIcon { id: boosterIcon anchors { @@ -85,21 +98,17 @@ SocialMediaFeedItem { topMargin: item.topMargin } visible: item._booster.length > 0 - source: "image://theme/icon-s-repost" + (item.highlighted ? "?" + Theme.highlightColor : "") + highlighted: item.highlighted + iconSource: "image://theme/icon-s-repost" } - Text { + SocialReshareText { 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 - + 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" @@ -147,61 +156,18 @@ SocialMediaFeedItem { plainText: item._bodyText } - Row { + SocialPostMetadataRow { 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 - } + 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 { @@ -217,39 +183,11 @@ SocialMediaFeedItem { } } - 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") + return FeedUtils.stringRole(model, ["mastodonId", "statusId", "id", "twitterId"], "") } function actionAccountId() { @@ -313,115 +251,56 @@ SocialMediaFeedItem { Component { id: actionMenuComponent - ContextMenu { + SocialInteractionContextMenu { id: actionMenu - property bool menuOpen: height > 0 - property bool wasOpened: false z: 10000 + mapSourceItem: _contentColumn + actionEnabled: item.postActions + && item.actionPostId().length > 0 + && item.actionAccountId() >= 0 + && !item.lockScreenActive + && !item.housekeeping + interactionItems: [ + { + name: "like", + symbol: "\u2605", + active: item.isFavourited, + inactiveText: item._favouriteActionText, + activeText: item._unfavouriteActionText + }, + { + name: "reblog", + symbol: "\u21BB", + active: item.isReblogged, + inactiveText: item._boostActionText, + activeText: item._unboostActionText + } + ] - onPositionChanged: { - horizontalActions.xPos = _contentColumn.mapFromItem(actionMenu, mouse.x, mouse.y).x + onInteractionMenuOpened: item._contextMenuOpen = true + onInteractionMenuClosed: { + item._contextMenuOpen = false + destroy() + item._actionMenu = null } - onMenuOpenChanged: { - if (menuOpen) { - wasOpened = true - item._contextMenuOpen = true - } else if (wasOpened) { - item._contextMenuOpen = false - destroy() - item._actionMenu = null + onInteractionTriggered: function(actionName) { + if (!actionEnabled) { + return } - } - - 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 = xPos < width / 2 ? 0 : 1 - onDownChanged: if (!down) hoveredIndex = -1 - - onClicked: { - xPos = _contentColumn.mapFromItem(actionMenu, actionMenu.mouseX, actionMenu.mouseY).x - var index = hoveredIndex >= 0 ? hoveredIndex : (xPos < width / 2 ? 0 : 1) - 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) - } + var postId = item.actionPostId() + var accountId = item.actionAccountId() + if (actionName === "like") { + if (item.isFavourited) { + item.postActions.unfavourite(accountId, postId) } else { - if (item.isReblogged) { - item.postActions.unboost(accountId, postId) - } else { - item.postActions.boost(accountId, postId) - } + item.postActions.favourite(accountId, postId) } - } - - Rectangle { - anchors.verticalCenter: parent.verticalCenter - x: horizontalActions.hoveredIndex === 1 ? parent.width / 2 : 0 - width: parent.width / 2 - height: parent.height - visible: horizontalActions.down && horizontalActions.hoveredIndex >= 0 - color: Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) - } - - Row { - anchors.fill: parent - - Label { - width: parent.width / 2 - 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 / 2 - 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) + } else if (actionName === "reblog") { + if (item.isReblogged) { + item.postActions.unboost(accountId, postId) + } else { + item.postActions.boost(accountId, postId) } } } diff --git a/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml b/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml index c8e8713..59006fb 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml +++ b/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml @@ -10,6 +10,7 @@ import org.nemomobile.socialcache 1.0 import com.jolla.eventsview.mastodon 1.0 import QtQml.Models 2.1 import "shared" +import "shared/SocialFeedUtils.js" as FeedUtils SocialMediaAccountDelegate { id: delegateItem @@ -24,32 +25,23 @@ SocialMediaAccountDelegate { socialNetwork: 9 dataType: SocialSync.Posts providerName: "mastodon" + periodicSyncLoopEnabled: true MastodonPostActions { id: mastodonPostActions } - model: MastodonPostsModel { - onCountChanged: { - if (count > 0) { - if (!updateTimer.running) { - shortUpdateTimer.start() - } - } else { - shortUpdateTimer.stop() - } - } - } + model: MastodonPostsModel {} 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"]) + imageList: FeedUtils.variantRole(model, ["images", "mediaAttachments", "media"], undefined) + avatarSource: FeedUtils.normalizeAvatarUrl(FeedUtils.stringRole(model, ["icon", "avatar", "avatarUrl"], "")) + fallbackAvatarSource: FeedUtils.stringRole(model, ["icon", "avatar", "avatarUrl"], "") resolvedStatusUrl: delegateItem.authorizeInteractionUrl(model) - postId: delegateItem.stringRole(model, ["mastodonId", "statusId", "id", "twitterId"]) + postId: FeedUtils.stringRole(model, ["mastodonId", "statusId", "id", "twitterId"], "") postActions: mastodonPostActions - accountId: delegateItem.firstAccountId(model) + accountId: FeedUtils.firstAccountId(model, -1) onTriggered: Qt.openUrlExternally(resolvedStatusUrl) @@ -69,94 +61,36 @@ SocialMediaAccountDelegate { if (viewVisible) { delegateItem.resetHasSyncableAccounts() delegateItem.model.refresh() - if (delegateItem.hasSyncableAccounts && !updateTimer.running) { - shortUpdateTimer.start() + if (delegateItem.hasSyncableAccounts) { + delegateItem.startPeriodicSyncLoop() } } else { - shortUpdateTimer.stop() + delegateItem.stopPeriodicSyncLoop() } } 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() + delegateItem.startPeriodicSyncLoop() } } - 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"]) + var directUrl = FeedUtils.stringRole(modelData, ["url", "link", "uri"], "") if (directUrl.length > 0) { return directUrl } - var instanceUrl = stringRole(modelData, ["instanceUrl", "serverUrl", "baseUrl"]) + var instanceUrl = FeedUtils.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) - } + instanceUrl = FeedUtils.stripTrailingSlashes(instanceUrl) - var accountName = stringRole(modelData, ["accountName", "acct", "screenName", "username"]) - var statusId = stringRole(modelData, ["mastodonId", "statusId", "id", "twitterId"]) + var accountName = FeedUtils.stringRole(modelData, ["accountName", "acct", "screenName", "username"], "") + var statusId = FeedUtils.stringRole(modelData, ["mastodonId", "statusId", "id", "twitterId"], "") if (accountName.length > 0 && statusId.length > 0) { - while (accountName.length > 0 && accountName.charAt(0) === "@") { - accountName = accountName.substring(1) - } + accountName = FeedUtils.trimLeadingCharacter(accountName, "@") return instanceUrl + "/@" + accountName + "/" + statusId } @@ -169,13 +103,11 @@ SocialMediaAccountDelegate { return targetUrl } - var instanceUrl = stringRole(modelData, ["instanceUrl", "serverUrl", "baseUrl"]) + var instanceUrl = FeedUtils.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) - } + instanceUrl = FeedUtils.stripTrailingSlashes(instanceUrl) // Links on the user's own instance should open directly. var sameServer = /^([a-z][a-z0-9+.-]*):\/\/([^\/?#]+)/i @@ -191,24 +123,4 @@ SocialMediaAccountDelegate { 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 index aa98a95..fe61320 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.cpp +++ b/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.cpp @@ -17,31 +17,7 @@ */ #include "mastodonpostsmodel.h" -#include - -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; -} - -} +#include MastodonPostsModel::MastodonPostsModel(QObject *parent) : QAbstractListModel(parent) @@ -123,13 +99,16 @@ void MastodonPostsModel::postsChanged() 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()); + SocialPostHelpers::appendCommonPostFields(&eventMap, post, + MastodonPostsModel::MastodonId, + MastodonPostsModel::Name, + MastodonPostsModel::Body, + MastodonPostsModel::Timestamp, + MastodonPostsModel::Icon, + MastodonPostsModel::Images, + MastodonPostsModel::Accounts); 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); @@ -140,18 +119,6 @@ void MastodonPostsModel::postsChanged() 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); } -- cgit v1.2.3