import QtQuick 2.15 import QtQuick.Layouts 1.15 import QtQuick.Window 2.15 import QtQuick.Dialogs 1.3 import SortFilterProxyModel 0.2 import StatusQ 0.1 import StatusQ.Components 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 StatusQUtils import StatusQ.Layout 0.1 import shared.panels 1.0 import shared.popups 1.0 import shared.stores 1.0 import shared.stores.send 1.0 import shared.views.chat 1.0 import utils 1.0 import AppLayouts.Communities.controls 1.0 import AppLayouts.Communities.panels 1.0 import AppLayouts.Communities.popups 1.0 import AppLayouts.Communities.helpers 1.0 import AppLayouts.Chat.stores 1.0 as ChatStores import AppLayouts.Wallet.stores 1.0 StatusSectionLayout { id: root notificationCount: activityCenterStore.unreadNotificationsCount hasUnseenNotifications: activityCenterStore.hasUnseenNotifications onNotificationButtonClicked: Global.openActivityCenterPopup() property ChatStores.RootStore rootStore property var chatCommunitySectionModule required property TokensStore tokensStore property var community required property TransactionStore transactionStore property bool communitySettingsDisabled property var sendModalPopup required property string enabledChainIds required property var walletAccountsModel // name, address, emoji, color readonly property bool isOwner: community.memberRole === Constants.memberRole.owner readonly property bool isAdmin: community.memberRole === Constants.memberRole.admin readonly property bool isTokenMasterOwner: community.memberRole === Constants.memberRole.tokenMaster readonly property bool isControlNode: community.isControlNode // Community transfer ownership related props: required property bool isPendingOwnershipRequest signal finaliseOwnershipClicked signal enableNetwork(int chainId) readonly property string filteredSelectedTags: { let tagsArray = [] if (community && community.tags) { try { const json = JSON.parse(community.tags) if (!!json) tagsArray = json.map(tag => tag.name) } catch (e) { console.warn("Error parsing community tags: ", community.tags, " error: ", e.message) } } return JSON.stringify(tagsArray) } signal backToCommunityClicked backButtonName: { if (!stackLayout.children[d.currentIndex].item || !stackLayout.children[d.currentIndex].item.previousPageName) { return "" } return stackLayout.children[d.currentIndex].item.previousPageName } //navigate to a specific section and subsection function goTo(section: int, subSection: int) { d.goTo(section, subSection) } onBackButtonClicked: stackLayout.children[d.currentIndex].item.navigateBack() leftPanel: Item { anchors.fill: parent ColumnLayout { anchors { top: parent.top bottom: backToCommunityButton.top bottomMargin: 12 topMargin: Theme.smallPadding horizontalCenter: parent.horizontalCenter } width: parent.width spacing: 32 clip: true StatusChatInfoButton { id: communityHeader title: community.name subTitle: qsTr("%n member(s)", "", community.members.count || 0) asset.name: community.image asset.color: community.color asset.isImage: true Layout.fillWidth: true Layout.leftMargin: Theme.halfPadding Layout.rightMargin: Theme.halfPadding type: StatusChatInfoButton.Type.OneToOneChat hoverEnabled: true onClicked: root.backToCommunityClicked() } StatusListView { id: listView Layout.fillWidth: true Layout.fillHeight: true Layout.leftMargin: Theme.halfPadding Layout.rightMargin: Theme.padding model: stackLayout.children spacing: Theme.halfPadding enabled: !root.communitySettingsDisabled delegate: StatusNavigationListItem { objectName: "CommunitySettingsView_NavigationListItem_" + model.sectionName width: ListView.view.width title: model.sectionName asset.name: model.sectionIcon selected: d.currentIndex === index && !root.communitySettingsDisabled onClicked: d.currentIndex = index visible: model.sectionEnabled height: visible ? implicitHeight : 0 } } } StatusBaseText { id: backToCommunityButton objectName: "communitySettingsBackToCommunityButton" anchors { bottom: parent.bottom bottomMargin: 16 horizontalCenter: parent.horizontalCenter } text: "<- " + qsTr("Back to community") color: Theme.palette.baseColor1 font.pixelSize: 15 font.underline: true MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: root.backToCommunityClicked() hoverEnabled: true } } } centerPanel: StackLayout { id: stackLayout anchors.fill: parent currentIndex: d.currentIndex onCurrentIndexChanged: { children[currentIndex].active = true } // OVERVIEW Loader { active: true readonly property int sectionKey: Constants.CommunitySettingsSections.Overview readonly property string sectionName: qsTr("Overview") readonly property string sectionIcon: "show" readonly property bool sectionEnabled: true sourceComponent: OverviewSettingsPanel { isOwner: root.isOwner isAdmin: root.isAdmin isTokenMaster: root.isTokenMasterOwner communityId: root.community.id name: root.community.name description: root.community.description introMessage: root.community.introMessage outroMessage: root.community.outroMessage logoImageData: root.community.image bannerImageData: root.community.bannerImageData color: root.community.color tags: root.rootStore.communityTags selectedTags: root.filteredSelectedTags archiveSupportEnabled: root.community.historyArchiveSupportEnabled archiveSupporVisible: root.community.isControlNode requestToJoinEnabled: root.community.access === Constants.communityChatOnRequestAccess pinMessagesEnabled: root.community.pinMessageAllMembersEnabled editable: true loginType: root.rootStore.loginType isControlNode: root.isControlNode communitySettingsDisabled: root.communitySettingsDisabled overviewChartData: rootStore.overviewChartData shardingEnabled: !isAdmin && !isTokenMaster && localAppSettings.wakuV2ShardedCommunitiesEnabled shardIndex: root.community.shardIndex shardingInProgress: root.chatCommunitySectionModule.shardingInProgress pubsubTopic: root.community.pubsubTopic pubsubTopicKey: root.community.pubsubTopicKey sendModalPopup: root.sendModalPopup ownerToken: tokensModelChangesTracker.ownerToken isPendingOwnershipRequest: root.isPendingOwnershipRequest onFinaliseOwnershipClicked: root.finaliseOwnershipClicked() onCollectCommunityMetricsMessagesCount: { rootStore.collectCommunityMetricsMessagesCount(intervals) } onEdited: { const error = root.chatCommunitySectionModule.editCommunity( StatusQUtils.Utils.filterXSS(item.name), StatusQUtils.Utils.filterXSS(item.description), StatusQUtils.Utils.filterXSS(item.introMessage), StatusQUtils.Utils.filterXSS(item.outroMessage), item.options.requestToJoinEnabled ? Constants.communityChatOnRequestAccess : Constants.communityChatPublicAccess, item.color.toString().toUpperCase(), item.selectedTags, Utils.getImageAndCropInfoJson(item.logoImagePath, item.logoCropRect), Utils.getImageAndCropInfoJson(item.bannerPath, item.bannerCropRect), item.options.archiveSupportEnabled, item.options.pinMessagesEnabled ) if (error) { errorDialog.text = error.error errorDialog.open() } } onInviteNewPeopleClicked: { Global.openInviteFriendsToCommunityPopup(root.community, root.chatCommunitySectionModule, null) } onAirdropTokensClicked: root.goTo(Constants.CommunitySettingsSections.Airdrops) onExportControlNodeClicked: { if(!root.isControlNode) return Global.openExportControlNodePopup(root.community) } onImportControlNodeClicked: { if(root.isControlNode) return Global.openImportControlNodePopup(root.community) } onMintOwnerTokenClicked: { root.goTo(Constants.CommunitySettingsSections.MintTokens) mintPanel.openNewTokenForm(false/*Collectible owner token*/) } onShardIndexEdited: if (root.community.shardIndex !== shardIndex) { root.chatCommunitySectionModule.setCommunityShard(shardIndex) } } } // MEMBERS Loader { active: false readonly property int sectionKey: Constants.CommunitySettingsSections.Members readonly property string sectionName: qsTr("Members") readonly property string sectionIcon: "group-chat" readonly property bool sectionEnabled: true sourceComponent: MembersSettingsPanel { rootStore: root.rootStore membersModel: root.community.members bannedMembersModel: root.community.bannedMembers pendingMemberRequestsModel: root.community.pendingMemberRequests declinedMemberRequestsModel: root.community.declinedMemberRequests editable: root.isAdmin || root.isOwner || root.isTokenMasterOwner memberRole: community.memberRole communityName: root.community.name onKickUserClicked: root.rootStore.removeUserFromCommunity(id) onBanUserClicked: root.rootStore.banUserFromCommunity(id, deleteAllMessages) onUnbanUserClicked: root.rootStore.unbanUserFromCommunity(id) onAcceptRequestToJoin: root.rootStore.acceptRequestToJoinCommunity(id, root.community.id) onDeclineRequestToJoin: root.rootStore.declineRequestToJoinCommunity(id, root.community.id) onViewMemberMessagesClicked: { root.rootStore.loadCommunityMemberMessages(root.community.id, pubKey) Global.openCommunityMemberMessagesPopupRequested(root.rootStore, root.chatCommunitySectionModule, pubKey, displayName) } } } // PERMISISONS Loader { active: false readonly property int sectionKey: Constants.CommunitySettingsSections.Permissions readonly property string sectionName: qsTr("Permissions") readonly property string sectionIcon: "objects" readonly property bool sectionEnabled: true sourceComponent: PermissionsSettingsPanel { readonly property PermissionsStore permissionsStore: rootStore.permissionsStore permissionsModel: permissionsStore.permissionsModel // temporary solution to provide icons for assets, similar // method is used in wallet (constructing filename from asset's // symbol) and is intended to be replaced by more robust // solution soon. assetsModel: rootStore.assetsModel collectiblesModel: rootStore.collectiblesModel channelsModel: rootStore.chatCommunitySectionModule.model ensCommunityPermissionsEnabled: rootStore.ensCommunityPermissionsEnabled communityDetails: d.communityDetails onCreatePermissionRequested: permissionsStore.createPermission(holdings, permissionType, isPrivate, channels) onUpdatePermissionRequested: permissionsStore.editPermission( key, holdings, permissionType, channels, isPrivate) onRemovePermissionRequested: permissionsStore.removePermission(key) onNavigateToMintTokenSettings: { root.goTo(Constants.CommunitySettingsSections.MintTokens) mintPanelLoader.item.openNewTokenForm(isAssetType) } } } // TOKEN Loader { id: mintPanelLoader active: false readonly property int sectionKey: Constants.CommunitySettingsSections.MintTokens readonly property string sectionName: qsTr("Tokens") readonly property string sectionIcon: "token" readonly property bool sectionEnabled: true sourceComponent: MintTokensSettingsPanel { enabledChainIds: root.enabledChainIds readonly property CommunityTokensStore communityTokensStore: rootStore.communityTokensStore // General community props communityId: root.community.id communityName: root.community.name communityLogo: root.community.image communityColor: root.community.color sendModalPopup: root.sendModalPopup // User profile props isOwner: root.isOwner isAdmin: root.isAdmin isTokenMasterOwner: root.isTokenMasterOwner // Owner and TMaster properties isOwnerTokenDeployed: tokensModelChangesTracker.isOwnerTokenDeployed isTMasterTokenDeployed: tokensModelChangesTracker.isTMasterTokenDeployed anyPrivilegedTokenFailed: tokensModelChangesTracker.isOwnerTokenFailed || tokensModelChangesTracker.isTMasterTokenFailed ownerOrTMasterTokenItemsExist: tokensModelChangesTracker.ownerOrTMasterTokenItemsExist // Models tokensModel: root.community.communityTokens membersModel: root.community.members flatNetworks: communityTokensStore.filteredFlatModel accounts: root.walletAccountsModel referenceAssetsBySymbolModel: root.tokensStore.assetsBySymbolModel onRegisterDeployFeesSubscriber: d.feesBroker.registerDeployFeesSubscriber(feeSubscriber) onRegisterSelfDestructFeesSubscriber: d.feesBroker.registerSelfDestructFeesSubscriber(feeSubscriber) onRegisterBurnTokenFeesSubscriber: d.feesBroker.registerBurnFeesSubscriber(feeSubscriber) onStartTokenHoldersManagement: communityTokensStore.startTokenHoldersManagement(root.community.id, chainId, address) onStopTokenHoldersManagement: communityTokensStore.stopTokenHoldersManagement() onEnableNetwork: root.enableNetwork(chainId) onMintCollectible: communityTokensStore.deployCollectible( root.community.id, collectibleItem) onMintAsset: communityTokensStore.deployAsset(root.community.id, assetItem) onMintOwnerToken: communityTokensStore.deployOwnerToken( root.community.id, ownerToken, tMasterToken) onRemotelyDestructCollectibles: communityTokensStore.remoteSelfDestructCollectibles( root.community.id, walletsAndAmounts, tokenKey, accountAddress) onRemotelyDestructAndBan: communityTokensStore.remotelyDestructAndBan( root.community.id, contactId, tokenKey, accountAddress) onRemotelyDestructAndKick: communityTokensStore.remotelyDestructAndKick( root.community.id, contactId, tokenKey, accountAddress) onBurnToken: communityTokensStore.burnToken(root.community.id, tokenKey, amount, accountAddress) onDeleteToken: communityTokensStore.deleteToken(root.community.id, tokenKey) onRefreshToken: communityTokensStore.refreshToken(tokenKey) onAirdropToken: { root.goTo(Constants.CommunitySettingsSections.Airdrops) // Force a token selection to be airdroped with given amount airdropPanelLoader.item.selectToken(tokenKey, amount, type) // Set given addresses as recipients airdropPanelLoader.item.addAddresses(addresses) } onKickUserRequested: root.rootStore.removeUserFromCommunity(contactId) onBanUserRequested: root.rootStore.banUserFromCommunity(contactId) } } // AIRDROPS Loader { id: airdropPanelLoader active: false readonly property int sectionKey: Constants.CommunitySettingsSections.Airdrops readonly property string sectionName: qsTr("Airdrops") readonly property string sectionIcon: "airdrop" readonly property bool sectionEnabled: true sourceComponent: AirdropsSettingsPanel { id: airdropsSettingsPanel communityDetails: d.communityDetails // Profile type isOwner: root.isOwner isTokenMasterOwner: root.isTokenMasterOwner isAdmin: root.isAdmin // Owner and TMaster properties isOwnerTokenDeployed: tokensModelChangesTracker.isOwnerTokenDeployed isTMasterTokenDeployed: tokensModelChangesTracker.isTMasterTokenDeployed readonly property CommunityTokensStore communityTokensStore: rootStore.communityTokensStore readonly property RolesRenamingModel renamedTokensBySymbolModel: RolesRenamingModel { sourceModel: root.community.communityTokens || null mapping: [ RoleRename { from: "symbol" to: "key" }, RoleRename { from: "image" to: "iconSource" } ] } assetsModel: SortFilterProxyModel { sourceModel: renamedTokensBySymbolModel filters: ValueFilter { roleName: "tokenType" value: Constants.TokenType.ERC20 } proxyRoles: [ ConstantRole { name: "category" value: TokenCategories.Category.Own }, ConstantRole { name: "communityId" value: "" } ] } collectiblesModel: SortFilterProxyModel { sourceModel: renamedTokensBySymbolModel filters: [ ValueFilter { roleName: "tokenType" value: Constants.TokenType.ERC721 }, AnyOf { ValueFilter { roleName: "privilegesLevel" value: Constants.TokenPrivilegesLevel.Community } ValueFilter { roleName: "privilegesLevel" value: Constants.TokenPrivilegesLevel.TMaster enabled: root.isOwner } } ] proxyRoles: [ ConstantRole { name: "category" value: TokenCategories.Category.Own }, ConstantRole { name: "communityId" value: "" } ] } Connections { target: root.rootStore.communityTokensStore function onAirdropStateChanged(communityId, tokenName, chainName, status, url) { if (root.community.id !== communityId) { return } if (status == Constants.ContractTransactionStatus.InProgress) { airdropsSettingsPanel.navigateBack() } } } membersModel: community.members enabledChainIds: root.enabledChainIds onEnableNetwork: root.enableNetwork(chainId) accountsModel: root.walletAccountsModel onAirdropClicked: communityTokensStore.airdrop( root.community.id, airdropTokens, addresses, feeAccountAddress) onNavigateToMintTokenSettings: { root.goTo(Constants.CommunitySettingsSections.MintTokens) mintPanelLoader.item.openNewTokenForm(isAssetType) } onRegisterAirdropFeeSubscriber: d.feesBroker.registerAirdropFeesSubscriber(feeSubscriber) } } } QtObject { id: d property int currentIndex: 0 readonly property QtObject communityDetails: QtObject { readonly property string id: root.community.id readonly property string name: root.community.name readonly property string image: root.community.image readonly property string color: root.community.color readonly property bool owner: root.community.memberRole === Constants.memberRole.owner readonly property bool admin: root.community.memberRole === Constants.memberRole.admin readonly property bool tokenMaster: root.community.memberRole === Constants.memberRole.tokenMaster } readonly property TransactionFeesBroker feesBroker: TransactionFeesBroker { communityTokensStore: root.rootStore.communityTokensStore active: root.Window.window.active } function goTo(section: int, subSection: int) { const stackContent = stackLayout.children for (let i = 0; stackContent.length; i++) { const item = stackContent[i] if (item.sectionKey === section) { d.currentIndex = i if(item.goTo) item.goTo(subSection) break } } } } StatusQUtils.ModelChangeTracker { id: tokensModelChangesTracker Component.onCompleted: { updateOwnerAndTMasterProperties() } // Owner and TMaster token deployment states property bool isOwnerTokenDeployed: false property bool isTMasterTokenDeployed: false property bool isOwnerTokenFailed: false property bool isTMasterTokenFailed: false property var ownerToken: null // It will monitorize if Owner and/or TMaster token items are included in the `model` despite the deployment state property bool ownerOrTMasterTokenItemsExist: false function checkIfPrivilegedTokenItemsExist() { return StatusQUtils.ModelUtils.contains(model, "privilegesLevel", Constants.TokenPrivilegesLevel.Owner) || StatusQUtils.ModelUtils.contains(model, "privilegesLevel", Constants.TokenPrivilegesLevel.TMaster) } function updateOwnerAndTMasterProperties() { // It will update property to know if Owner and TMaster token items have been added into the tokens list. ownerOrTMasterTokenItemsExist = checkIfPrivilegedTokenItemsExist() if(!ownerOrTMasterTokenItemsExist) return // It monitors the deployment: if(!isOwnerTokenDeployed) { isOwnerTokenDeployed = reviewTokenDeployState(true, Constants.ContractTransactionStatus.Completed) isOwnerTokenFailed = reviewTokenDeployState(true, Constants.ContractTransactionStatus.Failed) } if(!isTMasterTokenDeployed) { isTMasterTokenDeployed = reviewTokenDeployState(false, Constants.ContractTransactionStatus.Completed) isTMasterTokenFailed = reviewTokenDeployState(false, Constants.ContractTransactionStatus.Failed) } // Not necessary to track more changes since privileged tokens have been correctly deployed. if(isOwnerTokenDeployed && isTMasterTokenDeployed) { tokensModelChangesTracker.ownerToken = StatusQUtils.ModelUtils.getByKey(model, "privilegesLevel", Constants.TokenPrivilegesLevel.Owner) tokensModelChangesTracker.enabled = false } } function reviewTokenDeployState(isOwner, deployState) { const privileges = isOwner ? Constants.TokenPrivilegesLevel.Owner : Constants.TokenPrivilegesLevel.TMaster const index = StatusQUtils.ModelUtils.indexOf(model, "privilegesLevel", privileges) if(index === -1) return false const token = StatusQUtils.ModelUtils.get(model, index) // Some assertions: if(isOwner && token.privilegesLevel !== Constants.TokenPrivilegesLevel.Owner) return false if(!isOwner && token.privilegesLevel !== Constants.TokenPrivilegesLevel.TMaster) return false // Deploy state check: return token.deployState === deployState } model: root.community.communityTokens onRevisionChanged: { updateOwnerAndTMasterProperties() } } MessageDialog { id: errorDialog title: qsTr("Error editing the community") icon: StandardIcon.Critical standardButtons: StandardButton.Ok } Component { id: noPermissionsPopupCmp NoPermissionsToJoinPopup { onRejectButtonClicked: { root.rootStore.declineRequestToJoinCommunity(requestId, communityId) close() } onClosed: destroy() } } Connections { target: rootStore.communityTokensStore function onRemoteDestructStateChanged(communityId, tokenName, status, url) { if (root.community.id !== communityId) return let title = "" let loading = false let type = Constants.ephemeralNotificationType.normal switch (status) { case Constants.ContractTransactionStatus.InProgress: title = qsTr("Remotely destroying tokens...") loading = true break case Constants.ContractTransactionStatus.Completed: title = qsTr("%1 tokens destroyed").arg(tokenName) type = Constants.ephemeralNotificationType.success break case Constants.ContractTransactionStatus.Failed: title = qsTr("%1 tokens destruction failed").arg(tokenName) break default: console.warn("Unknown destruction state: "+status) return } Global.displayToastMessage(title, qsTr("View on etherscan"), "", loading, type, url) } function onAirdropStateChanged(communityId, tokenName, chainName, status, url) { if (root.community.id !== communityId) return let title = "" let loading = false let type = Constants.ephemeralNotificationType.normal switch (status) { case Constants.ContractTransactionStatus.InProgress: title = qsTr("Airdrop on %1 in progress...").arg(chainName) loading = true break case Constants.ContractTransactionStatus.Completed: title = qsTr("Airdrop on %1 in complete").arg(chainName) type = Constants.ephemeralNotificationType.success break case Constants.ContractTransactionStatus.Failed: title = qsTr("Airdrop on %1 failed").arg(chainName) break default: console.warn("Unknown airdrop state: "+status) return } Global.displayToastMessage(title, qsTr("View on etherscan"), "", loading, type, url) } function onBurnStateChanged(communityId, tokenName, status, url) { if (root.community.id !== communityId) return let title = "" let loading = false let type = Constants.ephemeralNotificationType.normal switch (status) { case Constants.ContractTransactionStatus.InProgress: title = qsTr("%1 being burned...").arg(tokenName) loading = true break case Constants.ContractTransactionStatus.Completed: title = qsTr("%1 burning is complete").arg(tokenName) type = Constants.ephemeralNotificationType.success break case Constants.ContractTransactionStatus.Failed: title = qsTr("%1 burning is failed").arg(tokenName) break default: console.warn("Unknown burning state: "+status) return } Global.displayToastMessage(title, qsTr("View on etherscan"), "", loading, type, url) } function onDeploymentStateChanged(communityId, status, url) { if (root.community.id !== communityId) return let title = "" let loading = false let type = Constants.ephemeralNotificationType.normal switch (status) { case Constants.ContractTransactionStatus.InProgress: title = qsTr("Token is being minted...") loading = true break case Constants.ContractTransactionStatus.Completed: title = qsTr("Token minting finished") type = Constants.ephemeralNotificationType.success break case Constants.ContractTransactionStatus.Failed: title = qsTr("Token minting failed") break default: console.warn("Unknown deploy state: "+status) return } Global.displayToastMessage(title, url === "" ? qsTr("Something went wrong") : qsTr("View on etherscan"), "", loading, type, url) } function onOwnerTokenDeploymentStateChanged(communityId, status, url) { if (root.community.id !== communityId) return if (status === Constants.ContractTransactionStatus.Completed) { let title1 = qsTr("%1 Owner and TokenMaster tokens minting complete").arg(community.name) let title2 = qsTr("%1 Owner token airdropped to you").arg(community.name) let title3 = qsTr("%1 Owner and TokenMaster permissions created").arg(community.name) let type = Constants.ephemeralNotificationType.normal Global.displayToastMessage(title1, url, "", true, type, url) Global.displayToastMessage(title2, url, "", true, type, url) Global.displayToastMessage(title3, url, "", true, type, url) } else if (status === Constants.ContractTransactionStatus.Failed) { let title = qsTr("%1 Owner and TokenMaster tokens minting failed").arg(community.name) let type = Constants.ephemeralNotificationType.normal Global.displayToastMessage(title, url, "", true, type, url) } } function onOwnerTokenDeploymentStarted(communityId, url) { if (root.community.id !== communityId) return let title1 = qsTr("%1 Owner and TokenMaster tokens are being minted...").arg(community.name) let title2 = qsTr("Airdropping %1 Owner token to you...").arg(community.name) let type = Constants.ephemeralNotificationType.normal Global.displayToastMessage(title1, url, "", true, type, url) Global.displayToastMessage(title2, url, "", true, type, url) } } Connections { target: root.chatCommunitySectionModule function onOpenNoPermissionsToJoinPopup(communityName: string, userName: string, communityId: string, requestId: string) { const properties = { communityName: communityName, userName: userName, communityId: communityId, requestId: requestId } Global.openPopup(noPermissionsPopupCmp, properties) } } }