diff options
| author | Andrew Branson <andrew.branson@jolla.com> | 2026-03-22 20:18:39 +0100 |
|---|---|---|
| committer | Andrew Branson <andrew.branson@jolla.com> | 2026-03-22 20:25:03 +0100 |
| commit | 218a05f6ac67f260288ff70344f0f004c7b48c7b (patch) | |
| tree | d213c375b94b467aa439aabf9810554087b29ec0 | |
| parent | 2b1a3046832074e47ad2ad703cd518526b9fb459 (diff) | |
Use shared buteo-common and split notifications service on mainmain
Keep main as the branch that builds against the newer shared social sync modules, while master stays self-contained.
- drop the bundled buteo-common sources and stop building or packaging libmastodonbuteocommon
- link the Mastodon sync plugins against buteosocialcommon and add the matching build/runtime package requirements
- install a dedicated mastodon-notifications account service and wire account creation/packaging around the separate notifications profile
- move the posts/events-view side over to the newer shared-helper style used with the updated socialcache stack
- clean up qmake project wiring for the shared-module layout and refresh the branch README to describe the split service model
- keep the notification schedule at the master value instead of carrying the temporary timing tweak
26 files changed, 229 insertions, 1708 deletions
@@ -17,7 +17,8 @@ Sailfish OS account integration for Mastodon. - 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-microblog`: sync service for posts. + - `mastodon-notifications`: sync service for notifications. - `mastodon-sharing`: Transfer Engine sharing service. ### `buteo-plugins/` @@ -80,10 +81,4 @@ Required SDK-provided dependencies include (not exhaustive): - `nemotransferengine-qt5` - related Qt/account stack packages listed in `rpm/sailfish-account-mastodon.spec` -## Typical Build Flow (Inside Sailfish SDK) - -1. Enter Sailfish SDK shell/target. -2. Build from repository root (`qmake` / `make`). -3. Build RPM package(s) from `rpm/sailfish-account-mastodon.spec`. - Outside Sailfish SDK, only static validation (wiring, paths, spec consistency) should be considered reliable. diff --git a/buteo-plugins/buteo-common/buteo-common.pri b/buteo-plugins/buteo-common/buteo-common.pri deleted file mode 100644 index 83452ac..0000000 --- a/buteo-plugins/buteo-common/buteo-common.pri +++ /dev/null @@ -1,13 +0,0 @@ -# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -# -# SPDX-License-Identifier: BSD-3-Clause - -INCLUDEPATH += $$PWD -DEPENDPATH += . - -QT += dbus - -CONFIG += link_pkgconfig -PKGCONFIG += accounts-qt5 buteosyncfw5 socialcache libsignon-qt5 libsailfishkeyprovider - -LIBS += -L$$PWD -lmastodonbuteocommon diff --git a/buteo-plugins/buteo-common/buteo-common.pro b/buteo-plugins/buteo-common/buteo-common.pro deleted file mode 100644 index c0b84a9..0000000 --- a/buteo-plugins/buteo-common/buteo-common.pro +++ /dev/null @@ -1,33 +0,0 @@ -# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -# -# SPDX-License-Identifier: BSD-3-Clause - -TEMPLATE = lib - -TARGET = mastodonbuteocommon -TARGET = $$qtLibraryTarget($$TARGET) - -QT -= gui -QT += network dbus -CONFIG += link_pkgconfig -PKGCONFIG += accounts-qt5 buteosyncfw5 socialcache - -INCLUDEPATH += $$PWD - -HEADERS += \ - $$PWD/buteosyncfw_p.h \ - $$PWD/socialdbuteoplugin.h \ - $$PWD/socialnetworksyncadaptor.h \ - $$PWD/socialdnetworkaccessmanager_p.h \ - $$PWD/trace.h - -SOURCES += \ - $$PWD/socialdbuteoplugin.cpp \ - $$PWD/socialnetworksyncadaptor.cpp \ - $$PWD/socialdnetworkaccessmanager_p.cpp \ - $$PWD/trace.cpp - -TARGETPATH = $$[QT_INSTALL_LIBS] -target.path = $$TARGETPATH - -INSTALLS += target diff --git a/buteo-plugins/buteo-common/buteosyncfw_p.h b/buteo-plugins/buteo-common/buteosyncfw_p.h deleted file mode 100644 index 9fd6a77..0000000 --- a/buteo-plugins/buteo-common/buteosyncfw_p.h +++ /dev/null @@ -1,39 +0,0 @@ -/**************************************************************************** - ** - ** Copyright (C) 2013-2026 Jolla Ltd. - ** Contact: Chris Adams <chris.adams@jollamobile.com> - ** - ** This program/library is free software; you can redistribute it and/or - ** modify it under the terms of the GNU Lesser General Public License - ** version 2.1 as published by the Free Software Foundation. - ** - ** This program/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 program/library; if not, write to the Free - ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - ** 02110-1301 USA - ** - ****************************************************************************/ - -#ifndef SOCIALD_BUTEOSYNCFW_P_H -#define SOCIALD_BUTEOSYNCFW_P_H - -#include <SyncCommonDefs.h> -#include <SyncPluginBase.h> -#include <ProfileManager.h> -#include <ClientPlugin.h> -#include <SyncResults.h> -#include <ProfileEngineDefs.h> -#include <SyncProfile.h> -#include <Profile.h> -#include <PluginCbInterface.h> - -#ifndef SOCIALD_TEST_DEFINE -#define PRIVILEGED_DATA_DIR QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + QLatin1String("/.local/share/system/privileged") -#endif - -#endif // SOCIALD_BUTEOSYNCFW_P_H diff --git a/buteo-plugins/buteo-common/socialdbuteoplugin.cpp b/buteo-plugins/buteo-common/socialdbuteoplugin.cpp deleted file mode 100644 index 0eb5f91..0000000 --- a/buteo-plugins/buteo-common/socialdbuteoplugin.cpp +++ /dev/null @@ -1,355 +0,0 @@ -/**************************************************************************** - ** - ** Copyright (C) 2013-2026 Jolla Ltd. - ** Contact: Chris Adams <chris.adams@jolla.com> - ** - ** This program/library is free software; you can redistribute it and/or - ** modify it under the terms of the GNU Lesser General Public License - ** version 2.1 as published by the Free Software Foundation. - ** - ** This program/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 program/library; if not, write to the Free - ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - ** 02110-1301 USA - ** - ****************************************************************************/ - -#include "socialdbuteoplugin.h" -#include "socialnetworksyncadaptor.h" -#include "trace.h" - -#include <QCoreApplication> -#include <QTranslator> - -#include <QDBusMessage> -#include <QDBusConnection> -#include <QDBusPendingCall> - -#include "buteosyncfw_p.h" - -#include <Accounts/Manager> -#include <Accounts/Account> -#include <Accounts/Service> - -namespace { - const QString SyncProfileTemplatesKey = QStringLiteral("sync_profile_templates"); - - QString SyncProfileIdKey(const QString &templateProfileName) - { - return QStringLiteral("%1/%2").arg(templateProfileName).arg(Buteo::KEY_PROFILE_ID); - } - - QString createProfile(Buteo::ProfileManager *profileManager, - const QString &templateProfileName, - Accounts::Account *account, - const Accounts::Service &srv, - bool enableProfile, - const QVariantMap &properties) - { - if (!account || !srv.isValid()) { - qWarning() << "Invalid account or service"; - return QString(); - } - if (templateProfileName.isEmpty()) { - qWarning() << "Invalid templateProfileName"; - return QString(); - } - - Accounts::Service prevService = account->selectedService(); - account->selectService(srv); - - Buteo::SyncProfile *templateProfile = profileManager->syncProfile(templateProfileName); - if (!templateProfile) { - account->selectService(prevService); - qWarning() << "Unable to load template profile:" << templateProfileName; - return QString(); - } - - Buteo::SyncProfile *profile = templateProfile->clone(); - if (!profile) { - delete templateProfile; - account->selectService(prevService); - qWarning() << "unable to clone template profile:" << templateProfileName; - return QString(); - } - - QString accountIdStr = QString::number(account->id()); - profile->setName(templateProfileName + "-" + accountIdStr); - profile->setKey(Buteo::KEY_DISPLAY_NAME, templateProfileName + "-" + account->displayName().toHtmlEscaped()); - profile->setKey(Buteo::KEY_ACCOUNT_ID, accountIdStr); - profile->setBoolKey(Buteo::KEY_USE_ACCOUNTS, true); - profile->setEnabled(enableProfile); - - // enable the profile schedule - Buteo::SyncSchedule schedule = profile->syncSchedule(); - schedule.setScheduleEnabled(true); - profile->setSyncSchedule(schedule); - - // set custom properties; note this may override any properties already set - Q_FOREACH (const QString &key, properties.keys()) { - profile->setKey(key, properties[key].toString()); - } - - QString profileId = profileManager->updateProfile(*profile); - if (profileId.isEmpty()) { - qWarning() << "Unable to save sync profile" << templateProfile->name(); - } else { - account->setValue(SyncProfileIdKey(templateProfile->name()), profile->name()); - } - - account->selectService(prevService); - delete profile; - delete templateProfile; - - return profileId; - } -} - -SocialdButeoPlugin::SocialdButeoPlugin(const QString& pluginName, - const Buteo::SyncProfile& profile, - Buteo::PluginCbInterface *callbackInterface, - const QString &socialServiceName, - const QString &dataTypeName) - : ClientPlugin(pluginName, profile, callbackInterface) - , m_socialNetworkSyncAdaptor(nullptr) - , m_socialServiceName(socialServiceName) - , m_dataTypeName(dataTypeName) - , m_profileAccountId(0) -{ -} - -SocialdButeoPlugin::~SocialdButeoPlugin() -{ -} - -bool SocialdButeoPlugin::init() -{ - m_profileAccountId = profile().key(Buteo::KEY_ACCOUNT_ID).toInt(); - m_socialNetworkSyncAdaptor = createSocialNetworkSyncAdaptor(); - if (m_socialNetworkSyncAdaptor) { - connect(m_socialNetworkSyncAdaptor, &SocialNetworkSyncAdaptor::statusChanged, - this, &SocialdButeoPlugin::syncStatusChanged); - return true; - } - - return false; -} - -bool SocialdButeoPlugin::uninit() -{ - delete m_socialNetworkSyncAdaptor; - m_socialNetworkSyncAdaptor = nullptr; - return true; -} - -bool SocialdButeoPlugin::startSync() -{ - if (!m_socialNetworkSyncAdaptor || !m_socialNetworkSyncAdaptor->enabled()) { - qCDebug(lcSocialPlugin) << "no enabled" << m_socialServiceName << "sync adaptor for" << m_dataTypeName; - return false; - } - - // if the profile being triggered is the template profile, then we - // need to ensure that the appropriate per-account profiles exist. - if (m_profileAccountId == 0) { - QList<Buteo::SyncProfile*> perAccountProfiles = ensurePerAccountSyncProfilesExist(); - m_socialNetworkSyncAdaptor->setAccountSyncProfile(nullptr); - - // we need to trigger sync with each profile separately, - // or (due to scheduling/etc) another plugin instance might - // be created to sync that profile at the same time, and - // we don't handle concurrency. - foreach (Buteo::SyncProfile *perAccountProfile, perAccountProfiles) { - QDBusMessage message = QDBusMessage::createMethodCall( - "com.meego.msyncd", "/synchronizer", "com.meego.msyncd", "startSync"); - message.setArguments(QVariantList() << perAccountProfile->name()); - QDBusConnection::sessionBus().asyncCall(message); - } - qDeleteAll(perAccountProfiles); - - // This template profile only dispatches account-specific sync profiles. - // Those child sync runs report their own individual results. - updateResults(Buteo::SyncResults(QDateTime::currentDateTime(), - Buteo::SyncResults::SYNC_RESULT_SUCCESS, - Buteo::SyncResults::NO_ERROR)); - emit success(getProfileName(), QString("%1 update dispatched").arg(getProfileName())); - return true; - } - - Buteo::SyncProfile *accountSyncProfile = profile().clone(); - if (accountSyncProfile - && m_dataTypeName == SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Notifications)) { - accountSyncProfile->setEnabled(true); - Buteo::SyncSchedule schedule = accountSyncProfile->syncSchedule(); - schedule.setScheduleEnabled(true); - accountSyncProfile->setSyncSchedule(schedule); - m_profileManager.updateProfile(*accountSyncProfile); - } - m_socialNetworkSyncAdaptor->setAccountSyncProfile(accountSyncProfile); - - // Now perform sync for the account-specific profile. - if (m_socialNetworkSyncAdaptor->status() == SocialNetworkSyncAdaptor::Inactive) { - qCDebug(lcSocialPlugin) << "performing sync of" << m_dataTypeName << "from" << m_socialServiceName - << "for account" << m_profileAccountId; - m_socialNetworkSyncAdaptor->sync(m_dataTypeName, m_profileAccountId); - return true; - } else { - qCDebug(lcSocialPlugin) << m_socialServiceName << "sync adaptor for" << m_dataTypeName - << "is still busy with last sync of account" << m_profileAccountId; - } - return false; -} - -void SocialdButeoPlugin::abortSync(Sync::SyncStatus status) -{ - // note: it seems buteo automatically calls abortSync on network connectivity loss... - qCInfo(lcSocialPlugin) << "aborting sync with status:" << status; - m_socialNetworkSyncAdaptor->abortSync(status); -} - -bool SocialdButeoPlugin::cleanUp() -{ - m_profileAccountId = profile().key(Buteo::KEY_ACCOUNT_ID).toInt(); - if (!m_socialNetworkSyncAdaptor) { - // might have already been initialized by the OOP framework via init(). - m_socialNetworkSyncAdaptor = createSocialNetworkSyncAdaptor(); - } - - if (m_socialNetworkSyncAdaptor && m_profileAccountId > 0) { - m_socialNetworkSyncAdaptor->purgeDataForOldAccount(m_profileAccountId, - SocialNetworkSyncAdaptor::CleanUpPurge); - } - - return true; -} - -Buteo::SyncResults SocialdButeoPlugin::getSyncResults() const -{ - return m_syncResults; -} - -void SocialdButeoPlugin::connectivityStateChanged(Sync::ConnectivityType type, bool state) -{ - // See TransportTracker.cpp:149 for example - // Sync::CONNECTIVITY_INTERNET, true|false - qCInfo(lcSocialPlugin) << "notified of connectivity change:" << type << state; - if (type == Sync::CONNECTIVITY_INTERNET && state == false) { - // we lost connectivity during sync. - abortSync(Sync::SYNC_CONNECTION_ERROR); - } -} - -void SocialdButeoPlugin::syncStatusChanged() -{ - if (m_socialNetworkSyncAdaptor) { - SocialNetworkSyncAdaptor::Status syncStatus = m_socialNetworkSyncAdaptor->status(); - // Busy change comes when sync starts -> let's ignore that. - if (syncStatus == SocialNetworkSyncAdaptor::Inactive) { - updateResults(Buteo::SyncResults(QDateTime::currentDateTime(), - Buteo::SyncResults::SYNC_RESULT_SUCCESS, - Buteo::SyncResults::NO_ERROR)); - emit success(getProfileName(), QString("%1 update succeeded").arg(getProfileName())); - } else if (syncStatus != SocialNetworkSyncAdaptor::Busy) { - updateResults(Buteo::SyncResults(QDateTime::currentDateTime(), - Buteo::SyncResults::SYNC_RESULT_FAILED, - Buteo::SyncResults::ABORTED)); - emit error(getProfileName(), QString("%1 update failed").arg(getProfileName()), - Buteo::SyncResults::ABORTED); - } - } else { - updateResults(Buteo::SyncResults(QDateTime::currentDateTime(), - Buteo::SyncResults::SYNC_RESULT_FAILED, - Buteo::SyncResults::ABORTED)); - emit error(getProfileName(), QString("%1 update failed").arg(getProfileName()), Buteo::SyncResults::ABORTED); - } -} - -void SocialdButeoPlugin::updateResults(const Buteo::SyncResults &results) -{ - m_syncResults = results; - m_syncResults.setScheduled(true); -} - -// This function is called when the non-per-account profile is triggered. -// The implementation does: -// - get all profiles from the ProfileManager -// - get all accounts from the AccountManager -// - build a mapping of profile -> account for the current data type. (should be one-to-one for the datatype). -// - any account which doesn't have a profile, print an error. -// - check the enabled status of the account -> ensure that the enabled status is reflected in the profile. -// It then returns a list of the appropriate (per account for this data-type) sync profiles. -// The caller takes ownership of the list. -QList<Buteo::SyncProfile*> SocialdButeoPlugin::ensurePerAccountSyncProfilesExist() -{ - Accounts::Manager am; - Accounts::AccountIdList accountIds = am.accountList(); - QList<Buteo::SyncProfile*> syncProfiles = m_profileManager.allSyncProfiles(); - QMap<Accounts::Account*, Buteo::SyncProfile*> perAccountProfiles; - - Accounts::Service dataTypeSyncService = am.service(m_socialNetworkSyncAdaptor->syncServiceName()); - if (!dataTypeSyncService.isValid()) { - qWarning() << Q_FUNC_INFO << "Invalid data type sync service name specified:" - << m_socialNetworkSyncAdaptor->syncServiceName(); - return QList<Buteo::SyncProfile*>(); - } - - for (int i = 0; i < accountIds.size(); ++i) { - Accounts::Account *currAccount = Accounts::Account::fromId(&am, accountIds.at(i), this); - if (!currAccount || currAccount->id() == 0 - || m_socialNetworkSyncAdaptor->syncServiceName().split('-').first() != currAccount->providerName()) { - // we only generate per-account sync profiles for accounts which - // are provided by the provider which provides our sync service. - continue; - } - - // for the current account, find the associated sync profile. - bool foundProfile = false; - for (int j = 0; j < syncProfiles.size(); ++j) { - if (syncProfiles[j]->key(Buteo::KEY_ACCOUNT_ID).toInt() == QString::number(currAccount->id()).toInt() - && syncProfiles[j]->clientProfile() != NULL - && syncProfiles[j]->clientProfile()->name() == profile().clientProfile()->name()) { - // we have found the sync profile for this datatype for this account. - foundProfile = true; - perAccountProfiles.insert(currAccount, syncProfiles.takeAt(j)); - break; - } - } - - if (!foundProfile) { - // it should have been generated for the account when the account was added. - qCInfo(lcSocialPlugin) << "no per-account" << profile().name() - << "sync profile exists for account:" << currAccount->id(); - - // create the per-account profile... we shouldn't need to do this... - QString profileName = createProfile(&m_profileManager, profile().name(), currAccount, dataTypeSyncService, true, QVariantMap()); - Buteo::SyncProfile *newProfile = m_profileManager.syncProfile(profileName); - if (!newProfile) { - qCWarning(lcSocialPlugin) << "unable to create per-account" << profile().name() - << "sync profile for account:" << currAccount->id(); - } else { - // enable the sync schedule for the profile. - Buteo::SyncSchedule schedule = newProfile->syncSchedule(); - schedule.setScheduleEnabled(true); - newProfile->setSyncSchedule(schedule); - m_profileManager.updateProfile(*newProfile); - // and return the profile in the map. - perAccountProfiles.insert(currAccount, newProfile); - } - } - } - - // Every account now has the appropriate sync profile. - qDeleteAll(syncProfiles); // these are for the wrong data type, ignore them. - QList<Buteo::SyncProfile *> retn; - foreach (Accounts::Account *acc, perAccountProfiles.keys()) { - retn.append(perAccountProfiles[acc]); - acc->deleteLater(); - } - - return retn; -} diff --git a/buteo-plugins/buteo-common/socialdbuteoplugin.h b/buteo-plugins/buteo-common/socialdbuteoplugin.h deleted file mode 100644 index 2988d27..0000000 --- a/buteo-plugins/buteo-common/socialdbuteoplugin.h +++ /dev/null @@ -1,75 +0,0 @@ -/**************************************************************************** - ** - ** Copyright (C) 2013-2026 Jolla Ltd. - ** Contact: Raine Makelainen <raine.makelainen@jollamobile.com> - ** - ** This program/library is free software; you can redistribute it and/or - ** modify it under the terms of the GNU Lesser General Public License - ** version 2.1 as published by the Free Software Foundation. - ** - ** This program/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 program/library; if not, write to the Free - ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - ** 02110-1301 USA - ** - ****************************************************************************/ - -#ifndef SOCIALDBUTEOPLUGIN_H -#define SOCIALDBUTEOPLUGIN_H - -#include <QtCore/qglobal.h> -#include "buteosyncfw_p.h" - -/* - Datatype-specific implementations of this class - allow per-account sync profiles for that data type. -*/ - -class SocialNetworkSyncAdaptor; -class Q_DECL_EXPORT SocialdButeoPlugin : public Buteo::ClientPlugin -{ - Q_OBJECT - -protected: - virtual SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor() = 0; - -public: - SocialdButeoPlugin(const QString& pluginName, - const Buteo::SyncProfile& profile, - Buteo::PluginCbInterface *cbInterface, - const QString &socialServiceName, - const QString &dataTypeName); - virtual ~SocialdButeoPlugin(); - - bool init() override; - bool uninit() override; - bool startSync() override; - void abortSync(Sync::SyncStatus status = Sync::SYNC_ABORTED) override; - Buteo::SyncResults getSyncResults() const override; - bool cleanUp() override; - -public Q_SLOTS: - void connectivityStateChanged(Sync::ConnectivityType type, bool state) override; - -private Q_SLOTS: - void syncStatusChanged(); - -protected: - QList<Buteo::SyncProfile*> ensurePerAccountSyncProfilesExist(); - -private: - void updateResults(const Buteo::SyncResults &results); - Buteo::SyncResults m_syncResults; - Buteo::ProfileManager m_profileManager; - SocialNetworkSyncAdaptor *m_socialNetworkSyncAdaptor; - QString m_socialServiceName; - QString m_dataTypeName; - int m_profileAccountId; -}; - -#endif // SOCIALDBUTEOPLUGIN_H diff --git a/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.cpp b/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.cpp deleted file mode 100644 index 37b4687..0000000 --- a/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/**************************************************************************** - ** - ** Copyright (C) 2013-2026 Jolla Ltd. - ** Contact: Chris Adams <chris.adams@jollamobile.com> - ** - ** This program/library is free software; you can redistribute it and/or - ** modify it under the terms of the GNU Lesser General Public License - ** version 2.1 as published by the Free Software Foundation. - ** - ** This program/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 program/library; if not, write to the Free - ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - ** 02110-1301 USA - ** - ****************************************************************************/ - -#include "socialdnetworkaccessmanager_p.h" - -/* The default implementation is just a normal QNetworkAccessManager */ - -SocialdNetworkAccessManager::SocialdNetworkAccessManager(QObject *parent) - : QNetworkAccessManager(parent) -{ -} - -QNetworkReply *SocialdNetworkAccessManager::createRequest( - QNetworkAccessManager::Operation op, - const QNetworkRequest &req, - QIODevice *outgoingData) -{ - return QNetworkAccessManager::createRequest(op, req, outgoingData); -} diff --git a/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.h b/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.h deleted file mode 100644 index 7adde92..0000000 --- a/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.h +++ /dev/null @@ -1,40 +0,0 @@ -/**************************************************************************** - ** - ** Copyright (C) 2013-2026 Jolla Ltd. - ** Contact: Chris Adams <chris.adams@jollamobile.com> - ** - ** This program/library is free software; you can redistribute it and/or - ** modify it under the terms of the GNU Lesser General Public License - ** version 2.1 as published by the Free Software Foundation. - ** - ** This program/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 program/library; if not, write to the Free - ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - ** 02110-1301 USA - ** - ****************************************************************************/ - -#ifndef SOCIALD_QNAMFACTORY_P_H -#define SOCIALD_QNAMFACTORY_P_H - -#include <QNetworkAccessManager> - -class SocialdNetworkAccessManager : public QNetworkAccessManager -{ - Q_OBJECT - -public: - SocialdNetworkAccessManager(QObject *parent = 0); - -protected: - QNetworkReply *createRequest(QNetworkAccessManager::Operation op, - const QNetworkRequest &req, - QIODevice *outgoingData = 0) override; -}; - -#endif diff --git a/buteo-plugins/buteo-common/socialnetworksyncadaptor.cpp b/buteo-plugins/buteo-common/socialnetworksyncadaptor.cpp deleted file mode 100644 index 42e6d95..0000000 --- a/buteo-plugins/buteo-common/socialnetworksyncadaptor.cpp +++ /dev/null @@ -1,470 +0,0 @@ -/**************************************************************************** - ** - ** Copyright (C) 2013-2026 Jolla Ltd. - ** Contact: Chris Adams <chris.adams@jollamobile.com> - ** - ** This program/library is free software; you can redistribute it and/or - ** modify it under the terms of the GNU Lesser General Public License - ** version 2.1 as published by the Free Software Foundation. - ** - ** This program/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 program/library; if not, write to the Free - ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - ** 02110-1301 USA - ** - ****************************************************************************/ - -#include "socialnetworksyncadaptor.h" -#include "socialdnetworkaccessmanager_p.h" -#include "trace.h" - -#include <QtCore/QJsonDocument> -#include <QtCore/QTimer> -#include <QtSql/QSqlDatabase> -#include <QtSql/QSqlQuery> -#include <QtSql/QSqlError> -#include <QtSql/QSqlRecord> - -#include <QtNetwork/QNetworkAccessManager> -#include <QtNetwork/QNetworkReply> - -#include "buteosyncfw_p.h" - -// libaccounts-qt5 -#include <Accounts/Manager> -#include <Accounts/Account> -#include <Accounts/Service> - -// libsocialcache -#include <socialcache/socialimagesdatabase.h> -#include <socialcache/socialnetworksyncdatabase.h> - -namespace { - QStringList validDataTypesInitialiser() - { - return QStringList() - << QStringLiteral("Contacts") - << QStringLiteral("Calendars") - << QStringLiteral("Notifications") - << QStringLiteral("Images") - << QStringLiteral("Videos") - << QStringLiteral("Posts") - << QStringLiteral("Messages") - << QStringLiteral("Emails") - << QStringLiteral("Signon") - << QStringLiteral("Backup") - << QStringLiteral("BackupQuery") - << QStringLiteral("BackupRestore"); - } -} - -SocialNetworkSyncAdaptor::SocialNetworkSyncAdaptor(const QString &serviceName, - SocialNetworkSyncAdaptor::DataType dataType, - QNetworkAccessManager *qnam, - QObject *parent) - : QObject(parent) - , m_dataType(dataType) - , m_accountManager(new Accounts::Manager(this)) - , m_networkAccessManager(qnam != 0 ? qnam : new SocialdNetworkAccessManager) - , m_accountSyncProfile(NULL) - , m_syncDb(new SocialNetworkSyncDatabase()) - , m_status(SocialNetworkSyncAdaptor::Invalid) - , m_enabled(false) - , m_syncAborted(false) - , m_serviceName(serviceName) -{ -} - -SocialNetworkSyncAdaptor::~SocialNetworkSyncAdaptor() -{ - delete m_networkAccessManager; - delete m_accountSyncProfile; - delete m_syncDb; -} - -// The SocialNetworkSyncAdaptor takes ownership of the sync profiles. -void SocialNetworkSyncAdaptor::setAccountSyncProfile(Buteo::SyncProfile* perAccountSyncProfile) -{ - delete m_accountSyncProfile; - m_accountSyncProfile = perAccountSyncProfile; -} - -SocialNetworkSyncAdaptor::Status SocialNetworkSyncAdaptor::status() const -{ - return m_status; -} - -bool SocialNetworkSyncAdaptor::enabled() const -{ - return m_enabled; -} - -QString SocialNetworkSyncAdaptor::serviceName() const -{ - return m_serviceName; -} - -bool SocialNetworkSyncAdaptor::syncAborted() const -{ - return m_syncAborted; -} - -void SocialNetworkSyncAdaptor::sync(const QString &dataType, int accountId) -{ - Q_UNUSED(dataType) - Q_UNUSED(accountId) - qCWarning(lcSocialPlugin) << "sync() must be overridden by derived types"; -} - -void SocialNetworkSyncAdaptor::abortSync(Sync::SyncStatus status) -{ - qCInfo(lcSocialPlugin) << "forcing timeout of outstanding replies due to abort:" << status; - m_syncAborted = true; - triggerReplyTimeouts(); -} - -/*! - * \brief SocialNetworkSyncAdaptor::checkAccount - * \param account - * \return true if synchronization of this adaptor's datatype is enabled for the account - * - * The default implementation checks that the account is enabled - * with the accounts&sso service associated with this sync adaptor. - */ -bool SocialNetworkSyncAdaptor::checkAccount(Accounts::Account *account) -{ - bool globallyEnabled = account->enabled(); - Accounts::Service srv(m_accountManager->service(syncServiceName())); - if (!srv.isValid()) { - qCInfo(lcSocialPlugin) << "invalid service" << syncServiceName() << "specified, account" << account->id() - << "will be disabled for" << m_serviceName << dataTypeName(m_dataType) << "sync"; - return false; - } - account->selectService(srv); - bool serviceEnabled = account->enabled(); - account->selectService(Accounts::Service()); - return globallyEnabled && serviceEnabled; -} - -/*! - \internal - Called when the semaphores for all accounts have been decreased - to zero. This is the final function which is called prior to - telling buteo that the sync plugin can be destroyed. - The implementation MUST be synchronous. -*/ -void SocialNetworkSyncAdaptor::finalCleanup() -{ -} - -/*! - \internal - Called when the semaphores decreased to 0, this method is used - to finalize something, like saving all data to a database. - - You can call incrementSemaphore to perform asynchronous tasks - in this method. finalize will then be called again when the - asynchronous task is finished (and when decrementSemaphore is - called), be sure to have a condition check in order not to run - into an infinite loop. - - It is unsafe to call decrementSemaphore in this method, as - the semaphore handling method will find that the semaphore - went to 0 twice and will perform cleanup operations twice. - Please call decrementSemaphore at the end of the asynchronous - task (preferably in a slot), and only call incrementSemaphore - for asynchronous tasks. - */ -void SocialNetworkSyncAdaptor::finalize(int accountId) -{ - Q_UNUSED(accountId) -} - -/*! - \internal - Returns the last sync timestamp for the given service, account and data type. - If data from prior to this timestamp is received in subsequent requests, it does not need to be synced. - This function will return an invalid QDateTime if no synchronisation has occurred. -*/ -QDateTime SocialNetworkSyncAdaptor::lastSyncTimestamp(const QString &serviceName, - const QString &dataType, - int accountId) const -{ - return m_syncDb->lastSyncTimestamp(serviceName, dataType, accountId); -} - -/*! - \internal - Updates the last sync timestamp for the given service, account and data type to the given \a timestamp. -*/ -bool SocialNetworkSyncAdaptor::updateLastSyncTimestamp(const QString &serviceName, - const QString &dataType, - int accountId, - const QDateTime ×tamp) -{ - // Workaround - // TODO: do better, with a queue - m_syncDb->addSyncTimestamp(serviceName, dataType, accountId, timestamp); - m_syncDb->commit(); - m_syncDb->wait(); - return m_syncDb->writeStatus() == AbstractSocialCacheDatabase::Finished; -} - -/*! - \internal - Returns the list of identifiers of accounts which have been synced for - the given \a dataType. -*/ -QList<int> SocialNetworkSyncAdaptor::syncedAccounts(const QString &dataType) -{ - return m_syncDb->syncedAccounts(m_serviceName, dataType); -} - -/*! - * \internal - * Changes status if there is real change and emits statusChanged() signal. - */ -void SocialNetworkSyncAdaptor::setStatus(Status status) -{ - if (m_status != status) { - m_status = status; - emit statusChanged(); - } -} - -/*! - * \internal - * Should be used in constructors to set the initial state - * of enabled and status, without emitting signals - * - */ -void SocialNetworkSyncAdaptor::setInitialActive(bool enabled) -{ - m_enabled = enabled; - if (enabled) { - m_status = Inactive; - } else { - m_status = Invalid; - } -} - -/*! - * \internal - * Should be called by any specific sync adapter when - * they've finished syncing data. The transition from - * busy status to inactive status is what causes the - * Buteo plugin to emit the sync results (and allows - * subsequent syncs to occur). - */ -void SocialNetworkSyncAdaptor::setFinishedInactive() -{ - finalCleanup(); - qCInfo(lcSocialPlugin) << "Finished" << m_serviceName << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) - << "sync at:" << QDateTime::currentDateTime().toString(Qt::ISODate); - setStatus(SocialNetworkSyncAdaptor::Inactive); -} - -void SocialNetworkSyncAdaptor::incrementSemaphore(int accountId) -{ - int semaphoreValue = m_accountSyncSemaphores.value(accountId); - semaphoreValue += 1; - m_accountSyncSemaphores.insert(accountId, semaphoreValue); - qCDebug(lcSocialPlugin) << "incremented busy semaphore for account" << accountId << "to:" << semaphoreValue; -} - -void SocialNetworkSyncAdaptor::decrementSemaphore(int accountId) -{ - if (!m_accountSyncSemaphores.contains(accountId)) { - qCWarning(lcSocialPlugin) << "no such semaphore for account" << accountId; - return; - } - - int semaphoreValue = m_accountSyncSemaphores.value(accountId); - semaphoreValue -= 1; - qCDebug(lcSocialPlugin) << "decremented busy semaphore for account" << accountId << "to:" << semaphoreValue; - if (semaphoreValue < 0) { - qCWarning(lcSocialPlugin) << "busy semaphore is negative for account" << accountId; - return; - } - m_accountSyncSemaphores.insert(accountId, semaphoreValue); - - if (semaphoreValue == 0) { - finalize(accountId); - - // With the newer implementation, in finalize we can raise semaphores, - // so if after calling finalize, the semaphore count is not the same anymore, - // we shouldn't update the sync timestamp - if (m_accountSyncSemaphores.value(accountId) > 0) { - return; - } - - // finished all outstanding sync requests for this account. - // update the sync time in the global sociald database. - updateLastSyncTimestamp(m_serviceName, - SocialNetworkSyncAdaptor::dataTypeName(m_dataType), accountId, - QDateTime::currentDateTime().toTimeSpec(Qt::UTC)); - - // if all outstanding requests for all accounts have finished, - // then update our status to Inactive / ready to handle more sync requests. - bool allAreZero = true; - QList<int> semaphores = m_accountSyncSemaphores.values(); - foreach (int sv, semaphores) { - if (sv != 0) { - allAreZero = false; - break; - } - } - - if (allAreZero) { - setFinishedInactive(); // Finished! - } - } -} - -void SocialNetworkSyncAdaptor::timeoutReply() -{ - QTimer *timer = qobject_cast<QTimer*>(sender()); - QNetworkReply *reply = timer->property("networkReply").value<QNetworkReply*>(); - int accountId = timer->property("accountId").toInt(); - - qCWarning(lcSocialPlugin) << "network request timed out while performing sync with account" << accountId; - - m_networkReplyTimeouts[accountId].remove(reply); - reply->setProperty("isError", QVariant::fromValue<bool>(true)); - reply->finished(); // invoke finished, so that the error handling there decrements the semaphore etc. - reply->disconnect(); -} - -void SocialNetworkSyncAdaptor::setupReplyTimeout(int accountId, QNetworkReply *reply, int msecs) -{ - // this function should be called whenever a new network request is performed. - QTimer *timer = new QTimer(this); - timer->setSingleShot(true); - timer->setInterval(msecs); - timer->setProperty("accountId", accountId); - timer->setProperty("networkReply", QVariant::fromValue<QNetworkReply*>(reply)); - connect(timer, &QTimer::timeout, this, &SocialNetworkSyncAdaptor::timeoutReply); - timer->start(); - m_networkReplyTimeouts[accountId].insert(reply, timer); -} - -void SocialNetworkSyncAdaptor::removeReplyTimeout(int accountId, QNetworkReply *reply) -{ - // this function should be called by the finished() handler for the reply. - QTimer *timer = m_networkReplyTimeouts[accountId].value(reply); - if (!reply) { - return; - } - - delete timer; - m_networkReplyTimeouts[accountId].remove(reply); -} - -void SocialNetworkSyncAdaptor::triggerReplyTimeouts() -{ - // if we've lost network connectivity, we should immediately timeout all replies. - Q_FOREACH (int accountId, m_networkReplyTimeouts.keys()) { - Q_FOREACH (QTimer *timer, m_networkReplyTimeouts[accountId]) { - timer->stop(); - timer->setInterval(1); - timer->start(); - } - } -} - -QJsonObject SocialNetworkSyncAdaptor::parseJsonObjectReplyData(const QByteArray &replyData, bool *ok) -{ - QJsonDocument jsonDocument = QJsonDocument::fromJson(replyData); - *ok = !jsonDocument.isEmpty(); - if (*ok && jsonDocument.isObject()) { - return jsonDocument.object(); - } - *ok = false; - return QJsonObject(); -} - -QJsonArray SocialNetworkSyncAdaptor::parseJsonArrayReplyData(const QByteArray &replyData, bool *ok) -{ - QJsonDocument jsonDocument = QJsonDocument::fromJson(replyData); - *ok = !jsonDocument.isEmpty(); - if (*ok && jsonDocument.isArray()) { - return jsonDocument.array(); - } - *ok = false; - return QJsonArray(); -} - -/* - Valid data types are data types which are known to the API. - Note that just because a data type is valid does not mean - that it will necessarily be supported by a given social network - sync adaptor. -*/ -QStringList SocialNetworkSyncAdaptor::validDataTypes() -{ - static QStringList retn(validDataTypesInitialiser()); - return retn; -} - -/* - String for Enum since the DBus API uses strings -*/ -QString SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::DataType t) -{ - switch (t) { - case SocialNetworkSyncAdaptor::Contacts: return QStringLiteral("Contacts"); - case SocialNetworkSyncAdaptor::Calendars: return QStringLiteral("Calendars"); - case SocialNetworkSyncAdaptor::Notifications: return QStringLiteral("Notifications"); - case SocialNetworkSyncAdaptor::Images: return QStringLiteral("Images"); - case SocialNetworkSyncAdaptor::Videos: return QStringLiteral("Videos"); - case SocialNetworkSyncAdaptor::Posts: return QStringLiteral("Posts"); - case SocialNetworkSyncAdaptor::Messages: return QStringLiteral("Messages"); - case SocialNetworkSyncAdaptor::Emails: return QStringLiteral("Emails"); - case SocialNetworkSyncAdaptor::Signon: return QStringLiteral("Signon"); - case SocialNetworkSyncAdaptor::Backup: return QStringLiteral("Backup"); - case SocialNetworkSyncAdaptor::BackupQuery: return QStringLiteral("BackupQuery"); - case SocialNetworkSyncAdaptor::BackupRestore: return QStringLiteral("BackupRestore"); - default: break; - } - - return QString(); -} - -void SocialNetworkSyncAdaptor::purgeCachedImages(SocialImagesDatabase *database, - int accountId) -{ - database->queryImages(accountId); - database->wait(); - - QList<SocialImage::ConstPtr> images = database->images(); - foreach (SocialImage::ConstPtr image, images) { - qCDebug(lcSocialPlugin) << "Purge cached image " << image->imageFile() << " for account " << image->accountId(); - QFile::remove(image->imageFile()); - } - - database->removeImages(images); - database->commit(); - database->wait(); -} - -void SocialNetworkSyncAdaptor::purgeExpiredImages(SocialImagesDatabase *database, - int accountId) -{ - database->queryExpired(accountId); - database->wait(); - - QList<SocialImage::ConstPtr> images = database->images(); - foreach (SocialImage::ConstPtr image, images) { - qCDebug(lcSocialPlugin) << "Purge expired image " << image->imageFile() << " for account " << image->accountId(); - QFile::remove(image->imageFile()); - } - - database->removeImages(images); - database->commit(); - database->wait(); -} diff --git a/buteo-plugins/buteo-common/socialnetworksyncadaptor.h b/buteo-plugins/buteo-common/socialnetworksyncadaptor.h deleted file mode 100644 index 7c76564..0000000 --- a/buteo-plugins/buteo-common/socialnetworksyncadaptor.h +++ /dev/null @@ -1,155 +0,0 @@ -/**************************************************************************** - ** - ** Copyright (C) 2013-2026 Jolla Ltd. - ** Contact: Chris Adams <chris.adams@jollamobile.com> - ** - ** This program/library is free software; you can redistribute it and/or - ** modify it under the terms of the GNU Lesser General Public License - ** version 2.1 as published by the Free Software Foundation. - ** - ** This program/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 program/library; if not, write to the Free - ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - ** 02110-1301 USA - ** - ****************************************************************************/ - -#ifndef SOCIALNETWORKSYNCADAPTOR_H -#define SOCIALNETWORKSYNCADAPTOR_H - -#include <QtCore/QObject> -#include <QtCore/QDateTime> -#include <QtCore/QString> -#include <QtCore/QJsonObject> -#include <QtCore/QJsonArray> -#include <QtCore/QMap> -#include <QtCore/QList> - -#include "buteosyncfw_p.h" - -class QSqlDatabase; -class QNetworkAccessManager; -class QTimer; -class QNetworkReply; -class SocialNetworkSyncDatabase; -class SocialImagesDatabase; - -namespace Accounts { - class Account; - class Manager; -} - -class SocialNetworkSyncAdaptor : public QObject -{ - Q_OBJECT - Q_PROPERTY(Status status READ status NOTIFY statusChanged) - Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged) - -public: - enum Status { - Initializing = 0, - Inactive, - Busy, - Error, - Invalid - }; - - enum PurgeMode { - SyncPurge = 0, - CleanUpPurge - }; - - enum DataType { - Contacts = 1, // "Contacts" - Calendars, // "Calendars" - Notifications, // "Notifications" - Images, // "Images" - Videos, // "Videos" - Posts, // "Posts" - Messages, // "Messages" - Emails, // "Emails" - Signon, // "Signon" -- for refreshing AccessTokens etc. - Backup, // "Backup" - BackupQuery, // "BackupQuery" - BackupRestore // "BackupRestore" - }; - static QStringList validDataTypes(); - static QString dataTypeName(DataType t); - -public: - SocialNetworkSyncAdaptor(const QString &serviceName, SocialNetworkSyncAdaptor::DataType dataType, - QNetworkAccessManager *qnam, QObject *parent); - virtual ~SocialNetworkSyncAdaptor(); - - virtual QString syncServiceName() const = 0; - void setAccountSyncProfile(Buteo::SyncProfile* perAccountSyncProfile); - - Status status() const; - bool enabled() const; - QString serviceName() const; - - virtual void sync(const QString &dataType, int accountId = 0); - virtual void purgeDataForOldAccount(int accountId, PurgeMode mode = SyncPurge) = 0; - virtual void abortSync(Sync::SyncStatus status); - -Q_SIGNALS: - void statusChanged(); - void enabledChanged(); - -protected: - virtual bool checkAccount(Accounts::Account *account); - virtual void finalCleanup(); - virtual void finalize(int accountId); - QDateTime lastSyncTimestamp(const QString &serviceName, const QString &dataType, - int accountId) const; - bool updateLastSyncTimestamp(const QString &serviceName, const QString &dataType, - int accountId, const QDateTime ×tamp); - QList<int> syncedAccounts(const QString &dataType); - void setStatus(Status status); - void setInitialActive(bool enabled); - void setFinishedInactive(); - - // whether the sync has been aborted (perhaps due to network connection loss) - bool syncAborted() const; - - // Semaphore system - void incrementSemaphore(int accountId); - void decrementSemaphore(int accountId); - - // network reply timeouts - void setupReplyTimeout(int accountId, QNetworkReply *reply, int msecs = 60000); - void removeReplyTimeout(int accountId, QNetworkReply *reply); - void triggerReplyTimeouts(); - - // Parsing methods - static QJsonObject parseJsonObjectReplyData(const QByteArray &replyData, bool *ok); - static QJsonArray parseJsonArrayReplyData(const QByteArray &replyData, bool *ok); - - // Cache management - void purgeCachedImages(SocialImagesDatabase *database, int accountId); - void purgeExpiredImages(SocialImagesDatabase *database, int accountId); - - const SocialNetworkSyncAdaptor::DataType m_dataType; - Accounts::Manager * const m_accountManager; - QNetworkAccessManager * const m_networkAccessManager; - Buteo::SyncProfile *m_accountSyncProfile; - -protected Q_SLOTS: - virtual void timeoutReply(); - -private: - SocialNetworkSyncDatabase *m_syncDb; - SocialNetworkSyncAdaptor::Status m_status; - bool m_enabled; - bool m_syncAborted; - QString m_serviceName; - QMap<int, int> m_accountSyncSemaphores; - QMap<int, QMap<QNetworkReply*, QTimer *> > m_networkReplyTimeouts; -}; - -#endif // SOCIALNETWORKSYNCADAPTOR_H diff --git a/buteo-plugins/buteo-common/trace.cpp b/buteo-plugins/buteo-common/trace.cpp deleted file mode 100644 index 05e3508..0000000 --- a/buteo-plugins/buteo-common/trace.cpp +++ /dev/null @@ -1,25 +0,0 @@ -/**************************************************************************** - ** - ** Copyright (C) 2013-2026 Jolla Ltd. - ** - ** This program/library is free software; you can redistribute it and/or - ** modify it under the terms of the GNU Lesser General Public License - ** version 2.1 as published by the Free Software Foundation. - ** - ** This program/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 program/library; if not, write to the Free - ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - ** 02110-1301 USA - ** - ****************************************************************************/ - -#include "trace.h" - -Q_LOGGING_CATEGORY(lcSocialPlugin, "buteo.plugin.social", QtWarningMsg) -Q_LOGGING_CATEGORY(lcSocialPluginTrace, "buteo.plugin.social.trace", QtWarningMsg) - diff --git a/buteo-plugins/buteo-common/trace.h b/buteo-plugins/buteo-common/trace.h deleted file mode 100644 index 2b9c1ae..0000000 --- a/buteo-plugins/buteo-common/trace.h +++ /dev/null @@ -1,30 +0,0 @@ -/**************************************************************************** - ** - ** Copyright (C) 2013-2026 Jolla Ltd. - ** Contact: Chris Adams <chris.adams@jollamobile.com> - ** - ** This program/library is free software; you can redistribute it and/or - ** modify it under the terms of the GNU Lesser General Public License - ** version 2.1 as published by the Free Software Foundation. - ** - ** This program/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 program/library; if not, write to the Free - ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - ** 02110-1301 USA - ** - ****************************************************************************/ - -#ifndef TRACE_H -#define TRACE_H - -#include <QLoggingCategory> - -Q_DECLARE_LOGGING_CATEGORY(lcSocialPlugin) -Q_DECLARE_LOGGING_CATEGORY(lcSocialPluginTrace) - -#endif // TRACE_H diff --git a/buteo-plugins/buteo-plugins.pro b/buteo-plugins/buteo-plugins.pro index ead60e6..a64041b 100644 --- a/buteo-plugins/buteo-plugins.pro +++ b/buteo-plugins/buteo-plugins.pro @@ -4,9 +4,5 @@ TEMPLATE = subdirs SUBDIRS += \ - buteo-common \ buteo-sync-plugin-mastodon-posts \ buteo-sync-plugin-mastodon-notifications - -buteo-sync-plugin-mastodon-posts.depends = buteo-common -buteo-sync-plugin-mastodon-notifications.depends = buteo-common diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/buteo-sync-plugin-mastodon-notifications.pro b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/buteo-sync-plugin-mastodon-notifications.pro index d16cc3d..dda88ff 100644 --- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/buteo-sync-plugin-mastodon-notifications.pro +++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/buteo-sync-plugin-mastodon-notifications.pro @@ -6,7 +6,6 @@ TARGET = mastodon-notifications-client QT -= gui -include($$PWD/../buteo-common/buteo-common.pri) include($$PWD/../../common/common.pri) TS_FILE = $$OUT_PWD/lipstick-jolla-home-mastodon-notifications.ts @@ -35,7 +34,14 @@ QMAKE_EXTRA_TARGETS += ts engineering_english PRE_TARGETDEPS += ts engineering_english CONFIG += link_pkgconfig -PKGCONFIG += mlite5 nemonotifications-qt5 +PKGCONFIG += \ + buteosocialcommon \ + socialcache \ + accounts-qt5 \ + buteosyncfw5 \ + libsignon-qt5 \ + mlite5 \ + nemonotifications-qt5 INCLUDEPATH += $$PWD diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/buteo-sync-plugin-mastodon-posts.pro b/buteo-plugins/buteo-sync-plugin-mastodon-posts/buteo-sync-plugin-mastodon-posts.pro index a9f65af..c25e5d4 100644 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/buteo-sync-plugin-mastodon-posts.pro +++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/buteo-sync-plugin-mastodon-posts.pro @@ -6,11 +6,17 @@ TARGET = mastodon-posts-client QT -= gui -include($$PWD/../buteo-common/buteo-common.pri) include($$PWD/../../common/common.pri) CONFIG += link_pkgconfig -PKGCONFIG += mlite5 nemonotifications-qt5 +PKGCONFIG += \ + buteosocialcommon \ + socialcache \ + accounts-qt5 \ + buteosyncfw5 \ + libsignon-qt5 \ + mlite5 \ + nemonotifications-qt5 INCLUDEPATH += $$PWD diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp index 7b47fe8..577e185 100644 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp +++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp @@ -20,8 +20,8 @@ #include "mastodondatatypesyncadaptor.h" #include "mastodonauthutils.h" -#include "trace.h" +#include <QtCore/QLoggingCategory> #include <QtCore/QVariantMap> #include <QtNetwork/QNetworkRequest> @@ -36,6 +36,8 @@ #include <SignOn/AuthSession> #include <SignOn/SessionData> +Q_LOGGING_CATEGORY(lcMastodonSync, "buteo.plugin.mastodon.sync", QtWarningMsg) + MastodonDataTypeSyncAdaptor::MastodonDataTypeSyncAdaptor( SocialNetworkSyncAdaptor::DataType dataType, QObject *parent) @@ -50,7 +52,7 @@ MastodonDataTypeSyncAdaptor::~MastodonDataTypeSyncAdaptor() void MastodonDataTypeSyncAdaptor::sync(const QString &dataTypeString, int accountId) { if (dataTypeString != SocialNetworkSyncAdaptor::dataTypeName(m_dataType)) { - qCWarning(lcSocialPlugin) << "Mastodon" << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) + qCWarning(lcMastodonSync) << "Mastodon" << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "sync adaptor was asked to sync" << dataTypeString; setStatus(SocialNetworkSyncAdaptor::Error); return; @@ -58,14 +60,14 @@ void MastodonDataTypeSyncAdaptor::sync(const QString &dataTypeString, int accoun setStatus(SocialNetworkSyncAdaptor::Busy); updateDataForAccount(accountId); - qCDebug(lcSocialPlugin) << "successfully triggered sync with profile:" << m_accountSyncProfile->name(); + qCDebug(lcMastodonSync) << "successfully triggered sync with profile:" << m_accountSyncProfile->name(); } void MastodonDataTypeSyncAdaptor::updateDataForAccount(int accountId) { Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); if (!account) { - qCWarning(lcSocialPlugin) << "existing account with id" << accountId << "couldn't be retrieved"; + qCWarning(lcMastodonSync) << "existing account with id" << accountId << "couldn't be retrieved"; setStatus(SocialNetworkSyncAdaptor::Error); return; } @@ -89,7 +91,7 @@ void MastodonDataTypeSyncAdaptor::errorHandler(QNetworkReply::NetworkError err) const int accountId = reply->property("accountId").toInt(); const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) + qCWarning(lcMastodonSync) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "request with account" << accountId << "experienced error:" << err << "HTTP:" << httpStatus; @@ -114,7 +116,7 @@ void MastodonDataTypeSyncAdaptor::sslErrorsHandler(const QList<QSslError> &errs) sslerrs.chop(2); } - qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) + qCWarning(lcMastodonSync) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) << "request with account" << sender()->property("accountId").toInt() << "experienced ssl errors:" << sslerrs; sender()->setProperty("isError", QVariant::fromValue<bool>(true)); @@ -122,7 +124,7 @@ void MastodonDataTypeSyncAdaptor::sslErrorsHandler(const QList<QSslError> &errs) void MastodonDataTypeSyncAdaptor::setCredentialsNeedUpdate(Accounts::Account *account) { - qCInfo(lcSocialPlugin) << "sociald:Mastodon: setting CredentialsNeedUpdate to true for account:" << account->id(); + qCInfo(lcMastodonSync) << "sociald:Mastodon: setting CredentialsNeedUpdate to true for account:" << account->id(); Accounts::Service srv(m_accountManager->service(syncServiceName())); account->selectService(srv); account->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue<bool>(true)); @@ -145,7 +147,7 @@ void MastodonDataTypeSyncAdaptor::signIn(Accounts::Account *account) ? SignOn::Identity::existingIdentity(account->credentialsId()) : 0; if (!identity) { - qCWarning(lcSocialPlugin) << "account" << accountId << "has no valid credentials, cannot sign in"; + qCWarning(lcMastodonSync) << "account" << accountId << "has no valid credentials, cannot sign in"; decrementSemaphore(accountId); return; } @@ -155,7 +157,7 @@ void MastodonDataTypeSyncAdaptor::signIn(Accounts::Account *account) const QString mechanism = accSrv.authData().mechanism(); SignOn::AuthSession *session = identity->createSession(method); if (!session) { - qCWarning(lcSocialPlugin) << "could not create signon session for account" << accountId; + qCWarning(lcMastodonSync) << "could not create signon session for account" << accountId; identity->deleteLater(); decrementSemaphore(accountId); return; @@ -183,7 +185,7 @@ void MastodonDataTypeSyncAdaptor::signOnError(const SignOn::Error &error) SignOn::Identity *identity = session->property("identity").value<SignOn::Identity*>(); const int accountId = account->id(); - qCWarning(lcSocialPlugin) << "credentials for account with id" << accountId + qCWarning(lcMastodonSync) << "credentials for account with id" << accountId << "couldn't be retrieved:" << error.type() << error.message(); if (error.type() == SignOn::Error::UserInteraction) { @@ -211,7 +213,7 @@ void MastodonDataTypeSyncAdaptor::signOnResponse(const SignOn::SessionData &resp accessToken = MastodonAuthUtils::accessToken(data); if (accessToken.isEmpty()) { - qCWarning(lcSocialPlugin) << "signon response for account with id" << accountId + qCWarning(lcMastodonSync) << "signon response for account with id" << accountId << "contained no access token; keys:" << data.keys(); } diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp index 160d6cc..6c9a280 100644 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp +++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp @@ -19,9 +19,9 @@ ****************************************************************************/ #include "mastodonpostssyncadaptor.h" -#include "trace.h" #include "mastodontextutils.h" +#include <QtCore/QLoggingCategory> #include <QtCore/QJsonArray> #include <QtCore/QJsonObject> #include <QtCore/QJsonValue> @@ -30,6 +30,8 @@ #include <QtNetwork/QNetworkRequest> namespace { + Q_LOGGING_CATEGORY(lcMastodonPostsSync, "buteo.plugin.mastodon.posts.sync", QtWarningMsg) + QString displayNameForAccount(const QJsonObject &account) { const QString displayName = account.value(QStringLiteral("display_name")).toString().trimmed(); @@ -80,7 +82,7 @@ void MastodonPostsSyncAdaptor::beginSync(int accountId, const QString &accessTok void MastodonPostsSyncAdaptor::finalize(int accountId) { if (syncAborted()) { - qCInfo(lcSocialPlugin) << "sync aborted, won't commit database changes"; + qCInfo(lcMastodonPostsSync) << "sync aborted, won't commit database changes"; } else { m_db.commit(); m_db.wait(); @@ -121,7 +123,7 @@ void MastodonPostsSyncAdaptor::requestPosts(int accountId, const QString &access incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } else { - qCWarning(lcSocialPlugin) << "unable to request home timeline posts from Mastodon account with id" << accountId; + qCWarning(lcMastodonPostsSync) << "unable to request home timeline posts from Mastodon account with id" << accountId; } } @@ -146,7 +148,7 @@ void MastodonPostsSyncAdaptor::finishedPostsHandler() m_db.removePosts(accountId); if (!statuses.size()) { - qCDebug(lcSocialPlugin) << "no feed posts received for account" << accountId; + qCDebug(lcMastodonPostsSync) << "no feed posts received for account" << accountId; decrementSemaphore(accountId); return; } @@ -249,7 +251,7 @@ void MastodonPostsSyncAdaptor::finishedPostsHandler() accountId); } } else { - qCWarning(lcSocialPlugin) << "unable to parse event feed data from request with account" << accountId + qCWarning(lcMastodonPostsSync) << "unable to parse event feed data from request with account" << accountId << ", got:" << QString::fromUtf8(replyData); } diff --git a/common/mastodonpostsdatabase.cpp b/common/mastodonpostsdatabase.cpp index 7f82162..2e4b9c1 100644 --- a/common/mastodonpostsdatabase.cpp +++ b/common/mastodonpostsdatabase.cpp @@ -17,6 +17,7 @@ */ #include "mastodonpostsdatabase.h" +#include <socialcache/socialposthelpers.h> static const char *DB_NAME = "mastodon.db"; static const char *ACCOUNT_NAME_KEY = "account_name"; @@ -71,72 +72,45 @@ void MastodonPostsDatabase::addMastodonPost( QString MastodonPostsDatabase::accountName(const SocialPost::ConstPtr &post) { - if (post.isNull()) { - return QString(); - } - return post->extra().value(ACCOUNT_NAME_KEY).toString(); + return SocialPostHelpers::extraString(post, QString::fromLatin1(ACCOUNT_NAME_KEY)); } QString MastodonPostsDatabase::url(const SocialPost::ConstPtr &post) { - if (post.isNull()) { - return QString(); - } - return post->extra().value(URL_KEY).toString(); + return SocialPostHelpers::extraString(post, QString::fromLatin1(URL_KEY)); } QString MastodonPostsDatabase::boostedBy(const SocialPost::ConstPtr &post) { - if (post.isNull()) { - return QString(); - } - return post->extra().value(BOOSTED_BY_KEY).toString(); + return SocialPostHelpers::extraString(post, QString::fromLatin1(BOOSTED_BY_KEY)); } int MastodonPostsDatabase::repliesCount(const SocialPost::ConstPtr &post) { - if (post.isNull()) { - return 0; - } - return post->extra().value(REPLIES_COUNT_KEY).toInt(); + return SocialPostHelpers::extraInt(post, QString::fromLatin1(REPLIES_COUNT_KEY), 0); } int MastodonPostsDatabase::favouritesCount(const SocialPost::ConstPtr &post) { - if (post.isNull()) { - return 0; - } - return post->extra().value(FAVOURITES_COUNT_KEY).toInt(); + return SocialPostHelpers::extraInt(post, QString::fromLatin1(FAVOURITES_COUNT_KEY), 0); } int MastodonPostsDatabase::reblogsCount(const SocialPost::ConstPtr &post) { - if (post.isNull()) { - return 0; - } - return post->extra().value(REBLOGS_COUNT_KEY).toInt(); + return SocialPostHelpers::extraInt(post, QString::fromLatin1(REBLOGS_COUNT_KEY), 0); } bool MastodonPostsDatabase::favourited(const SocialPost::ConstPtr &post) { - if (post.isNull()) { - return false; - } - return post->extra().value(FAVOURITED_KEY).toBool(); + return SocialPostHelpers::extraBool(post, QString::fromLatin1(FAVOURITED_KEY), false); } bool MastodonPostsDatabase::reblogged(const SocialPost::ConstPtr &post) { - if (post.isNull()) { - return false; - } - return post->extra().value(REBLOGGED_KEY).toBool(); + return SocialPostHelpers::extraBool(post, QString::fromLatin1(REBLOGGED_KEY), false); } QString MastodonPostsDatabase::instanceUrl(const SocialPost::ConstPtr &post) { - if (post.isNull()) { - return QString(); - } - return post->extra().value(INSTANCE_URL_KEY).toString(); + return SocialPostHelpers::extraString(post, QString::fromLatin1(INSTANCE_URL_KEY)); } diff --git a/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml b/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml index 63b9556..e9c76da 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml +++ b/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml @@ -15,14 +15,14 @@ SocialMediaFeedItem { id: item property variant imageList - property string resolvedStatusUrl: item.stringValue("url", "link", "uri") + property string resolvedStatusUrl: model && model.url ? model.url.toString() : "" property string postId property QtObject postActions - property int likeCount: item.intValue("favouritesCount", "likeCount", "favoriteCount") - property int commentCount: item.intValue("repliesCount", "commentCount") - property int boostCount: item.intValue("reblogsCount", "boostCount", "repostsCount") - property bool favourited: !!model.favourited - property bool reblogged: !!model.reblogged + property int likeCount: model && model.favouritesCount ? model.favouritesCount : 0 + property int commentCount: model && model.repliesCount ? model.repliesCount : 0 + property int boostCount: model && model.reblogsCount ? model.reblogsCount : 0 + property bool favourited: model ? !!model.favourited : false + property bool reblogged: model ? !!model.reblogged : false property int _likeCountOverride: -1 property int _boostCountOverride: -1 property int _favouritedOverride: -1 @@ -41,6 +41,18 @@ SocialMediaFeedItem { property string _accountName: model && model.accountName ? model.accountName.toString() : "" property string _bodyText: model && model.body ? model.body.toString() : "" //: Action label shown in Mastodon interaction menu. + //% "Favourite" + readonly property string _favouriteActionText: qsTrId("lipstick-jolla-home-la-mastodon_favourite") + //: Action label shown in Mastodon interaction menu when the post is already favourited. + //% "Unfavourite" + readonly property string _unfavouriteActionText: qsTrId("lipstick-jolla-home-la-mastodon_unfavourite") + //: Action label shown in Mastodon interaction menu. + //% "Boost" + readonly property string _boostActionText: qsTrId("lipstick-jolla-home-la-mastodon_boost") + //: Action label shown in Mastodon interaction menu when the post is already boosted. + //% "Undo boost" + readonly property string _unboostActionText: qsTrId("lipstick-jolla-home-la-mastodon_unboost") + //: Action label shown in Mastodon interaction menu. //% "Share" readonly property string _shareActionText: qsTrId("lipstick-jolla-home-la-mastodon_share") //: Link title used when sharing a Mastodon post. @@ -86,7 +98,7 @@ SocialMediaFeedItem { topMargin: item._booster.length > 0 ? Theme.paddingMedium : Theme.paddingLarge userRemovable: false - Image { + SocialReshareIcon { id: boosterIcon anchors { @@ -95,21 +107,17 @@ SocialMediaFeedItem { topMargin: item.topMargin } visible: item._booster.length > 0 - source: "image://theme/icon-s-repost" + (item.highlighted ? "?" + Theme.highlightColor : "") + highlighted: item.highlighted + iconSource: "image://theme/icon-s-repost" } - Text { + SocialReshareText { anchors { left: content.left right: content.right verticalCenter: boosterIcon.verticalCenter } - elide: Text.ElideRight - font.pixelSize: Theme.fontSizeExtraSmall - color: item.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor - textFormat: Text.PlainText - visible: text.length > 0 - + highlighted: item.highlighted text: item._booster.length > 0 ? //: Shown above a post that is boosted by another user. %1 = name of user who boosted //% "%1 boosted" @@ -157,61 +165,18 @@ SocialMediaFeedItem { plainText: item._bodyText } - Row { + SocialPostMetadataRow { id: metadataRow width: parent.width - height: previewRow.visible ? implicitHeight + Theme.paddingMedium : implicitHeight // add padding below - spacing: Theme.paddingSmall - - readonly property color passiveColor: item.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor - readonly property color activeColor: Theme.highlightColor - - Label { - font.pixelSize: Theme.fontSizeExtraSmall - text: "↩ " + item.commentCount - color: metadataRow.passiveColor - } - - Label { - font.pixelSize: Theme.fontSizeExtraSmall - text: "|" - color: metadataRow.passiveColor - } - - Label { - font.pixelSize: Theme.fontSizeExtraSmall - text: "★ " + (item._likeCountOverride >= 0 ? item._likeCountOverride : item.likeCount) - color: item.isFavourited ? metadataRow.activeColor : metadataRow.passiveColor - } - - Label { - font.pixelSize: Theme.fontSizeExtraSmall - text: "|" - color: metadataRow.passiveColor - } - - Label { - font.pixelSize: Theme.fontSizeExtraSmall - text: "↻ " + (item._boostCountOverride >= 0 ? item._boostCountOverride : item.boostCount) - color: item.isReblogged ? metadataRow.activeColor : metadataRow.passiveColor - } - - Label { - visible: item.formattedTime.length > 0 - font.pixelSize: Theme.fontSizeExtraSmall - text: "|" - color: metadataRow.passiveColor - } - - Label { - visible: item.formattedTime.length > 0 - width: Math.max(0, metadataRow.width - x) - truncationMode: TruncationMode.Fade - font.pixelSize: Theme.fontSizeExtraSmall - text: item.formattedTime - color: metadataRow.passiveColor - } + highlighted: item.highlighted + commentCount: item.commentCount + likeCount: item._likeCountOverride >= 0 ? item._likeCountOverride : item.likeCount + repostCount: item._boostCountOverride >= 0 ? item._boostCountOverride : item.boostCount + liked: item.isFavourited + reposted: item.isReblogged + timeText: item.formattedTime + addBottomPadding: previewRow.visible } SocialMediaPreviewRow { @@ -227,39 +192,11 @@ SocialMediaFeedItem { } } - function stringValue() { - for (var i = 0; i < arguments.length; ++i) { - var value = model[arguments[i]] - if (typeof value === "undefined" || value === null) { - continue - } - value = String(value) - if (value.length > 0) { - return value - } - } - return "" - } - - function intValue() { - for (var i = 0; i < arguments.length; ++i) { - var value = model[arguments[i]] - if (typeof value === "undefined" || value === null) { - continue - } - var number = Number(value) - if (!isNaN(number)) { - return Math.max(0, Math.floor(number)) - } - } - return 0 - } - function actionPostId() { if (item.postId.length > 0) { return item.postId } - return item.stringValue("mastodonId", "statusId", "id", "twitterId") + return model && model.mastodonId ? model.mastodonId.toString() : "" } function actionAccountId() { @@ -268,7 +205,7 @@ SocialMediaFeedItem { } function shareStatusUrl() { - return item.stringValue("url", "link", "uri") + return model && model.url ? model.url.toString() : "" } function topLevelParent() { @@ -327,141 +264,78 @@ SocialMediaFeedItem { Component { id: actionMenuComponent - ContextMenu { + SocialInteractionContextMenu { id: actionMenu - property bool menuOpen: height > 0 - property bool wasOpened: false z: 10000 + mapSourceItem: _contentColumn + actionEnabled: item.postActions + && item.actionPostId().length > 0 + && item.actionAccountId() >= 0 + && !item.lockScreenActive + && !item.housekeeping + interactionItems: [ + { + name: "like", + // U+2605 BLACK STAR + symbol: "\u2605", + active: item.isFavourited, + inactiveText: item._favouriteActionText, + activeText: item._unfavouriteActionText + }, + { + name: "reblog", + // U+21BB CLOCKWISE OPEN CIRCLE ARROW + symbol: "\u21BB", + active: item.isReblogged, + inactiveText: item._boostActionText, + activeText: item._unboostActionText + }, + { + name: "share", + // U+260D OPPOSITION (ironic doncha think) + symbol: "\u260D", + active: false, + inactiveText: item._shareActionText, + activeText: item._shareActionText + } + ] - onPositionChanged: { - horizontalActions.xPos = _contentColumn.mapFromItem(actionMenu, mouse.x, mouse.y).x + onInteractionMenuOpened: item._contextMenuOpen = true + onInteractionMenuClosed: { + item._contextMenuOpen = false + destroy() + item._actionMenu = null } - onMenuOpenChanged: { - if (menuOpen) { - wasOpened = true - item._contextMenuOpen = true - } else if (wasOpened) { - item._contextMenuOpen = false - destroy() - item._actionMenu = null + onInteractionTriggered: function(actionName) { + if (!actionEnabled) { + return } - } - - Item { - id: horizontalActions - - // Makes Silica treat this custom row as a context-menu item. - property int __silica_menuitem - property bool down - property bool highlighted - signal clicked - - property real xPos: 0 - property int hoveredIndex: -1 - readonly property bool actionEnabled: item.postActions - && item.actionPostId().length > 0 - && item.actionAccountId() >= 0 - && !item.lockScreenActive - && !item.housekeeping - - width: parent.width - height: Theme.itemSizeMedium - - onXPosChanged: hoveredIndex = Math.max(0, Math.min(2, Math.floor((xPos * 3) / Math.max(1, width)))) - onDownChanged: if (!down) hoveredIndex = -1 - - onClicked: { - xPos = _contentColumn.mapFromItem(actionMenu, actionMenu.mouseX, actionMenu.mouseY).x - var index = hoveredIndex >= 0 ? hoveredIndex : Math.max(0, Math.min(2, Math.floor((xPos * 3) / Math.max(1, width)))) - if (!actionEnabled) { - return - } - var postId = item.actionPostId() - var accountId = item.actionAccountId() - if (index === 0) { - if (item.isFavourited) { - item.postActions.unfavourite(accountId, postId) - } else { - item.postActions.favourite(accountId, postId) - } - } else if (index === 1) { - if (item.isReblogged) { - item.postActions.unboost(accountId, postId) - } else { - item.postActions.boost(accountId, postId) - } + var postId = item.actionPostId() + var accountId = item.actionAccountId() + if (actionName === "like") { + if (item.isFavourited) { + item.postActions.unfavourite(accountId, postId) } else { - var shareUrl = item.shareStatusUrl() - if (shareUrl.length === 0) { - return - } - item._shareAction.resources = [{ - "data": shareUrl, - "linkTitle": item._shareLinkTitle, - "type": "text/x-url" - }] - item._shareAction.trigger() + item.postActions.favourite(accountId, postId) } - } - - Rectangle { - anchors.verticalCenter: parent.verticalCenter - x: (horizontalActions.hoveredIndex >= 0 ? horizontalActions.hoveredIndex : 0) * (parent.width / 3) - width: parent.width / 3 - height: parent.height - visible: horizontalActions.down && horizontalActions.hoveredIndex >= 0 - color: Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity) - } - - Row { - anchors.fill: parent - - Label { - width: parent.width / 3 - height: parent.height - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: Theme.fontSizeExtraLarge - text: "★" - color: horizontalActions.actionEnabled - ? (item.isFavourited - ? Theme.highlightColor - : ((horizontalActions.down && horizontalActions.hoveredIndex === 0) - || (horizontalActions.highlighted && horizontalActions.hoveredIndex === 0) - ? Theme.secondaryHighlightColor : Theme.primaryColor)) - : Theme.rgba(Theme.secondaryColor, 0.4) - } - - Label { - width: parent.width / 3 - height: parent.height - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: Theme.fontSizeExtraLarge - text: "↻" - color: horizontalActions.actionEnabled - ? (item.isReblogged - ? Theme.highlightColor - : ((horizontalActions.down && horizontalActions.hoveredIndex === 1) - || (horizontalActions.highlighted && horizontalActions.hoveredIndex === 1) - ? Theme.secondaryHighlightColor : Theme.primaryColor)) - : Theme.rgba(Theme.secondaryColor, 0.4) + } else if (actionName === "reblog") { + if (item.isReblogged) { + item.postActions.unboost(accountId, postId) + } else { + item.postActions.boost(accountId, postId) } - - Label { - width: parent.width / 3 - height: parent.height - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: Theme.fontSizeExtraLarge - text: "\u260D" - color: horizontalActions.actionEnabled - ? (((horizontalActions.down && horizontalActions.hoveredIndex === 2) - || (horizontalActions.highlighted && horizontalActions.hoveredIndex === 2)) - ? Theme.secondaryHighlightColor : Theme.primaryColor) - : Theme.rgba(Theme.secondaryColor, 0.4) + } else if (actionName === "share") { + var shareUrl = item.shareStatusUrl() + if (shareUrl.length === 0) { + return } + item._shareAction.resources = [{ + "data": shareUrl, + "linkTitle": item._shareLinkTitle, + "type": "text/x-url" + }] + item._shareAction.trigger() } } } diff --git a/eventsview-plugins/eventsview-plugin-mastodon/eventsview-plugin-mastodon.pro b/eventsview-plugins/eventsview-plugin-mastodon/eventsview-plugin-mastodon.pro index 04be215..8c9663e 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/eventsview-plugin-mastodon.pro +++ b/eventsview-plugins/eventsview-plugin-mastodon/eventsview-plugin-mastodon.pro @@ -9,6 +9,7 @@ TARGET = $$qtLibraryTarget($$TARGET) MODULENAME = com/jolla/eventsview/mastodon TARGETPATH = $$[QT_INSTALL_QML]/$$MODULENAME +QT -= gui QT += qml network CONFIG += plugin link_pkgconfig PKGCONFIG += socialcache accounts-qt5 libsignon-qt5 sailfishaccounts diff --git a/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml b/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml index fac0b89..2a5d9c1 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml +++ b/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml @@ -13,6 +13,7 @@ import "shared" SocialMediaAccountDelegate { id: delegateItem + property string instanceHomeUrl: "" //: Mastodon posts //% "Posts" @@ -21,39 +22,35 @@ SocialMediaAccountDelegate { showRemainingCount: false services: ["Posts"] - socialNetwork: 9 + socialNetwork: SocialSync.Mastodon dataType: SocialSync.Posts providerName: "mastodon" + periodicSyncLoopEnabled: true MastodonPostActions { id: mastodonPostActions } - model: MastodonPostsModel { - onCountChanged: { - if (count > 0) { - if (!updateTimer.running) { - shortUpdateTimer.start() - } - } else { - shortUpdateTimer.stop() - } - } - } + model: MastodonPostsModel {} delegate: MastodonFeedItem { downloader: delegateItem.downloader - imageList: delegateItem.variantRole(model, ["images", "mediaAttachments", "media"]) - avatarSource: delegateItem.convertUrl(delegateItem.stringRole(model, ["icon", "avatar", "avatarUrl"])) - fallbackAvatarSource: delegateItem.stringRole(model, ["icon", "avatar", "avatarUrl"]) + imageList: model.images + avatarSource: model.icon + fallbackAvatarSource: model.icon resolvedStatusUrl: delegateItem.authorizeInteractionUrl(model) - postId: delegateItem.stringRole(model, ["mastodonId", "statusId", "id", "twitterId"]) + postId: model.mastodonId postActions: mastodonPostActions - accountId: delegateItem.firstAccountId(model) + accountId: delegateItem.firstAccountId(model, -1) - onTriggered: Qt.openUrlExternally(resolvedStatusUrl) + onTriggered: { + if (resolvedStatusUrl.length > 0) { + Qt.openUrlExternally(resolvedStatusUrl) + } + } Component.onCompleted: { + delegateItem.instanceHomeUrl = statusUrl({instanceUrl: model.instanceUrl}) refreshTimeCount = Qt.binding(function() { return delegateItem.refreshTimeCount }) connectedToNetwork = Qt.binding(function() { return delegateItem.connectedToNetwork }) eventsColumnMaxWidth = Qt.binding(function() { return delegateItem.eventsColumnMaxWidth }) @@ -62,101 +59,61 @@ SocialMediaAccountDelegate { //% "Show more in Mastodon" expandedLabel: qsTrId("lipstick-jolla-home-la-show-more-in-mastodon") - onHeaderClicked: Qt.openUrlExternally("https://mastodon.social/explore") - onExpandedClicked: Qt.openUrlExternally("https://mastodon.social/explore") + onHeaderClicked: { + if (delegateItem.instanceHomeUrl.length > 0) { + Qt.openUrlExternally(delegateItem.instanceHomeUrl) + } + } + onExpandedClicked: { + if (delegateItem.instanceHomeUrl.length > 0) { + Qt.openUrlExternally(delegateItem.instanceHomeUrl) + } + } onViewVisibleChanged: { if (viewVisible) { delegateItem.resetHasSyncableAccounts() delegateItem.model.refresh() - if (delegateItem.hasSyncableAccounts && !updateTimer.running) { - shortUpdateTimer.start() + if (delegateItem.hasSyncableAccounts) { + delegateItem.startPeriodicSyncLoop() } } else { - shortUpdateTimer.stop() + delegateItem.stopPeriodicSyncLoop() } } onConnectedToNetworkChanged: { if (viewVisible) { - if (!updateTimer.running) { - shortUpdateTimer.start() - } - } - } - - // The Mastodon feed is updated 3 seconds after the feed view becomes visible, - // unless it has been updated during last 60 seconds. After that it will be updated - // periodically in every 60 seconds as long as the feed view is visible. - - Timer { - id: shortUpdateTimer - - interval: 3000 - onTriggered: { - delegateItem.sync() - updateTimer.start() + delegateItem.startPeriodicSyncLoop() } } - Timer { - id: updateTimer + Connections { + target: delegateItem.model - interval: 60000 - repeat: true - onTriggered: { - if (delegateItem.viewVisible) { - delegateItem.sync() - } else { - stop() - } - } - } - - function variantRole(modelData, roleNames) { - for (var i = 0; i < roleNames.length; ++i) { - var value = modelData[roleNames[i]] - if (typeof value !== "undefined" && value !== null) { - return value - } - } - return undefined - } - - function stringRole(modelData, roleNames) { - for (var i = 0; i < roleNames.length; ++i) { - var value = modelData[roleNames[i]] - if (typeof value === "undefined" || value === null) { - continue - } - value = String(value) - if (value.length > 0) { - return value + onCountChanged: { + if (target.count === 0) { + delegateItem.instanceHomeUrl = "" } } - return "" } function statusUrl(modelData) { - var directUrl = stringRole(modelData, ["url", "link", "uri"]) + var directUrl = modelData && modelData.url ? modelData.url.toString() : "" if (directUrl.length > 0) { return directUrl } - var instanceUrl = stringRole(modelData, ["instanceUrl", "serverUrl", "baseUrl"]) + var instanceUrl = modelData && modelData.instanceUrl ? modelData.instanceUrl.toString() : "" + instanceUrl = stripTrailingSlashes(instanceUrl) if (instanceUrl.length === 0) { - instanceUrl = "https://mastodon.social" - } - while (instanceUrl.length > 0 && instanceUrl.charAt(instanceUrl.length - 1) === "/") { - instanceUrl = instanceUrl.slice(0, instanceUrl.length - 1) + return "" } - var accountName = stringRole(modelData, ["accountName", "acct", "screenName", "username"]) - var statusId = stringRole(modelData, ["mastodonId", "statusId", "id", "twitterId"]) + var accountName = modelData && modelData.accountName ? modelData.accountName.toString() : "" + var statusId = modelData && modelData.mastodonId ? modelData.mastodonId.toString() : "" if (accountName.length > 0 && statusId.length > 0) { - while (accountName.length > 0 && accountName.charAt(0) === "@") { - accountName = accountName.substring(1) - } + accountName = trimLeadingCharacter(accountName, "@") return instanceUrl + "/@" + accountName + "/" + statusId } @@ -169,13 +126,11 @@ SocialMediaAccountDelegate { return targetUrl } - var instanceUrl = stringRole(modelData, ["instanceUrl", "serverUrl", "baseUrl"]) + var instanceUrl = modelData && modelData.instanceUrl ? modelData.instanceUrl.toString() : "" if (instanceUrl.length === 0) { return targetUrl } - while (instanceUrl.length > 0 && instanceUrl.charAt(instanceUrl.length - 1) === "/") { - instanceUrl = instanceUrl.slice(0, instanceUrl.length - 1) - } + instanceUrl = stripTrailingSlashes(instanceUrl) // Links on the user's own instance should open directly. var sameServer = /^([a-z][a-z0-9+.-]*):\/\/([^\/?#]+)/i @@ -192,23 +147,34 @@ SocialMediaAccountDelegate { return instanceUrl + "/authorize_interaction?uri=" + encodeURIComponent(targetUrl) } - function convertUrl(source) { - if (source.indexOf("_normal.") !== -1) { - return source.replace("_normal.", "_bigger.") - } else if (source.indexOf("_mini.") !== -1) { - return source.replace("_mini.", "_bigger.") + function firstAccountId(modelData, defaultValue) { + var fallback = typeof defaultValue === "undefined" ? -1 : Number(defaultValue) + var accounts = modelData ? modelData.accounts : undefined + if (!accounts || accounts.length <= 0) { + return fallback } - return source + + var accountId = Number(accounts[0]) + return isNaN(accountId) ? fallback : accountId } - function firstAccountId(modelData) { - var accounts = modelData.accounts - if (accounts && accounts.length > 0) { - var accountId = Number(accounts[0]) - if (!isNaN(accountId)) { - return accountId - } + function stripTrailingSlashes(value) { + value = String(value || "") + while (value.length > 0 && value.charAt(value.length - 1) === "/") { + value = value.slice(0, value.length - 1) + } + return value + } + + function trimLeadingCharacter(value, character) { + value = String(value || "") + if (!character || character.length === 0) { + return value + } + + while (value.length > 0 && value.charAt(0) === character) { + value = value.substring(1) } - return -1 + return value } } diff --git a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.cpp b/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.cpp index aa98a95..fe61320 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.cpp +++ b/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.cpp @@ -17,31 +17,7 @@ */ #include "mastodonpostsmodel.h" -#include <QtCore/QVariantMap> - -namespace { - -static const char *URL_KEY = "url"; -static const char *TYPE_KEY = "type"; -static const char *TYPE_PHOTO = "photo"; -static const char *TYPE_VIDEO = "video"; - -QVariantMap createImageData(const SocialPostImage::ConstPtr &image) -{ - QVariantMap imageData; - imageData.insert(QLatin1String(URL_KEY), image->url()); - switch (image->type()) { - case SocialPostImage::Video: - imageData.insert(QLatin1String(TYPE_KEY), QLatin1String(TYPE_VIDEO)); - break; - default: - imageData.insert(QLatin1String(TYPE_KEY), QLatin1String(TYPE_PHOTO)); - break; - } - return imageData; -} - -} +#include <socialcache/socialposthelpers.h> MastodonPostsModel::MastodonPostsModel(QObject *parent) : QAbstractListModel(parent) @@ -123,13 +99,16 @@ void MastodonPostsModel::postsChanged() const bool favourited = m_database.favourited(post); const bool reblogged = m_database.reblogged(post); - eventMap.insert(MastodonPostsModel::MastodonId, post->identifier()); - eventMap.insert(MastodonPostsModel::Name, post->name()); + SocialPostHelpers::appendCommonPostFields(&eventMap, post, + MastodonPostsModel::MastodonId, + MastodonPostsModel::Name, + MastodonPostsModel::Body, + MastodonPostsModel::Timestamp, + MastodonPostsModel::Icon, + MastodonPostsModel::Images, + MastodonPostsModel::Accounts); eventMap.insert(MastodonPostsModel::AccountName, accountName); eventMap.insert(MastodonPostsModel::Acct, accountName); - eventMap.insert(MastodonPostsModel::Body, post->body()); - eventMap.insert(MastodonPostsModel::Timestamp, post->timestamp()); - eventMap.insert(MastodonPostsModel::Icon, post->icon()); eventMap.insert(MastodonPostsModel::Url, postUrl); eventMap.insert(MastodonPostsModel::Link, postUrl); eventMap.insert(MastodonPostsModel::BoostedBy, boostedBy); @@ -140,18 +119,6 @@ void MastodonPostsModel::postsChanged() eventMap.insert(MastodonPostsModel::Favourited, favourited); eventMap.insert(MastodonPostsModel::Reblogged, reblogged); eventMap.insert(MastodonPostsModel::InstanceUrl, m_database.instanceUrl(post)); - - QVariantList images; - Q_FOREACH (const SocialPostImage::ConstPtr &image, post->images()) { - images.append(createImageData(image)); - } - eventMap.insert(MastodonPostsModel::Images, images); - - QVariantList accountsVariant; - Q_FOREACH (int account, post->accounts()) { - accountsVariant.append(account); - } - eventMap.insert(MastodonPostsModel::Accounts, accountsVariant); data.append(eventMap); } diff --git a/rpm/sailfish-account-mastodon.spec b/rpm/sailfish-account-mastodon.spec index 97e790a..c321184 100644 --- a/rpm/sailfish-account-mastodon.spec +++ b/rpm/sailfish-account-mastodon.spec @@ -21,6 +21,7 @@ BuildRequires: pkgconfig(buteosyncfw5) >= 0.10.0 BuildRequires: pkgconfig(accounts-qt5) BuildRequires: pkgconfig(libsignon-qt5) BuildRequires: pkgconfig(socialcache) +BuildRequires: pkgconfig(buteosocialcommon) BuildRequires: pkgconfig(libsailfishkeyprovider) BuildRequires: pkgconfig(sailfishaccounts) BuildRequires: pkgconfig(nemotransferengine-qt5) >= 2.0.0 @@ -28,9 +29,10 @@ BuildRequires: pkgconfig(nemonotifications-qt5) Requires: jolla-settings-accounts-extensions-onlinesync Requires: qmf-oauth2-plugin >= 0.0.7 Requires: buteo-syncfw-qt5-msyncd +Requires: buteo-sync-plugins-social >= 0.4.0 Requires: systemd Requires: lipstick-jolla-home-qt5-components >= 1.2.50 -Requires: eventsview-extensions +Requires: eventsview-extensions >= 0.1.9-2 Requires: sailfishsilica-qt5 >= 1.1.108 Requires: declarative-transferengine-qt5 >= 0.3.13 Requires: nemo-transferengine-qt5 >= 2.0.0 @@ -74,8 +76,6 @@ fi %license LICENSES/LGPL-2.1-or-later.txt %{_libdir}/libmastodoncommon.so.* %exclude %{_libdir}/libmastodoncommon.so -%{_libdir}/libmastodonbuteocommon.so.* -%exclude %{_libdir}/libmastodonbuteocommon.so %{_datadir}/accounts/providers/mastodon.provider %{_datadir}/accounts/services/mastodon-microblog.service %{_datadir}/accounts/services/mastodon-notifications.service diff --git a/settings/accounts-translations-plugin/accounts-translations-plugin.pro b/settings/accounts-translations-plugin/accounts-translations-plugin.pro index aad978f..8a87592 100644 --- a/settings/accounts-translations-plugin/accounts-translations-plugin.pro +++ b/settings/accounts-translations-plugin/accounts-translations-plugin.pro @@ -9,6 +9,7 @@ TARGET = $$qtLibraryTarget($$TARGET) MODULENAME = com/jolla/settings/accounts/mastodon TARGETPATH = $$[QT_INSTALL_QML]/$$MODULENAME +QT -= gui QT += qml CONFIG += plugin diff --git a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro index 59fb7e1..011bcc5 100644 --- a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro +++ b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro @@ -9,6 +9,8 @@ DEPENDPATH += . INCLUDEPATH += .. INCLUDEPATH += ../../common +QT -= gui + CONFIG += link_pkgconfig PKGCONFIG += nemotransferengine-qt5 accounts-qt5 sailfishaccounts libsignon-qt5 diff --git a/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro b/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro index 422a889..2457a7a 100644 --- a/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro +++ b/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro @@ -9,6 +9,7 @@ DEPENDPATH += . INCLUDEPATH += .. INCLUDEPATH += ../../common +QT -= gui QT += network CONFIG += link_pkgconfig |
