status-desktop/ui/imports/shared/popups/CommunityMembershipSetupDia...

486 lines
18 KiB
QML

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import utils 1.0
import StatusQ 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import StatusQ.Core.Utils 0.1
import AppLayouts.Communities.panels 1.0
import AppLayouts.Communities.controls 1.0
import AppLayouts.Communities.helpers 1.0
import SortFilterProxyModel 0.2
StatusStackModal {
id: root
destroyOnClose: true
property bool isEditMode: false
required property string communityId
required property string communityName
required property string communityIcon
required property bool requirementsCheckPending
property bool checkingPermissionToJoinInProgress
property bool joinPermissionsCheckCompletedWithoutErrors
property string introMessage
property bool isInvitationPending: false
required property var walletAccountsModel // name, address, emoji, colorId
required property var walletAssetsModel
required property var walletCollectiblesModel
required property var permissionsModel // id, key, permissionType, holdingsListModel, channelsListModel, isPrivate, tokenCriteriaMet
required property var assetsModel
required property var collectiblesModel
property var keypairSigningModel
property var currentSharedAddresses: []
onCurrentSharedAddressesChanged: d.reEvaluateModels()
property string currentAirdropAddress: ""
onCurrentAirdropAddressChanged: d.reEvaluateModels()
property var getCurrencyAmount: function (balance, symbol){}
property var canProfileProveOwnershipOfProvidedAddressesFn: function(addresses) { return false }
readonly property bool profileProvesOwnershipOfSelectedAddresses: {
d.selectedSharedAddressesMap // needed for binding
const obj = d.getSelectedAddresses()
return root.canProfileProveOwnershipOfProvidedAddressesFn(obj.addresses)
}
readonly property bool allAddressesToRevealBelongToSingleNonProfileKeypair: {
const keyUids = new Set()
for (const [key, value] of d.selectedSharedAddressesMap) {
keyUids.add(value.keyUid)
}
return keyUids.size === 1 && !keyUids.has(userProfile.keyUid)
}
signal prepareForSigning(string airdropAddress, var sharedAddresses)
signal joinCommunity()
signal editRevealedAddresses()
signal signProfileKeypairAndAllNonKeycardKeypairs()
signal signSharedAddressesForKeypair(string keyUid)
signal cancelMembershipRequest()
signal sharedAddressesUpdated(var sharedAddresses)
width: 640 // by design
padding: 0
stackTitle: d.accessType === Constants.communityChatOnRequestAccess ?
qsTr("Request to join %1").arg(root.communityName)
: qsTr("Welcome to %1").arg(root.communityName)
rightButtons: [d.shareButton, finishButton]
finishButton: StatusButton {
interactive: {
if (root.isInvitationPending || d.accessType !== Constants.communityChatOnRequestAccess)
return true
if (root.checkingPermissionToJoinInProgress || !root.joinPermissionsCheckCompletedWithoutErrors)
return false
return d.eligibleToJoinAs !== PermissionTypes.Type.None
}
loading: root.checkingPermissionToJoinInProgress && !root.isInvitationPending
tooltip.text: {
if (interactive)
return ""
if (root.checkingPermissionToJoinInProgress)
return qsTr("Requirements check pending")
if (!root.joinPermissionsCheckCompletedWithoutErrors)
return qsTr("Checking permissions to join failed")
return ""
}
text: {
if (root.isInvitationPending) {
return qsTr("Cancel Membership Request")
}
if (d.selectedSharedAddressesCount === d.totalNumOfAddressesForSharing) {
return qsTr("Share all addresses to join")
}
return qsTr("Share %n address(s) to join", "", d.selectedSharedAddressesCount)
}
type: root.isInvitationPending ? StatusBaseButton.Type.Danger
: StatusBaseButton.Type.Normal
icon.name: {
if (root.isInvitationPending)
return ""
if (root.profileProvesOwnershipOfSelectedAddresses) {
if (userProfile.usingBiometricLogin) {
return "touch-id"
}
if (userProfile.isKeycardUser) {
return "keycard"
}
return "password"
}
if (root.allAddressesToRevealBelongToSingleNonProfileKeypair) {
return "keycard"
}
return ""
}
onClicked: {
if (root.isInvitationPending) {
root.cancelMembershipRequest()
root.close()
return
}
d.proceedToSigningOrSubmitRequest(d.communityIntroUid)
}
}
backButton: StatusBackButton {
visible: !!root.replaceLoader.item
&& !(root.replaceLoader.item.componentUid === d.shareAddressesUid && root.isEditMode)
onClicked: {
if (d.backActionGoesTo === d.communityIntroUid) {
if (root.replaceItem) {
root.replaceItem = undefined
}
return
}
if (d.backActionGoesTo === d.shareAddressesUid) {
d.backActionGoesTo = d.communityIntroUid
root.replace(sharedAddressesPanelComponent)
return
}
}
Layout.minimumWidth: implicitWidth
}
QtObject {
id: d
readonly property string communityIntroUid: "community-intro"
readonly property string shareAddressesUid: "shared-addresses"
readonly property string signingPanelUid: "signing-panel"
property string backActionGoesTo: d.communityIntroUid
readonly property int totalNumOfAddressesForSharing: root.walletAccountsModel.count
property var currentSharedAddressesMap: new Map() // Map[address, [keyUid, selected, isAirdrop]] - used in edit mode only
property var selectedSharedAddressesMap: new Map() // Map[address, [keyUid, selected, isAirdrop]]
readonly property int selectedSharedAddressesCount: d.selectedSharedAddressesMap.size
readonly property int accessType: d.eligibleToJoinAs !== -1 ? Constants.communityChatOnRequestAccess
: Constants.communityChatPublicAccess
property int eligibleToJoinAs: PermissionsHelpers.isEligibleToJoinAs(root.permissionsModel)
readonly property var _con: Connections {
target: root.permissionsModel
ignoreUnknownSignals: true
function onTokenCriteriaUpdated() {
d.eligibleToJoinAs = PermissionsHelpers.isEligibleToJoinAs(root.permissionsModel)
}
}
readonly property var initialAddressesModel: SortFilterProxyModel {
sourceModel: root.walletAccountsModel
}
function proceedToSigningOrSubmitRequest(uidOfComponentThisFunctionIsCalledFrom) {
const selected = d.getSelectedAddresses()
root.prepareForSigning(selected.airdropAddress, selected.addresses)
if (root.profileProvesOwnershipOfSelectedAddresses) {
root.signProfileKeypairAndAllNonKeycardKeypairs()
return
}
if (root.allAddressesToRevealBelongToSingleNonProfileKeypair) {
if (d.selectedSharedAddressesMap.size === 0) {
console.error("selected shared addresses must not be empty")
return
}
const keyUid = d.selectedSharedAddressesMap.values().next().value.keyUid
root.signSharedAddressesForKeypair(keyUid)
return
}
d.backActionGoesTo = uidOfComponentThisFunctionIsCalledFrom
root.replace(sharedAddressesSigningPanelComponent)
}
// This function deletes/adds it the address from/to the map.
function toggleAddressSelection(keyUid, address) {
const tmpMap = d.selectedSharedAddressesMap
const lAddress = address.toLowerCase()
const obj = tmpMap.get(lAddress)
if (!!obj) {
if (tmpMap.size === 1) {
console.error("cannot remove the last selected address")
}
tmpMap.delete(lAddress)
if (obj.isAirdrop) {
d.selectAirdropAddressForTheFirstSelectedAddress()
}
} else {
tmpMap.set(lAddress, {keyUid: keyUid, selected: true, isAirdrop: false})
}
d.selectedSharedAddressesMap = tmpMap
}
// This function selects new airdrop address, invalidating old airdrop address selection.
function selectAirdropAddressForTheFirstSelectedAddress() {
const tmpMap = d.selectedSharedAddressesMap
// clear previous airdrop address
for (const [key, value] of tmpMap) {
if (!value.isAirdrop) {
d.selectedSharedAddressesMap.set(key, {keyUid: value.keyUid, selected: value.selected, isAirdrop: true})
break
}
}
d.selectedSharedAddressesMap = tmpMap
}
// This function selects new airdrop address, invalidating old airdrop address selection.
function selectAirdropAddress(address) {
const tmpMap = d.selectedSharedAddressesMap
// clear previous airdrop address
for (const [key, value] of tmpMap) {
if (value.isAirdrop) {
tmpMap.set(key, {keyUid: value.keyUid, selected: value.selected, isAirdrop: false})
break
}
}
// set new airdrop address
const lAddress = address.toLowerCase()
const obj = tmpMap.get(lAddress)
if (!obj) {
console.error("cannot set airdrop address for unselected address")
return
}
obj.isAirdrop = true
tmpMap.set(lAddress, obj)
d.selectedSharedAddressesMap = tmpMap
}
// Returns an object containing all selected addresses and selected airdrop address
function getSelectedAddresses() {
const result = {addresses: [], airdropAddress: ""}
for (const [key, value] of d.selectedSharedAddressesMap) {
if (value.selected) {
result.addresses.push(key)
}
if (value.isAirdrop) {
result.airdropAddress = key
}
}
return result
}
function reEvaluateModels() {
const tmpSharedAddressesMap = new Map()
const tmpCurrentSharedAddressesMap = new Map()
for (let i=0; i < d.initialAddressesModel.count; ++i){
const obj = d.initialAddressesModel.get(i)
if (!!obj) {
const lAddress = obj.address.toLowerCase()
let isAirdrop = i === 0
if (root.isEditMode) {
if (root.currentSharedAddresses.indexOf(obj.address) === -1) {
continue
}
isAirdrop = lAddress === root.currentAirdropAddress.toLowerCase()
}
tmpSharedAddressesMap.set(lAddress, {keyUid: obj.keyUid, selected: true, isAirdrop: isAirdrop})
tmpCurrentSharedAddressesMap.set(lAddress, {keyUid: obj.keyUid, selected: true, isAirdrop: isAirdrop})
}
}
d.selectedSharedAddressesMap = tmpSharedAddressesMap
if (root.isEditMode) {
d.currentSharedAddressesMap = new Map(tmpCurrentSharedAddressesMap)
}
}
readonly property var shareButton: StatusFlatButton {
height: finishButton.height
visible: !root.isInvitationPending && !root.replaceItem
borderColor: "transparent"
text: qsTr("Select addresses to share")
onClicked: {
d.backActionGoesTo = d.communityIntroUid
root.replace(sharedAddressesPanelComponent)
}
}
}
Component.onCompleted: {
d.reEvaluateModels()
if (root.isEditMode) {
d.backActionGoesTo = d.shareAddressesUid
root.replace(sharedAddressesPanelComponent)
}
}
Component {
id: sharedAddressesPanelComponent
SharedAddressesPanel {
componentUid: d.shareAddressesUid
isEditMode: root.isEditMode
communityId: root.communityId
communityName: root.communityName
communityIcon: root.communityIcon
requirementsCheckPending: root.requirementsCheckPending
checkingPermissionToJoinInProgress: root.checkingPermissionToJoinInProgress
joinPermissionsCheckCompletedWithoutErrors: root.joinPermissionsCheckCompletedWithoutErrors
walletAccountsModel: d.initialAddressesModel
selectedSharedAddressesMap: d.selectedSharedAddressesMap
currentSharedAddressesMap: d.currentSharedAddressesMap
totalNumOfAddressesForSharing: d.totalNumOfAddressesForSharing
eligibleToJoinAs: d.eligibleToJoinAs
profileProvesOwnershipOfSelectedAddresses: root.profileProvesOwnershipOfSelectedAddresses
allAddressesToRevealBelongToSingleNonProfileKeypair: root.allAddressesToRevealBelongToSingleNonProfileKeypair
walletAssetsModel: root.walletAssetsModel
walletCollectiblesModel: root.walletCollectiblesModel
permissionsModel: root.permissionsModel
assetsModel: root.assetsModel
collectiblesModel: root.collectiblesModel
onClose: {
root.close()
}
onToggleAddressSelection: {
d.toggleAddressSelection(keyUid, address)
const obj = d.getSelectedAddresses()
root.sharedAddressesUpdated(obj.addresses)
}
onAirdropAddressSelected: {
d.selectAirdropAddress(address)
}
onShareSelectedAddressesClicked: {
d.proceedToSigningOrSubmitRequest(d.shareAddressesUid)
}
getCurrencyAmount: function (balance, symbol){
return root.getCurrencyAmount(balance, symbol)
}
}
}
Component {
id: sharedAddressesSigningPanelComponent
SharedAddressesSigningPanel {
componentUid: d.signingPanelUid
isEditMode: root.isEditMode
totalNumOfAddressesForSharing: d.totalNumOfAddressesForSharing
selectedSharedAddressesMap: d.selectedSharedAddressesMap
communityName: root.communityName
keypairSigningModel: root.keypairSigningModel
onSignProfileKeypairAndAllNonKeycardKeypairs: {
root.signProfileKeypairAndAllNonKeycardKeypairs()
}
onSignSharedAddressesForKeypair: {
root.signSharedAddressesForKeypair(keyUid)
}
onJoinCommunity: {
if (root.isEditMode) {
root.editRevealedAddresses()
} else {
root.joinCommunity()
}
root.close()
}
}
}
stackItems: [
Item {
implicitHeight: scrollView.contentHeight + scrollView.bottomPadding + eligibilityTag.anchors.bottomMargin
StatusScrollView {
id: scrollView
anchors.fill: parent
contentWidth: availableWidth
bottomPadding: 80
ColumnLayout {
spacing: Theme.bigPadding
width: parent.width
StatusRoundedImage {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 64
Layout.preferredHeight: width
visible: ((image.status == Image.Loading) ||
(image.status == Image.Ready)) &&
!image.isError
image.source: root.communityIcon
}
StatusBaseText {
Layout.fillWidth: true
text: root.introMessage || qsTr("Community <b>%1</b> has no intro message...").arg(root.communityName)
color: Theme.palette.directColor1
wrapMode: Text.Wrap
}
}
}
CommunityEligibilityTag {
id: eligibilityTag
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.bigPadding
eligibleToJoinAs: d.eligibleToJoinAs
isEditMode: root.isEditMode
visible: !root.isInvitationPending && !root.checkingPermissionToJoinInProgress && root.joinPermissionsCheckCompletedWithoutErrors &&
d.accessType === Constants.communityChatOnRequestAccess
}
}
]
}