status-desktop/ui/app/AppLayouts/Profile/views/MyProfileView.qml

457 lines
18 KiB
QML

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml 2.15
import utils 1.0
import shared 1.0
import shared.panels 1.0
import shared.popups 1.0
import shared.stores 1.0 as SharedStores
import shared.validators 1.0
import shared.controls.chat 1.0
import "../popups"
import "../controls"
import "./profile"
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import AppLayouts.Communities.stores 1.0 as CommunitiesStores
import AppLayouts.Profile.helpers 1.0
import AppLayouts.Profile.panels 1.0
import AppLayouts.Profile.stores 1.0 as ProfileStores
import AppLayouts.Wallet.stores 1.0 as WalletStores
SettingsContentBase {
id: root
property ProfileStores.ProfileStore profileStore
property ProfileStores.ContactsStore contactsStore
property CommunitiesStores.CommunitiesStore communitiesStore
property SharedStores.UtilsStore utilsStore
property bool sendToAccountEnabled: false
property alias communitiesShowcaseModel: showcaseModels.communitiesSourceModel
property alias accountsShowcaseModel: showcaseModels.accountsSourceModel
property alias collectiblesShowcaseModel: showcaseModels.collectiblesSourceModel
property alias socialLinksShowcaseModel: showcaseModels.socialLinksSourceModel
property var assetsModel
property var collectiblesModel
property bool sideBySidePreview
property bool toastClashesWithDirtyBubble
readonly property alias sideBySidePreviewComponent: myProfilePreviewComponent
readonly property QtObject liveValues: QtObject {
readonly property string displayName: descriptionPanel.displayName.text
readonly property string bio: descriptionPanel.bio.text
readonly property url profileLargeImage: profileHeader.previewIcon
}
enum TabIndex {
Identity = 0,
Communities = 1,
Accounts = 2,
Collectibles = 3,
//Assets = 4,
Web = 4
}
titleRowComponentLoader.sourceComponent: StatusButton {
text: qsTr("Preview")
onClicked: Global.openPopup(profilePreview)
visible: !root.sideBySidePreview
}
dirty: priv.isIdentityTabDirty ||
priv.hasAnyProfileShowcaseChanges
saveChangesButtonEnabled: !!descriptionPanel.displayName.text && descriptionPanel.displayName.valid && (descriptionPanel.bio.valid || descriptionPanel.bio.text === '')
toast.saveChangesTooltipText: saveChangesButtonEnabled ? "" : qsTr("Invalid changes made to Identity")
autoscrollWhenDirty: profileTabBar.currentIndex === MyProfileView.Identity
toast.loading: priv.expectedBackendResponses > 0
toast.additionalComponent.visible: false // TODO:Issue #13997 // !toast.loading && root.toastClashesWithDirtyBubble && priv.saveRequestFailed
toast.additionalComponent.text: qsTr("Changes could not be saved. Try again")
toast.additionalComponent.color: Theme.palette.dangerColor1
onResetChangesClicked: priv.reset()
onSaveChangesClicked: priv.save()
bottomHeaderComponents: StatusTabBar {
id: profileTabBar
StatusTabButton {
objectName: "identityTabButton"
width: implicitWidth
leftPadding: 0
text: qsTr("Identity")
}
StatusTabButton {
objectName: "communitiesTabButton"
width: implicitWidth
text: qsTr("Communities")
}
StatusTabButton {
objectName: "accountsTabButton"
width: implicitWidth
text: qsTr("Accounts")
}
StatusTabButton {
objectName: "collectiblesTabButton"
width: implicitWidth
text: qsTr("Collectibles")
}
// TODO: Uncomment when assets tab is implemented
// StatusTabButton {
// objectName: "assetsTabButton"
// width: implicitWidth
// text: qsTr("Assets")
// }
StatusTabButton {
objectName: "webTabButton"
width: implicitWidth
text: qsTr("Web")
}
}
onVisibleChanged: if (visible) profileStore.requestProfileShowcasePreferences()
Component.onCompleted: profileStore.requestProfileShowcasePreferences()
property QObject priv: QObject {
id: priv
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 {
id: showcaseModels
}
// 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 })
profileHeader.icon = Qt.binding(() => { return profileStore.profileLargeImage })
priv.showcaseModels.revert()
priv.saveRequestFailed = false
priv.expectedBackendResponses = 0
root.profileStore.requestProfileShowcasePreferences()
}
function save() {
// 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++
}
// 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. Update only those fields that have changed
if (isIdentityTabDirty) {
const imageChanged = profileHeader.icon !== profileStore.profileLargeImage
const displayNameChanged = descriptionPanel.displayName.text !== profileStore.displayName
const bioChanged = descriptionPanel.bio.text.trim() !== profileStore.bio.trim()
root.profileStore.saveProfileIdentityChanges(
displayNameChanged ? descriptionPanel.displayName.text : undefined,
bioChanged ? descriptionPanel.bio.text.trim() : undefined,
imageChanged ? {
source : profileHeader.icon,
aX: profileHeader.cropRect.x,
aY: profileHeader.cropRect.y,
bX: profileHeader.cropRect.x + profileHeader.cropRect.width,
bY: profileHeader.cropRect.y + profileHeader.cropRect.height
} : undefined
)
profileHeader.icon = Qt.binding(() => { return profileStore.profileLargeImage })
}
}
}
}
StackLayout {
id: stackLayout
width: root.contentWidth
height: profileTabBar.currentIndex === MyProfileView.Web ? implicitHeight : root.contentHeight
currentIndex: profileTabBar.currentIndex
onCurrentIndexChanged: {
if(root.profileStore.isFirstShowcaseInteraction && currentIndex !== MyProfileView.TabIndex.Identity) {
root.profileStore.setIsFirstShowcaseInteraction()
Global.openPopup(profileShowcaseInfoPopup)
}
}
// identity
ColumnLayout {
objectName: "myProfileSettingsView"
ProfileHeader {
id: profileHeader
Layout.fillWidth: true
Layout.leftMargin: Theme.padding
Layout.rightMargin: Theme.padding
displayName: profileStore.name
pubkey: profileStore.pubkey
icon: profileStore.profileLargeImage
imageSize: ProfileHeader.ImageSize.Big
displayNameVisible: false
pubkeyVisible: false
emojiHashVisible: false
editImageButtonVisible: true
}
ProfileDescriptionPanel {
id: descriptionPanel
readonly property bool isEnsName: profileStore.preferredName
Layout.fillWidth: true
displayName.focus: !isEnsName
displayName.input.edit.readOnly: isEnsName
displayName.text: profileStore.name
displayName.validationMode: StatusInput.ValidationMode.Always
displayName.validators: isEnsName ? [] : displayNameValidators.validators
bio.text: profileStore.bio
DisplayNameValidators {
id: displayNameValidators
utilsStore: root.utilsStore
myDisplayName: root.profileStore.name
communitiesStore: root.communitiesStore
}
}
}
// communities
ProfileShowcaseCommunitiesPanel {
id: profileShowcaseCommunitiesPanel
inShowcaseModel: priv.showcaseModels.communitiesVisibleModel
hiddenModel: priv.showcaseModels.communitiesHiddenModel
showcaseLimit: root.profileStore.getProfileShowcaseEntriesLimit()
onChangePositionRequested: function (from, to) {
priv.showcaseModels.changeCommunityPosition(from, to)
}
onSetVisibilityRequested: function (key, toVisibility) {
priv.showcaseModels.setCommunityVisibility(key, toVisibility)
}
}
// accounts
ProfileShowcaseAccountsPanel {
id: profileShowcaseAccountsPanel
inShowcaseModel: priv.showcaseModels.accountsVisibleModel
hiddenModel: priv.showcaseModels.accountsHiddenModel
showcaseLimit: root.profileStore.getProfileShowcaseEntriesLimit()
currentWallet: RootStore.overview.mixedcaseAddress
onChangePositionRequested: function (from, to) {
priv.showcaseModels.changeAccountPosition(from, to)
}
onSetVisibilityRequested: function (key, toVisibility) {
priv.showcaseModels.setAccountVisibility(key, toVisibility)
}
}
// collectibles
ProfileShowcaseCollectiblesPanel {
id: profileShowcaseCollectiblesPanel
inShowcaseModel: priv.showcaseModels.collectiblesVisibleModel
hiddenModel: priv.showcaseModels.collectiblesHiddenModel
showcaseLimit: root.profileStore.getProfileShowcaseEntriesLimit()
addAccountsButtonVisible: priv.showcaseModels.accountsHiddenModel.count > 0
onNavigateToAccountsTab: profileTabBar.currentIndex = MyProfileView.TabIndex.Accounts
onChangePositionRequested: function (from, to) {
priv.showcaseModels.changeCollectiblePosition(from, to)
}
onSetVisibilityRequested: function (key, toVisibility) {
priv.showcaseModels.setCollectibleVisibility(key, toVisibility)
}
}
// assets
// TODO: Integrate the assets tab with the new backend
// ProfileShowcaseAssetsPanel {
// id: profileShowcaseAssetsPanel
// baseModel: root.walletAssetsStore.groupedAccountAssetsModel // TODO: instantiate an assets model in profile module
// showcaseModel: root.contactsStore.showcaseContactAssetsModel
// addAccountsButtonVisible: root.contactsStore.showcaseContactAccountsModel.hiddenCount > 0
// formatCurrencyAmount: function(amount, symbol) {
// return root.currencyStore.formatCurrencyAmount(amount, symbol)
// }
// onShowcaseEntryChanged: priv.hasAnyProfileShowcaseChanges = true
// onNavigateToAccountsTab: profileTabBar.currentIndex = MyProfileView.TabIndex.Accounts
// }
// web
ProfileSocialLinksPanel {
showcaseLimit: root.profileStore.getProfileShowcaseSocialLinksLimit()
socialLinksModel: priv.showcaseModels.socialLinksVisibleModel
onAddSocialLink: function(url, text) {
priv.showcaseModels.appendSocialLink({ showcaseKey: "", text: text, url: url })
}
onUpdateSocialLink: function(index, url, text) {
priv.showcaseModels.updateSocialLink(index, { text: text, url: url })
}
onRemoveSocialLink: function(index) {
priv.showcaseModels.removeSocialLink(index)
}
onChangePosition: function(from, to) {
priv.showcaseModels.changeSocialLinkPosition(from, to)
}
}
Component {
id: profilePreview
ProfileDialog {
publicKey: root.contactsStore.myPublicKey
profileStore: root.profileStore
contactsStore: root.contactsStore
walletStore: WalletStores.RootStore
utilsStore: root.utilsStore
sendToAccountEnabled: root.sendToAccountEnabled
onClosed: destroy()
dirtyValues: root.liveValues
dirty: root.dirty
showcaseCommunitiesModel: priv.showcaseModels.communitiesVisibleModel
showcaseAccountsModel: priv.showcaseModels.accountsVisibleModel
showcaseCollectiblesModel: priv.showcaseModels.collectiblesVisibleModel
showcaseSocialLinksModel: priv.showcaseModels.socialLinksVisibleModel
//showcaseAssetsModel: priv.showcaseModels.assetsVisibleModel
assetsModel: root.assetsModel
collectiblesModel: root.collectiblesModel
}
}
Component {
id: myProfilePreviewComponent
MyProfilePreview {
profileStore: root.profileStore
contactsStore: root.contactsStore
utilsStore: root.utilsStore
sendToAccountEnabled: root.sendToAccountEnabled
dirtyValues: root.liveValues
dirty: root.dirty
showcaseCommunitiesModel: priv.showcaseModels.communitiesVisibleModel
showcaseAccountsModel: priv.showcaseModels.accountsVisibleModel
showcaseCollectiblesModel: priv.showcaseModels.collectiblesVisibleModel
showcaseSocialLinksModel: priv.showcaseModels.socialLinksVisibleModel
//showcaseAssetsModel: priv.showcaseModels.assetsVisibleModel
assetsModel: root.assetsModel
collectiblesModel: root.collectiblesModel
}
}
Component {
id: profileShowcaseInfoPopup
ProfileShowcaseInfoPopup {
destroyOnClose: true
}
}
}
}