status-desktop/ui/app/AppLayouts/Communities/views/EditPermissionView.qml

642 lines
23 KiB
QML
Raw Normal View History

import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQml 2.15
fix(permissions): fix hang when all channel perm check return (#14259) * fix(permissions): fix hang when all channel perm check return Fixes #14234 The problem was that we updated **all** the models from **all** the channels of a community each time the channel requirement checks returned. The fix is to first of all, make sure we don't call that check too often. It sometimes got called twice in a row by accident. The other better fix is to check if anything actually changed before updating. This solves the issue almost entirely. Since the permissions almost never change, the updates now take only a second. * fix(permisisons): never run permission checks for privileged users Also fixes #14234 but for admins, TMs and Owners. Admins+ were still getting the hang, because the permission checks always returned something different than the models, because the models knew that admins have access to everything, but the permission check was running as if it were a normal user (I think, un-tested). Anyway, the solution is more simple, we never need to run the permission checks on admins+, because they always have access to everything! * fix(Communities): prevent channels model from emitting unnecessary signals Closes: #14274 * chore(Communities): improve channels metadata lookup performance ChannelsSelectionModel is removed, replaced with plain LeftJoinModel. Transformations of left-side model are done in a single place, not in every delegate making the join. * only call update functions when there is something to update + move permission model creation when needed --------- Co-authored-by: Michał Cieślak <michalcieslak@status.im>
2024-04-04 15:26:44 +00:00
import StatusQ 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Utils 0.1
import utils 1.0
import shared.panels 1.0
import AppLayouts.Communities.controls 1.0
import AppLayouts.Communities.helpers 1.0
import AppLayouts.Communities.panels 1.0
import AppLayouts.Communities.popups 1.0
StatusScrollView {
id: root
required property var assetsModel
required property var collectiblesModel
required property var channelsModel
// id, name, image, color, owner properties expected
required property var communityDetails
readonly property bool saveEnabled: root.isFullyFilled
&& !root.permissionDuplicated
&& (isEditState ? !root.permissionTypeLimitExceeded : !root.permissionTypeLimitReached)
property int viewWidth: 560 // by design
property bool isEditState: false
readonly property bool dirty:
root.holdingsRequired !== d.dirtyValues.holdingsRequired ||
(d.dirtyValues.holdingsRequired && !holdingsModelComparator.equal) ||
!channelsModelComparator.equal ||
root.isPrivate !== d.dirtyValues.isPrivate ||
root.permissionType !== d.dirtyValues.permissionType
readonly property alias dirtyValues: d.dirtyValues
readonly property bool isFullyFilled: (dirtyValues.selectedHoldingsModel.count > 0 || !whoHoldsSwitch.checked) &&
dirtyValues.permissionType !== PermissionTypes.Type.None &&
(d.isCommunityPermission || !showChannelSelector || dirtyValues.selectedChannelsModel.count > 0)
property int permissionType: PermissionTypes.Type.None
property bool isPrivate: false
property bool holdingsRequired: true
property bool showChannelSelector: true
property bool ensCommunityPermissionsEnabled
// roles: type, key, name, amount, imageSource
property var selectedHoldingsModel: ListModel {}
// roles: itemId, text, icon, emoji, color, colorId
property var selectedChannelsModel: ListModel {}
property bool permissionDuplicated: false
property bool permissionTypeLimitReached: false
property bool permissionTypeLimitExceeded
signal createPermissionClicked
signal navigateToMintTokenSettings(bool isAssetType)
function resetChanges() {
d.loadInitValues()
}
ModelsComparator {
id: holdingsModelComparator
modelA: root.dirtyValues.selectedHoldingsModel
modelB: root.selectedHoldingsModel
roles: ["key", "amount"]
mode: ModelsComparator.CompareMode.Set
}
ModelsComparator {
id: channelsModelComparator
modelA: root.dirtyValues.selectedChannelsModel
modelB: root.selectedChannelsModel
roles: ["key"]
mode: ModelsComparator.CompareMode.Set
}
QtObject {
id: d
readonly property int maxHoldingsItems: 5
readonly property int dropdownHorizontalOffset: 4
readonly property int dropdownVerticalOffset: 1
readonly property bool isCommunityPermission:
PermissionTypes.isCommunityPermission(dirtyValues.permissionType)
onIsCommunityPermissionChanged: {
if (isCommunityPermission) {
d.dirtyValues.selectedChannelsModel.clear()
inSelector.wholeCommunitySelected = true
inSelector.model = inModelCommunity
} else {
inSelector.model = 0
inSelector.wholeCommunitySelected = false
inSelector.model = channelsSelectionModel
}
}
readonly property QtObject dirtyValues: QtObject {
readonly property ListModel selectedHoldingsModel: ListModel {}
readonly property ListModel selectedChannelsModel: ListModel {}
property int permissionType: PermissionTypes.Type.None
property bool isPrivate: false
property bool holdingsRequired: true
Binding on isPrivate {
value: d.dirtyValues.permissionType === PermissionTypes.Type.Admin
}
function getHoldingIndex(key) {
return ModelUtils.indexOf(selectedHoldingsModel, "key", key)
}
function getTokenKeysAndAmounts() {
return ModelUtils.modelToArray(selectedHoldingsModel, ["type", "key", "amount"])
.filter(item => item.type !== Constants.TokenType.ENS)
.map(item => ({ key: item.key, amount: item.amount }))
}
function getEnsNames() {
return ModelUtils.modelToArray(selectedHoldingsModel, ["type", "name"])
.filter(item => item.type === Constants.TokenType.ENS)
.map(item => item.name)
}
}
function loadInitValues() {
// Holdings:
d.dirtyValues.selectedHoldingsModel.clear()
d.dirtyValues.selectedHoldingsModel.append(
ModelUtils.modelToArray(root.selectedHoldingsModel,
["type", "key", "amount"]))
// Permissions:
d.dirtyValues.permissionType = root.permissionType
// Channels
d.dirtyValues.selectedChannelsModel.clear()
d.dirtyValues.selectedChannelsModel.append(
ModelUtils.modelToArray(root.selectedChannelsModel, ["key"]))
if (root.selectedChannelsModel &&
(root.selectedChannelsModel.rowCount()
|| d.dirtyValues.permissionType === PermissionTypes.Type.None)) {
inSelector.wholeCommunitySelected = false
inSelector.model = channelsSelectionModel
} else {
inSelector.wholeCommunitySelected = true
inSelector.model = inModelCommunity
}
// Is private permission
d.dirtyValues.isPrivate = root.isPrivate
// Are holdings required
d.dirtyValues.holdingsRequired = root.holdingsRequired
}
}
onPermissionTypeChanged: Qt.callLater(() => d.loadInitValues())
contentWidth: mainLayout.width
contentHeight: mainLayout.height
SequenceColumnLayout {
id: mainLayout
width: root.viewWidth
title: qsTr("Anyone")
StatusItemSelector {
id: tokensSelector
objectName: "tokensSelector"
property int editedIndex: -1
Layout.fillWidth: true
icon: Style.svg("contact_verified")
title: qsTr("Who holds")
placeholderText: qsTr("Example: 10 SNT")
tagLeftPadding: 2
asset.height: 28
asset.width: asset.height
addButton.visible: count < d.maxHoldingsItems &&
whoHoldsSwitch.checked
model: HoldingsSelectionModel {
sourceModel: d.dirtyValues.selectedHoldingsModel
assetsModel: root.assetsModel
collectiblesModel: root.collectiblesModel
}
label.enabled: whoHoldsSwitch.checked
placeholderItem.visible: count === 0 && whoHoldsSwitch.checked
Binding on model {
when: !whoHoldsSwitch.checked
value: 0
restoreMode: Binding.RestoreBindingOrValue
}
Binding on bottomPadding {
when: !whoHoldsSwitch.checked
value: 0
restoreMode: Binding.RestoreBindingOrValue
}
children: StatusSwitch {
id: whoHoldsSwitch
padding: 0
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 12
anchors.topMargin: 10
checked: d.dirtyValues.holdingsRequired
onToggled: d.dirtyValues.holdingsRequired = checked
}
HoldingsDropdown {
id: dropdown
communityId: root.communityDetails.id
assetsModel: root.assetsModel
collectiblesModel: root.collectiblesModel
showTokenAmount: false
ensCommunityPermissionsEnabled: root.ensCommunityPermissionsEnabled
function addItem(type, item, amount) {
const key = item.key
const symbol = item.symbol
d.dirtyValues.selectedHoldingsModel.append(
{ type, key, amount, symbol })
}
function prepareUpdateIndex(key) {
const itemIndex = tokensSelector.editedIndex
const existingIndex = d.dirtyValues.getHoldingIndex(key)
if (itemIndex !== -1 && existingIndex !== -1 && itemIndex !== existingIndex) {
const previousKey = d.dirtyValues.selectedHoldingsModel.get(itemIndex).key
d.dirtyValues.selectedHoldingsModel.remove(existingIndex)
return d.dirtyValues.getHoldingIndex(previousKey)
}
if (itemIndex === -1) {
return existingIndex
}
return itemIndex
}
onOpened: {
usedTokens = d.dirtyValues.getTokenKeysAndAmounts()
usedEnsNames = d.dirtyValues.getEnsNames().filter(item => item !== ensDomainName)
}
onAddAsset: {
const modelItem = PermissionsHelpers.getTokenByKey(
root.assetsModel, key)
addItem(Constants.TokenType.ERC20, modelItem, AmountsArithmetic.fromNumber(amount, modelItem.decimals).toFixed())
dropdown.close()
}
onAddCollectible: {
const modelItem = PermissionsHelpers.getTokenByKey(
root.collectiblesModel, key)
addItem(Constants.TokenType.ERC721, modelItem, String(amount))
dropdown.close()
}
onAddEns: {
d.dirtyValues.selectedHoldingsModel.append(
{ type: Constants.TokenType.ENS, key: domain, amount: "1" })
dropdown.close()
}
onUpdateAsset: {
const itemIndex = prepareUpdateIndex(key)
const modelItem = PermissionsHelpers.getTokenByKey(root.assetsModel, key)
d.dirtyValues.selectedHoldingsModel.set(
itemIndex, { type: Constants.TokenType.ERC20, key, amount: AmountsArithmetic.fromNumber(amount, modelItem.decimals).toFixed() })
dropdown.close()
}
onUpdateCollectible: {
const itemIndex = prepareUpdateIndex(key)
const modelItem = PermissionsHelpers.getTokenByKey(
root.collectiblesModel, key)
d.dirtyValues.selectedHoldingsModel.set(
itemIndex,
{ type: Constants.TokenType.ERC721, key, amount: String(amount), symbol: modelItem.symbol })
dropdown.close()
}
onUpdateEns: {
d.dirtyValues.selectedHoldingsModel.set(
tokensSelector.editedIndex,
{ type: Constants.TokenType.ENS, key: domain, amount: "1" })
dropdown.close()
}
onRemoveClicked: {
d.dirtyValues.selectedHoldingsModel.remove(tokensSelector.editedIndex)
dropdown.close()
}
onNavigateToMintTokenSettings: {
root.navigateToMintTokenSettings(isAssetType)
close()
}
}
addButton.onClicked: {
dropdown.parent = tokensSelector.addButton
dropdown.x = tokensSelector.addButton.width + d.dropdownHorizontalOffset
dropdown.y = 0
dropdown.open()
editedIndex = -1
}
onItemClicked: {
if (mouse.button !== Qt.LeftButton)
return
dropdown.parent = item
dropdown.x = mouse.x + d.dropdownHorizontalOffset
dropdown.y = d.dropdownVerticalOffset
const modelItem = tokensSelector.model.get(index)
switch(modelItem.type) {
case Constants.TokenType.ERC20:
dropdown.assetKey = modelItem.key
const decimals = PermissionsHelpers.getTokenByKey(root.assetsModel, modelItem.key).decimals
dropdown.assetAmount = AmountsArithmetic.toNumber(modelItem.amount, decimals)
break
case Constants.TokenType.ERC721:
dropdown.collectibleKey = modelItem.key
dropdown.collectibleAmount = modelItem.amount
break
case Constants.TokenType.ENS:
dropdown.ensDomainName = modelItem.key
break
default:
console.warn("Unsupported holdings type.")
}
dropdown.setActiveTab(modelItem.type)
dropdown.openUpdateFlow()
editedIndex = index
}
}
SequenceColumnLayout.Separator {}
StatusFlowSelector {
id: permissionsSelector
objectName: "permissionsSelector"
Layout.fillWidth: true
title: qsTr("Is allowed to")
placeholderText: qsTr("Example: View and post")
icon: Style.svg("profile/security")
readonly property bool empty:
d.dirtyValues.permissionType === PermissionTypes.Type.None
placeholderItem.visible: empty
addButton.visible: empty
StatusListItemTag {
readonly property int key: d.dirtyValues.permissionType
title: PermissionTypes.getName(key)
visible: !permissionsSelector.empty
asset.name: PermissionTypes.getIcon(key)
asset.bgColor: "transparent"
closeButtonVisible: false
titleText.font.pixelSize: Theme.primaryTextFontSize
leftPadding: 6
Binding on bgColor {
when: root.permissionTypeLimitReached && !root.isEditState
value: Theme.palette.dangerColor3
}
Binding on titleText.color {
when: root.permissionTypeLimitReached && !root.isEditState
value: Theme.palette.dangerColor1
}
Binding on asset.color {
when: root.permissionTypeLimitReached && !root.isEditState
value: Theme.palette.dangerColor1
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
permissionsDropdown.mode = PermissionsDropdown.Mode.Update
permissionsDropdown.parent = parent
permissionsDropdown.x = mouse.x + d.dropdownHorizontalOffset
permissionsDropdown.y = d.dropdownVerticalOffset
permissionsDropdown.open()
}
}
}
PermissionsDropdown {
id: permissionsDropdown
allowCommunityOptions: root.showChannelSelector
initialPermissionType: d.dirtyValues.permissionType
enableAdminPermission: root.communityDetails.owner
onDone: {
if (d.dirtyValues.permissionType === permissionType) {
permissionsDropdown.close()
return
}
d.dirtyValues.permissionType = permissionType
permissionsDropdown.close()
}
}
addButton.onClicked: {
permissionsDropdown.mode = PermissionsDropdown.Mode.Add
permissionsDropdown.parent = permissionsSelector.addButton
permissionsDropdown.x = permissionsSelector.addButton.width
+ d.dropdownHorizontalOffset
permissionsDropdown.y = 0
permissionsDropdown.open()
}
}
SequenceColumnLayout.Separator { visible: root.showChannelSelector }
StatusItemSelector {
id: inSelector
readonly property bool editable: !d.isCommunityPermission
addButton.visible: editable
itemsClickable: editable
visible: root.showChannelSelector
Layout.fillWidth: true
icon: d.isCommunityPermission ? Style.svg("communities") : Style.svg("create-category")
title: qsTr("In")
placeholderText: qsTr("Example: `#general` channel")
useLetterIdenticons: !wholeCommunitySelected || !inDropdown.communityImage
tagLeftPadding: wholeCommunitySelected ? 2 : 6
asset.width: wholeCommunitySelected ? 28 : 20
asset.height: asset.width
property bool wholeCommunitySelected: false
function openInDropdown(parent, x, y) {
inDropdown.parent = parent
inDropdown.x = x
inDropdown.y = y
const selectedChannels = []
if (!inSelector.wholeCommunitySelected) {
const model = d.dirtyValues.selectedChannelsModel
const count = model.count
for (let i = 0; i < count; i++)
selectedChannels.push(model.get(i).key)
}
inDropdown.setSelectedChannels(selectedChannels)
inDropdown.open()
}
ListModel {
id: inModelCommunity
Component.onCompleted: {
append({
imageSource: inDropdown.communityImage,
isIcon: false,
text: inDropdown.communityName,
operator: OperatorsUtils.Operators.None,
color: inDropdown.communityColor
})
}
}
fix(permissions): fix hang when all channel perm check return (#14259) * fix(permissions): fix hang when all channel perm check return Fixes #14234 The problem was that we updated **all** the models from **all** the channels of a community each time the channel requirement checks returned. The fix is to first of all, make sure we don't call that check too often. It sometimes got called twice in a row by accident. The other better fix is to check if anything actually changed before updating. This solves the issue almost entirely. Since the permissions almost never change, the updates now take only a second. * fix(permisisons): never run permission checks for privileged users Also fixes #14234 but for admins, TMs and Owners. Admins+ were still getting the hang, because the permission checks always returned something different than the models, because the models knew that admins have access to everything, but the permission check was running as if it were a normal user (I think, un-tested). Anyway, the solution is more simple, we never need to run the permission checks on admins+, because they always have access to everything! * fix(Communities): prevent channels model from emitting unnecessary signals Closes: #14274 * chore(Communities): improve channels metadata lookup performance ChannelsSelectionModel is removed, replaced with plain LeftJoinModel. Transformations of left-side model are done in a single place, not in every delegate making the join. * only call update functions when there is something to update + move permission model creation when needed --------- Co-authored-by: Michał Cieślak <michalcieslak@status.im>
2024-04-04 15:26:44 +00:00
LeftJoinModel {
id: channelsSelectionModel
fix(permissions): fix hang when all channel perm check return (#14259) * fix(permissions): fix hang when all channel perm check return Fixes #14234 The problem was that we updated **all** the models from **all** the channels of a community each time the channel requirement checks returned. The fix is to first of all, make sure we don't call that check too often. It sometimes got called twice in a row by accident. The other better fix is to check if anything actually changed before updating. This solves the issue almost entirely. Since the permissions almost never change, the updates now take only a second. * fix(permisisons): never run permission checks for privileged users Also fixes #14234 but for admins, TMs and Owners. Admins+ were still getting the hang, because the permission checks always returned something different than the models, because the models knew that admins have access to everything, but the permission check was running as if it were a normal user (I think, un-tested). Anyway, the solution is more simple, we never need to run the permission checks on admins+, because they always have access to everything! * fix(Communities): prevent channels model from emitting unnecessary signals Closes: #14274 * chore(Communities): improve channels metadata lookup performance ChannelsSelectionModel is removed, replaced with plain LeftJoinModel. Transformations of left-side model are done in a single place, not in every delegate making the join. * only call update functions when there is something to update + move permission model creation when needed --------- Co-authored-by: Michał Cieślak <michalcieslak@status.im>
2024-04-04 15:26:44 +00:00
leftModel: d.dirtyValues.selectedChannelsModel
rightModel: root.channelsModel
joinRole: "key"
}
InDropdown {
id: inDropdown
model: root.channelsModel
communityName: root.communityDetails.name
communityImage: root.communityDetails.image
communityColor: root.communityDetails.color
onChannelsSelected: {
d.dirtyValues.selectedChannelsModel.clear()
inSelector.model = 0
inSelector.wholeCommunitySelected = false
const modelData = channels.map(key => ({ key }))
d.dirtyValues.selectedChannelsModel.append(modelData)
inSelector.model = channelsSelectionModel
close()
}
onCommunitySelected: {
d.dirtyValues.selectedChannelsModel.clear()
inSelector.wholeCommunitySelected = true
inSelector.model = inModelCommunity
close()
}
}
addButton.onClicked: {
inDropdown.acceptMode = InDropdown.AcceptMode.Add
openInDropdown(inSelector.addButton,
inSelector.addButton.width + d.dropdownHorizontalOffset, 0)
}
onItemClicked: {
if (mouse.button !== Qt.LeftButton)
return
inDropdown.acceptMode = InDropdown.AcceptMode.Update
openInDropdown(item, mouse.x + d.dropdownHorizontalOffset,
d.dropdownVerticalOffset)
}
}
Separator {
Layout.topMargin: 24
color: Theme.palette.baseColor2
}
StatusIconSwitch {
Layout.topMargin: 12
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: Layout.leftMargin
enabled: d.dirtyValues.permissionType !== PermissionTypes.Type.Admin
checked: d.dirtyValues.isPrivate
title: qsTr("Hide permission")
subTitle: qsTr("Make this permission hidden from members who dont meet its requirements")
icon: "hide"
onToggled: d.dirtyValues.isPrivate = checked
}
WarningPanel {
id: duplicationPanel
objectName: "duplicationPanel"
Layout.fillWidth: true
Layout.topMargin: 50 // by desing
text: {
if (root.permissionTypeLimitReached)
return PermissionTypes.getPermissionsLimitWarning(
d.dirtyValues.permissionType)
if (root.permissionDuplicated)
return qsTr("Permission with same properties is already active, edit properties to create a new permission.")
return ""
}
visible: root.permissionDuplicated || (root.permissionTypeLimitReached && !root.isEditState)
}
StatusWarningBox {
Layout.fillWidth: true
Layout.topMargin: Style.current.padding
visible: root.showChannelSelector
icon: "desktop"
text: qsTr("Any changes to community permissions will take effect after the control node receives and processes them")
borderColor: Theme.palette.baseColor1
iconColor: textColor
}
StatusButton {
Layout.preferredHeight: 44
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.topMargin: Style.current.bigPadding
visible: !root.isEditState && root.showChannelSelector
objectName: "createPermissionButton"
text: qsTr("Create permission")
enabled: root.saveEnabled
onClicked: root.createPermissionClicked()
}
}
}