token-permissions: implement the UI "edit" flow for shared addresses

Fixes #11599
This commit is contained in:
Lukáš Tinkl 2023-08-02 19:39:42 +02:00 committed by Lukáš Tinkl
parent f721636452
commit 22258cc363
15 changed files with 518 additions and 69 deletions

View File

@ -26,13 +26,29 @@ SplitView {
loginType: ctrlLoginType.currentIndex
walletAccountsModel: WalletAccountsModel {}
permissionsModel: {
if (ctrlPermissions.checked && ctrlTokenGatedChannels.checked)
if (ctrlPermissions.checked && ctrlTokenGatedChannels.checked) {
if (selectedSharedAddresses.length === 1 && ctrlEditMode.checked) {
console.warn("Simulation: Losing BOTH the join and VIP read channel permissions !!!")
return PermissionsModel.complexCombinedPermissionsModelNotMet
}
return PermissionsModel.complexCombinedPermissionsModel
}
if (ctrlPermissions.checked) {
if (selectedSharedAddresses.length === 1 && ctrlEditMode.checked) {
console.warn("Simulation: Losing the join community permission !!!")
return PermissionsModel.complexPermissionsModelNotMet
}
return PermissionsModel.complexPermissionsModel
if (ctrlPermissions.checked)
return PermissionsModel.permissionsModel
if (ctrlTokenGatedChannels.checked)
}
if (ctrlTokenGatedChannels.checked) {
if (selectedSharedAddresses.length === 1 && ctrlEditMode.checked) {
console.warn("Simulation: Losing the VIP read channel permission !!!")
return PermissionsModel.channelsOnlyPermissionsModelNotMet
}
return PermissionsModel.channelsOnlyPermissionsModel
}
console.warn("!!! EMPTY MODEL !!!")
return emptyModel
}
@ -40,7 +56,18 @@ SplitView {
collectiblesModel: CollectiblesModel {}
visible: true
Binding on selectedSharedAddresses {
value: ["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8884"]
when: ctrlEditMode.checked
}
Binding on selectedAirdropAddress {
value: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881"
when: ctrlEditMode.checked
}
onShareSelectedAddressesClicked: logs.logEvent("::shareSelectedAddressesClicked", ["airdropAddress", "sharedAddresses"], arguments)
onSaveSelectedAddressesClicked: logs.logEvent("::saveSelectedAddressesClicked", ["airdropAddress", "sharedAddresses"], arguments)
onClosed: destroy()
}
}

View File

