feat(ProfileShowcase): Align UI save flow according to backend response

- Added loading state in dirty toast message.
- Added store connections in `MyProfileView` and save loading logic.
- Added toasts notifications.

Closes #13950

a
This commit is contained in:
Noelia 2024-03-14 17:38:30 +01:00 committed by Noelia
parent 1d15398ea7
commit d35c0bd3d1
5 changed files with 168 additions and 56 deletions

View File

@ -45,14 +45,38 @@ QtObject {
readonly property bool isFirstShowcaseInteraction: localAccountSettings.isFirstShowcaseInteraction
onUserDeclinedBackupBannerChanged: {
if (userDeclinedBackupBanner !== localAccountSensitiveSettings.userDeclinedBackupBanner) {
localAccountSensitiveSettings.userDeclinedBackupBanner = userDeclinedBackupBanner
property var details: Utils.getContactDetailsAsJson(pubkey)
// The following signals wrap the settings / preferences save request responses in one unique result (identity + preferences result)
signal profileSettingsSaveSucceeded()
signal profileSettingsSaveFailed()
// The following signals describe separate save request responses between identity and preferences
signal profileIdentitySaveSucceeded()
signal profileIdentitySaveFailed()
signal profileShowcasePreferencesSaveSucceeded()
signal profileShowcasePreferencesSaveFailed()
readonly property Connections profileModuleConnections: Connections {
target: root.profileModule
function onProfileIdentitySaveSucceeded() {
root.profileIdentitySaveSucceeded()
}
function onProfileIdentitySaveFailed() {
root.profileIdentitySaveFailed()
}
function onProfileShowcasePreferencesSaveSucceeded() {
root.profileShowcasePreferencesSaveSucceeded()
}
function onProfileShowcasePreferencesSaveFailed() {
root.profileShowcasePreferencesSaveFailed()
}
}
property var details: Utils.getContactDetailsAsJson(pubkey)
function getQrCodeSource(text) {
return globalUtils.qrCode(text)
}
@ -67,12 +91,12 @@ QtObject {
"displayName": displayName,
"bio": bio,
"image": source ? {
"source": source,
"aX": aX,
"aY": aY,
"bX": bX,
"bY": bY
} : null
"source": source,
"aX": aX,
"aY": aY,
"bX": bX,
"bY": bY
} : null
}
let json = JSON.stringify(identityInfo)
root.profileModule.saveProfileIdentity(json)
@ -102,28 +126,6 @@ QtObject {
root.profileModule.setIsFirstShowcaseInteraction()
}
signal profileIdentitySaveSucceeded()
signal profileIdentitySaveFailed()
signal profileShowcasePreferencesSaveSucceeded()
signal profileShowcasePreferencesSaveFailed()
readonly property Connections profileModuleConnections: Connections {
target: root.profileModule
function onProfileIdentitySaveSucceeded() {
root.profileIdentitySaveSucceeded()
}
function onProfileIdentitySaveFailed() {
root.profileIdentitySaveFailed()
}
function onProfileShowcasePreferencesSaveSucceeded() {
root.profileShowcasePreferencesSaveSucceeded()
}
function onProfileShowcasePreferencesSaveFailed() {
root.profileShowcasePreferencesSaveFailed()
}
}
// Social links related: All to be removed: Deprecated --> Issue #13688
function containsSocialLink(text, url) {
return root.profileModule.containsSocialLink(text, url)
@ -153,4 +155,10 @@ QtObject {
root.profileModule.saveSocialLinks(silent)
}
// End of social links to be removed
onUserDeclinedBackupBannerChanged: {
if (userDeclinedBackupBanner !== localAccountSensitiveSettings.userDeclinedBackupBanner) {
localAccountSensitiveSettings.userDeclinedBackupBanner = userDeclinedBackupBanner
}
}
}

View File

@ -71,7 +71,8 @@ SettingsContentBase {
toast.saveChangesTooltipText: saveChangesButtonEnabled ? "" : qsTr("Invalid changes made to Identity")
autoscrollWhenDirty: profileTabBar.currentIndex === MyProfileView.Identity
toast.loading: priv.expectedBackendResponses > 0
onResetChangesClicked: priv.reset()
onSaveChangesClicked: priv.save()
@ -124,12 +125,12 @@ SettingsContentBase {
readonly property var priv: QtObject {
id: priv
property bool hasAnyProfileShowcaseChanges: showcaseModels.dirty
property bool isIdentityTabDirty: (!descriptionPanel.isEnsName &&
descriptionPanel.displayName.text !== profileStore.displayName) ||
descriptionPanel.bio.text !== profileStore.bio ||
profileStore.socialLinksDirty ||
profileHeader.icon !== profileStore.profileLargeImage
readonly property bool hasAnyProfileShowcaseChanges: showcaseModels.dirty
readonly property bool isIdentityTabDirty: (!descriptionPanel.isEnsName &&
descriptionPanel.displayName.text !== profileStore.displayName) ||
descriptionPanel.bio.text !== profileStore.bio ||
profileStore.socialLinksDirty ||
profileHeader.icon !== profileStore.profileLargeImage
property ProfileShowcaseModels showcaseModels: ProfileShowcaseModels {
communitiesSourceModel: root.communitiesModel
@ -147,6 +148,62 @@ SettingsContentBase {
socialLinksSourceModel: root.profileStore.showcasePreferencesSocialLinksModel
}
// Used to track which are the expected backend responses (they can be 0, 1 or 2) depending on the dirty changes
property int expectedBackendResponses: 0
property bool saveRequestFailed: false
// Maximum save action waiting time controller.
// Backend response must be received before, otherwise it will be considered
// a failure and UI will be released.
property Timer saveLoadingTimeout : Timer {
interval: 5000
repeat: false
running: toast.active && toast.loading
onTriggered: {
// Forcing a failure
if(priv.expectedBackendResponses > 0) {
root.profileStore.profileSettingsSaveFailed()
priv.expectedBackendResponses = 0
}
}
}
// Save backend response received:
property Connections profileStoreConnection: Connections {
target: root.profileStore
function onProfileIdentitySaveSucceeded() {
priv.checkSaveResult(false)
}
function onProfileIdentitySaveFailed() {
priv.checkSaveResult(true)
}
function onProfileShowcasePreferencesSaveSucceeded() {
priv.checkSaveResult(false)
}
function onProfileShowcasePreferencesSaveFailed() {
priv.checkSaveResult(true)
}
}
function checkSaveResult(isFailure) {
priv.expectedBackendResponses--
if(isFailure)
priv.saveRequestFailed = isFailure
if(priv.expectedBackendResponses == 0) {
if(priv.saveRequestFailed || isFailure) {
root.profileStore.profileSettingsSaveFailed()
} else {
root.profileStore.profileSettingsSaveSucceeded()
}
}
}
function reset() {
descriptionPanel.displayName.text = Qt.binding(() => { return profileStore.displayName })
descriptionPanel.bio.text = Qt.binding(() => { return profileStore.bio })
@ -154,25 +211,41 @@ SettingsContentBase {
profileHeader.icon = Qt.binding(() => { return profileStore.profileLargeImage })
priv.showcaseModels.revert()
priv.saveRequestFailed = false
priv.expectedBackendResponses = 0
root.profileStore.requestProfileShowcasePreferences()
}
function save() {
// Accounts, Communities, Assets, Collectibles and social links info
if (hasAnyProfileShowcaseChanges) {
root.profileStore.saveProfileShowcasePreferences(showcaseModels.buildJSONModelsCurrentState())
// IMPORTANT: Save implies 2 calls in backend but 1 result in UI so the order in current save method is relevant
// First save stage: Review which are the expected responses before calling backend
priv.expectedBackendResponses = 0
priv.saveRequestFailed = false
if(hasAnyProfileShowcaseChanges) {
priv.expectedBackendResponses++
}
if (isIdentityTabDirty ) {
priv.expectedBackendResponses++
}
// Identity info
if (isIdentityTabDirty) {
root.profileStore.saveProfileIdentity(descriptionPanel.displayName.text,
descriptionPanel.bio.text.trim(),
profileHeader.icon,
profileHeader.cropRect.x,
profileHeader.cropRect.y,
(profileHeader.cropRect.x + profileHeader.cropRect.width),
(profileHeader.cropRect.y + profileHeader.cropRect.height))
profileHeader.icon = Qt.binding(() => { return profileStore.profileLargeImage })
// Second save stage: Ready to call backend
if(priv.expectedBackendResponses > 0) {
// Accounts, Communities, Assets, Collectibles and social links info
if (hasAnyProfileShowcaseChanges) {
root.profileStore.saveProfileShowcasePreferences(showcaseModels.buildJSONModelsCurrentState())
}
// Identity info
if (isIdentityTabDirty) {
root.profileStore.saveProfileIdentity(descriptionPanel.displayName.text,
descriptionPanel.bio.text.trim(),
profileHeader.icon,
profileHeader.cropRect.x,
profileHeader.cropRect.y,
(profileHeader.cropRect.x + profileHeader.cropRect.width),
(profileHeader.cropRect.y + profileHeader.cropRect.height))
profileHeader.icon = Qt.binding(() => { return profileStore.profileLargeImage })
}
}
}
}
@ -254,7 +327,7 @@ SettingsContentBase {
onChangePositionRequested: function (from, to) {
priv.showcaseModels.changeAccountPosition(from, to)
}
onSetVisibilityRequested: function (key, toVisibility) {
priv.showcaseModels.setAccountVisibility(key, toVisibility)

View File

@ -89,6 +89,7 @@ Item {
rootStore: appMain.rootStore
rootChatStore: appMain.rootChatStore
communityTokensStore: appMain.communityTokensStore
profileStore: appMain.rootStore.profileSectionStore.profileStore
sendModalPopup: sendModal
}

View File

@ -6,6 +6,7 @@ import AppLayouts.Wallet 1.0
import AppLayouts.stores 1.0
import AppLayouts.Chat.stores 1.0 as ChatStores
import AppLayouts.Profile.stores 1.0
import shared.stores 1.0 as SharedStores
@ -31,6 +32,7 @@ QtObject {
required property RootStore rootStore
required property ChatStores.RootStore rootChatStore
required property SharedStores.CommunityTokensStore communityTokensStore
required property ProfileStore profileStore
// Properties:
required property var sendModalPopup
@ -39,6 +41,7 @@ QtObject {
readonly property string viewOptimismExplorerText: qsTr("View on Optimism Explorer")
readonly property string checkmarkCircleAssetName: "checkmark-circle"
readonly property string crownOffAssetName: "crown-off"
readonly property string warningAssetName: "warning"
// Community Transfer Ownership related toasts:
readonly property Connections _communityTokensStoreConnections: Connections {
@ -76,7 +79,7 @@ QtObject {
} else if (status === Constants.ContractTransactionStatus.Failed) {
Global.displayToastMessage(qsTr("%1 smart contract update failed").arg(communityName),
root.viewOptimismExplorerText,
"warning",
root.warningAssetName,
false,
Constants.ephemeralNotificationType.danger,
url)
@ -205,6 +208,29 @@ QtObject {
}
}
// Profile settings related toasts:
readonly property Connections _profileStoreConnections: Connections {
target: root.profileStore
function onProfileSettingsSaveSucceeded() {
Global.displayToastMessage(qsTr("Profile changes saved"),
"",
root.checkmarkCircleAssetName,
false,
Constants.ephemeralNotificationType.success,
"")
}
function onProfileSettingsSaveFailed() {
Global.displayToastMessage(qsTr("Profile changes could not be saved"),
"",
root.warningAssetName,
false,
Constants.ephemeralNotificationType.danger,
"")
}
}
// It will cover all specific actions (different than open external links) that can be done after clicking toast link text
function doAction(actionType, actionData) {
switch(actionType) {

View File

@ -13,6 +13,7 @@ import StatusQ.Controls 0.1
Rectangle {
id: root
property bool loading: false
property bool active: false
property bool cancelButtonVisible: true
property bool saveChangesButtonEnabled: false
@ -133,7 +134,7 @@ Rectangle {
StatusButton {
id: cancelChangesButton
text: root.defaultCancelChangesText
enabled: root.active
enabled: !root.loading && root.active
visible: root.cancelButtonVisible
type: StatusBaseButton.Type.Danger
onClicked: root.resetChangesClicked()
@ -142,6 +143,7 @@ Rectangle {
StatusFlatButton {
id: saveForLaterButton
text: root.defaultSaveForLaterText
loading: root.loading
enabled: root.active && root.saveChangesButtonEnabled
visible: root.saveForLaterButtonVisible
onClicked: root.saveForLaterClicked()
@ -149,7 +151,9 @@ Rectangle {
StatusButton {
id: saveChangesButton
objectName: "settingsDirtyToastMessageSaveButton"
loading: root.loading
text: root.defaultSaveChangesText
interactive: root.active && root.saveChangesButtonEnabled
tooltip.text: root.saveChangesTooltipText