status-desktop/ui/app/AppLayouts/Profile/ProfileLayout.qml

568 lines
21 KiB
QML
Raw Normal View History

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15
import shared 1.0
import shared.panels 1.0
import shared.popups.keycard 1.0
import shared.stores 1.0 as SharedStores
import shared.stores.send 1.0
import utils 1.0
import "popups"
import "views"
import "views/profile"
feat: layouts for the Profile screens Work on this PR started before the build system was updated and at one point I upgraded `nim_status_client.nimble` to use NimScript so the nimble command would stop warning that the old format was being used. In team chat it was discussed that since we're no longer using nimble for package management we could simply delete `nim_status_client.nimble` to avoid confusion, which can be done in another PR. Introduce a BrowserLayout stub so the index will be calcualted correctly re: the active tab. Reorganize ChatLayout and NodeLayout into subdirs `Chat` and `Node`, respectively. Introduce ProfileLayout which uses a "LeftTab" approach similar to that of WalletLayout. There remains quite a bit of styling work to be done in ProfileLayout and its LeftTab. Also, it may be better to start over using a SplitView like the ChatLayout, I'm not really sure. It wasn't clear what should be the default view for the right-pane when Profile is selected in the left-most TabBar. In this PR, it defaults to showing the view corresponding to the ENS usernames button. In the archived Figma for the desktop design, it seemed a picture could be set, e.g. there is a headshot of a woman used in the Profile screen. To that end I explored how to take a square image and clip/mask it so it appears round and I included a larger placeholder image for that purpose. In the new design, and with respect to mobile, it may not be possible to set a profile picture so the code that rounds the image can maybe be dropped.
2020-05-19 19:44:45 +00:00
import StatusQ 0.1
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import StatusQ.Layout 0.1
import StatusQ.Popups.Dialog 0.1
import AppLayouts.Communities.stores 1.0 as CommunitiesStore
import AppLayouts.Profile.helpers 1.0
import AppLayouts.Profile.stores 1.0 as ProfileStores
import AppLayouts.Wallet.controls 1.0
import AppLayouts.Wallet.stores 1.0
import AppLayouts.stores 1.0 as AppLayoutsStores
import SortFilterProxyModel 0.2
StatusSectionLayout {
id: root
property alias settingsSubsection: profileContainer.currentIndex
property int settingsSubSubsection
objectName: "profileStatusSectionLayout"
property SharedStores.RootStore sharedRootStore
2024-10-21 22:01:34 +00:00
property SharedStores.UtilsStore utilsStore
property ProfileStores.ProfileSectionStore store
property AppLayoutsStores.RootStore globalStore
property CommunitiesStore.CommunitiesStore communitiesStore
required property var sendModalPopup
property var systemPalette
property var emojiPopup
property SharedStores.NetworkConnectionStore networkConnectionStore
required property TokensStore tokensStore
required property WalletAssetsStore walletAssetsStore
required property CollectiblesStore collectiblesStore
required property SharedStores.CurrenciesStore currencyStore
2024-10-21 22:01:34 +00:00
property var mutualContactsModel
property var blockedContactsModel
property var pendingReceivedRequestContactsModel
property var pendingSentRequestContactsModel
required property bool isCentralizedMetricsEnabled
backButtonName: root.store.backButtonName
notificationCount: activityCenterStore.unreadNotificationsCount
hasUnseenNotifications: activityCenterStore.hasUnseenNotifications
onNotificationButtonClicked: Global.openActivityCenterPopup()
onBackButtonClicked: {
switch (root.settingsSubsection) {
case Constants.settingsSubsection.contacts:
Global.changeAppSectionBySectionType(Constants.appSection.profile, Constants.settingsSubsection.messaging)
break;
case Constants.settingsSubsection.about_privacy:
case Constants.settingsSubsection.about_terms:
Global.changeAppSectionBySectionType(Constants.appSection.profile, Constants.settingsSubsection.about)
break;
case Constants.settingsSubsection.wallet:
walletView.item.resetStack()
break;
case Constants.settingsSubsection.keycard:
keycardView.item.handleBackAction()
break;
}
root.settingsSubSubsection = -1
}
Component.onCompleted: {
profileContainer.currentIndexChanged()
root.store.devicesStore.loadDevices() // Load devices to get non-paired number for badge
}
QtObject {
id: d
readonly property int contentWidth: 560
readonly property int rightPanelWidth: 768
readonly property bool isProfilePanelActive: profileContainer.currentIndex === Constants.settingsSubsection.profile
readonly property bool sideBySidePreviewAvailable: root.Window.width >= 1840 // design
// Used to alternatively add an error message to the dirty bubble if ephemeral notification
// can clash at smaller viewports
readonly property bool toastClashesWithDirtyBubble: root.Window.width <= 1650 // design
}
SettingsEntriesModel {
id: settingsEntriesModel
showWalletEntries: root.store.walletMenuItemEnabled
showBackUpSeed: !root.store.privacyStore.mnemonicBackedUp
syncingBadgeCount: root.store.devicesStore.devicesModel.count -
root.store.devicesStore.devicesModel.pairedCount
messagingBadgeCount: root.pendingReceivedRequestContactsModel.ModelCount.count
}
headerBackground: AccountHeaderGradient {
width: parent.width
overview: root.store.walletStore.selectedAccount
visible: profileContainer.currentIndex === Constants.settingsSubsection.wallet && !!root.store.walletStore.selectedAccount
}
leftPanel: LeftTabView {
anchors.fill: parent
model: settingsEntriesModel
2022-06-22 12:16:21 +00:00
onMenuItemClicked: {
if (profileContainer.currentItem.dirty && !profileContainer.currentItem.ignoreDirty) {
2022-06-22 12:16:21 +00:00
event.accepted = true;
profileContainer.currentItem.notifyDirty();
}
}
onSettingsSubsectionChanged: root.settingsSubsection = settingsSubsection
Binding on settingsSubsection {
value: root.settingsSubsection
}
feat: layouts for the Profile screens Work on this PR started before the build system was updated and at one point I upgraded `nim_status_client.nimble` to use NimScript so the nimble command would stop warning that the old format was being used. In team chat it was discussed that since we're no longer using nimble for package management we could simply delete `nim_status_client.nimble` to avoid confusion, which can be done in another PR. Introduce a BrowserLayout stub so the index will be calcualted correctly re: the active tab. Reorganize ChatLayout and NodeLayout into subdirs `Chat` and `Node`, respectively. Introduce ProfileLayout which uses a "LeftTab" approach similar to that of WalletLayout. There remains quite a bit of styling work to be done in ProfileLayout and its LeftTab. Also, it may be better to start over using a SplitView like the ChatLayout, I'm not really sure. It wasn't clear what should be the default view for the right-pane when Profile is selected in the left-most TabBar. In this PR, it defaults to showing the view corresponding to the ENS usernames button. In the archived Figma for the desktop design, it seemed a picture could be set, e.g. there is a headshot of a woman used in the Profile screen. To that end I explored how to take a square image and clip/mask it so it appears round and I included a larger placeholder image for that purpose. In the new design, and with respect to mobile, it may not be possible to set a profile picture so the code that rounds the image can maybe be dropped.
2020-05-19 19:44:45 +00:00
}
centerPanel: StackLayout {
id: profileContainer
readonly property var currentItem: (currentIndex >= 0 && currentIndex < children.length) ? children[currentIndex].item : null
2022-06-22 12:16:21 +00:00
anchors.fill: parent
anchors.leftMargin: Constants.settingsSection.leftMargin
onCurrentIndexChanged: {
if (!!children[currentIndex] && !children[currentIndex].active)
children[currentIndex].active = true
root.store.backButtonName = ""
if (currentIndex === Constants.settingsSubsection.contacts) {
root.store.backButtonName = settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.messaging)
} else if (currentIndex === Constants.settingsSubsection.about_privacy || currentIndex === Constants.settingsSubsection.about_terms) {
root.store.backButtonName = settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.about)
} else if (currentIndex === Constants.settingsSubsection.wallet) {
walletView.item.resetStack()
} else if (currentIndex === Constants.settingsSubsection.keycard) {
keycardView.item.handleBackAction()
}
}
Loader {
active: false
asynchronous: true
sourceComponent: MyProfileView {
id: myProfileView
implicitWidth: parent.width
implicitHeight: parent.height
profileStore: root.store.profileStore
2022-09-27 21:26:26 +00:00
contactsStore: root.store.contactsStore
communitiesStore: root.communitiesStore
2024-10-21 22:01:34 +00:00
utilsStore: root.utilsStore
sendToAccountEnabled: root.networkConnectionStore.sendBuyBridgeEnabled
sectionTitle: settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.profile)
contentWidth: d.contentWidth
sideBySidePreview: d.sideBySidePreviewAvailable
toastClashesWithDirtyBubble: d.toastClashesWithDirtyBubble
communitiesShowcaseModel: root.store.ownShowcaseCommunitiesModel
accountsShowcaseModel: root.store.ownShowcaseAccountsModel
socialLinksShowcaseModel: root.store.ownShowcaseSocialLinksModel
collectiblesShowcaseModel: SortFilterProxyModel {
sourceModel: root.store.ownShowcaseCollectiblesModel
sorters: [
FastExpressionSorter {
expression: {
root.collectiblesStore.collectiblesController.revision
return root.collectiblesStore.collectiblesController.compareTokens(modelLeft.uid, modelRight.uid)
}
expectedRoles: ["uid"]
}
]
filters: [
FastExpressionFilter {
expression: {
root.collectiblesStore.collectiblesController.revision
return root.collectiblesStore.collectiblesController.filterAcceptsSymbol(model.uid)
}
expectedRoles: ["uid"]
}
]
}
assetsModel: root.globalStore.globalAssetsModel
collectiblesModel: root.globalStore.globalCollectiblesModel
}
}
Loader {
active: false
asynchronous: true
sourceComponent: ChangePasswordView {
implicitWidth: parent.width
implicitHeight: parent.height
privacyStore: root.store.privacyStore
passwordStrengthScoreFunction: root.sharedRootStore.getPasswordStrengthScore
contentWidth: d.contentWidth
sectionTitle: settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.password)
}
}
Loader {
active: false
asynchronous: true
sourceComponent: ContactsView {
implicitWidth: parent.width
implicitHeight: parent.height
contactsStore: root.store.contactsStore
utilsStore: root.utilsStore
sectionTitle: qsTr("Contacts")
contentWidth: d.contentWidth
mutualContactsModel: root.mutualContactsModel
blockedContactsModel: root.blockedContactsModel
pendingReceivedRequestContactsModel: root.pendingReceivedRequestContactsModel
pendingSentRequestContactsModel: root.pendingSentRequestContactsModel
}
}
2020-05-24 22:23:00 +00:00
Loader {
id: ensContainer
active: false
asynchronous: true
sourceComponent: EnsView {
// TODO: we need to align structure for the entire this part using `SettingsContentBase` as root component
// TODO: handle structure for this subsection to match style used in onther sections
// using `SettingsContentBase` component as base.
implicitWidth: parent.width
implicitHeight: parent.height
ensUsernamesStore: root.store.ensUsernamesStore
walletAssetsStore: root.walletAssetsStore
sendModalPopup: root.sendModalPopup
contactsStore: root.store.contactsStore
networkConnectionStore: root.networkConnectionStore
profileContentWidth: d.contentWidth
}
}
2020-05-24 22:23:00 +00:00
Loader {
active: false
asynchronous: true
sourceComponent: MessagingView {
implicitWidth: parent.width
implicitHeight: parent.height
contentWidth: d.contentWidth
sectionTitle: settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.messaging)
requestsCount: root.pendingReceivedRequestContactsModel.ModelCount.count
messagingStore: root.store.messagingStore
}
}
Loader {
id: walletView
active: false
asynchronous: true
sourceComponent: WalletView {
implicitWidth: parent.width
implicitHeight: parent.height
contentWidth: d.contentWidth
settingsSubSubsection: root.settingsSubSubsection
rootStore: root.store
tokensStore: root.tokensStore
networkConnectionStore: root.networkConnectionStore
assetsStore: root.walletAssetsStore
collectiblesStore: root.collectiblesStore
myPublicKey: root.store.contactsStore.myPublicKey
currencySymbol: root.sharedRootStore.currencyStore.currentCurrency
emojiPopup: root.emojiPopup
sendModalPopup: root.sendModalPopup
sectionTitle: settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.wallet)
}
onLoaded: root.store.backButtonName = ""
}
Loader {
active: false
asynchronous: true
sourceComponent: AppearanceView {
implicitWidth: parent.width
implicitHeight: parent.height
appearanceStore: root.store.appearanceStore
sectionTitle: settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.appearance)
contentWidth: d.contentWidth
systemPalette: root.systemPalette
}
}
Loader {
active: false
asynchronous: true
sourceComponent: LanguageView {
implicitWidth: parent.width
implicitHeight: parent.height
languageSelectionEnabled: localAppSettings.translationsEnabled
languageStore: root.store.languageStore
currencyStore: root.currencyStore
sectionTitle: settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.language)
contentWidth: d.contentWidth
}
}
Loader {
active: false
asynchronous: true
sourceComponent: NotificationsView {
implicitWidth: parent.width
implicitHeight: parent.height
notificationsStore: root.store.notificationsStore
sectionTitle: settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.notifications)
contentWidth: d.contentWidth
}
}
Loader {
active: false
asynchronous: true
sourceComponent: SyncingView {
implicitWidth: parent.width
implicitHeight: parent.height
isProduction: production
profileStore: root.store.profileStore
devicesStore: root.store.devicesStore
privacyStore: root.store.privacyStore
advancedStore: root.store.advancedStore
sectionTitle: settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.syncingSettings)
contentWidth: d.contentWidth
}
}
Loader {
active: false
asynchronous: true
sourceComponent: AdvancedView {
implicitWidth: parent.width
implicitHeight: parent.height
messagingStore: root.store.messagingStore
advancedStore: root.store.advancedStore
walletStore: root.store.walletStore
isFleetSelectionEnabled: fleetSelectionEnabled
sectionTitle: settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.advanced)
contentWidth: d.contentWidth
}
}
Loader {
active: false
asynchronous: true
sourceComponent: AboutView {
implicitWidth: parent.width
implicitHeight: parent.height
sectionTitle: settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.about)
contentWidth: d.contentWidth
store: QtObject {
readonly property bool isProduction: production
function checkForUpdates() {
return root.store.checkForUpdates()
}
function getCurrentVersion() {
return root.store.getCurrentVersion()
}
function getStatusGoVersion() {
return root.store.getStatusGoVersion()
}
function qtRuntimeVersion() {
return SystemUtils.qtRuntimeVersion()
}
function getReleaseNotes() {
const link = isProduction ? "https://github.com/status-im/status-desktop/releases/tag/%1" :
"https://github.com/status-im/status-desktop/commit/%1"
openLink(link.arg(getCurrentVersion()))
}
function openLink(url) {
Global.openLink(url)
}
}
}
}
Loader {
active: false
asynchronous: true
Layout.fillWidth: true
Layout.fillHeight: true
sourceComponent: CommunitiesView {
implicitWidth: parent.width
implicitHeight: parent.height
profileSectionStore: root.store
rootStore: root.globalStore
currencyStore: root.currencyStore
walletAssetsStore: root.walletAssetsStore
sectionTitle: settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.communitiesSettings)
contentWidth: d.contentWidth
}
}
Loader {
id: keycardView
active: false
asynchronous: true
sourceComponent: KeycardView {
implicitWidth: parent.width
implicitHeight: parent.height
profileSectionStore: root.store
keycardStore: root.store.keycardStore
emojiPopup: root.emojiPopup
sectionTitle: settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.keycard)
mainSectionTitle: settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.keycard)
contentWidth: d.contentWidth
}
}
Loader {
active: false
asynchronous: true
Layout.fillWidth: true
Layout.fillHeight: true
sourceComponent: SettingsContentBase {
implicitWidth: parent.width
implicitHeight: parent.height
sectionTitle: "Status Software - Terms of Use"
contentWidth: d.contentWidth
StatusBaseText {
width: d.contentWidth
wrapMode: Text.Wrap
textFormat: Text.MarkdownText
text: SQUtils.StringUtils.readTextFile(":/imports/assets/docs/terms-of-use.mdwn")
onLinkActivated: Global.openLinkWithConfirmation(link, SQUtils.StringUtils.extractDomainFromLink(link))
}
}
}
Loader {
active: false
asynchronous: true
Layout.fillWidth: true
Layout.fillHeight: true
sourceComponent: SettingsContentBase {
implicitWidth: parent.width
implicitHeight: parent.height
sectionTitle: "Status Software - Privacy Policy"
contentWidth: d.contentWidth
StatusBaseText {
width: d.contentWidth
wrapMode: Text.Wrap
textFormat: Text.MarkdownText
text: SQUtils.StringUtils.readTextFile(":/imports/assets/docs/privacy.mdwn")
onLinkActivated: Global.openLinkWithConfirmation(link, SQUtils.StringUtils.extractDomainFromLink(link))
}
}
}
Loader {
active: false
asynchronous: true
sourceComponent: PrivacyAndSecurityView {
isCentralizedMetricsEnabled: root.isCentralizedMetricsEnabled
implicitWidth: parent.width
implicitHeight: parent.height
sectionTitle: settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.privacyAndSecurity)
contentWidth: d.contentWidth
}
}
}
showRightPanel: d.isProfilePanelActive && d.sideBySidePreviewAvailable
rightPanelWidth: d.rightPanelWidth
rightPanel: d.isProfilePanelActive ? profileContainer.currentItem.sideBySidePreviewComponent : null
Connections {
target: root.store.keycardStore.keycardModule
enabled: profileContainer.currentIndex === Constants.settingsSubsection.wallet ||
profileContainer.currentIndex === Constants.settingsSubsection.keycard
function onDisplayKeycardSharedModuleFlow() {
keycardPopup.active = true
}
function onDestroyKeycardSharedModuleFlow() {
keycardPopup.active = false
}
function onSharedModuleBusy() {
Global.openPopup(sharedModuleBusyPopupComponent)
}
}
Loader {
id: keycardPopup
active: false
sourceComponent: KeycardPopup {
myKeyUid: store.profileStore.keyUid
sharedKeycardModule: root.store.keycardStore.keycardModule.keycardSharedModule
emojiPopup: root.emojiPopup
}
onLoaded: {
keycardPopup.item.open()
}
}
Component {
id: sharedModuleBusyPopupComponent
StatusDialog {
id: titleContentDialog
title: qsTr("Status Keycard")
StatusBaseText {
anchors.fill: parent
font.pixelSize: Constants.keycard.general.fontSize2
color: Theme.palette.directColor1
text: qsTr("The Keycard module is still busy, please try again")
}
standardButtons: Dialog.Ok
}
}
}