/**************************************************************************** ** ** Copyright (C) 2013-2026 Jolla Ltd. ** ****************************************************************************/ import QtQuick 2.0 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: FeedUtils.stringRole(model, ["url", "link", "uri"], "") property string postId property QtObject postActions 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 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: 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) 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 FeedUtils.stringRole(model, ["mastodonId", "statusId", "id", "twitterId"], "") } function actionAccountId() { var parsed = Number(item.accountId) return isNaN(parsed) ? -1 : parsed } 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 SocialInteractionContextMenu { id: actionMenu 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 } ] 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) } } } } } 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 } } }