diff options
| author | Andrew Branson <andrew.branson@jolla.com> | 2026-02-10 23:16:37 +0100 |
|---|---|---|
| committer | Andrew Branson <andrew.branson@jolla.com> | 2026-02-10 23:16:37 +0100 |
| commit | 69628390815254297bbd8c95436f6780fa846fae (patch) | |
| tree | df6114043e489bf5d767ac39f0d20636e12cf3a2 | |
| parent | ff1c2efe40bf53c146b4a2e3b5046ae8ecb32264 (diff) | |
Translations fixed and other stuff
29 files changed, 462 insertions, 571 deletions
@@ -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 @@ -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 ¬ificationId) +{ + 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 ¬ificationValue, 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 ¬ificationId, 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 ¬ificationData) { - 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 ¬ificationId) { - 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 ¬ificationId) { 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 ¬ificationData); + Notification *createNotification(int accountId, const QString ¬ificationId); + Notification *findNotification(int accountId, const QString ¬ificationId); + void closeAccountNotifications(int accountId, const QSet<QString> &keepNotificationIds = QSet<QString>()); + static QString notificationObjectKey(int accountId, const QString ¬ificationId); 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 ×tamp, - 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 ×tamp, - 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 |
