From a35c9fa159173388d88ef77e1d31f53488aad094 Mon Sep 17 00:00:00 2001 From: Andrew Branson Date: Fri, 3 Apr 2026 22:55:30 +0200 Subject: Generalize for all fediverse accounts --- settings/accounts/ui/FediverseSettingsDisplay.qml | 181 ++++++ settings/accounts/ui/MastodonSettingsDisplay.qml | 174 ------ settings/accounts/ui/fediverse-settings.qml | 115 ++++ settings/accounts/ui/fediverse-update.qml | 229 ++++++++ settings/accounts/ui/fediverse.qml | 679 ++++++++++++++++++++++ settings/accounts/ui/mastodon-settings.qml | 107 ---- settings/accounts/ui/mastodon-update.qml | 228 -------- settings/accounts/ui/mastodon.qml | 492 ---------------- 8 files changed, 1204 insertions(+), 1001 deletions(-) create mode 100644 settings/accounts/ui/FediverseSettingsDisplay.qml delete mode 100644 settings/accounts/ui/MastodonSettingsDisplay.qml create mode 100644 settings/accounts/ui/fediverse-settings.qml create mode 100644 settings/accounts/ui/fediverse-update.qml create mode 100644 settings/accounts/ui/fediverse.qml delete mode 100644 settings/accounts/ui/mastodon-settings.qml delete mode 100644 settings/accounts/ui/mastodon-update.qml delete mode 100644 settings/accounts/ui/mastodon.qml (limited to 'settings/accounts/ui') diff --git a/settings/accounts/ui/FediverseSettingsDisplay.qml b/settings/accounts/ui/FediverseSettingsDisplay.qml new file mode 100644 index 0000000..7494460 --- /dev/null +++ b/settings/accounts/ui/FediverseSettingsDisplay.qml @@ -0,0 +1,181 @@ +/* + * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +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.fediverse 1.0 +import org.nemomobile.configuration 1.0 + +StandardAccountSettingsDisplay { + id: root + + settingsModified: true + property bool postsServiceEnabled: false + property string instanceTitle: { + var value = root.account.configurationValues("")["instance/Title"] + return value ? value.toString().trim() : "" + } + + function refreshDescriptionEditor() { + var description = root.account.configurationValues("")["description"] + var descriptionValue = description ? description.toString().trim() : "" + var credentialsUserName = root.account.defaultCredentialsUserName + ? root.account.defaultCredentialsUserName.toString().trim() + : "" + if (descriptionValue.length > 0 && credentialsUserName !== descriptionValue) { + root.account.setConfigurationValue("", "default_credentials_username", descriptionValue) + } + + // Reuse the standard "Description" field as the account handle editor. + if (descriptionValue.length > 0) { + root.account.displayName = descriptionValue + } else if (credentialsUserName.length > 0) { + root.account.displayName = credentialsUserName + } else { + root.account.displayName = "" + } + } + + function _providerDisplayName() { + if (instanceTitle.length > 0) { + return instanceTitle + } + + var providerDisplayName = root.accountProvider && root.accountProvider.displayName + ? root.accountProvider.displayName.toString().trim() + : "" + return providerDisplayName.length > 0 ? providerDisplayName : qsTrId("settings-accounts-fediverse-la-provider_name") + } + + onAboutToSaveAccount: { + settingsLoader.updateAllSyncProfiles() + + var storedDescriptionValue = root.account.configurationValues("")["description"] + var storedDescription = storedDescriptionValue ? storedDescriptionValue.toString().trim() : "" + var storedCredentialsUserName = root.account.defaultCredentialsUserName + ? root.account.defaultCredentialsUserName.toString().trim() + : "" + var editedDescription = root.account.displayName + ? root.account.displayName.toString().trim() + : "" + var providerDisplayName = _providerDisplayName() + if (editedDescription === providerDisplayName) { + // Avoid clobbering stored handle if displayName temporarily reverts to provider name. + editedDescription = storedDescription.length > 0 ? storedDescription : storedCredentialsUserName + } + + if (storedDescription !== editedDescription) { + root.account.setConfigurationValue("", "description", editedDescription) + } + + if (storedCredentialsUserName !== editedDescription) { + root.account.setConfigurationValue("", "default_credentials_username", editedDescription) + } + + // Keep account list title fixed to the discovered instance title. + root.account.displayName = providerDisplayName + + if (eventsSyncSwitch.checked !== root.account.configurationValues("")["FeedViewAutoSync"]) { + root.account.setConfigurationValue("", "FeedViewAutoSync", eventsSyncSwitch.checked) + } + } + + StandardAccountSettingsLoader { + id: settingsLoader + account: root.account + accountProvider: root.accountProvider + accountManager: root.accountManager + autoEnableServices: root.autoEnableAccount + + onSettingsLoaded: { + syncServicesRepeater.model = syncServices + otherServicesDisplay.serviceModel = otherServices + + refreshDescriptionEditor() + + var autoSync = root.account.configurationValues("")["FeedViewAutoSync"] + var isNewAccount = root.autoEnableAccount + eventsSyncSwitch.checked = (isNewAccount || autoSync === true) + } + } + + Column { + id: syncServicesDisplay + width: parent.width + + SectionHeader { + //: Options for data to be downloaded from a remote server + //% "Download" + text: qsTrId("settings-accounts-la-download_options") + } + + Repeater { + id: syncServicesRepeater + TextSwitch { + checked: model.enabled + text: model.serviceName === "fediverse-microblog" + //% "Posts" + ? qsTrId("settings-accounts-fediverse-la-service_posts") + : (model.serviceName === "fediverse-notifications" + //% "Notifications" + ? qsTrId("settings-accounts-fediverse-la-service_notifications") + : model.displayName) + description: model.serviceName === "fediverse-microblog" + //% "Show Fediverse posts in the Events view." + ? qsTrId("settings-accounts-fediverse-la-service_posts_description") + : (model.serviceName === "fediverse-notifications" + //% "Show Fediverse notifications." + ? qsTrId("settings-accounts-fediverse-la-service_notifications_description") + : "") + visible: text.length > 0 + onCheckedChanged: { + if (model.serviceName === "fediverse-microblog") { + root.postsServiceEnabled = checked + } + if (checked) { + root.account.enableWithService(model.serviceName) + } else { + root.account.disableWithService(model.serviceName) + } + } + } + } + + TextSwitch { + id: eventsSyncSwitch + + //% "Sync Fediverse feed automatically" + text: qsTrId("settings-accounts-fediverse-la-auto_sync_feed") + //% "Fetch new posts periodically when browsing Events Fediverse feed." + description: qsTrId("settings-accounts-fediverse-la-auto_sync_feed_description") + enabled: root.postsServiceEnabled + + onCheckedChanged: { + autoSyncConf.value = checked + } + } + } + + ConfigurationValue { + id: autoSyncConf + key: "/desktop/lipstick-jolla-home/events/auto_sync_feeds/" + root.account.identifier + } + + AccountServiceSettingsDisplay { + id: otherServicesDisplay + enabled: root.accountEnabled + + onUpdateServiceEnabledStatus: { + if (enabled) { + root.account.enableWithService(serviceName) + } else { + root.account.disableWithService(serviceName) + } + } + } +} diff --git a/settings/accounts/ui/MastodonSettingsDisplay.qml b/settings/accounts/ui/MastodonSettingsDisplay.qml deleted file mode 100644 index 13ac06c..0000000 --- a/settings/accounts/ui/MastodonSettingsDisplay.qml +++ /dev/null @@ -1,174 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -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 -import org.nemomobile.configuration 1.0 - -StandardAccountSettingsDisplay { - id: root - - settingsModified: true - property bool postsServiceEnabled: false - - function refreshDescriptionEditor() { - var description = root.account.configurationValues("")["description"] - var descriptionValue = description ? description.toString().trim() : "" - var credentialsUserName = root.account.defaultCredentialsUserName - ? root.account.defaultCredentialsUserName.toString().trim() - : "" - if (descriptionValue.length > 0 && credentialsUserName !== descriptionValue) { - root.account.setConfigurationValue("", "default_credentials_username", descriptionValue) - } - - // Reuse the standard "Description" field as the account handle editor. - if (descriptionValue.length > 0) { - root.account.displayName = descriptionValue - } else if (credentialsUserName.length > 0) { - root.account.displayName = credentialsUserName - } else { - root.account.displayName = "" - } - } - - function _providerDisplayName() { - var providerDisplayName = root.accountProvider && root.accountProvider.displayName - ? root.accountProvider.displayName.toString().trim() - : "" - //% "Mastodon" - return providerDisplayName.length > 0 ? providerDisplayName : qsTrId("settings-accounts-mastodon-la-provider_name") - } - - onAboutToSaveAccount: { - settingsLoader.updateAllSyncProfiles() - - var storedDescriptionValue = root.account.configurationValues("")["description"] - var storedDescription = storedDescriptionValue ? storedDescriptionValue.toString().trim() : "" - var storedCredentialsUserName = root.account.defaultCredentialsUserName - ? root.account.defaultCredentialsUserName.toString().trim() - : "" - var editedDescription = root.account.displayName - ? root.account.displayName.toString().trim() - : "" - var providerDisplayName = _providerDisplayName() - if (editedDescription === providerDisplayName) { - // Avoid clobbering stored handle if displayName temporarily reverts to provider name. - editedDescription = storedDescription.length > 0 ? storedDescription : storedCredentialsUserName - } - - if (storedDescription !== editedDescription) { - root.account.setConfigurationValue("", "description", editedDescription) - } - - if (storedCredentialsUserName !== editedDescription) { - root.account.setConfigurationValue("", "default_credentials_username", editedDescription) - } - - // Keep account list title fixed to provider name. - root.account.displayName = providerDisplayName - - if (eventsSyncSwitch.checked !== root.account.configurationValues("")["FeedViewAutoSync"]) { - root.account.setConfigurationValue("", "FeedViewAutoSync", eventsSyncSwitch.checked) - } - } - - StandardAccountSettingsLoader { - id: settingsLoader - account: root.account - accountProvider: root.accountProvider - accountManager: root.accountManager - autoEnableServices: root.autoEnableAccount - - onSettingsLoaded: { - syncServicesRepeater.model = syncServices - otherServicesDisplay.serviceModel = otherServices - - refreshDescriptionEditor() - - var autoSync = root.account.configurationValues("")["FeedViewAutoSync"] - var isNewAccount = root.autoEnableAccount - eventsSyncSwitch.checked = (isNewAccount || autoSync === true) - } - } - - Column { - id: syncServicesDisplay - width: parent.width - - SectionHeader { - //: Options for data to be downloaded from a remote server - //% "Download" - text: qsTrId("settings-accounts-la-download_options") - } - - Repeater { - id: syncServicesRepeater - TextSwitch { - checked: model.enabled - text: model.serviceName === "mastodon-microblog" - //% "Posts" - ? qsTrId("settings-accounts-mastodon-la-service_posts") - : (model.serviceName === "mastodon-notifications" - //% "Notifications" - ? qsTrId("settings-accounts-mastodon-la-service_notifications") - : model.displayName) - description: model.serviceName === "mastodon-microblog" - //% "Show Mastodon posts in the Events view." - ? qsTrId("settings-accounts-mastodon-la-service_posts_description") - : (model.serviceName === "mastodon-notifications" - //% "Show Mastodon notifications." - ? qsTrId("settings-accounts-mastodon-la-service_notifications_description") - : "") - visible: text.length > 0 - onCheckedChanged: { - if (model.serviceName === "mastodon-microblog") { - root.postsServiceEnabled = checked - } - if (checked) { - root.account.enableWithService(model.serviceName) - } else { - root.account.disableWithService(model.serviceName) - } - } - } - } - - TextSwitch { - id: eventsSyncSwitch - - //% "Sync Mastodon feed automatically" - text: qsTrId("settings-accounts-mastodon-la-auto_sync_feed") - //% "Fetch new posts periodically when browsing Events Mastodon feed." - description: qsTrId("settings-accounts-mastodon-la-auto_sync_feed_description") - enabled: root.postsServiceEnabled - - onCheckedChanged: { - autoSyncConf.value = checked - } - } - } - - ConfigurationValue { - id: autoSyncConf - key: "/desktop/lipstick-jolla-home/events/auto_sync_feeds/" + root.account.identifier - } - - AccountServiceSettingsDisplay { - id: otherServicesDisplay - enabled: root.accountEnabled - - onUpdateServiceEnabledStatus: { - if (enabled) { - root.account.enableWithService(serviceName) - } else { - root.account.disableWithService(serviceName) - } - } - } -} diff --git a/settings/accounts/ui/fediverse-settings.qml b/settings/accounts/ui/fediverse-settings.qml new file mode 100644 index 0000000..25e0d99 --- /dev/null +++ b/settings/accounts/ui/fediverse-settings.qml @@ -0,0 +1,115 @@ +/* + * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Accounts 1.0 +import com.jolla.settings.accounts 1.0 + +AccountSettingsAgent { + id: root + + property string accountSubtitle: { + var instanceDescription = account.configurationValues("")["instance/Description"] + var detail = instanceDescription ? instanceDescription.toString().trim() : "" + if (detail.length > 0) { + return detail + } + + 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) + } + if (host.length > 0) { + return host + } + + var description = account.configurationValues("")["description"] + var handle = description ? description.toString().trim() : "" + if (handle.length > 0) { + return handle + } + + var displayName = account.displayName ? account.displayName.toString().trim() : "" + if (displayName.length > 0) { + return displayName + } + return host + } + + Account { + id: account + identifier: root.accountId + } + + initialPage: Page { + onStatusChanged: { + if (status === PageStatus.Active && !credentialsUpdater.running) { + settingsDisplay.refreshDescriptionEditor() + } + } + + onPageContainerChanged: { + if (pageContainer == null && !credentialsUpdater.running) { + root.delayDeletion = true + settingsDisplay.saveAccount() + } + } + + Component.onDestruction: { + if (status == PageStatus.Active) { + settingsDisplay.saveAccount(true) + } + } + + AccountCredentialsUpdater { + id: credentialsUpdater + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: header.height + settingsDisplay.height + Theme.paddingLarge + + StandardAccountSettingsPullDownMenu { + visible: settingsDisplay.accountValid + allowSync: true + onCredentialsUpdateRequested: { + credentialsUpdater.replaceWithCredentialsUpdatePage(root.accountId) + } + onAccountDeletionRequested: { + root.accountDeletionRequested() + pageStack.pop() + } + onSyncRequested: { + settingsDisplay.saveAccountAndSync() + } + } + + PageHeader { + id: header + title: root.accountsHeaderText + description: root.accountSubtitle + } + + FediverseSettingsDisplay { + id: settingsDisplay + anchors.top: header.bottom + accountManager: root.accountManager + accountProvider: root.accountProvider + accountId: root.accountId + + onAccountSaveCompleted: { + root.delayDeletion = false + } + } + + VerticalScrollDecorator {} + } + } +} diff --git a/settings/accounts/ui/fediverse-update.qml b/settings/accounts/ui/fediverse-update.qml new file mode 100644 index 0000000..fe75089 --- /dev/null +++ b/settings/accounts/ui/fediverse-update.qml @@ -0,0 +1,229 @@ +/* + * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +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.fediverse 1.0 + +AccountCredentialsAgent { + id: root + + property bool _started + + readonly property string callbackUri: "http://ipv4.jolla.com/online/status.html" + readonly property string defaultServerHost: "mastodon.social" + + function normalizeApiHost(rawHost) { + var host = rawHost ? rawHost.trim() : "" + if (host.length === 0) { + host = defaultServerHost + } + + host = host.replace(/^https?:\/\//i, "") + var pathSeparator = host.indexOf("/") + if (pathSeparator !== -1) { + host = host.substring(0, pathSeparator) + } + host = host.replace(/\/+$/, "") + + if (host.length === 0) { + host = defaultServerHost + } + return "https://" + host.toLowerCase() + } + + function _valueFromServiceConfig(config, key) { + return config && config[key] ? config[key].toString() : "" + } + + function _extractAccountName(responseData) { + if (!responseData) { + return "" + } + + var candidates = [ + "AccountUsername", + "UserName", + "user_name", + "acct", + "username", + "preferred_username", + "login", + "ScreenName" + ] + for (var i = 0; i < candidates.length; ++i) { + var value = responseData[candidates[i]] + if (value) { + var userName = value.toString().trim() + if (userName.length > 0) { + return userName + } + } + } + + return "" + } + + function _formatFediverseAccountId(accountName, apiHost) { + var value = accountName ? accountName.toString().trim() : "" + if (value.length === 0) { + return "" + } + + value = value.replace(/^@+/, "") + if (value.indexOf("@") !== -1) { + return "@" + value + } + + var host = apiHost.replace(/^https?:\/\//i, "") + if (host.length === 0) { + return "" + } + + return "@" + value + "@" + host + } + + function _extractAccessToken(responseData) { + if (!responseData) { + return "" + } + + var token = responseData["AccessToken"] + if (!token || token.toString().trim().length === 0) { + token = responseData["access_token"] + } + return token ? token.toString().trim() : "" + } + + function _isFediverseAccountId(value) { + var text = value ? value.toString().trim() : "" + return /^@[^@]+@[^@]+$/.test(text) + } + + function _completeUpdate() { + root.credentialsUpdated(root.accountId) + root.goToEndDestination() + } + + function _saveDescription(description) { + if (description.length > 0) { + account.setConfigurationValue("", "description", description) + if (_isFediverseAccountId(description)) { + account.setConfigurationValue("", "default_credentials_username", description) + } + } + account.sync() + _completeUpdate() + } + + function _updateDescription(responseData) { + var config = account.configurationValues("fediverse-microblog") + var apiHost = normalizeApiHost(_valueFromServiceConfig(config, "api/Host")) + var description = _formatFediverseAccountId(_extractAccountName(responseData), apiHost) + if (description.length > 0) { + _saveDescription(description) + return + } + + var accessToken = _extractAccessToken(responseData) + if (accessToken.length === 0) { + _completeUpdate() + return + } + + var xhr = new XMLHttpRequest() + xhr.onreadystatechange = function() { + if (xhr.readyState !== XMLHttpRequest.DONE) { + return + } + + var fetchedDescription = "" + if (xhr.status >= 200 && xhr.status < 300) { + try { + var response = JSON.parse(xhr.responseText) + fetchedDescription = _formatFediverseAccountId(_extractAccountName(response), apiHost) + } catch (err) { + } + } + + if (fetchedDescription.length > 0) { + _saveDescription(fetchedDescription) + } else { + _completeUpdate() + } + } + + xhr.open("GET", apiHost + "/api/v1/accounts/verify_credentials") + xhr.setRequestHeader("Authorization", "Bearer " + accessToken) + xhr.send() + } + + function _startUpdate() { + if (_started || initialPage.status !== PageStatus.Active || account.status !== Account.Initialized) { + return + } + + var config = account.configurationValues("fediverse-microblog") + var apiHost = normalizeApiHost(_valueFromServiceConfig(config, "api/Host")) + var oauthHost = _valueFromServiceConfig(config, "auth/oauth2/web_server/Host") + if (oauthHost.length === 0) { + oauthHost = apiHost.replace(/^https?:\/\//i, "") + } + + var clientId = _valueFromServiceConfig(config, "auth/oauth2/web_server/ClientId") + var clientSecret = _valueFromServiceConfig(config, "auth/oauth2/web_server/ClientSecret") + if (clientId.length === 0 || clientSecret.length === 0) { + //% "Missing Fediverse OAuth client credentials" + credentialsUpdateError(qsTrId("settings-accounts-fediverse-la-missing_client_credentials")) + return + } + + _started = true + + var sessionData = { + "ClientId": clientId, + "ClientSecret": clientSecret, + "Host": oauthHost, + "AuthPath": "oauth/authorize", + "TokenPath": "oauth/token", + "ResponseType": "code", + "Scope": ["read", "write"], + "RedirectUri": callbackUri + } + initialPage.prepareAccountCredentialsUpdate(account, root.accountProvider, "fediverse-microblog", sessionData) + } + + Account { + id: account + identifier: root.accountId + + onStatusChanged: { + root._startUpdate() + } + } + + initialPage: OAuthAccountSetupPage { + onStatusChanged: { + root._startUpdate() + } + + onAccountCredentialsUpdated: { + root._updateDescription(responseData) + } + + onAccountCredentialsUpdateError: { + root.credentialsUpdateError(errorMessage) + } + + onPageContainerChanged: { + if (pageContainer == null) { + cancelSignIn() + } + } + } +} diff --git a/settings/accounts/ui/fediverse.qml b/settings/accounts/ui/fediverse.qml new file mode 100644 index 0000000..3f5968a --- /dev/null +++ b/settings/accounts/ui/fediverse.qml @@ -0,0 +1,679 @@ +/* + * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +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.fediverse 1.0 + +AccountCreationAgent { + id: root + + property Item _oauthPage + property Item _settingsDialog + property QtObject _accountSetup + + property string _pendingApiHost + property var _pendingInstanceContext: ({}) + property bool _registering + + readonly property string callbackUri: "http://ipv4.jolla.com/online/status.html" + readonly property string defaultServerHost: "mastodon.social" + readonly property string defaultApiHost: normalizeApiHost(defaultServerHost) + readonly property string genericIconPath: "image://theme/icon-l-fediverse" + + function normalizeApiHost(rawHost) { + var host = rawHost ? rawHost.trim() : "" + if (host.length === 0) { + return "" + } + + host = host.replace(/^https?:\/\//i, "") + var pathSeparator = host.indexOf("/") + if (pathSeparator !== -1) { + host = host.substring(0, pathSeparator) + } + host = host.replace(/\/+$/, "") + + if (host.length === 0) { + return "" + } + return "https://" + host.toLowerCase() + } + + function oauthHost(apiHost) { + return apiHost.replace(/^https?:\/\//i, "") + } + + function _trimmedString(value) { + return value ? value.toString().trim() : "" + } + + function _normalizeResourceUrl(rawUrl, apiHost) { + var value = _trimmedString(rawUrl) + if (value.length === 0) { + return "" + } + if (/^https?:\/\//i.test(value)) { + return value + } + if (value.indexOf("//") === 0) { + return "https:" + value + } + if (value.charAt(0) === "/") { + return apiHost + value + } + return apiHost + "/" + value.replace(/^\/+/, "") + } + + function _displayName(apiHost) { + return oauthHost(apiHost) + } + + function _fallbackDisplayName(apiHost) { + var display = _displayName(apiHost) + if (display.length > 0) { + return display + } + return _displayName(defaultApiHost) + } + + function _instanceContext(apiHost) { + var normalizedApiHost = normalizeApiHost(apiHost) + var title = _fallbackDisplayName(normalizedApiHost) + return { + "apiHost": normalizedApiHost, + "oauthHost": oauthHost(normalizedApiHost), + "instanceTitle": title.length > 0 ? title : qsTrId("settings-accounts-fediverse-la-provider_name"), + "instanceDescription": "", + "instanceIconUrl": "", + "instanceIconPath": instanceIconCache.cachedIconPath(normalizedApiHost) + } + } + + function _extractInstanceTitle(responseData, apiHost) { + var candidates = [ + responseData ? responseData.title : "", + responseData ? responseData.name : "", + responseData ? responseData.uri : "" + ] + for (var i = 0; i < candidates.length; ++i) { + var value = _trimmedString(candidates[i]) + if (value.length > 0) { + return value + } + } + return _fallbackDisplayName(apiHost) + } + + function _extractInstanceDescription(responseData) { + var candidates = [ + responseData ? responseData.short_description : "", + responseData ? responseData.description : "", + responseData ? responseData.shortDescription : "" + ] + for (var i = 0; i < candidates.length; ++i) { + var value = _trimmedString(candidates[i]) + if (value.length > 0) { + return value + } + } + return "" + } + + function _extractInstanceIconUrl(responseData, apiHost) { + if (!responseData) { + return "" + } + + if (responseData.thumbnail) { + if (typeof responseData.thumbnail === "string") { + return _normalizeResourceUrl(responseData.thumbnail, apiHost) + } + if (responseData.thumbnail.url) { + return _normalizeResourceUrl(responseData.thumbnail.url, apiHost) + } + if (responseData.thumbnail.static_url) { + return _normalizeResourceUrl(responseData.thumbnail.static_url, apiHost) + } + } + + if (responseData.icon) { + if (typeof responseData.icon === "string") { + return _normalizeResourceUrl(responseData.icon, apiHost) + } + if (responseData.icon.url) { + return _normalizeResourceUrl(responseData.icon.url, apiHost) + } + } + + return "" + } + + function _showRegistrationError(message, busyPage) { + _registering = false + accountCreationError(message) + if (busyPage) { + busyPage.state = "info" + busyPage.infoDescription = message + busyPage.infoExtraDescription = "" + busyPage.infoButtonText = "" + } + } + + function _showOAuthPage(context) { + _registering = false + if (_oauthPage != null) { + _oauthPage.cancelSignIn() + _oauthPage.destroy() + } + _oauthPage = oAuthComponent.createObject(root, { "context": context }) + pageStack.replace(_oauthPage) + } + + function _registerClientApplication(context, busyPage) { + if (_registering) { + return + } + _registering = true + + if (context.instanceIconUrl.length > 0 && context.instanceIconPath.length === 0) { + instanceIconCache.cacheIcon(context.apiHost, context.instanceIconUrl) + } + + var xhr = new XMLHttpRequest() + xhr.onreadystatechange = function() { + if (xhr.readyState !== XMLHttpRequest.DONE) { + return + } + + if (xhr.status < 200 || xhr.status >= 300) { + _showRegistrationError(qsTrId("settings-accounts-fediverse-la-register_app_failed").arg(context.apiHost), busyPage) + return + } + + var response + try { + response = JSON.parse(xhr.responseText) + } catch (err) { + _showRegistrationError(qsTrId("settings-accounts-fediverse-la-invalid_app_registration_response"), busyPage) + return + } + + if (!response.client_id || !response.client_secret) { + _showRegistrationError(qsTrId("settings-accounts-fediverse-la-app_registration_missing_credentials"), busyPage) + return + } + + context.clientId = response.client_id + context.clientSecret = response.client_secret + _showOAuthPage(context) + } + + var postData = [] + postData.push("client_name=" + encodeURIComponent(qsTrId("settings-accounts-fediverse-la-client_name"))) + postData.push("redirect_uris=" + encodeURIComponent(callbackUri)) + postData.push("scopes=" + encodeURIComponent("read write")) + postData.push("website=" + encodeURIComponent("https://sailfishos.org")) + + xhr.open("POST", context.apiHost + "/api/v1/apps") + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") + xhr.send(postData.join("&")) + } + + function _discoverInstanceContext(apiHost, busyPage) { + var context = _instanceContext(apiHost) + _pendingInstanceContext = context + + function continueWithContext() { + _pendingInstanceContext = context + _registerClientApplication(context, busyPage) + } + + function loadEndpoint(paths, index) { + if (index >= paths.length) { + continueWithContext() + return + } + + var xhr = new XMLHttpRequest() + xhr.onreadystatechange = function() { + if (xhr.readyState !== XMLHttpRequest.DONE) { + return + } + + if (xhr.status >= 200 && xhr.status < 300) { + try { + var response = JSON.parse(xhr.responseText) + context.instanceTitle = _extractInstanceTitle(response, context.apiHost) + context.instanceDescription = _extractInstanceDescription(response) + context.instanceIconUrl = _extractInstanceIconUrl(response, context.apiHost) + if (context.instanceIconUrl.length > 0) { + var cachedPath = instanceIconCache.cachedIconPath(context.apiHost) + if (cachedPath.length > 0) { + context.instanceIconPath = cachedPath + } + } + } catch (err) { + } + continueWithContext() + return + } + + loadEndpoint(paths, index + 1) + } + + xhr.open("GET", context.apiHost + paths[index]) + xhr.send() + } + + loadEndpoint(["/api/v2/instance", "/api/v1/instance"], 0) + } + + function _extractAccountName(responseData) { + if (!responseData) { + return "" + } + + var candidates = [ + "AccountUsername", + "UserName", + "user_name", + "acct", + "username", + "preferred_username", + "login", + "ScreenName" + ] + for (var i = 0; i < candidates.length; ++i) { + var value = responseData[candidates[i]] + if (value) { + var userName = value.toString().trim() + if (userName.length > 0) { + return userName + } + } + } + + return "" + } + + function _formatFediverseAccountId(accountName, apiHost) { + var value = accountName ? accountName.toString().trim() : "" + if (value.length === 0) { + return "" + } + + value = value.replace(/^@+/, "") + if (value.indexOf("@") !== -1) { + return "@" + value + } + + var host = oauthHost(apiHost) + if (host.length === 0) { + return "" + } + + return "@" + value + "@" + host + } + + function _isFediverseAccountId(value) { + var text = value ? value.toString().trim() : "" + return /^@[^@]+@[^@]+$/.test(text) + } + + function _extractAccessToken(responseData) { + if (!responseData) { + return "" + } + + var token = responseData["AccessToken"] + if (!token || token.toString().trim().length === 0) { + token = responseData["access_token"] + } + return token ? token.toString().trim() : "" + } + + function _handleAccountCreated(accountId, context, responseData) { + var props = { + "accountId": accountId, + "apiHost": context.apiHost, + "oauthHost": context.oauthHost, + "clientId": context.clientId, + "clientSecret": context.clientSecret, + "accessToken": _extractAccessToken(responseData), + "accountDescription": _formatFediverseAccountId(_extractAccountName(responseData), context.apiHost), + "instanceTitle": context.instanceTitle, + "instanceDescription": context.instanceDescription, + "instanceIconUrl": context.instanceIconUrl, + "instanceIconPath": context.instanceIconPath + } + _accountSetup = accountSetupComponent.createObject(root, props) + _accountSetup.done.connect(function() { + accountCreated(accountId) + _goToSettings(accountId) + }) + _accountSetup.error.connect(function() { + accountCreationError(qsTrId("settings-accounts-fediverse-la-account_setup_failed")) + }) + } + + function _goToSettings(accountId) { + if (_settingsDialog != null) { + _settingsDialog.destroy() + } + _settingsDialog = settingsComponent.createObject(root, {"accountId": accountId}) + pageStack.replace(_settingsDialog) + } + + FediverseInstanceIconCache { + id: instanceIconCache + + onIconReady: { + var normalizedHost = root.normalizeApiHost(apiHost) + if (root._pendingInstanceContext.apiHost === normalizedHost) { + root._pendingInstanceContext.instanceIconPath = iconPath + } + if (root._accountSetup && root._accountSetup.apiHost === normalizedHost) { + root._accountSetup.updateInstanceIcon(iconPath) + } + } + } + + initialPage: Dialog { + id: setupDialog + + property string normalizedHost: root.normalizeApiHost(instanceField.text) + + canAccept: !root._registering + acceptDestinationAction: PageStackAction.Push + acceptDestination: busyComponent + + onAccepted: { + root._pendingApiHost = normalizedHost.length > 0 ? normalizedHost : root.defaultApiHost + } + + DialogHeader { + id: header + acceptText: qsTrId("settings-accounts-common-bt-sign_in") + } + + Column { + anchors.top: header.bottom + anchors.topMargin: Theme.paddingLarge + spacing: Theme.paddingLarge + width: parent.width + + Row { + x: Theme.horizontalPageMargin + width: parent.width - x * 2 + spacing: Theme.paddingMedium + + Image { + id: promptIcon + width: Theme.iconSizeMedium + height: Theme.iconSizeMedium + source: root.genericIconPath + fillMode: Image.PreserveAspectFit + sourceSize.width: width + sourceSize.height: height + } + + Label { + width: parent.width - promptIcon.width - parent.spacing + wrapMode: Text.Wrap + color: Theme.highlightColor + text: qsTrId("settings-accounts-fediverse-la-enter_server_then_sign_in") + } + } + + TextField { + id: instanceField + x: Theme.horizontalPageMargin + width: parent.width - x * 2 + label: qsTrId("settings-accounts-fediverse-la-server") + placeholderText: root.defaultServerHost + inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhUrlCharactersOnly + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: { + if (setupDialog.canAccept) { + setupDialog.accept() + } + } + } + } + } + + Component { + id: busyComponent + AccountBusyPage { + busyDescription: qsTrId("settings-accounts-fediverse-la-preparing_sign_in") + onStatusChanged: { + if (status === PageStatus.Active && root._pendingApiHost.length > 0) { + root._discoverInstanceContext(root._pendingApiHost, this) + } + } + } + } + + Component { + id: oAuthComponent + OAuthAccountSetupPage { + property var context + + Component.onCompleted: { + var sessionData = { + "ClientId": context.clientId, + "ClientSecret": context.clientSecret, + "Host": context.oauthHost, + "AuthPath": "oauth/authorize", + "TokenPath": "oauth/token", + "ResponseType": "code", + "Scope": ["read", "write"], + "RedirectUri": root.callbackUri + } + prepareAccountCreation(root.accountProvider, "fediverse-microblog", sessionData) + } + + onAccountCreated: { + root._handleAccountCreated(accountId, context, responseData) + } + + onAccountCreationError: { + root.accountCreationError(errorMessage) + } + } + } + + Component { + id: accountSetupComponent + QtObject { + id: accountSetup + + property int accountId + property string apiHost + property string oauthHost + property string clientId + property string clientSecret + property string accessToken + property string accountDescription + property string instanceTitle + property string instanceDescription + property string instanceIconUrl + property string instanceIconPath + property bool hasConfigured + + signal done() + signal error() + + property Account newAccount: Account { + identifier: accountSetup.accountId + + onStatusChanged: { + if (status === Account.Initialized || status === Account.Synced) { + if (!accountSetup.hasConfigured) { + accountSetup.configure() + } else { + accountSetup.done() + } + } else if (status === Account.Invalid && accountSetup.hasConfigured) { + accountSetup.error() + } + } + } + + function _serviceNames() { + return ["fediverse-microblog", "fediverse-notifications", "fediverse-sharing"] + } + + function _effectiveInstanceTitle() { + var title = root._trimmedString(instanceTitle) + return title.length > 0 ? title : qsTrId("settings-accounts-fediverse-la-provider_name") + } + + function _effectiveIconPath() { + return root._trimmedString(instanceIconPath).length > 0 ? instanceIconPath : root.genericIconPath + } + + function _applyIconPath(iconPath) { + instanceIconPath = iconPath + newAccount.setConfigurationValue("", "iconPath", _effectiveIconPath()) + var services = _serviceNames() + for (var i = 0; i < services.length; ++i) { + newAccount.setConfigurationValue(services[i], "iconPath", _effectiveIconPath()) + } + if (hasConfigured) { + newAccount.sync() + } + } + + function updateInstanceIcon(iconPath) { + if (root._trimmedString(iconPath).length === 0) { + return + } + _applyIconPath(iconPath) + } + + function configure() { + hasConfigured = true + + var services = _serviceNames() + var providerDisplayName = _effectiveInstanceTitle() + newAccount.displayName = providerDisplayName + + newAccount.setConfigurationValue("", "api/Host", apiHost) + newAccount.setConfigurationValue("", "FeedViewAutoSync", true) + newAccount.setConfigurationValue("", "instance/Title", providerDisplayName) + newAccount.setConfigurationValue("", "instance/Description", root._trimmedString(instanceDescription)) + newAccount.setConfigurationValue("", "instance/IconUrl", root._trimmedString(instanceIconUrl)) + _applyIconPath(_effectiveIconPath()) + + if (accountDescription.length > 0) { + newAccount.setConfigurationValue("", "description", accountDescription) + if (root._isFediverseAccountId(accountDescription)) { + newAccount.setConfigurationValue("", "default_credentials_username", accountDescription) + } + } + + for (var i = 0; i < services.length; ++i) { + var service = services[i] + newAccount.setConfigurationValue(service, "api/Host", apiHost) + newAccount.setConfigurationValue(service, "auth/oauth2/web_server/Host", oauthHost) + newAccount.setConfigurationValue(service, "auth/oauth2/web_server/AuthPath", "oauth/authorize") + newAccount.setConfigurationValue(service, "auth/oauth2/web_server/TokenPath", "oauth/token") + newAccount.setConfigurationValue(service, "auth/oauth2/web_server/ResponseType", "code") + newAccount.setConfigurationValue(service, "auth/oauth2/web_server/RedirectUri", root.callbackUri) + newAccount.setConfigurationValue(service, "auth/oauth2/web_server/Scope", ["read", "write"]) + newAccount.setConfigurationValue(service, "auth/oauth2/web_server/ClientId", clientId) + newAccount.setConfigurationValue(service, "auth/oauth2/web_server/ClientSecret", clientSecret) + } + + for (var j = 0; j < services.length; ++j) { + newAccount.enableWithService(services[j]) + } + + if (instanceIconUrl.length > 0 && instanceIconPath.length === 0) { + instanceIconCache.cacheIcon(apiHost, instanceIconUrl) + } + + if (accountDescription.length > 0 || accessToken.length === 0) { + newAccount.sync() + return + } + + var xhr = new XMLHttpRequest() + xhr.onreadystatechange = function() { + if (xhr.readyState !== XMLHttpRequest.DONE) { + return + } + + if (xhr.status >= 200 && xhr.status < 300) { + try { + var response = JSON.parse(xhr.responseText) + var fetchedDescription = root._formatFediverseAccountId(root._extractAccountName(response), apiHost) + if (fetchedDescription.length > 0) { + accountDescription = fetchedDescription + newAccount.setConfigurationValue("", "description", fetchedDescription) + if (root._isFediverseAccountId(fetchedDescription)) { + newAccount.setConfigurationValue("", "default_credentials_username", fetchedDescription) + } + } + } catch (err) { + } + } + + newAccount.sync() + } + + xhr.open("GET", apiHost + "/api/v1/accounts/verify_credentials") + xhr.setRequestHeader("Authorization", "Bearer " + accessToken) + xhr.send() + } + } + } + + Component { + id: settingsComponent + Dialog { + property alias accountId: settingsDisplay.accountId + + acceptDestination: root.endDestination + acceptDestinationAction: root.endDestinationAction + acceptDestinationProperties: root.endDestinationProperties + acceptDestinationReplaceTarget: root.endDestinationReplaceTarget + backNavigation: false + + onAccepted: { + root.delayDeletion = true + settingsDisplay.saveAccountAndSync() + } + + SilicaFlickable { + anchors.fill: parent + contentHeight: header.height + settingsDisplay.height + + DialogHeader { + id: header + } + + FediverseSettingsDisplay { + id: settingsDisplay + anchors.top: header.bottom + accountManager: root.accountManager + accountProvider: root.accountProvider + autoEnableAccount: true + + onAccountSaveCompleted: { + root.delayDeletion = false + } + } + + VerticalScrollDecorator {} + } + } + } +} diff --git a/settings/accounts/ui/mastodon-settings.qml b/settings/accounts/ui/mastodon-settings.qml deleted file mode 100644 index 0538e44..0000000 --- a/settings/accounts/ui/mastodon-settings.qml +++ /dev/null @@ -1,107 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -import QtQuick 2.0 -import Sailfish.Silica 1.0 -import Sailfish.Accounts 1.0 -import com.jolla.settings.accounts 1.0 - -AccountSettingsAgent { - id: root - - property string accountSubtitle: { - var description = account.configurationValues("")["description"] - var detail = description ? description.toString().trim() : "" - if (detail.length > 0) { - return detail - } - 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) - } - if (host.length > 0) { - return host - } - var displayName = account.displayName ? account.displayName.toString().trim() : "" - if (displayName.length > 0) { - return displayName - } - return host - } - - Account { - id: account - identifier: root.accountId - } - - initialPage: Page { - onStatusChanged: { - if (status === PageStatus.Active && !credentialsUpdater.running) { - settingsDisplay.refreshDescriptionEditor() - } - } - - onPageContainerChanged: { - if (pageContainer == null && !credentialsUpdater.running) { - root.delayDeletion = true - settingsDisplay.saveAccount() - } - } - - Component.onDestruction: { - if (status == PageStatus.Active) { - settingsDisplay.saveAccount(true) - } - } - - AccountCredentialsUpdater { - id: credentialsUpdater - } - - SilicaFlickable { - anchors.fill: parent - contentHeight: header.height + settingsDisplay.height + Theme.paddingLarge - - StandardAccountSettingsPullDownMenu { - visible: settingsDisplay.accountValid - allowSync: true - onCredentialsUpdateRequested: { - credentialsUpdater.replaceWithCredentialsUpdatePage(root.accountId) - } - onAccountDeletionRequested: { - root.accountDeletionRequested() - pageStack.pop() - } - onSyncRequested: { - settingsDisplay.saveAccountAndSync() - } - } - - PageHeader { - id: header - title: root.accountsHeaderText - description: root.accountSubtitle - } - - MastodonSettingsDisplay { - id: settingsDisplay - anchors.top: header.bottom - accountManager: root.accountManager - accountProvider: root.accountProvider - accountId: root.accountId - - onAccountSaveCompleted: { - root.delayDeletion = false - } - } - - VerticalScrollDecorator {} - } - } -} diff --git a/settings/accounts/ui/mastodon-update.qml b/settings/accounts/ui/mastodon-update.qml deleted file mode 100644 index 2485a83..0000000 --- a/settings/accounts/ui/mastodon-update.qml +++ /dev/null @@ -1,228 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -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 - - property bool _started - - readonly property string callbackUri: "http://ipv4.jolla.com/online/status.html" - - function normalizeApiHost(rawHost) { - var host = rawHost ? rawHost.trim() : "" - if (host.length === 0) { - host = "https://mastodon.social" - } - - host = host.replace(/^https?:\/\//i, "") - var pathSeparator = host.indexOf("/") - if (pathSeparator !== -1) { - host = host.substring(0, pathSeparator) - } - host = host.replace(/\/+$/, "") - - if (host.length === 0) { - host = "mastodon.social" - } - return "https://" + host.toLowerCase() - } - - function _valueFromServiceConfig(config, key) { - return config && config[key] ? config[key].toString() : "" - } - - function _extractAccountName(responseData) { - if (!responseData) { - return "" - } - - var candidates = [ - "AccountUsername", - "UserName", - "user_name", - "acct", - "username", - "preferred_username", - "login", - "ScreenName" - ] - for (var i = 0; i < candidates.length; ++i) { - var value = responseData[candidates[i]] - if (value) { - var userName = value.toString().trim() - if (userName.length > 0) { - return userName - } - } - } - - return "" - } - - function _formatMastodonAccountId(accountName, apiHost) { - var value = accountName ? accountName.toString().trim() : "" - if (value.length === 0) { - return "" - } - - value = value.replace(/^@+/, "") - if (value.indexOf("@") !== -1) { - return "@" + value - } - - var host = apiHost.replace(/^https?:\/\//i, "") - if (host.length === 0) { - return "" - } - - return "@" + value + "@" + host - } - - function _extractAccessToken(responseData) { - if (!responseData) { - return "" - } - - var token = responseData["AccessToken"] - if (!token || token.toString().trim().length === 0) { - token = responseData["access_token"] - } - return token ? token.toString().trim() : "" - } - - function _isMastodonAccountId(value) { - var text = value ? value.toString().trim() : "" - return /^@[^@]+@[^@]+$/.test(text) - } - - function _completeUpdate() { - root.credentialsUpdated(root.accountId) - root.goToEndDestination() - } - - function _saveDescription(description) { - if (description.length > 0) { - account.setConfigurationValue("", "description", description) - if (_isMastodonAccountId(description)) { - account.setConfigurationValue("", "default_credentials_username", description) - } - } - account.sync() - _completeUpdate() - } - - function _updateDescription(responseData) { - var config = account.configurationValues("mastodon-microblog") - var apiHost = normalizeApiHost(_valueFromServiceConfig(config, "api/Host")) - var description = _formatMastodonAccountId(_extractAccountName(responseData), apiHost) - if (description.length > 0) { - _saveDescription(description) - return - } - - var accessToken = _extractAccessToken(responseData) - if (accessToken.length === 0) { - _completeUpdate() - return - } - - var xhr = new XMLHttpRequest() - xhr.onreadystatechange = function() { - if (xhr.readyState !== XMLHttpRequest.DONE) { - return - } - - var fetchedDescription = "" - if (xhr.status >= 200 && xhr.status < 300) { - try { - var response = JSON.parse(xhr.responseText) - fetchedDescription = _formatMastodonAccountId(_extractAccountName(response), apiHost) - } catch (err) { - } - } - - if (fetchedDescription.length > 0) { - _saveDescription(fetchedDescription) - } else { - _completeUpdate() - } - } - - xhr.open("GET", apiHost + "/api/v1/accounts/verify_credentials") - xhr.setRequestHeader("Authorization", "Bearer " + accessToken) - xhr.send() - } - - function _startUpdate() { - if (_started || initialPage.status !== PageStatus.Active || account.status !== Account.Initialized) { - return - } - - var config = account.configurationValues("mastodon-microblog") - var apiHost = normalizeApiHost(_valueFromServiceConfig(config, "api/Host")) - var oauthHost = _valueFromServiceConfig(config, "auth/oauth2/web_server/Host") - if (oauthHost.length === 0) { - oauthHost = apiHost.replace(/^https?:\/\//i, "") - } - - var clientId = _valueFromServiceConfig(config, "auth/oauth2/web_server/ClientId") - var clientSecret = _valueFromServiceConfig(config, "auth/oauth2/web_server/ClientSecret") - if (clientId.length === 0 || clientSecret.length === 0) { - //% "Missing Mastodon OAuth client credentials" - credentialsUpdateError(qsTrId("settings-accounts-mastodon-la-missing_client_credentials")) - return - } - - _started = true - - var sessionData = { - "ClientId": clientId, - "ClientSecret": clientSecret, - "Host": oauthHost, - "AuthPath": "oauth/authorize", - "TokenPath": "oauth/token", - "ResponseType": "code", - "Scope": ["read", "write"], - "RedirectUri": callbackUri - } - initialPage.prepareAccountCredentialsUpdate(account, root.accountProvider, "mastodon-microblog", sessionData) - } - - Account { - id: account - identifier: root.accountId - - onStatusChanged: { - root._startUpdate() - } - } - - initialPage: OAuthAccountSetupPage { - onStatusChanged: { - root._startUpdate() - } - - onAccountCredentialsUpdated: { - root._updateDescription(responseData) - } - - onAccountCredentialsUpdateError: { - root.credentialsUpdateError(errorMessage) - } - - onPageContainerChanged: { - if (pageContainer == null) { - cancelSignIn() - } - } - } -} diff --git a/settings/accounts/ui/mastodon.qml b/settings/accounts/ui/mastodon.qml deleted file mode 100644 index 129fda5..0000000 --- a/settings/accounts/ui/mastodon.qml +++ /dev/null @@ -1,492 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -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 - - property Item _oauthPage - property Item _settingsDialog - property QtObject _accountSetup - - property string _pendingApiHost - property bool _registering - - readonly property string callbackUri: "http://ipv4.jolla.com/online/status.html" - readonly property string defaultApiHost: "https://mastodon.social" - - function normalizeApiHost(rawHost) { - var host = rawHost ? rawHost.trim() : "" - if (host.length === 0) { - return "" - } - - host = host.replace(/^https?:\/\//i, "") - var pathSeparator = host.indexOf("/") - if (pathSeparator !== -1) { - host = host.substring(0, pathSeparator) - } - host = host.replace(/\/+$/, "") - - if (host.length === 0) { - return "" - } - return "https://" + host.toLowerCase() - } - - function oauthHost(apiHost) { - return apiHost.replace(/^https?:\/\//i, "") - } - - function _displayName(apiHost) { - 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) - if (busyPage) { - busyPage.state = "info" - busyPage.infoDescription = message - busyPage.infoExtraDescription = "" - busyPage.infoButtonText = "" - } - } - - function _showOAuthPage(context) { - _registering = false - if (_oauthPage != null) { - _oauthPage.cancelSignIn() - _oauthPage.destroy() - } - _oauthPage = oAuthComponent.createObject(root, { "context": context }) - pageStack.replace(_oauthPage) - } - - function _registerClientApplication(apiHost, busyPage) { - if (_registering) { - return - } - _registering = true - - var xhr = new XMLHttpRequest() - xhr.onreadystatechange = function() { - if (xhr.readyState !== XMLHttpRequest.DONE) { - return - } - - if (xhr.status < 200 || xhr.status >= 300) { - //% "Failed to register Mastodon app for %1" - _showRegistrationError(qsTrId("settings-accounts-mastodon-la-register_app_failed").arg(apiHost), busyPage) - return - } - - var response - try { - response = JSON.parse(xhr.responseText) - } catch (err) { - //% "Invalid Mastodon app registration response" - _showRegistrationError(qsTrId("settings-accounts-mastodon-la-invalid_app_registration_response"), busyPage) - return - } - - if (!response.client_id || !response.client_secret) { - //% "Mastodon app registration did not return credentials" - _showRegistrationError(qsTrId("settings-accounts-mastodon-la-app_registration_missing_credentials"), busyPage) - return - } - - _showOAuthPage({ - "apiHost": apiHost, - "oauthHost": oauthHost(apiHost), - "clientId": response.client_id, - "clientSecret": response.client_secret - }) - } - - var postData = [] - //% "Mastodon in SailfishOS" - postData.push("client_name=" + encodeURIComponent(qsTrId("settings-accounts-mastodon-la-client_name"))) - postData.push("redirect_uris=" + encodeURIComponent(callbackUri)) - postData.push("scopes=" + encodeURIComponent("read write")) - postData.push("website=" + encodeURIComponent("https://sailfishos.org")) - - xhr.open("POST", apiHost + "/api/v1/apps") - xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") - xhr.send(postData.join("&")) - } - - function _extractAccountName(responseData) { - if (!responseData) { - return "" - } - - var candidates = [ - "AccountUsername", - "UserName", - "user_name", - "acct", - "username", - "preferred_username", - "login", - "ScreenName" - ] - for (var i = 0; i < candidates.length; ++i) { - var value = responseData[candidates[i]] - if (value) { - var userName = value.toString().trim() - if (userName.length > 0) { - return userName - } - } - } - - return "" - } - - function _formatMastodonAccountId(accountName, apiHost) { - var value = accountName ? accountName.toString().trim() : "" - if (value.length === 0) { - return "" - } - - value = value.replace(/^@+/, "") - if (value.indexOf("@") !== -1) { - return "@" + value - } - - var host = oauthHost(apiHost) - if (host.length === 0) { - return "" - } - - return "@" + value + "@" + host - } - - function _isMastodonAccountId(value) { - var text = value ? value.toString().trim() : "" - return /^@[^@]+@[^@]+$/.test(text) - } - - function _extractAccessToken(responseData) { - if (!responseData) { - return "" - } - - var token = responseData["AccessToken"] - if (!token || token.toString().trim().length === 0) { - token = responseData["access_token"] - } - return token ? token.toString().trim() : "" - } - - function _handleAccountCreated(accountId, context, responseData) { - var props = { - "accountId": accountId, - "apiHost": context.apiHost, - "oauthHost": context.oauthHost, - "clientId": context.clientId, - "clientSecret": context.clientSecret, - "accessToken": _extractAccessToken(responseData), - "accountDescription": _formatMastodonAccountId(_extractAccountName(responseData), context.apiHost) - } - _accountSetup = accountSetupComponent.createObject(root, props) - _accountSetup.done.connect(function() { - accountCreated(accountId) - _goToSettings(accountId) - }) - _accountSetup.error.connect(function() { - //% "Failed to finish Mastodon account setup" - accountCreationError(qsTrId("settings-accounts-mastodon-la-account_setup_failed")) - }) - } - - function _goToSettings(accountId) { - if (_settingsDialog != null) { - _settingsDialog.destroy() - } - _settingsDialog = settingsComponent.createObject(root, {"accountId": accountId}) - pageStack.replace(_settingsDialog) - } - - initialPage: Dialog { - id: setupDialog - - property string normalizedHost: root.normalizeApiHost(instanceField.text) - - canAccept: !root._registering - acceptDestinationAction: PageStackAction.Push - acceptDestination: busyComponent - - onAccepted: { - root._pendingApiHost = normalizedHost.length > 0 ? normalizedHost : root.defaultApiHost - } - - DialogHeader { - id: header - //% "Sign in" - acceptText: qsTrId("settings-accounts-common-bt-sign_in") - } - - Column { - anchors.top: header.bottom - anchors.topMargin: Theme.paddingLarge - spacing: Theme.paddingLarge - width: parent.width - - Row { - x: Theme.horizontalPageMargin - width: parent.width - x * 2 - spacing: Theme.paddingMedium - - Image { - id: promptIcon - width: Theme.iconSizeMedium - height: Theme.iconSizeMedium - source: "image://theme/icon-l-mastodon" - fillMode: Image.PreserveAspectFit - sourceSize.width: width - sourceSize.height: height - } - - //: Prompt shown in account setup before OAuth sign-in. - Label { - width: parent.width - promptIcon.width - parent.spacing - wrapMode: Text.Wrap - color: Theme.highlightColor - //% "Enter your Mastodon server, then sign in." - text: qsTrId("settings-accounts-mastodon-la-enter_server_then_sign_in") - } - } - - TextField { - id: instanceField - x: Theme.horizontalPageMargin - width: parent.width - x * 2 - //% "Server" - label: qsTrId("settings-accounts-mastodon-la-server") - placeholderText: "mastodon.social" - inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhUrlCharactersOnly - EnterKey.iconSource: "image://theme/icon-m-enter-next" - EnterKey.onClicked: { - if (setupDialog.canAccept) { - setupDialog.accept() - } - } - } - } - } - - Component { - id: busyComponent - AccountBusyPage { - //% "Preparing Mastodon sign-in..." - busyDescription: qsTrId("settings-accounts-mastodon-la-preparing_sign_in") - onStatusChanged: { - if (status === PageStatus.Active && root._pendingApiHost.length > 0) { - root._registerClientApplication(root._pendingApiHost, this) - } - } - } - } - - Component { - id: oAuthComponent - OAuthAccountSetupPage { - property var context - - Component.onCompleted: { - var sessionData = { - "ClientId": context.clientId, - "ClientSecret": context.clientSecret, - "Host": context.oauthHost, - "AuthPath": "oauth/authorize", - "TokenPath": "oauth/token", - "ResponseType": "code", - "Scope": ["read", "write"], - "RedirectUri": root.callbackUri - } - prepareAccountCreation(root.accountProvider, "mastodon-microblog", sessionData) - } - - onAccountCreated: { - root._handleAccountCreated(accountId, context, responseData) - } - - onAccountCreationError: { - root.accountCreationError(errorMessage) - } - } - } - - Component { - id: accountSetupComponent - QtObject { - id: accountSetup - - property int accountId - property string apiHost - property string oauthHost - property string clientId - property string clientSecret - property string accessToken - property string accountDescription - property bool hasConfigured - - signal done() - signal error() - - property Account newAccount: Account { - identifier: accountSetup.accountId - - onStatusChanged: { - if (status === Account.Initialized || status === Account.Synced) { - if (!accountSetup.hasConfigured) { - accountSetup.configure() - } else { - accountSetup.done() - } - } else if (status === Account.Invalid && accountSetup.hasConfigured) { - accountSetup.error() - } - } - } - - function configure() { - hasConfigured = true - - var services = ["mastodon-microblog", "mastodon-notifications", "mastodon-sharing"] - var providerDisplayName = root.accountProvider && root.accountProvider.displayName - ? root.accountProvider.displayName.toString().trim() - : "" - if (providerDisplayName.length === 0) { - //% "Mastodon" - providerDisplayName = qsTrId("settings-accounts-mastodon-la-provider_name") - } - newAccount.displayName = providerDisplayName - - newAccount.setConfigurationValue("", "api/Host", apiHost) - newAccount.setConfigurationValue("", "FeedViewAutoSync", true) - if (accountDescription.length > 0) { - newAccount.setConfigurationValue("", "description", accountDescription) - if (root._isMastodonAccountId(accountDescription)) { - newAccount.setConfigurationValue("", "default_credentials_username", accountDescription) - } - } else { - var hostDisplayName = root._fallbackDisplayName(apiHost) - if (hostDisplayName.length > 0) { - newAccount.setConfigurationValue("", "description", hostDisplayName) - } - } - - for (var i = 0; i < services.length; ++i) { - var service = services[i] - newAccount.setConfigurationValue(service, "api/Host", apiHost) - newAccount.setConfigurationValue(service, "auth/oauth2/web_server/Host", oauthHost) - newAccount.setConfigurationValue(service, "auth/oauth2/web_server/AuthPath", "oauth/authorize") - newAccount.setConfigurationValue(service, "auth/oauth2/web_server/TokenPath", "oauth/token") - newAccount.setConfigurationValue(service, "auth/oauth2/web_server/ResponseType", "code") - newAccount.setConfigurationValue(service, "auth/oauth2/web_server/RedirectUri", root.callbackUri) - newAccount.setConfigurationValue(service, "auth/oauth2/web_server/Scope", ["read", "write"]) - newAccount.setConfigurationValue(service, "auth/oauth2/web_server/ClientId", clientId) - newAccount.setConfigurationValue(service, "auth/oauth2/web_server/ClientSecret", clientSecret) - } - - for (var j = 0; j < services.length; ++j) { - newAccount.enableWithService(services[j]) - } - - if (accountDescription.length > 0 || accessToken.length === 0) { - newAccount.sync() - return - } - - var xhr = new XMLHttpRequest() - xhr.onreadystatechange = function() { - if (xhr.readyState !== XMLHttpRequest.DONE) { - return - } - - if (xhr.status >= 200 && xhr.status < 300) { - try { - var response = JSON.parse(xhr.responseText) - var fetchedDescription = root._formatMastodonAccountId(root._extractAccountName(response), apiHost) - if (fetchedDescription.length > 0) { - accountDescription = fetchedDescription - newAccount.setConfigurationValue("", "description", fetchedDescription) - if (root._isMastodonAccountId(fetchedDescription)) { - newAccount.setConfigurationValue("", "default_credentials_username", fetchedDescription) - } - } - } catch (err) { - } - } - - newAccount.sync() - } - - xhr.open("GET", apiHost + "/api/v1/accounts/verify_credentials") - xhr.setRequestHeader("Authorization", "Bearer " + accessToken) - xhr.send() - } - } - } - - Component { - id: settingsComponent - Dialog { - property alias accountId: settingsDisplay.accountId - - acceptDestination: root.endDestination - acceptDestinationAction: root.endDestinationAction - acceptDestinationProperties: root.endDestinationProperties - acceptDestinationReplaceTarget: root.endDestinationReplaceTarget - backNavigation: false - - onAccepted: { - root.delayDeletion = true - settingsDisplay.saveAccountAndSync() - } - - SilicaFlickable { - anchors.fill: parent - contentHeight: header.height + settingsDisplay.height - - DialogHeader { - id: header - } - - MastodonSettingsDisplay { - id: settingsDisplay - anchors.top: header.bottom - accountManager: root.accountManager - accountProvider: root.accountProvider - autoEnableAccount: true - - onAccountSaveCompleted: { - root.delayDeletion = false - } - } - - VerticalScrollDecorator {} - } - } - } - -} -- cgit v1.2.3