import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import StatusQ 0.1 import StatusQ.Core 0.1 import StatusQ.Components 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Core.Utils 0.1 import SortFilterProxyModel 0.2 import AppLayouts.Communities.controls 1.0 import AppLayouts.Communities.views 1.0 import AppLayouts.Communities.helpers 1.0 import utils 1.0 Rectangle { id: root required property int /*PermissionTypes.Type*/ eligibleToJoinAs property bool isEditMode property string communityName property string communityIcon property var permissionsModel property var assetsModel property var collectiblesModel property bool requirementsCheckPending readonly property bool lostPermissionToJoin: d.lostPermissionToJoin readonly property bool lostChannelPermissions: d.lostChannelPermissions implicitHeight: permissionsScrollView.contentHeight - permissionsScrollView.anchors.topMargin color: Theme.palette.baseColor4 readonly property bool hasAnyVisiblePermission: root.permissionsModel && root.permissionsModel.count && (d.tokenMasterPermissionsModel.count > 0 || d.adminPermissionsModel.count > 0 || d.joinPermissionsModel.count > 0 || d.channelsPermissionsModel.count > 0) QtObject { id: d // UI readonly property int absLeftMargin: 12 readonly property color tableBorderColor: Theme.palette.directColor7 // internal logic readonly property bool lostPermissionToJoin: root.isEditMode && joinPermissionsModel.count && !joinPermissionPanel.tokenCriteriaMet readonly property var uniquePermissionChannels: d.channelsPermissionsModel.count ? PermissionsHelpers.getUniquePermissionChannels(root.permissionsModel, [PermissionTypes.Type.Read, PermissionTypes.Type.ViewAndPost]) : [] property var initialChannelPermissions function getChannelPermissions() { var result = {} for (let i = 0; i < channelPermissionsPanel.count; i++) { const channel = channelPermissionsPanel.itemAt(i) const ckey = channel.channelKey result[ckey] = [channel.readPermissionMet, channel.viewAndPostPermissionMet] } return result } readonly property bool lostChannelPermissions: root.isEditMode && d.uniquePermissionChannels.length > 0 && channelPermissionsPanel.anyPermissionLost // models readonly property var tokenMasterPermissionsModel: SortFilterProxyModel { id: tokenMasterPermissionsModel sourceModel: root.permissionsModel filters: [ ValueFilter { roleName: "permissionType" value: Constants.permissionType.becomeTokenMaster }, ValueFilter { roleName: "tokenCriteriaMet" value: true } ] } readonly property var adminPermissionsModel: SortFilterProxyModel { id: adminPermissionsModel sourceModel: root.permissionsModel filters: [ ValueFilter { roleName: "permissionType" value: Constants.permissionType.admin }, AnyOf { ValueFilter { roleName: "isPrivate" value: false } AllOf { ValueFilter { roleName: "tokenCriteriaMet" value: true } ValueFilter { roleName: "isPrivate" value: true } } } ] } readonly property var joinPermissionsModel: SortFilterProxyModel { id: joinPermissionsModel sourceModel: root.permissionsModel filters: [ ValueFilter { roleName: "permissionType" value: Constants.permissionType.member }, AnyOf { ValueFilter { roleName: "isPrivate" value: false } AllOf { ValueFilter { roleName: "tokenCriteriaMet" value: true } ValueFilter { roleName: "isPrivate" value: true } } } ] } // used to check if there are any visible channel permissions readonly property var channelsPermissionsModel: SortFilterProxyModel { id: channelsPermissionsModel sourceModel: root.permissionsModel filters: [ AnyOf { ValueFilter { roleName: "permissionType" value: Constants.permissionType.read } ValueFilter { roleName: "permissionType" value: Constants.permissionType.viewAndPost } }, AnyOf { ValueFilter { roleName: "isPrivate" value: false } AllOf { ValueFilter { roleName: "tokenCriteriaMet" value: true } ValueFilter { roleName: "isPrivate" value: true } } } ] } } Component.onCompleted: { d.initialChannelPermissions = d.getChannelPermissions() } StatusScrollView { id: permissionsScrollView anchors.fill: parent anchors.topMargin: -Style.current.padding bottomPadding: eligibilityHintBubble.visible ? eligibilityHintBubble.height + eligibilityHintBubble.anchors.bottomMargin*2 : 16 contentWidth: availableWidth ColumnLayout { width: parent.width spacing: Style.current.halfPadding // header RowLayout { Layout.fillWidth: true Layout.bottomMargin: 4 spacing: Style.current.padding StatusRoundedImage { Layout.preferredWidth: 40 Layout.preferredHeight: 40 Layout.leftMargin: d.absLeftMargin image.source: root.communityIcon } StatusBaseText { font.weight: Font.Medium text: qsTr("Permissions") } Item { Layout.fillWidth: true } RowLayout { Layout.rightMargin: Style.current.halfPadding spacing: 4 visible: root.requirementsCheckPending StatusBaseText { text: qsTr("Updating eligibility") font.pixelSize: Style.current.tertiaryTextFontSize color: Theme.palette.baseColor1 } StatusLoadingIndicator { Layout.preferredWidth: 12 Layout.preferredHeight: 12 } } } // permission types PermissionPanel { id: joinPermissionPanel permissionType: PermissionTypes.Type.Member permissionsModel: d.joinPermissionsModel } PermissionPanel { permissionType: PermissionTypes.Type.Admin permissionsModel: d.adminPermissionsModel } PermissionPanel { id: tokenMasterPermissionPanel permissionType: PermissionTypes.Type.TokenMaster permissionsModel: d.tokenMasterPermissionsModel } Repeater { // channel repeater id: channelPermissionsPanel model: d.uniquePermissionChannels delegate: ChannelPermissionPanel {} readonly property bool anyPermissionLost: { for (let i = 0; i < channelPermissionsPanel.count; i++) { const channel = channelPermissionsPanel.itemAt(i) if (channel && channel.anyPermissionLost) return true } return false } } } } CommunityEligibilityTag { id: eligibilityHintBubble eligibleToJoinAs: root.eligibleToJoinAs visible: !root.isEditMode anchors.bottom: parent.bottom anchors.bottomMargin: 24 anchors.horizontalCenter: parent.horizontalCenter } component PanelBg: Rectangle { color: Theme.palette.statusListItem.backgroundColor border.width: 1 border.color: Theme.palette.baseColor2 radius: Style.current.radius } component PanelIcon: StatusRoundIcon { Layout.preferredWidth: 40 Layout.preferredHeight: 40 Layout.alignment: Qt.AlignTop asset.name: { switch (permissionType) { case PermissionTypes.Type.TokenMaster: return "arbitrator" case PermissionTypes.Type.Admin: return "admin" case PermissionTypes.Type.Member: return "communities" default: return "channel" } } radius: height/2 } component PanelHeading: StatusBaseText { Layout.fillWidth: true elide: Text.ElideRight font.weight: Font.Medium text: { switch (permissionType) { case PermissionTypes.Type.Admin: return qsTr("Become an admin") case PermissionTypes.Type.Member: return qsTr("Join %1").arg(root.communityName) case PermissionTypes.Type.TokenMaster: return qsTr("Become a TokenMaster") default: return d.uniquePermissionChannels[index][1] } } } component SinglePermissionFlow: Flow { Layout.fillWidth: true spacing: Style.current.halfPadding Repeater { model: HoldingsSelectionModel { sourceModel: model.holdingsListModel assetsModel: root.assetsModel collectiblesModel: root.collectiblesModel } delegate: Row { spacing: 4 StatusRoundedImage { anchors.verticalCenter: parent.verticalCenter width: 16 height: 16 image.source: model.imageSource visible: !isError } StatusBaseText { anchors.verticalCenter: parent.verticalCenter font.pixelSize: Theme.tertiaryTextFontSize text: model.text color: model.available ? Theme.palette.successColor1 : Theme.palette.baseColor1 } } } } component PermissionPanel: Control { id: permissionPanel property int permissionType: PermissionTypes.Type.None property var permissionsModel readonly property bool tokenCriteriaMet: overallPermissionRow.tokenCriteriaMet visible: permissionsModel.count Layout.fillWidth: true padding: d.absLeftMargin background: PanelBg {} contentItem: RowLayout { spacing: Style.current.padding PanelIcon {} ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true PanelHeading {} Rectangle { Layout.fillWidth: true Layout.preferredHeight: grid.implicitHeight + grid.anchors.margins*2 border.width: 1 border.color: d.tableBorderColor radius: Style.current.radius color: "transparent" GridLayout { id: grid anchors.fill: parent anchors.margins: Style.current.halfPadding rowSpacing: Style.current.halfPadding columnSpacing: Style.current.halfPadding columns: 2 Repeater { id: permissionsRepeater property int revision onItemAdded: revision++ onItemRemoved: revision++ model: permissionPanel.permissionsModel delegate: ColumnLayout { Layout.column: 0 Layout.row: index Layout.fillWidth: true spacing: Style.current.halfPadding readonly property bool tokenCriteriaMet: model.tokenCriteriaMet ?? false onTokenCriteriaMetChanged: permissionsRepeater.revision++ SinglePermissionFlow {} Rectangle { Layout.fillWidth: true Layout.leftMargin: -grid.anchors.leftMargin Layout.rightMargin: -grid.anchors.rightMargin Layout.preferredHeight: 1 color: d.tableBorderColor visible: index < permissionsRepeater.count - 1 } } } RowLayout { id: overallPermissionRow Layout.column: 1 Layout.rowSpan: permissionsRepeater.count || 1 Layout.preferredWidth: 110 Layout.fillHeight: true readonly property bool tokenCriteriaMet: { permissionsRepeater.revision for (var i = 0; i < permissionsRepeater.count; i++) { // NB no let/const here b/c of https://bugreports.qt.io/browse/QTBUG-91917 var permissionItem = permissionsRepeater.itemAt(i); if (permissionItem && permissionItem.tokenCriteriaMet) return true } return false } Rectangle { Layout.preferredWidth: 1 Layout.fillHeight: true Layout.topMargin: -Style.current.halfPadding Layout.bottomMargin: -Style.current.halfPadding color: d.tableBorderColor } Row { Layout.alignment: Qt.AlignCenter spacing: 4 StatusIcon { anchors.verticalCenter: parent.verticalCenter width: 16 height: 16 icon: overallPermissionRow.tokenCriteriaMet ? "tiny/checkmark" : "tiny/secure" color: { if (d.lostPermissionToJoin) return Theme.palette.dangerColor1 if (overallPermissionRow.tokenCriteriaMet) return Theme.palette.successColor1 return Theme.palette.baseColor1 } } StatusBaseText { anchors.verticalCenter: parent.verticalCenter font.pixelSize: Theme.tertiaryTextFontSize text: { switch (permissionPanel.permissionType) { case PermissionTypes.Type.Admin: return qsTr("Admin") case PermissionTypes.Type.Member: return qsTr("Join") case PermissionTypes.Type.Read: return qsTr("View only") case PermissionTypes.Type.ViewAndPost: return qsTr("View & post") case PermissionTypes.Type.TokenMaster: return qsTr("TokenMaster") default: return "???" } } color: { if (d.lostPermissionToJoin) return Theme.palette.dangerColor1 if (overallPermissionRow.tokenCriteriaMet) return Theme.palette.directColor1 return Theme.palette.baseColor1 } } } } } } } } } component ChannelPermissionPanel: Control { id: channelPermsPanel Layout.fillWidth: true spacing: 10 padding: d.absLeftMargin background: PanelBg {} visible: { for (var i = 0; i < channelPermsRepeater.count; i++) { var chanPermissionItem = channelPermsRepeater.itemAt(i); if (chanPermissionItem.channelPermissionsModel.count > 0) return true } return false } readonly property string channelKey: modelData[0] readonly property bool readPermissionMet: channelPermsRepeater.count > 0 ? channelPermsRepeater.itemAt(0).tokenCriteriaMet : false readonly property bool viewAndPostPermissionMet: channelPermsRepeater.count > 1 ? channelPermsRepeater.itemAt(1).tokenCriteriaMet : false readonly property bool anyPermissionLost: channelPermsRepeater.count > 0 ? channelPermsRepeater.itemAt(0).permissionLost || channelPermsRepeater.itemAt(1).permissionLost : false contentItem: RowLayout { spacing: Style.current.padding PanelIcon {} ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true spacing: Style.current.smallPadding PanelHeading {} Repeater { // permissions repeater id: channelPermsRepeater model: [PermissionTypes.Type.Read, PermissionTypes.Type.ViewAndPost] delegate: Rectangle { id: channelPermsSubPanel readonly property int permissionType: modelData readonly property alias tokenCriteriaMet: overallPermissionRow2.tokenCriteriaMet readonly property bool permissionLost: d.initialChannelPermissions[channelPermsPanel.channelKey][index] && !tokenCriteriaMet readonly property alias channelPermissionsModel: permissionsRepeater2.model Layout.fillWidth: true Layout.preferredHeight: grid2.implicitHeight + grid2.anchors.margins*2 border.width: 1 border.color: d.tableBorderColor radius: Style.current.radius color: "transparent" visible: permissionsRepeater2.model.count > 0 GridLayout { id: grid2 anchors.fill: parent anchors.margins: Style.current.halfPadding rowSpacing: Style.current.halfPadding columnSpacing: Style.current.halfPadding columns: 2 Repeater { id: permissionsRepeater2 property int revision onItemAdded: revision++ onItemRemoved: revision++ model: SortFilterProxyModel { id: channelPermissionsModel sourceModel: d.channelsPermissionsModel function filterPredicate(channelsListModel) { return ModelUtils.contains(channelsListModel, "key", channelPermsPanel.channelKey) } filters: [ ValueFilter { roleName: "permissionType" value: channelPermsSubPanel.permissionType }, FastExpressionFilter { expression: channelPermissionsModel.filterPredicate(model.channelsListModel) // filter and group by channel "key" expectedRoles: ["channelsListModel"] } ] } delegate: ColumnLayout { Layout.column: 0 Layout.row: index Layout.fillWidth: true spacing: Style.current.halfPadding readonly property bool tokenCriteriaMet: model.tokenCriteriaMet ?? false onTokenCriteriaMetChanged: permissionsRepeater2.revision++ SinglePermissionFlow {} Rectangle { Layout.fillWidth: true Layout.leftMargin: -grid2.anchors.leftMargin Layout.rightMargin: -grid2.anchors.rightMargin Layout.preferredHeight: 1 color: d.tableBorderColor visible: index < permissionsRepeater2.count - 1 } } } RowLayout { id: overallPermissionRow2 Layout.column: 1 Layout.rowSpan: channelPermissionsModel.count || 1 Layout.preferredWidth: 110 Layout.fillHeight: true readonly property bool tokenCriteriaMet: { permissionsRepeater2.revision for (var i = 0; i < permissionsRepeater2.count; i++) { // NB no let/const here b/c of https://bugreports.qt.io/browse/QTBUG-91917 const permissionItem = permissionsRepeater2.itemAt(i); if (permissionItem && permissionItem.tokenCriteriaMet) return true } return false } Rectangle { Layout.preferredWidth: 1 Layout.fillHeight: true Layout.topMargin: -Style.current.halfPadding Layout.bottomMargin: -Style.current.halfPadding color: d.tableBorderColor } Row { Layout.alignment: Qt.AlignCenter spacing: 4 StatusIcon { anchors.verticalCenter: parent.verticalCenter width: 16 height: 16 icon: overallPermissionRow2.tokenCriteriaMet ? "tiny/checkmark" : "tiny/secure" color: { if (channelPermsSubPanel.permissionLost) return Theme.palette.dangerColor1 if (overallPermissionRow2.tokenCriteriaMet) return Theme.palette.successColor1 return Theme.palette.baseColor1 } } StatusBaseText { anchors.verticalCenter: parent.verticalCenter font.pixelSize: Theme.tertiaryTextFontSize text: { switch (channelPermsSubPanel.permissionType) { case PermissionTypes.Type.Read: return qsTr("View only") case PermissionTypes.Type.ViewAndPost: return qsTr("View & post") default: return "???" } } color: { if (channelPermsSubPanel.permissionLost) return Theme.palette.dangerColor1 if (overallPermissionRow2.tokenCriteriaMet) return Theme.palette.directColor1 return Theme.palette.baseColor1 } } } } } } } } } } }