summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Branson <andrew.branson@jolla.com>2026-02-10 23:16:37 +0100
committerAndrew Branson <andrew.branson@jolla.com>2026-02-10 23:16:37 +0100
commit69628390815254297bbd8c95436f6780fa846fae (patch)
treedf6114043e489bf5d767ac39f0d20636e12cf3a2
parentff1c2efe40bf53c146b4a2e3b5046ae8ecb32264 (diff)
Translations fixed and other stuff
-rw-r--r--.gitignore4
-rw-r--r--README.md13
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp91
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.h1
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.cpp256
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.h37
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp91
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.h1
-rw-r--r--common/common.pro7
-rw-r--r--common/mastodonauthutils.h143
-rw-r--r--common/mastodonnotificationsdatabase.cpp87
-rw-r--r--common/mastodonnotificationsdatabase.h46
-rw-r--r--rpm/sailfish-account-mastodon.spec1
-rw-r--r--settings/accounts-translations-plugin/accounts-translations-plugin.pro17
-rw-r--r--settings/accounts-translations-plugin/plugin.cpp50
-rw-r--r--settings/accounts-translations-plugin/qmldir2
-rw-r--r--settings/accounts/accounts.pro27
-rw-r--r--settings/accounts/providers/mastodon.provider1
-rw-r--r--settings/accounts/services/mastodon-microblog.service1
-rw-r--r--settings/accounts/services/mastodon-sharing.service1
-rw-r--r--settings/accounts/ui/MastodonSettingsDisplay.qml15
-rw-r--r--settings/accounts/ui/mastodon-settings.qml13
-rw-r--r--settings/accounts/ui/mastodon-update.qml1
-rw-r--r--settings/accounts/ui/mastodon.qml19
-rw-r--r--settings/settings.pro4
-rw-r--r--transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro1
-rw-r--r--transferengine-plugins/mastodonshareservicestatus.cpp101
-rw-r--r--transferengine-plugins/mastodonshareservicestatus.h1
-rw-r--r--transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro1
29 files changed, 462 insertions, 571 deletions
diff --git a/.gitignore b/.gitignore
index 6f4965a..f0950d7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,6 @@ Makefile
*.so.*
moc_*.cpp
*.moc
-*.png \ No newline at end of file
+*.png
+*.ts
+*.qm \ No newline at end of file
diff --git a/README.md b/README.md
index b355c2b..eb764aa 100644
--- a/README.md
+++ b/README.md
@@ -6,11 +6,16 @@ Sailfish OS account integration for Mastodon.
### `common/`
- Shared C++ library code used by multiple plugins.
-- Includes socialcache-backed databases for Mastodon posts and notifications.
+- Includes socialcache-backed storage for Mastodon posts and shared Mastodon auth helpers.
### `settings/`
- Sailfish Accounts provider, service definitions, and account UI.
- OAuth2 (`web_server`) account flow with per-instance Mastodon app registration.
+- Translations:
+ - QML translation-loader module at `/usr/lib*/qt5/qml/com/jolla/settings/accounts/mastodon/` loads `settings-accounts-mastodon` catalogs for `qsTrId` strings.
+ - Engineering English catalog: `/usr/share/translations/settings-accounts-mastodon_eng_en.qm`
+ - Translation source catalog: `/usr/share/translations/source/settings-accounts-mastodon.ts`
+ - Provider/service metadata uses `<translations>/usr/share/translations/settings-accounts-mastodon</translations>` for metadata string translation paths.
- Services:
- `mastodon-microblog`: sync service for posts and notifications.
- `mastodon-sharing`: Transfer Engine sharing service.
@@ -43,8 +48,11 @@ Sailfish OS account integration for Mastodon.
- `buteo-sync-plugin-mastodon-notifications`
- `eventsview-extensions-mastodon`
- `transferengine-plugin-mastodon`
-- Main package requires all feature subpackages.
+ - `sailfish-account-mastodon-ts-devel`
+- Main package requires runtime feature subpackages; `sailfish-account-mastodon-ts-devel` is optional.
- `%qmake5_install` already installs icon outputs from the `icons/` subproject; avoid a second explicit `icons` `make install` in `%install`.
+- Translation source `.ts` files are packaged in `sailfish-account-mastodon-ts-devel` (runtime packages ship `.qm` only).
+- Runtime package ships the Mastodon settings translation-loader QML plugin under `%{_libdir}/qt5/qml/com/jolla/settings/accounts/mastodon/`.
### Root project
- `sailfish-account-mastodon.pro` ties subprojects together.
@@ -54,6 +62,7 @@ Sailfish OS account integration for Mastodon.
- Events view shows Mastodon posts (not notification entries).
- System notifications are produced by `buteo-sync-plugin-mastodon-notifications`.
- Notifications sync fetches unread items using Mastodon markers (`last_read_id`).
+- Each unread Mastodon notification is published as a separate Sailfish system notification.
- Dismissing the Sailfish notification marks those items as read on Mastodon via markers API.
- Notification template profile dispatches per-account sync profiles on schedule (default every 30 minutes), not only at boot.
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp
index f915507..295d7b9 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp
@@ -19,10 +19,10 @@
****************************************************************************/
#include "mastodondatatypesyncadaptor.h"
+#include "mastodonauthutils.h"
#include "trace.h"
#include <QtCore/QVariantMap>
-#include <QtCore/QUrl>
#include <QtNetwork/QNetworkRequest>
// libaccounts-qt5
@@ -131,29 +131,6 @@ void MastodonNotificationsDataTypeSyncAdaptor::setCredentialsNeedUpdate(Accounts
account->syncAndBlock();
}
-QString MastodonNotificationsDataTypeSyncAdaptor::normalizeApiHost(const QString &rawHost)
-{
- QString host = rawHost.trimmed();
- if (host.isEmpty()) {
- host = QStringLiteral("https://mastodon.social");
- }
- if (!host.startsWith(QLatin1String("https://"))
- && !host.startsWith(QLatin1String("http://"))) {
- host.prepend(QStringLiteral("https://"));
- }
-
- QUrl url(host);
- if (!url.isValid() || url.host().isEmpty()) {
- return QStringLiteral("https://mastodon.social");
- }
-
- QString normalized = QString::fromLatin1(url.toEncoded(QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment));
- if (normalized.endsWith(QLatin1Char('/'))) {
- normalized.chop(1);
- }
- return normalized;
-}
-
void MastodonNotificationsDataTypeSyncAdaptor::signIn(Accounts::Account *account)
{
const int accountId = account->id();
@@ -185,59 +162,7 @@ void MastodonNotificationsDataTypeSyncAdaptor::signIn(Accounts::Account *account
}
QVariantMap signonSessionData = accSrv.authData().parameters();
- QString configuredHost = account->value(QStringLiteral("auth/oauth2/web_server/Host")).toString().trimmed();
- if (configuredHost.isEmpty()) {
- configuredHost = normalizeApiHost(account->value(QStringLiteral("api/Host")).toString());
- }
- if (configuredHost.startsWith(QLatin1String("https://"))) {
- configuredHost.remove(0, 8);
- } else if (configuredHost.startsWith(QLatin1String("http://"))) {
- configuredHost.remove(0, 7);
- }
- while (configuredHost.endsWith(QLatin1Char('/'))) {
- configuredHost.chop(1);
- }
- if (configuredHost.isEmpty()) {
- configuredHost = QStringLiteral("mastodon.social");
- }
- signonSessionData.insert(QStringLiteral("Host"), configuredHost);
-
- const QString authPath = account->value(QStringLiteral("auth/oauth2/web_server/AuthPath")).toString().trimmed();
- if (!authPath.isEmpty()) {
- signonSessionData.insert(QStringLiteral("AuthPath"), authPath);
- }
-
- const QString tokenPath = account->value(QStringLiteral("auth/oauth2/web_server/TokenPath")).toString().trimmed();
- if (!tokenPath.isEmpty()) {
- signonSessionData.insert(QStringLiteral("TokenPath"), tokenPath);
- }
-
- const QString responseType = account->value(QStringLiteral("auth/oauth2/web_server/ResponseType")).toString().trimmed();
- if (!responseType.isEmpty()) {
- signonSessionData.insert(QStringLiteral("ResponseType"), responseType);
- }
-
- const QString redirectUri = account->value(QStringLiteral("auth/oauth2/web_server/RedirectUri")).toString().trimmed();
- if (!redirectUri.isEmpty()) {
- signonSessionData.insert(QStringLiteral("RedirectUri"), redirectUri);
- }
-
- const QVariant scopeValue = account->value(QStringLiteral("auth/oauth2/web_server/Scope"));
- if (scopeValue.isValid()) {
- signonSessionData.insert(QStringLiteral("Scope"), scopeValue);
- }
-
- const QString clientId = account->value(QStringLiteral("auth/oauth2/web_server/ClientId")).toString().trimmed();
- if (!clientId.isEmpty()) {
- signonSessionData.insert(QStringLiteral("ClientId"), clientId);
- }
-
- const QString clientSecret = account->value(QStringLiteral("auth/oauth2/web_server/ClientSecret")).toString().trimmed();
- if (!clientSecret.isEmpty()) {
- signonSessionData.insert(QStringLiteral("ClientSecret"), clientSecret);
- }
-
- signonSessionData.insert(QStringLiteral("UiPolicy"), SignOn::NoUserInteractionPolicy);
+ MastodonAuthUtils::addSignOnSessionParameters(account, &signonSessionData);
connect(session, SIGNAL(response(SignOn::SessionData)),
this, SLOT(signOnResponse(SignOn::SessionData)),
@@ -276,10 +201,7 @@ void MastodonNotificationsDataTypeSyncAdaptor::signOnError(const SignOn::Error &
void MastodonNotificationsDataTypeSyncAdaptor::signOnResponse(const SignOn::SessionData &responseData)
{
- QVariantMap data;
- foreach (const QString &key, responseData.propertyNames()) {
- data.insert(key, responseData.getProperty(key));
- }
+ const QVariantMap data = MastodonAuthUtils::responseDataToMap(responseData);
QString accessToken;
SignOn::AuthSession *session = qobject_cast<SignOn::AuthSession*>(sender());
@@ -287,16 +209,13 @@ void MastodonNotificationsDataTypeSyncAdaptor::signOnResponse(const SignOn::Sess
SignOn::Identity *identity = session->property("identity").value<SignOn::Identity*>();
const int accountId = account->id();
- accessToken = data.value(QLatin1String("AccessToken")).toString().trimmed();
- if (accessToken.isEmpty()) {
- accessToken = data.value(QLatin1String("access_token")).toString().trimmed();
- }
+ accessToken = MastodonAuthUtils::accessToken(data);
if (accessToken.isEmpty()) {
qCWarning(lcSocialPlugin) << "signon response for account with id" << accountId
<< "contained no access token; keys:" << data.keys();
}
- m_apiHosts.insert(accountId, normalizeApiHost(account->value(QStringLiteral("api/Host")).toString()));
+ m_apiHosts.insert(accountId, MastodonAuthUtils::normalizeApiHost(account->value(QStringLiteral("api/Host")).toString()));
session->disconnect(this);
identity->destroySession(session);
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.h b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.h
index 1c2d13f..3bb6e23 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.h
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.h
@@ -59,7 +59,6 @@ private Q_SLOTS:
void signOnResponse(const SignOn::SessionData &responseData);
private:
- static QString normalizeApiHost(const QString &rawHost);
void setCredentialsNeedUpdate(Accounts::Account *account);
void signIn(Accounts::Account *account);
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.cpp
index aa1089c..79b996c 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.cpp
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.cpp
@@ -31,6 +31,8 @@
#include <notification.h>
+#include <algorithm>
+
#define OPEN_BROWSER_ACTION(openUrlArgs) \
Notification::remoteAction( \
"default", \
@@ -45,8 +47,9 @@
namespace {
const char *const NotificationCategory = "x-nemo.social.mastodon.notification";
const char *const LastReadIdProperty = "mastodonLastReadId";
+ const char *const NotificationIdHint = "x-nemo.sociald.notification-id";
const int NotificationsPageLimit = 80;
- const uint NotificationDismissedReason = 2;
+ const uint NotificationDismissedReason = 1;
QString decodeHtmlEntities(QString text)
{
@@ -118,41 +121,12 @@ namespace {
return QStringLiteral("sent you a notification");
}
- QList<QPair<QString, SocialPostImage::ImageType> > parseMediaAttachments(const QJsonObject &statusObject)
- {
- QList<QPair<QString, SocialPostImage::ImageType> > imageList;
-
- const QJsonArray mediaAttachments = statusObject.value(QStringLiteral("media_attachments")).toArray();
- foreach (const QJsonValue &attachmentValue, mediaAttachments) {
- const QJsonObject attachment = attachmentValue.toObject();
- const QString mediaType = attachment.value(QStringLiteral("type")).toString();
-
- QString mediaUrl;
- SocialPostImage::ImageType imageType = SocialPostImage::Invalid;
- if (mediaType == QLatin1String("image")) {
- mediaUrl = attachment.value(QStringLiteral("url")).toString();
- imageType = SocialPostImage::Photo;
- } else if (mediaType == QLatin1String("video") || mediaType == QLatin1String("gifv")) {
- mediaUrl = attachment.value(QStringLiteral("preview_url")).toString();
- if (mediaUrl.isEmpty()) {
- mediaUrl = attachment.value(QStringLiteral("url")).toString();
- }
- imageType = SocialPostImage::Video;
- }
-
- if (!mediaUrl.isEmpty() && imageType != SocialPostImage::Invalid) {
- imageList.append(qMakePair(mediaUrl, imageType));
- }
- }
-
- return imageList;
- }
}
MastodonNotificationsSyncAdaptor::MastodonNotificationsSyncAdaptor(QObject *parent)
: MastodonNotificationsDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Notifications, parent)
{
- setInitialActive(m_db.isValid());
+ setInitialActive(true);
}
MastodonNotificationsSyncAdaptor::~MastodonNotificationsSyncAdaptor()
@@ -166,17 +140,7 @@ QString MastodonNotificationsSyncAdaptor::syncServiceName() const
void MastodonNotificationsSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode)
{
- Notification *notification = findNotification(oldId);
- if (notification) {
- notification->close();
- notification->deleteLater();
- }
-
- m_db.removePosts(oldId);
- m_db.commit();
- m_db.wait();
-
- purgeCachedImages(&m_imageCacheDb, oldId);
+ closeAccountNotifications(oldId);
m_pendingSyncStates.remove(oldId);
m_accessTokens.remove(oldId);
@@ -193,12 +157,10 @@ void MastodonNotificationsSyncAdaptor::beginSync(int accountId, const QString &a
void MastodonNotificationsSyncAdaptor::finalize(int accountId)
{
if (syncAborted()) {
- qCInfo(lcSocialPlugin) << "sync aborted, won't commit database changes";
- } else {
- m_db.commit();
- m_db.wait();
- purgeExpiredImages(&m_imageCacheDb, accountId);
+ qCInfo(lcSocialPlugin) << "sync aborted, won't update notifications";
}
+
+ Q_UNUSED(accountId)
}
QString MastodonNotificationsSyncAdaptor::sanitizeContent(const QString &content)
@@ -271,6 +233,11 @@ int MastodonNotificationsSyncAdaptor::compareNotificationIds(const QString &left
return left < right ? -1 : 1;
}
+QString MastodonNotificationsSyncAdaptor::notificationObjectKey(int accountId, const QString &notificationId)
+{
+ return QString::number(accountId) + QLatin1Char(':') + notificationId;
+}
+
void MastodonNotificationsSyncAdaptor::requestUnreadMarker(int accountId, const QString &accessToken)
{
QUrl url(apiHost(accountId) + QStringLiteral("/api/v1/markers"));
@@ -369,7 +336,6 @@ void MastodonNotificationsSyncAdaptor::finishedUnreadMarkerHandler()
PendingSyncState state;
state.accessToken = accessToken;
state.minReadId = minReadId;
- state.maxNotificationId = minReadId;
m_pendingSyncStates.insert(accountId, state);
requestNotifications(accountId, accessToken, minReadId);
@@ -397,33 +363,19 @@ void MastodonNotificationsSyncAdaptor::finishedNotificationsHandler()
if (state.accessToken.isEmpty()) {
state.accessToken = accessToken;
state.minReadId = minId;
- state.maxNotificationId = minId;
}
bool ok = false;
const QJsonArray notifications = parseJsonArrayReplyData(replyData, &ok);
if (!isError && ok) {
if (!notifications.size()) {
- if (!state.dbCleared) {
- m_db.removePosts(accountId);
- state.dbCleared = true;
- }
- Notification *notification = findNotification(accountId);
- if (notification) {
- notification->close();
- notification->deleteLater();
- }
+ closeAccountNotifications(accountId);
qCDebug(lcSocialPlugin) << "no notifications received for account" << accountId;
m_pendingSyncStates.remove(accountId);
decrementSemaphore(accountId);
return;
}
- if (!state.dbCleared) {
- m_db.removePosts(accountId);
- state.dbCleared = true;
- }
-
QString pageMinNotificationId;
foreach (const QJsonValue &notificationValue, notifications) {
@@ -441,10 +393,6 @@ void MastodonNotificationsSyncAdaptor::finishedNotificationsHandler()
|| compareNotificationIds(notificationId, pageMinNotificationId) < 0) {
pageMinNotificationId = notificationId;
}
- if (state.maxNotificationId.isEmpty()
- || compareNotificationIds(notificationId, state.maxNotificationId) > 0) {
- state.maxNotificationId = notificationId;
- }
const QString notificationType = notificationObject.value(QStringLiteral("type")).toString();
const QJsonObject actorObject = notificationObject.value(QStringLiteral("account")).toObject();
@@ -464,11 +412,6 @@ void MastodonNotificationsSyncAdaptor::finishedNotificationsHandler()
const QString displayName = displayNameForAccount(actorObject);
const QString accountName = actorObject.value(QStringLiteral("acct")).toString();
- QString icon = actorObject.value(QStringLiteral("avatar_static")).toString();
- if (icon.isEmpty()) {
- icon = actorObject.value(QStringLiteral("avatar")).toString();
- }
-
const QString statusBody = sanitizeContent(statusObject.value(QStringLiteral("content")).toString());
const QString action = actionText(notificationType);
QString body;
@@ -495,33 +438,13 @@ void MastodonNotificationsSyncAdaptor::finishedNotificationsHandler()
url = QStringLiteral("%1/@%2").arg(apiHost(accountId), accountName);
}
- QString boostedBy;
- if (notificationType == QLatin1String("reblog")
- || notificationType == QLatin1String("favourite")) {
- boostedBy = displayName;
- }
-
- const QList<QPair<QString, SocialPostImage::ImageType> > imageList = parseMediaAttachments(statusObject);
-
- m_db.addMastodonNotification(QStringLiteral("n:%1").arg(notificationId),
- displayName,
- accountName,
- body,
- eventTimestamp,
- icon,
- imageList,
- url,
- boostedBy,
- apiHost(accountId),
- accountId);
-
- ++state.newNotificationCount;
- if (state.newNotificationCount == 1) {
- state.singleSummary = displayName;
- state.singleBody = body;
- state.singleLink = url;
- state.singleTimestamp = eventTimestamp;
- }
+ PendingNotification pendingNotification;
+ pendingNotification.notificationId = notificationId;
+ pendingNotification.summary = displayName;
+ pendingNotification.body = body;
+ pendingNotification.link = url;
+ pendingNotification.timestamp = eventTimestamp;
+ state.pendingNotifications.insert(notificationId, pendingNotification);
}
if (notifications.size() >= NotificationsPageLimit
@@ -533,8 +456,19 @@ void MastodonNotificationsSyncAdaptor::finishedNotificationsHandler()
return;
}
- if (state.newNotificationCount > 0) {
- publishSystemNotification(accountId, state);
+ if (state.pendingNotifications.size() > 0) {
+ QStringList notificationIds = state.pendingNotifications.keys();
+ std::sort(notificationIds.begin(), notificationIds.end(), [](const QString &left, const QString &right) {
+ return compareNotificationIds(left, right) > 0;
+ });
+
+ QSet<QString> keepNotificationIds;
+ foreach (const QString &notificationId, notificationIds) {
+ const PendingNotification pendingNotification = state.pendingNotifications.value(notificationId);
+ publishSystemNotification(accountId, pendingNotification);
+ keepNotificationIds.insert(notificationId);
+ }
+ closeAccountNotifications(accountId, keepNotificationIds);
}
} else {
qCWarning(lcSocialPlugin) << "unable to parse notifications data from request with account" << accountId
@@ -601,30 +535,76 @@ void MastodonNotificationsSyncAdaptor::finishedMarkReadHandler()
decrementSemaphore(accountId);
}
-void MastodonNotificationsSyncAdaptor::publishSystemNotification(int accountId, const PendingSyncState &state)
+void MastodonNotificationsSyncAdaptor::publishSystemNotification(int accountId,
+ const PendingNotification &notificationData)
{
- Notification *notification = createNotification(accountId);
- notification->setItemCount(state.newNotificationCount);
-
- QStringList openUrlArgs;
- if (notification->itemCount() == 1) {
- notification->setTimestamp(state.singleTimestamp.isValid() ? state.singleTimestamp : QDateTime::currentDateTimeUtc());
- notification->setSummary(state.singleSummary.isEmpty() ? QStringLiteral("Mastodon") : state.singleSummary);
- notification->setBody(state.singleBody.isEmpty() ? QStringLiteral("New notification") : state.singleBody);
- openUrlArgs << (state.singleLink.isEmpty() ? apiHost(accountId) + QStringLiteral("/notifications") : state.singleLink);
- } else {
- notification->setTimestamp(QDateTime::currentDateTimeUtc());
- notification->setSummary(QStringLiteral("Mastodon"));
- notification->setBody(QStringLiteral("You have %1 new notifications").arg(notification->itemCount()));
- openUrlArgs << apiHost(accountId) + QStringLiteral("/notifications");
- }
-
- notification->setProperty(LastReadIdProperty, state.maxNotificationId);
+ Notification *notification = createNotification(accountId, notificationData.notificationId);
+ notification->setItemCount(1);
+ notification->setTimestamp(notificationData.timestamp.isValid()
+ ? notificationData.timestamp
+ : QDateTime::currentDateTimeUtc());
+ notification->setSummary(notificationData.summary.isEmpty()
+ ? QStringLiteral("Mastodon")
+ : notificationData.summary);
+ notification->setBody(notificationData.body.isEmpty()
+ ? QStringLiteral("New notification")
+ : notificationData.body);
+ notification->setPreviewSummary(notificationData.summary);
+ notification->setPreviewBody(notificationData.body);
+
+ const QString openUrl = notificationData.link.isEmpty()
+ ? apiHost(accountId) + QStringLiteral("/notifications")
+ : notificationData.link;
+ notification->setProperty(LastReadIdProperty, notificationData.notificationId);
notification->setUrgency(Notification::Low);
- notification->setRemoteAction(OPEN_BROWSER_ACTION(openUrlArgs));
+ notification->setRemoteAction(OPEN_BROWSER_ACTION(QStringList() << openUrl));
notification->publish();
if (notification->replacesId() == 0) {
- qCWarning(lcSocialPlugin) << "failed to publish Mastodon notification";
+ qCWarning(lcSocialPlugin) << "failed to publish Mastodon notification"
+ << notificationData.notificationId;
+ }
+}
+
+void MastodonNotificationsSyncAdaptor::closeAccountNotifications(int accountId,
+ const QSet<QString> &keepNotificationIds)
+{
+ QStringList cachedKeys = m_notificationObjects.keys();
+ foreach (const QString &objectKey, cachedKeys) {
+ Notification *notification = m_notificationObjects.value(objectKey);
+ if (!notification
+ || notification->hintValue("x-nemo.sociald.account-id").toInt() != accountId) {
+ continue;
+ }
+
+ const QString notificationId = notification->hintValue(NotificationIdHint).toString();
+ if (!notificationId.isEmpty() && keepNotificationIds.contains(notificationId)) {
+ continue;
+ }
+
+ notification->close();
+ m_notificationObjects.remove(objectKey);
+ notification->deleteLater();
+ }
+
+ QList<QObject *> notifications = Notification::notifications();
+ foreach (QObject *object, notifications) {
+ Notification *notification = qobject_cast<Notification *>(object);
+ if (!notification) {
+ delete object;
+ continue;
+ }
+
+ if (notification->category() == QLatin1String(NotificationCategory)
+ && notification->hintValue("x-nemo.sociald.account-id").toInt() == accountId) {
+ const QString notificationId = notification->hintValue(NotificationIdHint).toString();
+ if (notificationId.isEmpty() || !keepNotificationIds.contains(notificationId)) {
+ notification->close();
+ }
+ }
+
+ if (notification->parent() != this) {
+ delete notification;
+ }
}
}
@@ -662,30 +642,42 @@ void MastodonNotificationsSyncAdaptor::markReadFromNotification(Notification *no
requestMarkRead(accountId, accessToken, lastReadId);
}
-Notification *MastodonNotificationsSyncAdaptor::createNotification(int accountId)
+Notification *MastodonNotificationsSyncAdaptor::createNotification(int accountId, const QString &notificationId)
{
- Notification *notification = findNotification(accountId);
+ const QString objectKey = notificationObjectKey(accountId, notificationId);
+ Notification *notification = m_notificationObjects.value(objectKey);
+ if (!notification) {
+ notification = findNotification(accountId, notificationId);
+ }
if (!notification) {
notification = new Notification(this);
- notification->setAppName(QStringLiteral("Mastodon"));
- notification->setAppIcon(QStringLiteral("icon-l-mastodon"));
- notification->setHintValue("x-nemo.sociald.account-id", accountId);
- notification->setHintValue("x-nemo-feedback", QStringLiteral("social"));
- notification->setCategory(QLatin1String(NotificationCategory));
+ } else if (notification->parent() != this) {
+ notification->setParent(this);
}
+
+ notification->setAppName(QStringLiteral("Mastodon"));
+ notification->setAppIcon(QStringLiteral("icon-l-mastodon"));
+ notification->setHintValue("x-nemo.sociald.account-id", accountId);
+ notification->setHintValue(NotificationIdHint, notificationId);
+ notification->setHintValue("x-nemo-feedback", QStringLiteral("social"));
+ notification->setCategory(QLatin1String(NotificationCategory));
+
connect(notification, SIGNAL(closed(uint)), this, SLOT(notificationClosedWithReason(uint)), Qt::UniqueConnection);
+ m_notificationObjects.insert(objectKey, notification);
return notification;
}
-Notification *MastodonNotificationsSyncAdaptor::findNotification(int accountId)
+Notification *MastodonNotificationsSyncAdaptor::findNotification(int accountId, const QString &notificationId)
{
Notification *notification = 0;
QList<QObject *> notifications = Notification::notifications();
foreach (QObject *object, notifications) {
- Notification *castedNotification = static_cast<Notification *>(object);
- if (castedNotification->category() == QLatin1String(NotificationCategory)
- && castedNotification->hintValue("x-nemo.sociald.account-id").toInt() == accountId) {
+ Notification *castedNotification = qobject_cast<Notification *>(object);
+ if (castedNotification
+ && castedNotification->category() == QLatin1String(NotificationCategory)
+ && castedNotification->hintValue("x-nemo.sociald.account-id").toInt() == accountId
+ && castedNotification->hintValue(NotificationIdHint).toString() == notificationId) {
notification = castedNotification;
break;
}
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.h b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.h
index 8c79d7d..0cd63a4 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.h
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.h
@@ -25,11 +25,9 @@
#include <QtCore/QDateTime>
#include <QtCore/QHash>
+#include <QtCore/QSet>
#include <QtNetwork/QNetworkReply>
-#include "mastodonnotificationsdatabase.h"
-#include <socialcache/socialimagesdatabase.h>
-
class Notification;
class MastodonNotificationsSyncAdaptor : public MastodonNotificationsDataTypeSyncAdaptor
@@ -48,22 +46,18 @@ protected:
void finalize(int accountId) override;
private:
+ struct PendingNotification {
+ QString notificationId;
+ QString summary;
+ QString body;
+ QString link;
+ QDateTime timestamp;
+ };
+
struct PendingSyncState {
QString accessToken;
QString minReadId;
- QString maxNotificationId;
- int newNotificationCount;
- QString singleSummary;
- QString singleBody;
- QString singleLink;
- QDateTime singleTimestamp;
- bool dbCleared;
-
- PendingSyncState()
- : newNotificationCount(0)
- , dbCleared(false)
- {
- }
+ QHash<QString, PendingNotification> pendingNotifications;
};
static QString sanitizeContent(const QString &content);
@@ -76,9 +70,11 @@ private:
const QString &minId,
const QString &maxId = QString());
void requestMarkRead(int accountId, const QString &accessToken, const QString &lastReadId);
- void publishSystemNotification(int accountId, const PendingSyncState &state);
- Notification *createNotification(int accountId);
- Notification *findNotification(int accountId);
+ void publishSystemNotification(int accountId, const PendingNotification &notificationData);
+ Notification *createNotification(int accountId, const QString &notificationId);
+ Notification *findNotification(int accountId, const QString &notificationId);
+ void closeAccountNotifications(int accountId, const QSet<QString> &keepNotificationIds = QSet<QString>());
+ static QString notificationObjectKey(int accountId, const QString &notificationId);
void markReadFromNotification(Notification *notification);
private Q_SLOTS:
@@ -88,11 +84,10 @@ private Q_SLOTS:
void notificationClosedWithReason(uint reason);
private:
- MastodonNotificationsDatabase m_db;
- SocialImagesDatabase m_imageCacheDb;
QHash<int, PendingSyncState> m_pendingSyncStates;
QHash<int, QString> m_accessTokens;
QHash<int, QString> m_lastMarkedReadIds;
+ QHash<QString, Notification *> m_notificationObjects;
};
#endif // MASTODONNOTIFICATIONSSYNCADAPTOR_H
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp
index 83a5249..7b47fe8 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp
@@ -19,10 +19,10 @@
****************************************************************************/
#include "mastodondatatypesyncadaptor.h"
+#include "mastodonauthutils.h"
#include "trace.h"
#include <QtCore/QVariantMap>
-#include <QtCore/QUrl>
#include <QtNetwork/QNetworkRequest>
// libaccounts-qt5
@@ -131,29 +131,6 @@ void MastodonDataTypeSyncAdaptor::setCredentialsNeedUpdate(Accounts::Account *ac
account->syncAndBlock();
}
-QString MastodonDataTypeSyncAdaptor::normalizeApiHost(const QString &rawHost)
-{
- QString host = rawHost.trimmed();
- if (host.isEmpty()) {
- host = QStringLiteral("https://mastodon.social");
- }
- if (!host.startsWith(QLatin1String("https://"))
- && !host.startsWith(QLatin1String("http://"))) {
- host.prepend(QStringLiteral("https://"));
- }
-
- QUrl url(host);
- if (!url.isValid() || url.host().isEmpty()) {
- return QStringLiteral("https://mastodon.social");
- }
-
- QString normalized = QString::fromLatin1(url.toEncoded(QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment));
- if (normalized.endsWith(QLatin1Char('/'))) {
- normalized.chop(1);
- }
- return normalized;
-}
-
void MastodonDataTypeSyncAdaptor::signIn(Accounts::Account *account)
{
const int accountId = account->id();
@@ -185,59 +162,7 @@ void MastodonDataTypeSyncAdaptor::signIn(Accounts::Account *account)
}
QVariantMap signonSessionData = accSrv.authData().parameters();
- QString configuredHost = account->value(QStringLiteral("auth/oauth2/web_server/Host")).toString().trimmed();
- if (configuredHost.isEmpty()) {
- configuredHost = normalizeApiHost(account->value(QStringLiteral("api/Host")).toString());
- }
- if (configuredHost.startsWith(QLatin1String("https://"))) {
- configuredHost.remove(0, 8);
- } else if (configuredHost.startsWith(QLatin1String("http://"))) {
- configuredHost.remove(0, 7);
- }
- while (configuredHost.endsWith(QLatin1Char('/'))) {
- configuredHost.chop(1);
- }
- if (configuredHost.isEmpty()) {
- configuredHost = QStringLiteral("mastodon.social");
- }
- signonSessionData.insert(QStringLiteral("Host"), configuredHost);
-
- const QString authPath = account->value(QStringLiteral("auth/oauth2/web_server/AuthPath")).toString().trimmed();
- if (!authPath.isEmpty()) {
- signonSessionData.insert(QStringLiteral("AuthPath"), authPath);
- }
-
- const QString tokenPath = account->value(QStringLiteral("auth/oauth2/web_server/TokenPath")).toString().trimmed();
- if (!tokenPath.isEmpty()) {
- signonSessionData.insert(QStringLiteral("TokenPath"), tokenPath);
- }
-
- const QString responseType = account->value(QStringLiteral("auth/oauth2/web_server/ResponseType")).toString().trimmed();
- if (!responseType.isEmpty()) {
- signonSessionData.insert(QStringLiteral("ResponseType"), responseType);
- }
-
- const QString redirectUri = account->value(QStringLiteral("auth/oauth2/web_server/RedirectUri")).toString().trimmed();
- if (!redirectUri.isEmpty()) {
- signonSessionData.insert(QStringLiteral("RedirectUri"), redirectUri);
- }
-
- const QVariant scopeValue = account->value(QStringLiteral("auth/oauth2/web_server/Scope"));
- if (scopeValue.isValid()) {
- signonSessionData.insert(QStringLiteral("Scope"), scopeValue);
- }
-
- const QString clientId = account->value(QStringLiteral("auth/oauth2/web_server/ClientId")).toString().trimmed();
- if (!clientId.isEmpty()) {
- signonSessionData.insert(QStringLiteral("ClientId"), clientId);
- }
-
- const QString clientSecret = account->value(QStringLiteral("auth/oauth2/web_server/ClientSecret")).toString().trimmed();
- if (!clientSecret.isEmpty()) {
- signonSessionData.insert(QStringLiteral("ClientSecret"), clientSecret);
- }
-
- signonSessionData.insert(QStringLiteral("UiPolicy"), SignOn::NoUserInteractionPolicy);
+ MastodonAuthUtils::addSignOnSessionParameters(account, &signonSessionData);
connect(session, SIGNAL(response(SignOn::SessionData)),
this, SLOT(signOnResponse(SignOn::SessionData)),
@@ -276,10 +201,7 @@ void MastodonDataTypeSyncAdaptor::signOnError(const SignOn::Error &error)
void MastodonDataTypeSyncAdaptor::signOnResponse(const SignOn::SessionData &responseData)
{
- QVariantMap data;
- foreach (const QString &key, responseData.propertyNames()) {
- data.insert(key, responseData.getProperty(key));
- }
+ const QVariantMap data = MastodonAuthUtils::responseDataToMap(responseData);
QString accessToken;
SignOn::AuthSession *session = qobject_cast<SignOn::AuthSession*>(sender());
@@ -287,16 +209,13 @@ void MastodonDataTypeSyncAdaptor::signOnResponse(const SignOn::SessionData &resp
SignOn::Identity *identity = session->property("identity").value<SignOn::Identity*>();
const int accountId = account->id();
- accessToken = data.value(QLatin1String("AccessToken")).toString().trimmed();
- if (accessToken.isEmpty()) {
- accessToken = data.value(QLatin1String("access_token")).toString().trimmed();
- }
+ accessToken = MastodonAuthUtils::accessToken(data);
if (accessToken.isEmpty()) {
qCWarning(lcSocialPlugin) << "signon response for account with id" << accountId
<< "contained no access token; keys:" << data.keys();
}
- m_apiHosts.insert(accountId, normalizeApiHost(account->value(QStringLiteral("api/Host")).toString()));
+ m_apiHosts.insert(accountId, MastodonAuthUtils::normalizeApiHost(account->value(QStringLiteral("api/Host")).toString()));
session->disconnect(this);
identity->destroySession(session);
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.h b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.h
index ad8321d..3ebbbf5 100644
--- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.h
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.h
@@ -59,7 +59,6 @@ private Q_SLOTS:
void signOnResponse(const SignOn::SessionData &responseData);
private:
- static QString normalizeApiHost(const QString &rawHost);
void setCredentialsNeedUpdate(Accounts::Account *account);
void signIn(Accounts::Account *account);
diff --git a/common/common.pro b/common/common.pro
index 89f0117..02c78ee 100644
--- a/common/common.pro
+++ b/common/common.pro
@@ -10,12 +10,11 @@ TARGET = mastodoncommon
TARGET = $$qtLibraryTarget($$TARGET)
HEADERS += \
- $$PWD/mastodonpostsdatabase.h \
- $$PWD/mastodonnotificationsdatabase.h
+ $$PWD/mastodonauthutils.h \
+ $$PWD/mastodonpostsdatabase.h
SOURCES += \
- $$PWD/mastodonpostsdatabase.cpp \
- $$PWD/mastodonnotificationsdatabase.cpp
+ $$PWD/mastodonpostsdatabase.cpp
TARGETPATH = $$[QT_INSTALL_LIBS]
target.path = $$TARGETPATH
diff --git a/common/mastodonauthutils.h b/common/mastodonauthutils.h
new file mode 100644
index 0000000..3f1fc85
--- /dev/null
+++ b/common/mastodonauthutils.h
@@ -0,0 +1,143 @@
+/*
+ * 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 MASTODONAUTHUTILS_H
+#define MASTODONAUTHUTILS_H
+
+#include <QtCore/QVariantMap>
+#include <QtCore/QUrl>
+
+#include <Accounts/Account>
+
+#include <SignOn/SessionData>
+
+namespace MastodonAuthUtils {
+
+inline QString normalizeApiHost(const QString &rawHost)
+{
+ QString host = rawHost.trimmed();
+ if (host.isEmpty()) {
+ host = QStringLiteral("https://mastodon.social");
+ }
+ if (!host.startsWith(QLatin1String("https://"))
+ && !host.startsWith(QLatin1String("http://"))) {
+ host.prepend(QStringLiteral("https://"));
+ }
+
+ QUrl url(host);
+ if (!url.isValid() || url.host().isEmpty()) {
+ return QStringLiteral("https://mastodon.social");
+ }
+
+ QString normalized = QString::fromLatin1(url.toEncoded(QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment));
+ if (normalized.endsWith(QLatin1Char('/'))) {
+ normalized.chop(1);
+ }
+ return normalized;
+}
+
+inline QString signOnHost(Accounts::Account *account)
+{
+ QString configuredHost = account->value(QStringLiteral("auth/oauth2/web_server/Host")).toString().trimmed();
+ if (configuredHost.isEmpty()) {
+ configuredHost = normalizeApiHost(account->value(QStringLiteral("api/Host")).toString());
+ }
+
+ if (configuredHost.startsWith(QLatin1String("https://"))) {
+ configuredHost.remove(0, 8);
+ } else if (configuredHost.startsWith(QLatin1String("http://"))) {
+ configuredHost.remove(0, 7);
+ }
+
+ const int separator = configuredHost.indexOf(QLatin1Char('/'));
+ if (separator > -1) {
+ configuredHost.truncate(separator);
+ }
+ while (configuredHost.endsWith(QLatin1Char('/'))) {
+ configuredHost.chop(1);
+ }
+ if (configuredHost.isEmpty()) {
+ configuredHost = QStringLiteral("mastodon.social");
+ }
+
+ return configuredHost;
+}
+
+inline void addSignOnSessionParameters(Accounts::Account *account, QVariantMap *sessionData)
+{
+ sessionData->insert(QStringLiteral("Host"), signOnHost(account));
+
+ const QString authPath = account->value(QStringLiteral("auth/oauth2/web_server/AuthPath")).toString().trimmed();
+ if (!authPath.isEmpty()) {
+ sessionData->insert(QStringLiteral("AuthPath"), authPath);
+ }
+
+ const QString tokenPath = account->value(QStringLiteral("auth/oauth2/web_server/TokenPath")).toString().trimmed();
+ if (!tokenPath.isEmpty()) {
+ sessionData->insert(QStringLiteral("TokenPath"), tokenPath);
+ }
+
+ const QString responseType = account->value(QStringLiteral("auth/oauth2/web_server/ResponseType")).toString().trimmed();
+ if (!responseType.isEmpty()) {
+ sessionData->insert(QStringLiteral("ResponseType"), responseType);
+ }
+
+ const QString redirectUri = account->value(QStringLiteral("auth/oauth2/web_server/RedirectUri")).toString().trimmed();
+ if (!redirectUri.isEmpty()) {
+ sessionData->insert(QStringLiteral("RedirectUri"), redirectUri);
+ }
+
+ const QVariant scopeValue = account->value(QStringLiteral("auth/oauth2/web_server/Scope"));
+ if (scopeValue.isValid()) {
+ sessionData->insert(QStringLiteral("Scope"), scopeValue);
+ }
+
+ const QString clientId = account->value(QStringLiteral("auth/oauth2/web_server/ClientId")).toString().trimmed();
+ if (!clientId.isEmpty()) {
+ sessionData->insert(QStringLiteral("ClientId"), clientId);
+ }
+
+ const QString clientSecret = account->value(QStringLiteral("auth/oauth2/web_server/ClientSecret")).toString().trimmed();
+ if (!clientSecret.isEmpty()) {
+ sessionData->insert(QStringLiteral("ClientSecret"), clientSecret);
+ }
+
+ sessionData->insert(QStringLiteral("UiPolicy"), SignOn::NoUserInteractionPolicy);
+}
+
+inline QString accessToken(const QVariantMap &sessionResponseData)
+{
+ QString token = sessionResponseData.value(QLatin1String("AccessToken")).toString().trimmed();
+ if (token.isEmpty()) {
+ token = sessionResponseData.value(QLatin1String("access_token")).toString().trimmed();
+ }
+ return token;
+}
+
+inline QVariantMap responseDataToMap(const SignOn::SessionData &responseData)
+{
+ QVariantMap data;
+ foreach (const QString &key, responseData.propertyNames()) {
+ data.insert(key, responseData.getProperty(key));
+ }
+ return data;
+}
+
+} // namespace MastodonAuthUtils
+
+#endif // MASTODONAUTHUTILS_H
diff --git a/common/mastodonnotificationsdatabase.cpp b/common/mastodonnotificationsdatabase.cpp
deleted file mode 100644
index abb55b9..0000000
--- a/common/mastodonnotificationsdatabase.cpp
+++ /dev/null
@@ -1,87 +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 "mastodonnotificationsdatabase.h"
-
-static const char *DB_NAME = "mastodonNotifications.db";
-static const char *ACCOUNT_NAME_KEY = "account_name";
-static const char *URL_KEY = "url";
-static const char *BOOSTED_BY_KEY = "boosted_by";
-static const char *INSTANCE_URL_KEY = "instance_url";
-
-MastodonNotificationsDatabase::MastodonNotificationsDatabase()
- : AbstractSocialPostCacheDatabase(QStringLiteral("mastodon"), QLatin1String(DB_NAME))
-{
-}
-
-MastodonNotificationsDatabase::~MastodonNotificationsDatabase()
-{
-}
-
-void MastodonNotificationsDatabase::addMastodonNotification(
- const QString &identifier,
- const QString &name,
- const QString &accountName,
- const QString &body,
- const QDateTime &timestamp,
- const QString &icon,
- const QList<QPair<QString, SocialPostImage::ImageType> > &images,
- const QString &url,
- const QString &boostedBy,
- const QString &instanceUrl,
- int account)
-{
- QVariantMap extra;
- extra.insert(ACCOUNT_NAME_KEY, accountName);
- extra.insert(URL_KEY, url);
- extra.insert(BOOSTED_BY_KEY, boostedBy);
- extra.insert(INSTANCE_URL_KEY, instanceUrl);
- addPost(identifier, name, body, timestamp, icon, images, extra, account);
-}
-
-QString MastodonNotificationsDatabase::accountName(const SocialPost::ConstPtr &post)
-{
- if (post.isNull()) {
- return QString();
- }
- return post->extra().value(ACCOUNT_NAME_KEY).toString();
-}
-
-QString MastodonNotificationsDatabase::url(const SocialPost::ConstPtr &post)
-{
- if (post.isNull()) {
- return QString();
- }
- return post->extra().value(URL_KEY).toString();
-}
-
-QString MastodonNotificationsDatabase::boostedBy(const SocialPost::ConstPtr &post)
-{
- if (post.isNull()) {
- return QString();
- }
- return post->extra().value(BOOSTED_BY_KEY).toString();
-}
-
-QString MastodonNotificationsDatabase::instanceUrl(const SocialPost::ConstPtr &post)
-{
- if (post.isNull()) {
- return QString();
- }
- return post->extra().value(INSTANCE_URL_KEY).toString();
-}
diff --git a/common/mastodonnotificationsdatabase.h b/common/mastodonnotificationsdatabase.h
deleted file mode 100644
index cc69095..0000000
--- a/common/mastodonnotificationsdatabase.h
+++ /dev/null
@@ -1,46 +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 MASTODONNOTIFICATIONSDATABASE_H
-#define MASTODONNOTIFICATIONSDATABASE_H
-
-#include <socialcache/abstractsocialpostcachedatabase.h>
-
-class MastodonNotificationsDatabase: public AbstractSocialPostCacheDatabase
-{
- Q_OBJECT
-public:
- MastodonNotificationsDatabase();
- ~MastodonNotificationsDatabase();
-
- void addMastodonNotification(const QString &identifier, const QString &name,
- const QString &accountName, const QString &body,
- const QDateTime &timestamp,
- const QString &icon,
- const QList<QPair<QString, SocialPostImage::ImageType> > &images,
- const QString &url, const QString &boostedBy,
- const QString &instanceUrl,
- int account);
-
- static QString accountName(const SocialPost::ConstPtr &post);
- static QString url(const SocialPost::ConstPtr &post);
- static QString boostedBy(const SocialPost::ConstPtr &post);
- static QString instanceUrl(const SocialPost::ConstPtr &post);
-};
-
-#endif // MASTODONNOTIFICATIONSDATABASE_H
diff --git a/rpm/sailfish-account-mastodon.spec b/rpm/sailfish-account-mastodon.spec
index e1afb1a..94bc073 100644
--- a/rpm/sailfish-account-mastodon.spec
+++ b/rpm/sailfish-account-mastodon.spec
@@ -112,6 +112,7 @@ fi
%{_datadir}/accounts/ui/mastodon.qml
%{_datadir}/accounts/ui/mastodon-settings.qml
%{_datadir}/accounts/ui/mastodon-update.qml
+%{_libdir}/qt5/qml/com/jolla/settings/accounts/mastodon/*
%{_datadir}/translations/settings-accounts-mastodon_eng_en.qm
%{_datadir}/themes/sailfish-default/silica/*/icons/icon-l-mastodon.png
diff --git a/settings/accounts-translations-plugin/accounts-translations-plugin.pro b/settings/accounts-translations-plugin/accounts-translations-plugin.pro
new file mode 100644
index 0000000..48b5a84
--- /dev/null
+++ b/settings/accounts-translations-plugin/accounts-translations-plugin.pro
@@ -0,0 +1,17 @@
+TEMPLATE = lib
+TARGET = mastodonaccountstranslationsplugin
+TARGET = $$qtLibraryTarget($$TARGET)
+
+MODULENAME = com/jolla/settings/accounts/mastodon
+TARGETPATH = $$[QT_INSTALL_QML]/$$MODULENAME
+
+QT += qml
+CONFIG += plugin
+
+SOURCES += plugin.cpp
+
+target.path = $$TARGETPATH
+qmldir.files = qmldir
+qmldir.path = $$TARGETPATH
+
+INSTALLS += target qmldir
diff --git a/settings/accounts-translations-plugin/plugin.cpp b/settings/accounts-translations-plugin/plugin.cpp
new file mode 100644
index 0000000..2bd504f
--- /dev/null
+++ b/settings/accounts-translations-plugin/plugin.cpp
@@ -0,0 +1,50 @@
+#include <QCoreApplication>
+#include <QLocale>
+#include <QQmlEngine>
+#include <QQmlExtensionPlugin>
+#include <QTranslator>
+#include <QtQml>
+
+class AppTranslator : public QTranslator
+{
+ Q_OBJECT
+public:
+ explicit AppTranslator(QObject *parent)
+ : QTranslator(parent)
+ {
+ qApp->installTranslator(this);
+ }
+
+ ~AppTranslator() override
+ {
+ qApp->removeTranslator(this);
+ }
+};
+
+class MastodonAccountsTranslationsPlugin : public QQmlExtensionPlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "com.jolla.settings.accounts.mastodon")
+
+public:
+ void initializeEngine(QQmlEngine *engine, const char *uri) override
+ {
+ Q_UNUSED(uri)
+
+ AppTranslator *engineeringEnglish = new AppTranslator(engine);
+ engineeringEnglish->load("settings-accounts-mastodon_eng_en", "/usr/share/translations");
+
+ AppTranslator *translator = new AppTranslator(engine);
+ translator->load(QLocale(), "settings-accounts-mastodon", "-", "/usr/share/translations");
+ }
+
+ void registerTypes(const char *uri) override
+ {
+ Q_ASSERT(QLatin1String(uri) == QLatin1String("com.jolla.settings.accounts.mastodon"));
+ qmlRegisterUncreatableType<MastodonAccountsTranslationsPlugin>(uri, 1, 0,
+ "MastodonTranslationPlugin",
+ QString());
+ }
+};
+
+#include "plugin.moc"
diff --git a/settings/accounts-translations-plugin/qmldir b/settings/accounts-translations-plugin/qmldir
new file mode 100644
index 0000000..9b7a872
--- /dev/null
+++ b/settings/accounts-translations-plugin/qmldir
@@ -0,0 +1,2 @@
+module com.jolla.settings.accounts.mastodon
+plugin mastodonaccountstranslationsplugin
diff --git a/settings/accounts/accounts.pro b/settings/accounts/accounts.pro
index 7adc97e..d451438 100644
--- a/settings/accounts/accounts.pro
+++ b/settings/accounts/accounts.pro
@@ -1,5 +1,30 @@
TEMPLATE = aux
+TS_FILE = $$OUT_PWD/settings-accounts-mastodon.ts
+EE_QM = $$OUT_PWD/settings-accounts-mastodon_eng_en.qm
+
+ts.commands += lupdate $$PWD/ui -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
+
+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
+
OTHER_FILES += \
$$PWD/providers/mastodon.provider \
$$PWD/services/mastodon-microblog.service \
@@ -24,4 +49,4 @@ ui.files += \
$$PWD/ui/mastodon-update.qml
ui.path = /usr/share/accounts/ui/
-INSTALLS += provider services ui
+INSTALLS += provider services ui ts_install engineering_english_install
diff --git a/settings/accounts/providers/mastodon.provider b/settings/accounts/providers/mastodon.provider
index 8523691..73700c4 100644
--- a/settings/accounts/providers/mastodon.provider
+++ b/settings/accounts/providers/mastodon.provider
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE provider>
<provider version="1.0" id="mastodon">
+ <translations>/usr/share/translations/settings-accounts-mastodon</translations>
<name>Mastodon</name>
<description>Mastodon social network</description>
<icon>image://theme/icon-l-mastodon</icon>
diff --git a/settings/accounts/services/mastodon-microblog.service b/settings/accounts/services/mastodon-microblog.service
index bbc5945..703b3c5 100644
--- a/settings/accounts/services/mastodon-microblog.service
+++ b/settings/accounts/services/mastodon-microblog.service
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<service id="mastodon-microblog">
<type>microblogging</type>
+ <translations>/usr/share/translations/settings-accounts-mastodon</translations>
<name>Posts</name>
<icon>image://theme/icon-l-mastodon</icon>
<provider>mastodon</provider>
diff --git a/settings/accounts/services/mastodon-sharing.service b/settings/accounts/services/mastodon-sharing.service
index 19e3e24..cbf007d 100644
--- a/settings/accounts/services/mastodon-sharing.service
+++ b/settings/accounts/services/mastodon-sharing.service
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<service id="mastodon-sharing">
<type>sharing</type>
+ <translations>/usr/share/translations/settings-accounts-mastodon</translations>
<name>Sharing</name>
<icon>image://theme/icon-l-mastodon</icon>
<provider>mastodon</provider>
diff --git a/settings/accounts/ui/MastodonSettingsDisplay.qml b/settings/accounts/ui/MastodonSettingsDisplay.qml
index a9e2c5a..9a45e45 100644
--- a/settings/accounts/ui/MastodonSettingsDisplay.qml
+++ b/settings/accounts/ui/MastodonSettingsDisplay.qml
@@ -9,6 +9,16 @@ StandardAccountSettingsDisplay {
settingsModified: true
+ function _displayNameFromApiHost(apiHost) {
+ var host = apiHost ? apiHost.toString().trim() : ""
+ host = host.replace(/^https?:\/\//i, "")
+ var pathSeparator = host.indexOf("/")
+ if (pathSeparator !== -1) {
+ host = host.substring(0, pathSeparator)
+ }
+ return host
+ }
+
onAboutToSaveAccount: {
settingsLoader.updateAllSyncProfiles()
@@ -33,6 +43,11 @@ StandardAccountSettingsDisplay {
: ""
if (credentialsUserName.length > 0 && root.account.displayName !== credentialsUserName) {
root.account.displayName = credentialsUserName
+ } else if ((!root.account.displayName || root.account.displayName.toString().trim().length === 0)) {
+ var fallback = _displayNameFromApiHost(root.account.configurationValues("")["api/Host"])
+ if (fallback.length > 0) {
+ root.account.displayName = fallback
+ }
}
var autoSync = root.account.configurationValues("")["FeedViewAutoSync"]
diff --git a/settings/accounts/ui/mastodon-settings.qml b/settings/accounts/ui/mastodon-settings.qml
index 630f51d..6b25bb3 100644
--- a/settings/accounts/ui/mastodon-settings.qml
+++ b/settings/accounts/ui/mastodon-settings.qml
@@ -13,7 +13,18 @@ AccountSettingsAgent {
if (credentialsUserName.length > 0) {
return credentialsUserName
}
- return account.displayName
+ var displayName = account.displayName ? account.displayName.toString().trim() : ""
+ if (displayName.length > 0) {
+ return displayName
+ }
+ var apiHost = account.configurationValues("")["api/Host"]
+ var host = apiHost ? apiHost.toString().trim() : ""
+ host = host.replace(/^https?:\/\//i, "")
+ var pathSeparator = host.indexOf("/")
+ if (pathSeparator !== -1) {
+ host = host.substring(0, pathSeparator)
+ }
+ return host
}
Account {
diff --git a/settings/accounts/ui/mastodon-update.qml b/settings/accounts/ui/mastodon-update.qml
index 3a6c25c..d577b05 100644
--- a/settings/accounts/ui/mastodon-update.qml
+++ b/settings/accounts/ui/mastodon-update.qml
@@ -2,6 +2,7 @@ import QtQuick 2.0
import Sailfish.Silica 1.0
import Sailfish.Accounts 1.0
import com.jolla.settings.accounts 1.0
+import com.jolla.settings.accounts.mastodon 1.0
AccountCredentialsAgent {
id: root
diff --git a/settings/accounts/ui/mastodon.qml b/settings/accounts/ui/mastodon.qml
index 758ed9b..3359ce8 100644
--- a/settings/accounts/ui/mastodon.qml
+++ b/settings/accounts/ui/mastodon.qml
@@ -2,6 +2,7 @@ import QtQuick 2.0
import Sailfish.Silica 1.0
import Sailfish.Accounts 1.0
import com.jolla.settings.accounts 1.0
+import com.jolla.settings.accounts.mastodon 1.0
AccountCreationAgent {
id: root
@@ -43,6 +44,14 @@ AccountCreationAgent {
return oauthHost(apiHost)
}
+ function _fallbackDisplayName(apiHost) {
+ var display = _displayName(apiHost)
+ if (display.length > 0) {
+ return display
+ }
+ return _displayName(defaultApiHost)
+ }
+
function _showRegistrationError(message, busyPage) {
_registering = false
accountCreationError(message)
@@ -290,10 +299,12 @@ AccountCreationAgent {
? newAccount.defaultCredentialsUserName.toString().trim()
: ""
var providerDisplayName = root._displayName(apiHost)
- if (credentialsUserName.length > 0) {
- newAccount.displayName = credentialsUserName
- } else if (providerDisplayName.length > 0) {
- newAccount.displayName = providerDisplayName
+ var accountDescription = credentialsUserName.length > 0
+ ? credentialsUserName
+ : root._fallbackDisplayName(apiHost)
+ if (accountDescription.length > 0) {
+ newAccount.displayName = accountDescription
+ newAccount.setConfigurationValue("", "description", accountDescription)
}
newAccount.setConfigurationValue("", "api/Host", apiHost)
diff --git a/settings/settings.pro b/settings/settings.pro
index 5f2c9b0..84f2a75 100644
--- a/settings/settings.pro
+++ b/settings/settings.pro
@@ -1,2 +1,4 @@
TEMPLATE = subdirs
-SUBDIRS += accounts
+SUBDIRS += \
+ accounts \
+ accounts-translations-plugin
diff --git a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro
index 6de949e..1ab7c6c 100644
--- a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro
+++ b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro
@@ -3,6 +3,7 @@ TARGET = $$qtLibraryTarget(mastodonshareplugin)
CONFIG += plugin
DEPENDPATH += .
INCLUDEPATH += ..
+INCLUDEPATH += ../../common
CONFIG += link_pkgconfig
PKGCONFIG += nemotransferengine-qt5 accounts-qt5 sailfishaccounts libsignon-qt5
diff --git a/transferengine-plugins/mastodonshareservicestatus.cpp b/transferengine-plugins/mastodonshareservicestatus.cpp
index 8c70e12..6ff4cb8 100644
--- a/transferengine-plugins/mastodonshareservicestatus.cpp
+++ b/transferengine-plugins/mastodonshareservicestatus.cpp
@@ -1,4 +1,5 @@
#include "mastodonshareservicestatus.h"
+#include "mastodonauthutils.h"
#include <Accounts/Account>
#include <Accounts/AccountService>
@@ -22,33 +23,6 @@ MastodonShareServiceStatus::MastodonShareServiceStatus(QObject *parent)
{
}
-QString MastodonShareServiceStatus::normalizeApiHost(const QString &rawHost)
-{
- QString host = rawHost.trimmed();
- if (host.isEmpty()) {
- host = QStringLiteral("https://mastodon.social");
- }
-
- if (!host.startsWith(QLatin1String("https://"))
- && !host.startsWith(QLatin1String("http://"))) {
- host.prepend(QStringLiteral("https://"));
- }
-
- QUrl url(host);
- if (!url.isValid() || url.host().isEmpty()) {
- return QStringLiteral("https://mastodon.social");
- }
-
- QString normalized = QString::fromLatin1(url.toEncoded(QUrl::RemovePath
- | QUrl::RemoveQuery
- | QUrl::RemoveFragment));
- if (normalized.endsWith(QLatin1Char('/'))) {
- normalized.chop(1);
- }
-
- return normalized;
-}
-
void MastodonShareServiceStatus::signIn(int accountId)
{
Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this);
@@ -93,66 +67,7 @@ void MastodonShareServiceStatus::signIn(int accountId)
QVariantMap signonSessionData = accountService.authData().parameters();
- QString configuredHost = account->value(QStringLiteral("auth/oauth2/web_server/Host")).toString().trimmed();
- if (configuredHost.isEmpty()) {
- configuredHost = normalizeApiHost(account->value(QStringLiteral("api/Host")).toString());
- }
-
- if (configuredHost.startsWith(QLatin1String("https://"))) {
- configuredHost.remove(0, 8);
- } else if (configuredHost.startsWith(QLatin1String("http://"))) {
- configuredHost.remove(0, 7);
- }
-
- const int separator = configuredHost.indexOf(QLatin1Char('/'));
- if (separator > -1) {
- configuredHost.truncate(separator);
- }
- while (configuredHost.endsWith(QLatin1Char('/'))) {
- configuredHost.chop(1);
- }
-
- if (configuredHost.isEmpty()) {
- configuredHost = QStringLiteral("mastodon.social");
- }
- signonSessionData.insert(QStringLiteral("Host"), configuredHost);
-
- const QString authPath = account->value(QStringLiteral("auth/oauth2/web_server/AuthPath")).toString().trimmed();
- if (!authPath.isEmpty()) {
- signonSessionData.insert(QStringLiteral("AuthPath"), authPath);
- }
-
- const QString tokenPath = account->value(QStringLiteral("auth/oauth2/web_server/TokenPath")).toString().trimmed();
- if (!tokenPath.isEmpty()) {
- signonSessionData.insert(QStringLiteral("TokenPath"), tokenPath);
- }
-
- const QString responseType = account->value(QStringLiteral("auth/oauth2/web_server/ResponseType")).toString().trimmed();
- if (!responseType.isEmpty()) {
- signonSessionData.insert(QStringLiteral("ResponseType"), responseType);
- }
-
- const QString redirectUri = account->value(QStringLiteral("auth/oauth2/web_server/RedirectUri")).toString().trimmed();
- if (!redirectUri.isEmpty()) {
- signonSessionData.insert(QStringLiteral("RedirectUri"), redirectUri);
- }
-
- const QVariant scopeValue = account->value(QStringLiteral("auth/oauth2/web_server/Scope"));
- if (scopeValue.isValid()) {
- signonSessionData.insert(QStringLiteral("Scope"), scopeValue);
- }
-
- const QString clientId = account->value(QStringLiteral("auth/oauth2/web_server/ClientId")).toString().trimmed();
- if (!clientId.isEmpty()) {
- signonSessionData.insert(QStringLiteral("ClientId"), clientId);
- }
-
- const QString clientSecret = account->value(QStringLiteral("auth/oauth2/web_server/ClientSecret")).toString().trimmed();
- if (!clientSecret.isEmpty()) {
- signonSessionData.insert(QStringLiteral("ClientSecret"), clientSecret);
- }
-
- signonSessionData.insert(QStringLiteral("UiPolicy"), SignOn::NoUserInteractionPolicy);
+ MastodonAuthUtils::addSignOnSessionParameters(account, &signonSessionData);
connect(session, SIGNAL(response(SignOn::SessionData)),
this, SLOT(signOnResponse(SignOn::SessionData)),
@@ -168,20 +83,14 @@ void MastodonShareServiceStatus::signIn(int accountId)
void MastodonShareServiceStatus::signOnResponse(const SignOn::SessionData &responseData)
{
- QVariantMap data;
- Q_FOREACH (const QString &key, responseData.propertyNames()) {
- data.insert(key, responseData.getProperty(key));
- }
+ const QVariantMap data = MastodonAuthUtils::responseDataToMap(responseData);
SignOn::AuthSession *session = qobject_cast<SignOn::AuthSession *>(sender());
Accounts::Account *account = session->property("account").value<Accounts::Account *>();
SignOn::Identity *identity = session->property("identity").value<SignOn::Identity *>();
const int accountId = account ? account->id() : 0;
- QString accessToken = data.value(QLatin1String("AccessToken")).toString().trimmed();
- if (accessToken.isEmpty()) {
- accessToken = data.value(QLatin1String("access_token")).toString().trimmed();
- }
+ QString accessToken = MastodonAuthUtils::accessToken(data);
if (accountId > 0 && m_accountIdToDetailsIdx.contains(accountId)) {
AccountDetails &accountDetails(m_accountDetails[m_accountIdToDetailsIdx[accountId]]);
@@ -308,7 +217,7 @@ void MastodonShareServiceStatus::queryStatus(QueryStatusMode mode)
if (!m_accountIdToDetailsIdx.contains(id)) {
AccountDetails details;
details.accountId = id;
- details.apiHost = normalizeApiHost(acc->value(QStringLiteral("api/Host")).toString());
+ details.apiHost = MastodonAuthUtils::normalizeApiHost(acc->value(QStringLiteral("api/Host")).toString());
QUrl apiUrl(details.apiHost);
details.providerName = apiUrl.host();
diff --git a/transferengine-plugins/mastodonshareservicestatus.h b/transferengine-plugins/mastodonshareservicestatus.h
index 571efd5..d7968f5 100644
--- a/transferengine-plugins/mastodonshareservicestatus.h
+++ b/transferengine-plugins/mastodonshareservicestatus.h
@@ -60,7 +60,6 @@ private:
Error
};
- static QString normalizeApiHost(const QString &rawHost);
void setAccountDetailsState(int accountId, AccountDetailsState state);
void signIn(int accountId);
diff --git a/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro b/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro
index b37bf17..8782156 100644
--- a/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro
+++ b/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro
@@ -3,6 +3,7 @@ TARGET = $$qtLibraryTarget(mastodontransferplugin)
CONFIG += plugin
DEPENDPATH += .
INCLUDEPATH += ..
+INCLUDEPATH += ../../common
QT += network