mirror of
https://github.com/status-im/status-desktop.git
synced 2025-02-10 13:46:35 +00:00
Problem: There were no difference between the fact that the avatar is not changed and the avatar is deleted. The same for bio and displayName On NIM, this state was simply rewritten Solution (The problem was discovered due to the fact that if the picture does not change, then Nim receives a base64 encoded source - instead of the path to the new Avatar. Which Status go could not parse as a file.) On the QML side, if we see that there have been changes, we add changes to changesMap Json, if there are no changes, then do not add. (This is the way to implement Option). On the NIM Side - convert to saveData On the Module side - distinguish Nil from an empty string fixes #14464
449 lines
17 KiB
QML
449 lines
17 KiB
QML
import QtQuick 2.13
|
|
import QtQuick.Controls 2.3
|
|
import QtQuick.Layouts 1.13
|
|
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
|
|
import shared.controls.chat 1.0
|
|
|
|
import "../popups"
|
|
import "../stores"
|
|
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.Profile.helpers 1.0
|
|
import AppLayouts.Profile.panels 1.0
|
|
import AppLayouts.Wallet.stores 1.0
|
|
|
|
SettingsContentBase {
|
|
id: root
|
|
|
|
property ProfileStore profileStore
|
|
property ContactsStore contactsStore
|
|
|
|
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
|
|
|
|
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
|
|
|
|
communitiesSearcherText: profileShowcaseCommunitiesPanel.searcherText
|
|
accountsSearcherText: profileShowcaseAccountsPanel.searcherText
|
|
collectiblesSearcherText: profileShowcaseCollectiblesPanel.searcherText
|
|
}
|
|
|
|
// 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 })
|
|
profileStore.resetSocialLinks()
|
|
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: Style.current.padding
|
|
Layout.rightMargin: Style.current.padding
|
|
|
|
store: root.profileStore
|
|
|
|
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 ? [] : Constants.validators.displayName
|
|
bio.text: profileStore.bio
|
|
}
|
|
}
|
|
|
|
// 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
|
|
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
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|