diff --git a/ui/app/AppLayouts/Chat/controls/community/EnsPanel.qml b/ui/app/AppLayouts/Chat/controls/community/EnsPanel.qml index 2b62bae121b..d07cecff4a1 100644 --- a/ui/app/AppLayouts/Chat/controls/community/EnsPanel.qml +++ b/ui/app/AppLayouts/Chat/controls/community/EnsPanel.qml @@ -14,12 +14,16 @@ ColumnLayout { property alias domainNameValid: domainNameInput.valid property alias addButtonEnabled: addOrUpdateButton.enabled + property var reservedNames: [] + signal addClicked signal updateClicked signal removeClicked spacing: 0 + onReservedNamesChanged: domainNameInput.validate() + StatusInput { id: domainNameInput @@ -33,13 +37,22 @@ ColumnLayout { font.pixelSize: 13 input.placeholderText: "name.eth" + errorMessageCmp.visible: false + validators: StatusRegularExpressionValidator { // TODO: check ens domain validator regularExpression: /^(\*\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?$/i - errorMessage: qsTr("Subdomain not recognized") + errorMessage: alreadyUsed + ? qsTr("This condition has already been added") + : qsTr("This is not ENS name") + + property bool alreadyUsed: false validate: function (value) { - return value === "*.eth" || regularExpression.test(value) + alreadyUsed = reservedNames.includes(value) + + return (value === "*.eth" || regularExpression.test(value)) + && !alreadyUsed } } @@ -64,11 +77,34 @@ ColumnLayout { lineHeightMode: Text.FixedHeight } + Item { + Layout.topMargin: 8 + Layout.fillWidth: true + Layout.preferredHeight: Math.max(18, errorText.height) // by design + + StatusBaseText { + id: errorText + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + text: domainNameInput.errorMessageCmp.text + + font.pixelSize: 13 + lineHeight: 18 + lineHeightMode: Text.FixedHeight + + color: Theme.palette.dangerColor1 + wrapMode: Text.Wrap + } + } + StatusButton { id: addOrUpdateButton text: (root.mode === HoldingTypes.Mode.Add) ? qsTr("Add") : qsTr("Update") - Layout.topMargin: 40 + Layout.topMargin: 13 // by design Layout.preferredHeight: 44 // by design Layout.fillWidth: true onClicked: root.mode === HoldingTypes.Mode.Add @@ -80,7 +116,7 @@ ColumnLayout { Layout.topMargin: 16 // by design Layout.preferredHeight: 44 // by design Layout.fillWidth: true - visible: root.mode === HoldingTypes.Mode.Update + visible: root.mode === HoldingTypes.Mode.UpdateOrRemove type: StatusBaseButton.Type.Danger onClicked: root.removeClicked() diff --git a/ui/app/AppLayouts/Chat/controls/community/ExtendedDropdownContent.qml b/ui/app/AppLayouts/Chat/controls/community/ExtendedDropdownContent.qml index bc1778869fe..c0647493c57 100644 --- a/ui/app/AppLayouts/Chat/controls/community/ExtendedDropdownContent.qml +++ b/ui/app/AppLayouts/Chat/controls/community/ExtendedDropdownContent.qml @@ -16,6 +16,7 @@ Item { id: root property var store + property var checkedKeys: [] property int type: ExtendedDropdownContent.Type.Assets readonly property bool canGoBack: root.state !== d.listView_depth1_State @@ -272,10 +273,14 @@ Item { TokenItem { id: tokenGroupItem + Layout.fillWidth: true + key: d.currentItemKey name: d.currentItemName iconSource: d.currentItemSource + + selected: root.checkedKeys.includes(key) enabled: true onItemClicked: root.itemClicked(d.currentItemKey, d.currentItemName, @@ -301,6 +306,8 @@ Item { } isHeaderVisible: false // TEMPORARILY hidden. These 2 header options will be implemented after MVP. model: d.currentModel + checkedKeys: root.checkedKeys + onHeaderItemClicked: { if(key === "MINT") console.log("TODO: Mint asset") else if(key === "IMPORT") console.log("TODO: Import existing asset") @@ -319,6 +326,7 @@ Item { } model: d.currentModel + checkedKeys: root.checkedKeys onHeaderItemClicked: { if(key === "MINT") console.log("TODO: Mint collectible") @@ -351,6 +359,8 @@ Item { padding: 0 model: d.currentModel + checkedKeys: root.checkedKeys + onItemClicked: { d.reset() root.itemClicked(key, name, iconSource) diff --git a/ui/app/AppLayouts/Chat/controls/community/HoldingTypes.qml b/ui/app/AppLayouts/Chat/controls/community/HoldingTypes.qml index b1c0f9c622d..9b01f1bce5a 100644 --- a/ui/app/AppLayouts/Chat/controls/community/HoldingTypes.qml +++ b/ui/app/AppLayouts/Chat/controls/community/HoldingTypes.qml @@ -6,6 +6,6 @@ QtObject { } enum Mode { - Add, Update + Add, Update, UpdateOrRemove } } diff --git a/ui/app/AppLayouts/Chat/controls/community/HoldingsDropdown.qml b/ui/app/AppLayouts/Chat/controls/community/HoldingsDropdown.qml index 2cfbbc4204a..8934a6a6d97 100644 --- a/ui/app/AppLayouts/Chat/controls/community/HoldingsDropdown.qml +++ b/ui/app/AppLayouts/Chat/controls/community/HoldingsDropdown.qml @@ -13,6 +13,8 @@ StatusDropdown { id: root property var store + property var usedTokens: [] + property var usedEnsNames: [] property string assetKey: "" property real assetAmount: 0 @@ -37,7 +39,7 @@ StatusDropdown { } function openUpdateFlow() { - d.currentHoldingMode = HoldingTypes.Mode.Update + d.initialHoldingMode = HoldingTypes.Mode.UpdateOrRemove if(d.currentHoldingType !== HoldingTypes.Type.Ens) { if(statesStack.size === 0) statesStack.push(HoldingsDropdown.FlowType.List_Deep1) @@ -53,16 +55,13 @@ StatusDropdown { function reset() { d.currentHoldingType = HoldingTypes.Type.Asset - d.currentHoldingMode = HoldingTypes.Mode.Add + d.initialHoldingMode = HoldingTypes.Mode.Add - d.assetAmountText = "" - d.collectibleAmountText = "" root.assetKey = "" root.collectibleKey = "" - root.assetAmount = 0 - root.collectibleAmount = 1 root.ensDomainName = "" + d.setDefaultAmounts() d.setInitialFlow() } @@ -79,7 +78,14 @@ StatusDropdown { property int extendedDropdownType: ExtendedDropdownContent.Type.Assets property int currentHoldingType: HoldingTypes.Type.Asset - property int currentHoldingMode: HoldingTypes.Mode.Add + + property bool updateSelected: false + + property int initialHoldingMode: HoldingTypes.Mode.Add + property int effectiveHoldingMode: initialHoldingMode === HoldingTypes.Mode.UpdateOrRemove + ? HoldingTypes.Mode.UpdateOrRemove + : (updateSelected ? HoldingTypes.Mode.Update : HoldingTypes.Mode.Add) + property bool extendedDeepNavigation: false property var currentSubItems property string currentItemKey: "" @@ -105,6 +111,13 @@ StatusDropdown { else statesStack.push(HoldingsDropdown.FlowType.Selected) } + + function setDefaultAmounts() { + d.assetAmountText = "" + d.collectibleAmountText = "" + root.assetAmount = 0 + root.collectibleAmount = 1 + } } QtObject { @@ -236,9 +249,27 @@ StatusDropdown { id: listPanel store: root.store + checkedKeys: root.usedTokens.map(entry => entry.key) type: d.extendedDropdownType onItemClicked: { + d.assetAmountText = "" + d.collectibleAmountText = "" + + if (checkedKeys.includes(key)) { + const amount = root.usedTokens.find(entry => entry.key === key).amount + + if(d.extendedDropdownType === ExtendedDropdownContent.Type.Assets) + root.assetAmount = amount + else + root.collectibleAmount = amount + + d.updateSelected = true + } else { + d.setDefaultAmounts() + d.updateSelected = false + } + if(d.extendedDropdownType === ExtendedDropdownContent.Type.Assets) root.assetKey = key else @@ -293,7 +324,7 @@ StatusDropdown { amountText: d.assetAmountText tokenCategoryText: qsTr("Asset") addOrUpdateButtonEnabled: d.assetsReady - mode: d.currentHoldingMode + mode: d.effectiveHoldingMode onEffectiveAmountChanged: root.assetAmount = effectiveAmount onAmountTextChanged: d.assetAmountText = amountText @@ -329,7 +360,7 @@ StatusDropdown { tokenCategoryText: qsTr("Collectible") addOrUpdateButtonEnabled: d.collectiblesReady allowDecimals: false - mode: d.currentHoldingMode + mode: d.effectiveHoldingMode onEffectiveAmountChanged: root.collectibleAmount = effectiveAmount onAmountTextChanged: d.collectibleAmountText = amountText @@ -356,7 +387,8 @@ StatusDropdown { EnsPanel { addButtonEnabled: d.ensReady domainName: root.ensDomainName - mode: d.currentHoldingMode + mode: d.initialHoldingMode + reservedNames: root.usedEnsNames onDomainNameChanged: root.ensDomainName = domainName onDomainNameValidChanged: d.ensDomainNameValid = domainNameValid diff --git a/ui/app/AppLayouts/Chat/controls/community/ListDropdownContent.qml b/ui/app/AppLayouts/Chat/controls/community/ListDropdownContent.qml index 155b1f77288..9ff4572505d 100644 --- a/ui/app/AppLayouts/Chat/controls/community/ListDropdownContent.qml +++ b/ui/app/AppLayouts/Chat/controls/community/ListDropdownContent.qml @@ -12,6 +12,8 @@ import StatusQ.Components 0.1 StatusListView { id: root + property var checkedKeys: [] + property var headerModel property bool isHeaderVisible: true property int maxHeight: 381 // default by design @@ -60,7 +62,8 @@ StatusListView { shortName: !!model.shortName ? model.shortName : "" iconSource: model.iconSource subItems: model.subItems - enabled: true + selected: root.checkedKeys.includes(model.key) + onItemClicked: root.itemClicked(model.key, model.name, model.shortName, diff --git a/ui/app/AppLayouts/Chat/controls/community/ThumbnailsDropdownContent.qml b/ui/app/AppLayouts/Chat/controls/community/ThumbnailsDropdownContent.qml index b2821b6aa5e..ce312fad68f 100644 --- a/ui/app/AppLayouts/Chat/controls/community/ThumbnailsDropdownContent.qml +++ b/ui/app/AppLayouts/Chat/controls/community/ThumbnailsDropdownContent.qml @@ -13,6 +13,8 @@ StatusScrollView { property url titleImage: "" property string subtitle: "" property ListModel model + property var checkedKeys: [] + property int maxHeight: 381 // default by design signal itemClicked(var key, string name, url iconSource) @@ -47,6 +49,29 @@ StatusScrollView { Image { source: model.imageSource ? model.imageSource : "" anchors.fill: parent + + Rectangle { + width: 32 + height: 32 + + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 8 + + radius: width / 2 + visible: root.checkedKeys.includes(model.key) + // TODO: use color from theme when defined properly in the design + color: "#F5F6F8" + + StatusIcon { + anchors.centerIn: parent + icon: "checkmark" + + color: Theme.palette.baseColor1 + width: 16 + height: 16 + } + } } MouseArea { anchors.fill: parent diff --git a/ui/app/AppLayouts/Chat/controls/community/TokenItem.qml b/ui/app/AppLayouts/Chat/controls/community/TokenItem.qml index 7eceb1eae4b..5892a866a68 100644 --- a/ui/app/AppLayouts/Chat/controls/community/TokenItem.qml +++ b/ui/app/AppLayouts/Chat/controls/community/TokenItem.qml @@ -15,6 +15,7 @@ Control { property string shortName property url iconSource property var subItems + property bool selected: false signal itemClicked(string key, string name, string shortName, url iconSource, var subItems) @@ -71,8 +72,10 @@ Control { } StatusIcon { - icon: "tiny/chevron-right" - visible: !!root.subItems && root.subItems.count > 0 + readonly property bool hasSubItems: !!root.subItems && root.subItems.count > 0 + + icon: root.selected && !hasSubItems ? "checkmark" : "tiny/chevron-right" + visible: root.selected || hasSubItems Layout.alignment: Qt.AlignVCenter Layout.rightMargin: 16 color: Theme.palette.baseColor1 diff --git a/ui/app/AppLayouts/Chat/controls/community/TokenPanel.qml b/ui/app/AppLayouts/Chat/controls/community/TokenPanel.qml index ca8b813acff..37fa8b61807 100644 --- a/ui/app/AppLayouts/Chat/controls/community/TokenPanel.qml +++ b/ui/app/AppLayouts/Chat/controls/community/TokenPanel.qml @@ -83,7 +83,7 @@ ColumnLayout { Layout.preferredHeight: d.defaultHeight Layout.fillWidth: true Layout.topMargin: d.defaultSpacing - visible: root.mode === HoldingTypes.Mode.Update + visible: root.mode === HoldingTypes.Mode.UpdateOrRemove type: StatusBaseButton.Type.Danger onClicked: root.removeClicked() diff --git a/ui/app/AppLayouts/Chat/views/communities/CommunityNewPermissionView.qml b/ui/app/AppLayouts/Chat/views/communities/CommunityNewPermissionView.qml index 1ab5083fc21..28e335b47b9 100644 --- a/ui/app/AppLayouts/Chat/views/communities/CommunityNewPermissionView.qml +++ b/ui/app/AppLayouts/Chat/views/communities/CommunityNewPermissionView.qml @@ -87,6 +87,48 @@ StatusScrollView { } property bool isPrivateDirty: false + function getIndexOfKey(key) { + const count = holdingsModel.count + + for (let i = 0; i < count; i++) + if (holdingsModel.get(i).key === key) + return i + + return -1 + } + + function getTokenKeysAndAmounts() { + const keysAndAmounts = [] + const count = holdingsModel.count + + for (let i = 0; i < count; i++) { + const item = holdingsModel.get(i) + + if (item.type === HoldingTypes.Type.Ens) + continue + + keysAndAmounts.push({ key: item.key, amount: item.amount }) + } + + return keysAndAmounts + } + + function getEnsNames() { + const names = [] + const count = holdingsModel.count + + for (let i = 0; i < count; i++) { + const item = holdingsModel.get(i) + + if (item.type !== HoldingTypes.Type.Ens) + continue + + names.push(item.name) + } + + return names + } + // TODO: Channels } @@ -195,7 +237,7 @@ StatusScrollView { StatusItemSelector { id: tokensSelector - property int editedIndex + property int editedIndex: -1 Layout.fillWidth: true icon: Style.svg("contact_verified") @@ -227,6 +269,28 @@ StatusScrollView { d.dirtyValues.holdingsModel.append({ type, key, name, amount, imageSource }) } + function prepareUpdateIndex(key) { + const itemIndex = tokensSelector.editedIndex + const existingIndex = d.dirtyValues.getIndexOfKey(key) + + if (itemIndex !== -1 && existingIndex !== -1 && itemIndex !== existingIndex) { + const previousKey = d.dirtyValues.holdingsModel.get(itemIndex).key + d.dirtyValues.holdingsModel.remove(existingIndex) + return d.dirtyValues.getIndexOfKey(previousKey) + } + + if (itemIndex === -1) { + return existingIndex + } + + return itemIndex + } + + onOpened: { + usedTokens = d.dirtyValues.getTokenKeysAndAmounts() + usedEnsNames = d.dirtyValues.getEnsNames().filter(item => item !== ensDomainName) + } + onAddAsset: { const modelItem = CommunityPermissionsHelpers.getTokenByKey(store.assetsModel, key) addItem(HoldingTypes.Type.Asset, modelItem, amount) @@ -240,7 +304,7 @@ StatusScrollView { } onAddEns: { - const key = "ENS" + const key = "ENS_" + domain const icon = Style.svg("profile/ensUsernames") d.dirtyValues.holdingsModel.append({type: HoldingTypes.Type.Ens, key, name: domain, amount: 1, imageSource: icon }) @@ -248,27 +312,31 @@ StatusScrollView { } onUpdateAsset: { + const itemIndex = prepareUpdateIndex(key) + const modelItem = CommunityPermissionsHelpers.getTokenByKey(store.assetsModel, key) const name = modelItem.shortName ? modelItem.shortName : modelItem.name const imageSource = modelItem.iconSource.toString() - d.dirtyValues.holdingsModel.set(tokensSelector.editedIndex, { type: HoldingTypes.Type.Asset, key, name, amount, imageSource }) + d.dirtyValues.holdingsModel.set(itemIndex, { type: HoldingTypes.Type.Asset, key, name, amount, imageSource }) d.triggerDirtyTool = !d.triggerDirtyTool dropdown.close() } onUpdateCollectible: { + const itemIndex = prepareUpdateIndex(key) + const modelItem = CommunityPermissionsHelpers.getTokenByKey(store.collectiblesModel, key) const name = modelItem.name const imageSource = modelItem.iconSource.toString() - d.dirtyValues.holdingsModel.set(tokensSelector.editedIndex, { type: HoldingTypes.Type.Collectible, key, name, amount, imageSource }) + d.dirtyValues.holdingsModel.set(itemIndex, { type: HoldingTypes.Type.Collectible, key, name, amount, imageSource }) d.triggerDirtyTool = !d.triggerDirtyTool dropdown.close() } onUpdateEns: { - const key = "ENS" + const key = "ENS_" + domain const icon = Style.svg("profile/ensUsernames") d.dirtyValues.holdingsModel.set(tokensSelector.editedIndex, { type: HoldingTypes.Type.Ens, key, name: domain, amount: 1, imageSource: icon }) @@ -287,6 +355,8 @@ StatusScrollView { dropdown.x = tokensSelector.addButton.width + d.dropdownHorizontalOffset dropdown.y = 0 dropdown.open() + + editedIndex = -1 } onItemClicked: {