@ -28,6 +28,23 @@ QtObject {
}
]
readonly property var permissionsModelDataNotMet: [
{
holdingsListModel: root.createHoldingsModel1(),
channelsListModel: root.createChannelsModel1(),
permissionType: PermissionTypes.Type.Admin,
isPrivate: true,
tokenCriteriaMet: false
},
{
holdingsListModel: root.createHoldingsModel1(),
channelsListModel: root.createChannelsModel2(),
permissionType: PermissionTypes.Type.Member,
isPrivate: false,
tokenCriteriaMet: false
}
]
readonly property var shortPermissionsModelData: [
{
holdingsListModel: root.createHoldingsModel4(),
@ -197,6 +214,41 @@ QtObject {
}
]
readonly property var complexPermissionsModelDataNotMet: [
{
id: "admin1",
holdingsListModel: root.createHoldingsModel1(),
channelsListModel: root.createChannelsModel2(),
permissionType: PermissionTypes.Type.Admin,
isPrivate: false,
tokenCriteriaMet: false
},
{
id: "admin2",
holdingsListModel: root.createHoldingsModel3(),
channelsListModel: root.createChannelsModel2(),
permissionType: PermissionTypes.Type.Admin,
isPrivate: false,
tokenCriteriaMet: false
},
{
id: "member1",
holdingsListModel: root.createHoldingsModel1(),
channelsListModel: root.createChannelsModel2(),
permissionType: PermissionTypes.Type.Member,
isPrivate: false,
tokenCriteriaMet: false
},
{
id: "member2",
holdingsListModel: root.createHoldingsModel4(),
channelsListModel: root.createChannelsModel2(),
permissionType: PermissionTypes.Type.Member,
isPrivate: false,
tokenCriteriaMet: false
}
]
readonly property var channelsOnlyPermissionsModelData: [
{
id: "read1a",
@ -290,6 +342,89 @@ QtObject {
}
]
readonly property var channelsOnlyPermissionsModelDataNotMet: [
{
id: "read1a",
holdingsListModel: root.createHoldingsModel1b(),
channelsListModel: root.createChannelsModel1(),
permissionType: PermissionTypes.Type.Read,
isPrivate: false,
tokenCriteriaMet: true
},
{
id: "read1b",
holdingsListModel: root.createHoldingsModel1(),
channelsListModel: root.createChannelsModel1(),
permissionType: PermissionTypes.Type.Read,
isPrivate: false,
tokenCriteriaMet: false
},
{
id: "read1c",
holdingsListModel: root.createHoldingsModel3(),
channelsListModel: root.createChannelsModel1(),
permissionType: PermissionTypes.Type.Read,
isPrivate: false,
tokenCriteriaMet: false
},
{
id: "read2a",
holdingsListModel: root.createHoldingsModel1(),
channelsListModel: root.createChannelsModel3(),
permissionType: PermissionTypes.Type.Read,
isPrivate: false,
tokenCriteriaMet: false
},
{
id: "read2b",
holdingsListModel: root.createHoldingsModel3(),
channelsListModel: root.createChannelsModel3(),
permissionType: PermissionTypes.Type.Read,
isPrivate: false,
tokenCriteriaMet: false
},
{
id: "viewAndPost1a",
holdingsListModel: root.createHoldingsModel3(),
channelsListModel: root.createChannelsModel1(),
permissionType: PermissionTypes.Type.ViewAndPost,
isPrivate: false,
tokenCriteriaMet: false
},
{
id: "viewAndPost1b",
holdingsListModel: root.createHoldingsModel2b(),
channelsListModel: root.createChannelsModel1(),
permissionType: PermissionTypes.Type.ViewAndPost,
isPrivate: false,
tokenCriteriaMet: true
},
{
id: "viewAndPost2a",
holdingsListModel: root.createHoldingsModel3(),
channelsListModel: root.createChannelsModel3(),
permissionType: PermissionTypes.Type.ViewAndPost,
isPrivate: false,
tokenCriteriaMet: false
},
{
id: "viewAndPost2b",
holdingsListModel: root.createHoldingsModel5(),
channelsListModel: root.createChannelsModel3(),
permissionType: PermissionTypes.Type.ViewAndPost,
isPrivate: false,
tokenCriteriaMet: false
},
{
id: "viewAndPost2c",
holdingsListModel: root.createHoldingsModel1(),
channelsListModel: root.createChannelsModel3(),
permissionType: PermissionTypes.Type.ViewAndPost,
isPrivate: false,
tokenCriteriaMet: false
}
]
readonly property ListModel permissionsModel: ListModel {
readonly property ModelChangeGuard guard: ModelChangeGuard {
model: root.permissionsModel
@ -301,6 +436,17 @@ QtObject {
}
}
readonly property ListModel permissionsModelNotMet: ListModel {
readonly property ModelChangeGuard guard: ModelChangeGuard {
model: root.permissionsModelNotMet
}
Component.onCompleted: {
append(permissionsModelDataNotMet)
guard.enabled = true
}
}
readonly property var shortPermissionsModel: ListModel {
readonly property ModelChangeGuard guard: ModelChangeGuard {
model: root.shortPermissionsModel
@ -367,6 +513,30 @@ QtObject {
}
}
readonly property var complexCombinedPermissionsModel: ListModel {
readonly property ModelChangeGuard guard: ModelChangeGuard {
model: root.complexCombinedPermissionsModel
}
Component.onCompleted: {
append(complexPermissionsModelData)
append(channelsOnlyPermissionsModelData)
guard.enabled = true
}
}
readonly property var complexCombinedPermissionsModelNotMet: ListModel {
readonly property ModelChangeGuard guard: ModelChangeGuard {
model: root.complexCombinedPermissionsModelNotMet
}
Component.onCompleted: {
append(complexPermissionsModelDataNotMet)
append(channelsOnlyPermissionsModelDataNotMet)
guard.enabled = true
}
}
readonly property var complexPermissionsModel: ListModel {
readonly property ModelChangeGuard guard: ModelChangeGuard {
model: root.complexPermissionsModel
@ -374,7 +544,17 @@ QtObject {
Component.onCompleted: {
append(complexPermissionsModelData)
append(channelsOnlyPermissionsModelData)
guard.enabled = true
}
}
readonly property var complexPermissionsModelNotMet: ListModel {
readonly property ModelChangeGuard guard: ModelChangeGuard {
model: root.complexPermissionsModelNotMet
}
Component.onCompleted: {
append(complexPermissionsModelDataNotMet)
guard.enabled = true
}
}
@ -390,6 +570,17 @@ QtObject {
}
}
readonly property var channelsOnlyPermissionsModelNotMet: ListModel {
readonly property ModelChangeGuard guard: ModelChangeGuard {
model: root.channelsOnlyPermissionsModelNotMet
}
Component.onCompleted: {
append(channelsOnlyPermissionsModelDataNotMet)
guard.enabled = true
}
}
function createHoldingsModel1() {
return [
{

View File

@ -1,5 +1,6 @@
#pragma once
#include <QJSValue>
#include <QObject>
#include <QString>
#include <QVariant>
@ -28,6 +29,10 @@ public:
Q_INVOKABLE bool contains(QAbstractItemModel *model, const QString &roleName, const QVariant &value, int mode = Qt::CaseSensitive) const;
///< performs a strict check whether @lhs and @rhs arrays (QList<T>) contain the same elements;
/// eg. `["a", "c", "b"]` and `["b", "c", "a"]` are considered equal
Q_INVOKABLE bool isSameArray(const QJSValue& lhs, const QJSValue& rhs) const;
static QObject* qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine)
{
Q_UNUSED(engine);

View File

@ -15,7 +15,7 @@ public:
//!< traverse the permissions @p model, and look for unique token keys recursively under holdingsListModel->key
Q_INVOKABLE QStringList getUniquePermissionTokenKeys(QAbstractItemModel *model) const;
//!< traverse the permissions @p model, and look for unique channels recursively under channelsListModel->key; filtering out @permissionTypes ([PermissionTypes.Type.FOO])
//! @return an array of array<key,channelName>
//!< traverse the permissions @p model, and look for unique channels recursively under channelsListModel->key; filtering out @p permissionTypes ([PermissionTypes.Type.FOO])
//! @return an array of `array<key,channelName>`, sorted by `channelName`
Q_INVOKABLE QJsonArray getUniquePermissionChannels(QAbstractItemModel *model, const QList<int> &permissionTypes = {}) const;
};

View File

@ -72,3 +72,20 @@ bool ModelUtilsInternal::contains(QAbstractItemModel* model,
const auto indexes = model->match(model->index(0, 0), roleByName(model, roleName), value, 1, flags);
return !indexes.isEmpty();
}
bool ModelUtilsInternal::isSameArray(const QJSValue& lhs, const QJSValue& rhs) const
{
if (!lhs.isArray() || !rhs.isArray())
return false;
auto left = lhs.toVariant().toStringList();
auto right = rhs.toVariant().toStringList();
if (left.size() != right.size())
return false;
left.sort();
right.sort();
return left == right;
}

View File

@ -65,7 +65,7 @@ StackLayout {
viewAndPostHoldingsModel: root.permissionsStore.viewAndPostPermissionsModel
assetsModel: root.rootStore.assetsModel
collectiblesModel: root.rootStore.collectiblesModel
isInvitationPending: root.rootStore.isCommunityRequestPending(communityData.id)
isInvitationPending: root.rootStore.isCommunityRequestPending(communityId)
notificationCount: activityCenterStore.unreadNotificationsCount
hasUnseenNotifications: activityCenterStore.hasUnseenNotifications
openCreateChat: rootStore.openCreateChat
@ -74,7 +74,7 @@ StackLayout {
onAdHocChatButtonClicked: rootStore.openCloseCreateChatView()
onRevealAddressClicked: {
Global.openPopup(communityIntroDialogPopup, {
communityId: communityData.id,
communityId: joinCommunityView.communityId,
isInvitationPending: joinCommunityView.isInvitationPending,
name: communityData.name,
introMessage: communityData.introMessage,
@ -83,15 +83,15 @@ StackLayout {
})
}
onInvitationPendingClicked: {
root.rootStore.cancelPendingRequest(communityData.id)
joinCommunityView.isInvitationPending = root.rootStore.isCommunityRequestPending(communityData.id)
root.rootStore.cancelPendingRequest(communityId)
joinCommunityView.isInvitationPending = root.rootStore.isCommunityRequestPending(communityId)
}
Connections {
target: root.rootStore.communitiesModuleInst
function onCommunityAccessRequested(communityId: string) {
if (communityId === joinCommunityView.communityData.id) {
joinCommunityView.isInvitationPending = root.rootStore.isCommunityRequestPending(communityData.id)
if (communityId === joinCommunityView.communityId) {
joinCommunityView.isInvitationPending = root.rootStore.isCommunityRequestPending(communityId)
}
}
}

View File

@ -670,7 +670,7 @@ QtObject {
target: mainModuleInst
enabled: !!chatCommunitySectionModule
function onOpenCommunityMembershipRequestsView(sectionId: string) {
if(root.getMySectionId() != sectionId)
if(root.getMySectionId() !== sectionId)
return
root.goToMembershipRequestsPage()
@ -703,6 +703,7 @@ QtObject {
readonly property string id: model.id
readonly property int sectionType: model.sectionType
readonly property string name: model.name
readonly property string image: model.image
readonly property bool joined: model.joined
readonly property bool amIBanned: model.amIBanned
// add others when needed..

View File

@ -69,7 +69,6 @@ QtObject {
}
function getUniquePermissionChannels(model, permissionsTypesArray = []) {
// TODO return a QVariantMap (https://github.com/status-im/status-desktop/issues/11481)
return Internal.PermissionUtils.getUniquePermissionChannels(model, permissionsTypesArray)
}

View File

@ -18,8 +18,10 @@ StatusListView {
property var uniquePermissionTokenKeys
// read/write properties
property string selectedAirdropAddress: selectedSharedAddresses.length ? selectedSharedAddresses[0] : ""
property var selectedSharedAddresses: count ? ModelUtils.modelToFlatArray(model, "address") : []
property string selectedAirdropAddress
property var selectedSharedAddresses: []
signal addressesChanged()
leftMargin: d.absLeftMargin
topMargin: Style.current.padding
@ -105,6 +107,7 @@ StatusListView {
visible: shareAddressCheckbox.checked
opacity: enabled ? 1.0 : 0.3
onCheckedChanged: if (checked) root.selectedAirdropAddress = model.address
onToggled: root.addressesChanged()
StatusToolTip {
text: qsTr("Use this address for any Community airdrops")
@ -134,6 +137,8 @@ StatusListView {
if (!checked && model.address === root.selectedAirdropAddress) {
d.selectFirstAvailableAirdropAddress()
}
root.addressesChanged()
}
}
]

View File

@ -9,6 +9,7 @@ import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Internal 0.1 as SQInternal
import SortFilterProxyModel 0.2
@ -18,6 +19,7 @@ import AppLayouts.Communities.views 1.0
import AppLayouts.Communities.helpers 1.0
import utils 1.0
import shared.panels 1.0
Control {
id: root
@ -44,18 +46,20 @@ Control {
onClicked: root.close()
}
StatusButton {
enabled: root.selectedSharedAddresses.length && root.selectedAirdropAddress
enabled: d.dirty
type: d.lostCommunityPermission || d.lostChannelPermissions ? StatusBaseButton.Type.Danger : StatusBaseButton.Type.Normal
visible: root.isEditMode
icon.name: Constants.authenticationIconByType[root.loginType]
text: qsTr("Save changes")
icon.name: type === StatusBaseButton.Type.Normal && d.selectedAddressesDirty ? Constants.authenticationIconByType[root.loginType] : ""
text: d.lostCommunityPermission ? qsTr("Save changes & leave %1").arg(root.communityName) :
d.lostChannelPermissions ? qsTr("Save changes & update my permissions")
: qsTr("Save changes")
onClicked: {
// TODO connect to backend
root.saveSelectedAddressesClicked(root.selectedAirdropAddress, root.selectedSharedAddresses)
root.close()
}
}
StatusButton {
visible: !root.isEditMode
enabled: root.selectedAirdropAddress && root.selectedSharedAddresses.length
text: qsTr("Share selected addresses to join")
onClicked: {
root.shareSelectedAddressesClicked(root.selectedAirdropAddress, root.selectedSharedAddresses)
@ -67,12 +71,15 @@ Control {
readonly property var rightButtons: [buttons.get(buttons.count-1)] // "magically" used by CommunityIntroDialog StatusStackModal impl
readonly property string selectedAirdropAddress: accountSelector.selectedAirdropAddress
readonly property var selectedSharedAddresses: accountSelector.selectedSharedAddresses
property var selectedSharedAddresses: []
property string selectedAirdropAddress
signal shareSelectedAddressesClicked(string airdropAddress, var sharedAddresses)
signal saveSelectedAddressesClicked(string airdropAddress, var sharedAddresses)
signal close()
signal shareSelectedAddressesClicked(string airdropAddress, var sharedAddresses)
padding: 0
spacing: Style.current.padding
QtObject {
@ -80,12 +87,73 @@ Control {
// internal logic
readonly property bool hasPermissions: root.permissionsModel && root.permissionsModel.count
// initial state (not bindings, we want a static snapshot of the initial state)
property var initialSelectedSharedAddresses: []
property string initialSelectedAirdropAddress
// dirty state handling
readonly property bool selectedAddressesDirty: !SQInternal.ModelUtils.isSameArray(d.initialSelectedSharedAddresses, root.selectedSharedAddresses)
readonly property bool selectedAirdropAddressDirty: root.selectedAirdropAddress !== d.initialSelectedAirdropAddress
readonly property bool dirty: selectedAddressesDirty || selectedAirdropAddressDirty
// warning states
readonly property bool lostCommunityPermission: root.isEditMode && permissionsView.lostPermissionToJoin
readonly property bool lostChannelPermissions: root.isEditMode && permissionsView.lostChannelPermissions
}
padding: 0
Component.onCompleted: {
// initialize the state
d.initialSelectedSharedAddresses = root.selectedSharedAddresses.length ? root.selectedSharedAddresses
: filteredAccountsModel.count ? ModelUtils.modelToFlatArray(filteredAccountsModel, "address")
: []
d.initialSelectedAirdropAddress = !!root.selectedAirdropAddress ? root.selectedAirdropAddress
: d.initialSelectedSharedAddresses.length ? d.initialSelectedSharedAddresses[0] : ""
root.selectedSharedAddresses = accountSelector.selectedSharedAddresses
root.selectedAirdropAddress = accountSelector.selectedAirdropAddress
}
SortFilterProxyModel {
id: filteredAccountsModel
sourceModel: root.walletAccountsModel
filters: ValueFilter {
roleName: "walletType"
value: Constants.watchWalletType
inverted: true
}
sorters: [
ExpressionSorter {
function isGenerated(modelData) {
return modelData.walletType === Constants.generatedWalletType
}
expression: {
return isGenerated(modelLeft)
}
},
RoleSorter {
roleName: "position"
},
RoleSorter {
roleName: "name"
}
]
}
contentItem: ColumnLayout {
spacing: 0
// warning panel
ModuleWarning {
Layout.fillWidth: true
Layout.preferredHeight: 32
text: d.lostCommunityPermission ? qsTr("Selected addresses have insufficient tokens to maintain %1 membership").arg(root.communityName) :
d.lostChannelPermissions ? qsTr("By deselecting these addresses, you will lose channel permissions") :
""
visible: d.lostCommunityPermission || d.lostChannelPermissions
closeBtnVisible: false
}
// addresses
SharedAddressesAccountSelector {
id: accountSelector
@ -96,6 +164,12 @@ Control {
Layout.maximumHeight: hasPermissions ? permissionsView.implicitHeight > root.availableHeight / 2 ? root.availableHeight / 2 : root.availableHeight : -1
Layout.fillHeight: !hasPermissions
model: root.walletAccountsModel
selectedSharedAddresses: d.initialSelectedSharedAddresses
selectedAirdropAddress: d.initialSelectedAirdropAddress
onAddressesChanged: {
root.selectedSharedAddresses = selectedSharedAddresses
root.selectedAirdropAddress = selectedAirdropAddress
}
}
// divider with top rounded corners + drop shadow
@ -121,6 +195,7 @@ Control {
// permissions
SharedAddressesPermissionsPanel {
id: permissionsView
isEditMode: root.isEditMode
permissionsModel: root.permissionsModel
assetsModel: root.assetsModel
collectiblesModel: root.collectiblesModel

View File

@ -18,11 +18,17 @@ import utils 1.0
Rectangle {
id: root
property bool isEditMode
property string communityName
property string communityIcon
property var permissionsModel
property var assetsModel
property var collectiblesModel
property string communityName
property string communityIcon
readonly property bool lostPermissionToJoin: d.lostPermissionToJoin
readonly property bool lostChannelPermissions: d.lostChannelPermissions
implicitHeight: permissionsScrollView.contentHeight - permissionsScrollView.anchors.topMargin
color: Theme.palette.baseColor2
@ -35,11 +41,26 @@ Rectangle {
readonly property color tableBorderColor: Theme.palette.directColor7
// internal logic
readonly property bool lostPermissionToJoin: root.isEditMode && joinPermissionsModel.count && !joinPermissionPanel.tokenCriteriaMet
readonly property var uniquePermissionChannels:
root.permissionsModel && root.permissionsModel.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 adminPermissionsModel: SortFilterProxyModel {
id: adminPermissionsModel
@ -65,6 +86,10 @@ Rectangle {
}
}
Component.onCompleted: {
d.initialChannelPermissions = d.getChannelPermissions()
}
StatusScrollView {
id: permissionsScrollView
anchors.fill: parent
@ -94,6 +119,7 @@ Rectangle {
// permission types
PermissionPanel {
id: joinPermissionPanel
permissionType: PermissionTypes.Type.Member
permissionsModel: d.joinPermissionsModel
}
@ -103,8 +129,17 @@ Rectangle {
}
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
}
}
}
}
@ -150,7 +185,7 @@ Rectangle {
}
component SinglePermissionFlow: Flow {
width: parent.width
Layout.fillWidth: true
spacing: Style.current.halfPadding
Repeater {
model: HoldingsSelectionModel {
@ -214,9 +249,11 @@ Rectangle {
id: permissionsRepeater
property int revision
onItemAdded: revision++
onItemRemoved: revision++
model: permissionPanel.permissionsModel
delegate: Column {
delegate: ColumnLayout {
Layout.column: 0
Layout.row: index
Layout.fillWidth: true
@ -228,9 +265,10 @@ Rectangle {
SinglePermissionFlow {}
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width + grid.anchors.margins*2
height: 1
Layout.fillWidth: true
Layout.leftMargin: -grid.anchors.leftMargin
Layout.rightMargin: -grid.anchors.rightMargin
Layout.preferredHeight: 1
color: d.tableBorderColor
visible: index < permissionsRepeater.count - 1
}
@ -244,10 +282,10 @@ Rectangle {
Layout.fillHeight: true
readonly property bool tokenCriteriaMet: {
permissionsRepeater.revision // NB no let/const here b/c of https://bugreports.qt.io/browse/QTBUG-91917
for (var i = 0; i < permissionsRepeater.count; i++) {
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.tokenCriteriaMet)
if (permissionItem && permissionItem.tokenCriteriaMet)
return true
}
return false
@ -267,7 +305,13 @@ Rectangle {
width: 16
height: 16
icon: overallPermissionRow.tokenCriteriaMet ? "tiny/checkmark" : "tiny/secure"
color: overallPermissionRow.tokenCriteriaMet ? Theme.palette.successColor1 : Theme.palette.baseColor1
color: {
if (d.lostPermissionToJoin)
return Theme.palette.dangerColor1
if (overallPermissionRow.tokenCriteriaMet)
return Theme.palette.successColor1
return Theme.palette.baseColor1
}
}
StatusBaseText {
anchors.verticalCenter: parent.verticalCenter
@ -287,7 +331,13 @@ Rectangle {
}
}
color: overallPermissionRow.tokenCriteriaMet ? Theme.palette.directColor1 : Theme.palette.baseColor1
color: {
if (d.lostPermissionToJoin)
return Theme.palette.dangerColor1
if (overallPermissionRow.tokenCriteriaMet)
return Theme.palette.directColor1
return Theme.palette.baseColor1
}
}
}
}
@ -304,7 +354,10 @@ Rectangle {
padding: d.absLeftMargin
background: PanelBg {}
readonly property string channelKey: d.uniquePermissionChannels[index][0]
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
@ -315,12 +368,15 @@ Rectangle {
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
Layout.fillWidth: true
Layout.preferredHeight: grid2.implicitHeight + grid2.anchors.margins*2
@ -341,6 +397,8 @@ Rectangle {
id: permissionsRepeater2
property int revision
onItemAdded: revision++
onItemRemoved: revision++
model: SortFilterProxyModel {
id: channelPermissionsModel
@ -354,7 +412,7 @@ Rectangle {
expression: channelPermissionsModel.filterPredicate(model)
}
}
delegate: Column {
delegate: ColumnLayout {
Layout.column: 0
Layout.row: index
Layout.fillWidth: true
@ -366,9 +424,10 @@ Rectangle {
SinglePermissionFlow {}
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width + grid2.anchors.margins*2
height: 1
Layout.fillWidth: true
Layout.leftMargin: -grid2.anchors.leftMargin
Layout.rightMargin: -grid2.anchors.rightMargin
Layout.preferredHeight: 1
color: d.tableBorderColor
visible: index < permissionsRepeater2.count - 1
}
@ -384,9 +443,9 @@ Rectangle {
readonly property bool tokenCriteriaMet: {
permissionsRepeater2.revision
for (let i = 0; i < permissionsRepeater2.count; i++) {
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.tokenCriteriaMet)
if (permissionItem && permissionItem.tokenCriteriaMet)
return true
}
return false
@ -406,7 +465,13 @@ Rectangle {
width: 16
height: 16
icon: overallPermissionRow2.tokenCriteriaMet ? "tiny/checkmark" : "tiny/secure"
color: overallPermissionRow2.tokenCriteriaMet ? Theme.palette.successColor1 : Theme.palette.baseColor1
color: {
if (channelPermsSubPanel.permissionLost)
return Theme.palette.dangerColor1
if (overallPermissionRow2.tokenCriteriaMet)
return Theme.palette.successColor1
return Theme.palette.baseColor1
}
}
StatusBaseText {
anchors.verticalCenter: parent.verticalCenter
@ -424,7 +489,13 @@ Rectangle {
}
}
color: overallPermissionRow2.tokenCriteriaMet ? Theme.palette.directColor1 : Theme.palette.baseColor1
color: {
if (channelPermsSubPanel.permissionLost)
return Theme.palette.dangerColor1
if (overallPermissionRow2.tokenCriteriaMet)
return Theme.palette.directColor1
return Theme.palette.baseColor1
}
}
}
}

View File

@ -20,10 +20,11 @@ StatusDialog {
required property var assetsModel
required property var collectiblesModel
readonly property string selectedAirdropAddress: panel.selectedAirdropAddress
readonly property var selectedSharedAddresses: panel.selectedSharedAddresses
property alias selectedSharedAddresses: panel.selectedSharedAddresses
property alias selectedAirdropAddress: panel.selectedAirdropAddress
signal shareSelectedAddressesClicked(string airdropAddress, var sharedAddresses)
signal saveSelectedAddressesClicked(string airdropAddress, var sharedAddresses)
title: panel.title
implicitWidth: 640 // by design
@ -40,6 +41,7 @@ StatusDialog {
assetsModel: root.assetsModel
collectiblesModel: root.collectiblesModel
onShareSelectedAddressesClicked: root.shareSelectedAddressesClicked(airdropAddress, sharedAddresses)
onSaveSelectedAddressesClicked: root.saveSelectedAddressesClicked(airdropAddress, sharedAddresses)
onClose: root.close()
}

View File

@ -128,7 +128,6 @@ StatusListView {
onTriggered: {
moreMenu.close()
Global.openEditSharedAddressesFlow(model.id)
// TODO shared addresses flow, cf https://github.com/status-im/status-desktop/issues/11138
}
}
StatusMenuSeparator {

View File

@ -449,9 +449,10 @@ Item {
popupMenu: Component {
StatusMenu {
id: communityContextMenu
width: 180
property var chatCommunitySectionModule
readonly property bool isSpectator: model.spectated && !model.joined
openHandler: function () {
// we cannot return QVariant if we pass another parameter in a function call
// that's why we're using it this way
@ -496,20 +497,36 @@ Item {
}
}
StatusAction {
text: qsTr("Edit Shared Addresses")
icon.name: "wallet"
enabled: {
if (model.memberRole === Constants.memberRole.owner)
return false
if (communityContextMenu.isSpectator && !appMain.rootStore.isCommunityRequestPending(model.id))
return false
return true
}
onTriggered: {
communityContextMenu.close()
Global.openEditSharedAddressesFlow(model.id)
}
}
StatusMenuSeparator { visible: leaveCommunityMenuItem.enabled }
StatusAction {
id: leaveCommunityMenuItem
enabled: model.memberRole !== Constants.memberRole.owner
text: {
if (model.spectated)
if (communityContextMenu.isSpectator)
return qsTr("Close Community")
return qsTr("Leave Community")
}
icon.name: model.spectated ? "close-circle" : "arrow-left"
icon.name: communityContextMenu.isSpectator ? "close-circle" : "arrow-left"
type: StatusAction.Type.Danger
onTriggered: model.spectated ? communityContextMenu.chatCommunitySectionModule.leaveCommunity()
: popups.openLeaveCommunityPopup(model.name, model.id, model.outroMessage)
onTriggered: communityContextMenu.isSpectator ? communityContextMenu.chatCommunitySectionModule.leaveCommunity()
: popups.openLeaveCommunityPopup(model.name, model.id, model.outroMessage)
}
}
}
@ -1198,6 +1215,7 @@ Item {
emojiPopup: statusEmojiPopup.item
stickersPopup: statusStickersPopupLoader.item
sectionItemModel: model
createChatPropertiesStore: appMain.createChatPropertiesStore
communitySettingsDisabled: production && appMain.rootStore.profileSectionStore.walletStore.areTestNetworksEnabled
rootStore: ChatStores.RootStore {

View File

@ -14,11 +14,13 @@ import AppLayouts.Chat.popups 1.0
import AppLayouts.Profile.popups 1.0
import AppLayouts.Communities.popups 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStore
import AppLayouts.Chat.stores 1.0 as ChatStore
import shared.popups 1.0
import shared.status 1.0
import utils 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStore
QtObject {
id: root
@ -61,6 +63,7 @@ QtObject {
Global.openTestnetPopup.connect(openTestnetPopup)
Global.openExportControlNodePopup.connect(openExportControlNodePopup)
Global.openImportControlNodePopup.connect(openImportControlNodePopup)
Global.openEditSharedAddressesFlow.connect(openEditSharedAddressesPopup)
}
property var currentPopup
@ -234,7 +237,11 @@ QtObject {
imageSrc: imageSrc,
accessType: accessType,
isInvitationPending: isInvitationPending
});
})
}
function openEditSharedAddressesPopup(communityId) {
openPopup(editSharedAddressesPopupComponent, {communityId: communityId, isEditMode: true})
}
function openDiscordImportProgressPopup() {
@ -498,18 +505,15 @@ QtObject {
ImportCommunityPopup {
store: root.communitiesStore
onJoinCommunity: {
Global.communityIntroPopupRequested(
communityId,
communityDetails.name,
communityDetails.introMessage,
communityDetails.image,
Constants.communityChatOnRequestAccess,
root.rootStore.isCommunityRequestPending(communityId));
close();
}
onClosed: {
destroy();
close()
openCommunityIntroPopup(communityId,
communityDetails.name,
communityDetails.introMessage,
communityDetails.image,
communityDetails.access,
root.rootStore.isCommunityRequestPending(communityId))
}
onClosed: destroy()
}
},
@ -528,10 +532,14 @@ QtObject {
Connections {
target: root.communitiesStore.communitiesModuleInst
function onCommunityAccessRequested(communityId: string) {
if (communityId !== communityIntroDialog.communityId)
return
root.communitiesStore.spectateCommunity(communityId);
communityIntroDialog.close();
}
function onCommunityAccessFailed(communityId: string) {
if (communityId !== communityIntroDialog.communityId)
return
communityIntroDialog.close();
}
function onUserAuthenticationCanceled() {
@ -675,7 +683,38 @@ QtObject {
ImportControlNodePopup {
onClosed: destroy()
}
}
},
Component {
id: editSharedAddressesPopupComponent
SharedAddressesPopup {
id: editSharedAddressesPopup
property string communityId
readonly property var chatStore: ChatStore.RootStore {
contactsStore: root.rootStore.contactStore
chatCommunitySectionModule: {
root.rootStore.mainModuleInst.prepareCommunitySectionModuleForCommunityId(editSharedAddressesPopup.communityId)
return root.rootStore.mainModuleInst.getCommunitySectionModule()
}
}
communityName: chatStore.sectionDetails.name
communityIcon: chatStore.sectionDetails.image
// FIXME get these from the community settings (from the initial "join" call)
//selectedSharedAddresses: [???]
//selectedAirdropAddress: "???"
loginType: chatStore.loginType
walletAccountsModel: WalletStore.RootStore.receiveAccounts
permissionsModel: chatStore.permissionsStore.permissionsModel
assetsModel: chatStore.assetsModel
collectiblesModel: chatStore.collectiblesModel
onSaveSelectedAddressesClicked: console.warn("!!! FIXME implement saving the shared & airdrop addresses for the community",
JSON.stringify(sharedAddresses), airdropAddress)
onClosed: destroy()
}
}
]
}