From cf82772aed479d24d7d7c13d65ec95a855d084c2 Mon Sep 17 00:00:00 2001 From: Alex Jbanca Date: Tue, 6 Feb 2024 11:31:36 +0200 Subject: [PATCH] feat(ChannelPermissions): Add permissions section in create/edit channel popup Changes: 1. Make PermissionsView/EditPermissionsView configurable to support channel permissions config 2. Adding channel permissions support in the create/edit channel popup 3. Connect the channel permissions to backend 4. Cleaning unneeded emojiPopup --- .../Chat/views/ChatHeaderContentView.qml | 25 +- ui/app/AppLayouts/Chat/views/ChatView.qml | 13 + .../Chat/views/ContactsColumnView.qml | 1 - .../models/ChannelPermissionsModelEditor.qml | 16 +- .../panels/PermissionsSettingsPanel.qml | 111 ++- .../Communities/popups/CreateChannelPopup.qml | 898 ++++++++++++------ .../popups/PermissionsDropdown.qml | 5 +- .../Communities/views/CommunityColumnView.qml | 82 +- .../Communities/views/EditPermissionView.qml | 20 +- .../Communities/views/PermissionsView.qml | 159 ++-- .../shared/views/chat/ChatContextMenuView.qml | 36 +- 11 files changed, 887 insertions(+), 479 deletions(-) diff --git a/ui/app/AppLayouts/Chat/views/ChatHeaderContentView.qml b/ui/app/AppLayouts/Chat/views/ChatHeaderContentView.qml index 8b41aae82f..58c10a06f9 100644 --- a/ui/app/AppLayouts/Chat/views/ChatHeaderContentView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatHeaderContentView.qml @@ -27,6 +27,14 @@ Item { property int padding: Style.current.halfPadding signal searchButtonClicked() + signal displayEditChannelPopup(string chatId, + string chatName, + string chatDescription, + string chatEmoji, + string chatColor, + string chatCategoryId, + int channelPosition, + var deleteDialog) function addRemoveGroupMember() { root.state = d.stateMembersSelectorContent @@ -140,7 +148,6 @@ Item { ChatContextMenuView { id: contextMenu objectName: "moreOptionsContextMenu" - emojiPopup: root.emojiPopup showDebugOptions: root.rootStore.isDebugEnabled openHandler: function () { if(!chatContentModule) { @@ -218,17 +225,11 @@ Item { onDisplayProfilePopup: { Global.openProfilePopup(publicKey) } - - onEditCommunityChannel: { - root.rootStore.editCommunityChannel( - chatId, - newName, - newDescription, - newEmoji, - newColor, - newCategory, - channelPosition // TODO change this to the signal once it is modifiable - ) + onDisplayEditChannelPopup: { + root.displayEditChannelPopup(chatId, chatName, chatDescription, + chatEmoji, chatColor, + chatCategoryId, channelPosition, + contextMenu.deleteChatConfirmationDialog); } onAddRemoveGroupMember: { root.addRemoveGroupMember() diff --git a/ui/app/AppLayouts/Chat/views/ChatView.qml b/ui/app/AppLayouts/Chat/views/ChatView.qml index 2c54bccceb..b0d22c15b0 100644 --- a/ui/app/AppLayouts/Chat/views/ChatView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatView.qml @@ -166,6 +166,19 @@ StatusSectionLayout { rootStore: root.rootStore emojiPopup: root.emojiPopup onSearchButtonClicked: root.openAppSearch() + onDisplayEditChannelPopup: { + Global.openPopup(contactColumnLoader.item.createChannelPopup, { + isEdit: true, + chatId: chatId, + channelName: chatName, + channelDescription: chatDescription, + channelEmoji: chatEmoji, + channelColor: chatColor, + categoryId: chatCategoryId, + channelPosition: channelPosition, + deleteChatConfirmationDialog: deleteDialog + }); + } } } diff --git a/ui/app/AppLayouts/Chat/views/ContactsColumnView.qml b/ui/app/AppLayouts/Chat/views/ContactsColumnView.qml index ce87cf2f55..72378145ac 100644 --- a/ui/app/AppLayouts/Chat/views/ContactsColumnView.qml +++ b/ui/app/AppLayouts/Chat/views/ContactsColumnView.qml @@ -141,7 +141,6 @@ Item { popupMenu: ChatContextMenuView { id: chatContextMenuView - emojiPopup: root.emojiPopup showDebugOptions: root.store.isDebugEnabled openHandler: function (id) { diff --git a/ui/app/AppLayouts/Communities/models/ChannelPermissionsModelEditor.qml b/ui/app/AppLayouts/Communities/models/ChannelPermissionsModelEditor.qml index 1e726fdbb9..79ec292348 100644 --- a/ui/app/AppLayouts/Communities/models/ChannelPermissionsModelEditor.qml +++ b/ui/app/AppLayouts/Communities/models/ChannelPermissionsModelEditor.qml @@ -58,23 +58,9 @@ QtObject { d.editPermission(key, permissionType, holdings, channels, isPrivate) } - // Function duplicating a permission. The new permission will be have a different id and key - function duplicatePermission(index) { - const permission = channelPermissionsModel.get(index) - - if (!permission) - return - - permission.id = Utils.uuid() - permission.key = Utils.uuid() - permission.holdingsListModel = d.newHoldingsModel(StatusQUtils.ModelUtils.modelToArray(permission.holdingsListModel)) - permission.channelsListModel = d.newChannelsModel(StatusQUtils.ModelUtils.modelToArray(permission.channelsListModel)) - channelPermissionsModel.append(permission) - } - // Function removing a permission by index function removePermission(index) { - channelPermissionsModel.remove(index, 1); + return channelPermissionsModel.remove(index, 1); } diff --git a/ui/app/AppLayouts/Communities/panels/PermissionsSettingsPanel.qml b/ui/app/AppLayouts/Communities/panels/PermissionsSettingsPanel.qml index 5d7283f9da..34ae319c02 100644 --- a/ui/app/AppLayouts/Communities/panels/PermissionsSettingsPanel.qml +++ b/ui/app/AppLayouts/Communities/panels/PermissionsSettingsPanel.qml @@ -5,9 +5,11 @@ import AppLayouts.Communities.controls 1.0 import AppLayouts.Communities.layouts 1.0 import AppLayouts.Communities.views 1.0 +import StatusQ.Core 0.1 import StatusQ.Controls 0.1 import StatusQ.Core.Utils 0.1 +import utils 1.0 import shared.popups 1.0 StackView { @@ -17,6 +19,8 @@ StackView { required property var assetsModel required property var collectiblesModel required property var channelsModel + property bool showChannelSelector: true + property alias initialPage: initialItem // id, name, image, color, owner properties expected required property var communityDetails @@ -38,8 +42,13 @@ StackView { pop(StackView.Immediate) } + function pushEditView(properties) { + root.push(newPermissionView, properties, StackView.Immediate); + } + // Community Permissions possible view contents: initialItem: SettingsPage { + id: initialItem implicitWidth: 0 title: qsTr("Permissions") @@ -52,45 +61,51 @@ StackView { onClicked: root.push(newPermissionView, StackView.Immediate) } - contentItem: PermissionsView { - permissionsModel: root.permissionsModel - assetsModel: root.assetsModel - collectiblesModel: root.collectiblesModel - channelsModel: root.channelsModel - communityDetails: root.communityDetails + contentItem: StatusScrollView { + contentHeight: (permissionsView.height + topPadding) + topPadding: permissionsView.topPadding + padding: 0 + PermissionsView { + id: permissionsView + permissionsModel: root.permissionsModel + assetsModel: root.assetsModel + collectiblesModel: root.collectiblesModel + channelsModel: root.channelsModel + communityDetails: root.communityDetails - viewWidth: root.viewWidth + viewWidth: root.viewWidth - onEditPermissionRequested: { - const item = ModelUtils.get(root.permissionsModel, index) + onEditPermissionRequested: { + const item = ModelUtils.get(root.permissionsModel, index) - const properties = { - permissionKeyToEdit: item.key, - holdingsToEditModel: item.holdingsListModel, - channelsToEditModel: item.channelsListModel, - permissionTypeToEdit: item.permissionType, - isPrivateToEditValue: item.isPrivate + const properties = { + permissionKeyToEdit: item.key, + holdingsToEditModel: item.holdingsListModel, + channelsToEditModel: item.channelsListModel, + permissionTypeToEdit: item.permissionType, + isPrivateToEditValue: item.isPrivate + } + + root.pushEditView(properties); } - root.push(newPermissionView, properties, StackView.Immediate) - } + onDuplicatePermissionRequested: { + const item = ModelUtils.get(root.permissionsModel, index) - onDuplicatePermissionRequested: { - const item = ModelUtils.get(root.permissionsModel, index) + const properties = { + holdingsToEditModel: item.holdingsListModel, + channelsToEditModel: item.channelsListModel, + permissionTypeToEdit: item.permissionType, + isPrivateToEditValue: item.isPrivate + } - const properties = { - holdingsToEditModel: item.holdingsListModel, - channelsToEditModel: item.channelsListModel, - permissionTypeToEdit: item.permissionType, - isPrivateToEditValue: item.isPrivate + root.pushEditView(properties); } - root.push(newPermissionView, properties, StackView.Immediate) - } - - onRemovePermissionRequested: { - const key = ModelUtils.get(root.permissionsModel, index, "key") - root.removePermissionRequested(key) + onRemovePermissionRequested: { + const key = ModelUtils.get(root.permissionsModel, index, "key") + root.removePermissionRequested(key) + } } } } @@ -100,20 +115,33 @@ StackView { SettingsPage { id: newPermissionViewPage - implicitWidth: 0 - title: isEditState ? qsTr("Edit permission") : qsTr("New permission") + property alias isDirty: editPermissionView.dirty + property alias isFullyFilled: editPermissionView.isFullyFilled + property alias isPrivateToEditValue: editPermissionView.isPrivate + property alias permissionTypeToEdit: editPermissionView.permissionType property alias holdingsToEditModel: editPermissionView.selectedHoldingsModel property alias channelsToEditModel: editPermissionView.selectedChannelsModel - property alias permissionTypeToEdit: editPermissionView.permissionType - property alias isPrivateToEditValue: editPermissionView.isPrivate + + property bool holdingsRequired: editPermissionView.dirtyValues.holdingsRequired property string permissionKeyToEdit readonly property bool isEditState: !!permissionKeyToEdit + readonly property alias toast: settingsDirtyToastMessage + function resetChanges() { + editPermissionView.resetChanges(); + } + function updatePermission() { + editPermissionView.saveChanges(); + } + function createPermission() { + editPermissionView.createPermissionClicked(); + } + contentItem: EditPermissionView { id: editPermissionView @@ -123,7 +151,7 @@ StackView { collectiblesModel: root.collectiblesModel channelsModel: root.channelsModel communityDetails: root.communityDetails - + showChannelSelector: root.showChannelSelector isEditState: newPermissionViewPage.isEditState holdingsRequired: selectedHoldingsModel ? selectedHoldingsModel.count > 0 : false @@ -191,14 +219,18 @@ StackView { dirtyValues.selectedHoldingsModel, ["key", "type", "amount"]) : [] - const channels = ModelUtils.modelToArray( - dirtyValues.selectedChannelsModel, ["key"]) + const channels = root.showChannelSelector ? + ModelUtils.modelToArray( + dirtyValues.selectedChannelsModel, ["key"]) : + ModelUtils.modelToArray(selectedChannelsModel, ["key"]) root.createPermissionRequested( dirtyValues.permissionType, holdings, channels, dirtyValues.isPrivate) - root.pop(StackView.Immediate) + if (root.showChannelSelector) { + root.pop(StackView.Immediate) + } } onNavigateToMintTokenSettings: root.navigateToMintTokenSettings(isAssetType) @@ -261,7 +293,8 @@ StackView { // delay to avoid toast blinking on entry settingsDirtyToastMessage.active = Qt.binding( () => editPermissionView.isEditState && - editPermissionView.dirty) + editPermissionView.dirty && + root.showChannelSelector) } } } diff --git a/ui/app/AppLayouts/Communities/popups/CreateChannelPopup.qml b/ui/app/AppLayouts/Communities/popups/CreateChannelPopup.qml index 79f0598167..9d0c34fb65 100644 --- a/ui/app/AppLayouts/Communities/popups/CreateChannelPopup.qml +++ b/ui/app/AppLayouts/Communities/popups/CreateChannelPopup.qml @@ -2,11 +2,14 @@ import QtQuick 2.15 import QtQuick.Layouts 1.15 import QtQuick.Controls 2.15 import QtQuick.Dialogs 1.3 +import QtQml 2.15 import QtQml.Models 2.15 import utils 1.0 import shared.panels 1.0 + +import StatusQ 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Core.Utils 0.1 as StatusQUtils @@ -15,8 +18,10 @@ import StatusQ.Controls.Validators 0.1 import StatusQ.Components 0.1 import StatusQ.Popups 0.1 -import AppLayouts.Communities.controls 1.0 +import AppLayouts.Communities.views 1.0 import AppLayouts.Communities.panels 1.0 +import AppLayouts.Communities.models 1.0 +import AppLayouts.Communities.controls 1.0 StatusStackModal { id: root @@ -26,9 +31,12 @@ StatusStackModal { property bool isDiscordImport // creating new or importing from discord? property bool isEdit: false property bool isDeleteable: false + property bool viewOnlyCanAddReaction + property bool hideIfPermissionsNotMet property string communityId: "" - property string chatId: "" + property string chatId: "_newChannel" + property string categoryId: "" property string channelName: "" property string channelDescription: "" @@ -39,20 +47,90 @@ StatusStackModal { readonly property int communityColorValidator: Utils.Validate.NoEmpty | Utils.Validate.TextHexColor + property var activeCommunity + required property var assetsModel + required property var collectiblesModel + required property var permissionsModel + + required property var channelsModel + readonly property int maxChannelNameLength: 24 readonly property int maxChannelDescLength: 140 + // channel signals signal createCommunityChannel(string chName, string chDescription, string chEmoji, string chColor, string chCategoryId) signal editCommunityChannel(string chName, string chDescription, string chEmoji, string chColor, string chCategoryId) signal deleteCommunityChannel() + // Permissions signals: + // permissions arg is a list of objects with the following properties: + // - key: string + // - id: string + // - permissionType: string + // - holdings: list of objects with the following properties: + // - key: string + // - type: string + // - amount: string + // - channels: list of objects with the following properties: + // - key: string + // - isPrivate: bool + signal addPermissions(var permissions) + signal removePermissions(var permissions) + signal editPermissions(var permissions) + signal setViewOnlyCanAddReaction(bool checked) + signal setHideIfPermissionsNotMet(bool checked) + width: 640 + leftPadding: 0 + rightPadding: 0 + currentIndex: d.currentPage + + enum CurrentPage { + ChannelDetails, //0 + ColorPicker, //1 + ChannelPermissions, //2 + DiscordImportUploadFile, //3 + DiscordImportUploadStart //4 + } QtObject { id: d + property int currentPage: CreateChannelPopup.CurrentPage.ChannelDetails + + readonly property QtObject communityDetails: QtObject { + readonly property string id: root.activeCommunity.id + readonly property string name: root.activeCommunity.name + readonly property string image: root.activeCommunity.image + readonly property string color: root.activeCommunity.color + readonly property bool owner: root.activeCommunity.memberRole === Constants.memberRole.owner + readonly property bool admin: root.activeCommunity.memberRole === Constants.memberRole.admin + readonly property bool tokenMaster: root.activeCommunity.memberRole === Constants.memberRole.tokenMaster + } + + readonly property ChannelPermissionsModelEditor channelEditModel: ChannelPermissionsModelEditor { + channelId: root.chatId + name: nameInput.input.text + emoji: nameInput.input.asset.emoji + color: colorPanel.color.toString().toUpperCase() + channelsModel: root.channelsModel + permissionsModel: root.permissionsModel + newChannelMode: !root.isEdit + + property Connections rootConnection: Connections { + target: root + function onClosed() { + d.channelEditModel.reset() + } + } + } + + property bool viewOnlyCanAddReaction: root.viewOnlyCanAddReaction + property bool hideIfPermissionsNotMet: root.hideIfPermissionsNotMet + property bool colorPickerOpened: false + function isFormValid() { return nameInput.valid && descriptionTextArea.valid && - Utils.validateAndReturnError(colorDialog.color.toString().toUpperCase(), communityColorValidator) === "" + Utils.validateAndReturnError(colorPanel.color.toString().toUpperCase(), communityColorValidator) === "" } function openEmojiPopup(leftSide = false) { @@ -70,7 +148,7 @@ StatusStackModal { categoryId: root.categoryId, name: StatusQUtils.Utils.filterXSS(nameInput.input.text), description: StatusQUtils.Utils.filterXSS(descriptionTextArea.text), - color: colorDialog.color.toString().toUpperCase(), + color: colorPanel.color.toString().toUpperCase(), emoji: StatusQUtils.Emoji.deparse(nameInput.input.asset.emoji), options: { // TODO @@ -85,66 +163,99 @@ StatusStackModal { creatingError.open() } } - } - stackTitle: isDiscordImport ? qsTr("New Channel With Imported Chat History") : - isEdit ? qsTr("Edit #%1").arg(root.channelName) - : qsTr("New channel") - - nextButton: StatusButton { - objectName: "createChannelNextBtn" - font.weight: Font.Medium - text: typeof currentItem.nextButtonText !== "undefined" ? currentItem.nextButtonText : qsTr("Import chat history") - enabled: typeof(currentItem.canGoNext) == "undefined" || currentItem.canGoNext - loading: root.communitiesStore.discordDataExtractionInProgress - onClicked: { - const nextAction = currentItem.nextAction - if (typeof(nextAction) == "function") { - return nextAction() + function saveAndClose() { + let emoji = StatusQUtils.Emoji.deparse(nameInput.input.asset.emoji) + if (!isEdit) { + root.createCommunityChannel(StatusQUtils.Utils.filterXSS(nameInput.input.text), + StatusQUtils.Utils.filterXSS(descriptionTextArea.text), + emoji, + colorPanel.color.toString().toUpperCase(), + root.categoryId) + } else { + root.editCommunityChannel(StatusQUtils.Utils.filterXSS(nameInput.input.text), + StatusQUtils.Utils.filterXSS(descriptionTextArea.text), + emoji, + colorPanel.color.toString().toUpperCase(), + root.categoryId) } - root.currentIndex++ + + if (d.channelEditModel.dirtyPermissions) { + var newPermissions = d.channelEditModel.getAddedPermissions(); + if (newPermissions.length > 0) { + root.addPermissions(newPermissions); + } + + var editedPermissions = d.channelEditModel.getEditedPermissions(); + if (editedPermissions.length > 0) { + root.editPermissions(editedPermissions); + } + + var removedPermissions = d.channelEditModel.getRemovedPermissions(); + if (removedPermissions.length > 0) { + root.removePermissions(removedPermissions); + } + } + + if (root.viewOnlyCanAddReaction !== d.viewOnlyCanAddReaction) { + root.setViewOnlyCanAddReaction(d.viewOnlyCanAddReaction); + } + + if (root.hideIfPermissionsNotMet !== d.hideIfPermissionsNotMet) { + root.setHideIfPermissionsNotMet(d.hideIfPermissionsNotMet); + } + + // TODO Open the channel once we have designs for it + root.close() } } - finishButton: StatusButton { + stackTitle: isDiscordImport ? qsTr("New Channel With Imported Chat History") : + !!currentItem.stackTitleText ? currentItem.stackTitleText : + (isEdit ? qsTr("Edit #%1").arg(root.channelName) : qsTr("New channel")) + + nextButton: StatusButton { objectName: "createOrEditCommunityChannelBtn" font.weight: Font.Medium - text: isDiscordImport ? qsTr("Import chat history") : isEdit ? qsTr("Save changes") : qsTr("Create channel") + height: 44 + visible: !d.colorPickerOpened enabled: typeof(currentItem.canGoNext) == "undefined" || currentItem.canGoNext + text: !!currentItem.nextButtonText ? currentItem.nextButtonText : + d.colorPickerOpened ? qsTr("Set channel color") : ( + isDiscordImport ? qsTr("Import chat history") : + isEdit ? qsTr("Save changes") : qsTr("Create channel")) + loading: root.communitiesStore.discordDataExtractionInProgress onClicked: { let nextAction = currentItem.nextAction if (typeof (nextAction) == "function") { return nextAction() } - if (!root.isDiscordImport) { - if (!d.isFormValid()) { - scrollView.scrollBackUp() - return - } - let emoji = StatusQUtils.Emoji.deparse(nameInput.input.asset.emoji) + } + } - if (!isEdit) { - root.createCommunityChannel(StatusQUtils.Utils.filterXSS(nameInput.input.text), - StatusQUtils.Utils.filterXSS(descriptionTextArea.text), - emoji, - colorDialog.color.toString().toUpperCase(), - root.categoryId) - } else { - root.editCommunityChannel(StatusQUtils.Utils.filterXSS(nameInput.input.text), - StatusQUtils.Utils.filterXSS(descriptionTextArea.text), - emoji, - colorDialog.color.toString().toUpperCase(), - root.categoryId) - } - // TODO Open the channel once we have designs for it - root.close() + finishButton: StatusButton { + objectName: "createChannelNextBtn" + font.weight: Font.Medium + height: 44 + text: (typeof currentItem.nextButtonText !== "undefined") ? currentItem.nextButtonText : + qsTr("Import chat history") + enabled: typeof(currentItem.canGoNext) == "undefined" || currentItem.canGoNext + onClicked: { + const nextAction = currentItem.nextAction + if (typeof(nextAction) == "function") { + return nextAction() } } } + //TODO + onCurrentIndexChanged: { + d.colorPickerOpened = false; + } readonly property StatusButton clearFilesButton: StatusButton { font.weight: Font.Medium text: qsTr("Clear all") + height: 44 type: StatusBaseButton.Type.Danger visible: typeof currentItem.isFileListView !== "undefined" && currentItem.isFileListView enabled: !fileListView.fileListModelEmpty && !root.communitiesStore.discordDataExtractionInProgress @@ -153,12 +264,32 @@ StatusStackModal { readonly property StatusButton deleteChannelButton: StatusButton { objectName: "deleteCommunityChannelBtn" - visible: isEdit && isDeleteable && !isDiscordImport && typeof(replaceItem) === "undefined" - text: qsTr("Delete channel") + height: 44 + visible: isEdit && isDeleteable && !isDiscordImport && (d.currentPage === CreateChannelPopup.CurrentPage.ChannelDetails) || + !!currentItem.deleteButtonText + text: (d.currentPage === CreateChannelPopup.CurrentPage.ChannelPermissions) ? currentItem.deleteButtonText : qsTr("Delete channel") + enabled: (d.currentPage === CreateChannelPopup.CurrentPage.ChannelPermissions) ? currentItem.deleteButtonEnabled : true type: StatusBaseButton.Type.Danger - onClicked: root.deleteCommunityChannel() + onClicked: { + const nextAction = currentItem.nextDeleteAction + if (typeof(nextAction) == "function") { + return nextAction() + } else { + root.deleteCommunityChannel(); + } + } } + property Item backButton: StatusBackButton { + visible: d.currentPage !== CreateChannelPopup.CurrentPage.ChannelDetails + onClicked: { + d.currentPage = (d.currentPage === CreateChannelPopup.CurrentPage.DiscordImportUploadStart) ? + CreateChannelPopup.CurrentPage.DiscordImportUploadFile : CreateChannelPopup.CurrentPage.ChannelDetails + } + } + + + leftButtons: [ backButton ] rightButtons: [clearFilesButton, deleteChannelButton, nextButton, finishButton] onAboutToShow: { @@ -179,7 +310,7 @@ StatusStackModal { if (root.channelEmoji) { nameInput.input.asset.emoji = root.channelEmoji } - colorDialog.color = root.channelColor + colorPanel.color = root.channelColor } else { nameInput.input.asset.isLetterIdenticon = true; } @@ -188,10 +319,8 @@ StatusStackModal { } readonly property list discordPages: [ - ColumnLayout { - id: fileListView - spacing: 24 - + Item { + id: fileListViewItem readonly property bool isFileListView: true readonly property var fileListModel: root.communitiesStore.discordFileList @@ -205,154 +334,157 @@ StatusStackModal { : fileListModel.selectedCount ? qsTr("Validate (%1/%2) files").arg(fileListModel.selectedCount).arg(fileListModel.count) : qsTr("Start channel import") readonly property var nextAction: function () { - if (!fileListView.fileListModel.selectedFilesValid) + if (!fileListViewItem.fileListModel.selectedFilesValid) return root.communitiesStore.requestExtractChannelsAndCategories() - root.currentIndex++ + d.currentPage = CreateChannelPopup.CurrentPage.DiscordImportUploadStart; } + ColumnLayout { + id: fileListView + anchors.fill: parent + anchors.leftMargin: 16 + anchors.rightMargin: 16 + spacing: 24 - RowLayout { - Layout.fillWidth: true - spacing: 12 - StatusBaseText { + RowLayout { Layout.fillWidth: true - maximumLineCount: 2 - wrapMode: Text.Wrap - elide: Text.ElideRight - text: fileListView.fileListModelEmpty ? qsTr("Select Discord channel JSON files to import") : - root.communitiesStore.discordImportErrorsCount ? qsTr("Some of your community files cannot be used") : - qsTr("Uncheck any files you would like to exclude from the import") - } - StatusBaseText { - visible: fileListView.fileListModelEmpty && !issuePill.visible - font.pixelSize: 12 - color: Theme.palette.baseColor1 - text: qsTr("(JSON file format only)") - } - IssuePill { - id: issuePill - type: root.communitiesStore.discordImportErrorsCount ? IssuePill.Type.Error : IssuePill.Type.Warning - count: root.communitiesStore.discordImportErrorsCount || root.communitiesStore.discordImportWarningsCount || 0 - visible: !!count && !fileListView.fileListModelEmpty - } - StatusButton { - Layout.alignment: Qt.AlignRight - text: qsTr("Browse files") - type: StatusBaseButton.Type.Primary - onClicked: fileDialog.open() - enabled: !root.communitiesStore.discordDataExtractionInProgress - } - } - - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - color: Theme.palette.baseColor4 - - ColumnLayout { - visible: fileListView.fileListModelEmpty - anchors.top: parent.top - anchors.topMargin: 60 - anchors.horizontalCenter: parent.horizontalCenter - spacing: 8 - - StatusRoundIcon { - Layout.alignment: Qt.AlignHCenter - asset.name: "info" + spacing: 12 + StatusBaseText { + Layout.fillWidth: true + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Text.ElideRight + text: fileListViewItem.fileListModelEmpty ? qsTr("Select Discord channel JSON files to import") : + root.communitiesStore.discordImportErrorsCount ? qsTr("Some of your community files cannot be used") : + qsTr("Uncheck any files you would like to exclude from the import") } StatusBaseText { - Layout.topMargin: 8 - Layout.alignment: Qt.AlignHCenter - horizontalAlignment: Qt.AlignHCenter - text: qsTr("Export the Discord channel’s chat history data using %1").arg("DiscordChatExporter") - onLinkActivated: Global.openLink(link) - HoverHandler { - id: handler1 - } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton - cursorShape: handler1.hovered && parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor - } + visible: fileListViewItem.fileListModelEmpty && !issuePill.visible + font.pixelSize: 12 + color: Theme.palette.baseColor1 + text: qsTr("(JSON file format only)") } - StatusBaseText { - Layout.alignment: Qt.AlignHCenter - horizontalAlignment: Qt.AlignHCenter - text: qsTr("Refer to this documentation if you have any queries") - onLinkActivated: Global.openLink(link) - HoverHandler { - id: handler2 - } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton - cursorShape: handler2.hovered && parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor - } + IssuePill { + id: issuePill + type: root.communitiesStore.discordImportErrorsCount ? IssuePill.Type.Error : IssuePill.Type.Warning + count: root.communitiesStore.discordImportErrorsCount || root.communitiesStore.discordImportWarningsCount || 0 + visible: !!count && !fileListViewItem.fileListModelEmpty + } + StatusButton { + Layout.alignment: Qt.AlignRight + text: qsTr("Browse files") + type: StatusBaseButton.Type.Primary + onClicked: fileDialog.open() + enabled: !root.communitiesStore.discordDataExtractionInProgress } } - Component { - id: floatingDivComp - Rectangle { - anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined - width: ListView.view ? ListView.view.width : 0 - height: 4 - color: Theme.palette.directColor8 - } - } + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Theme.palette.baseColor4 - StatusListView { - visible: !fileListView.fileListModelEmpty - enabled: !root.communitiesStore.discordDataExtractionInProgress - anchors.fill: parent - leftMargin: 8 - rightMargin: 8 - model: fileListView.fileListModel - header: !atYBeginning ? floatingDivComp : null - headerPositioning: ListView.OverlayHeader - footer: !atYEnd ? floatingDivComp : null - footerPositioning: ListView.OverlayHeader - delegate: ColumnLayout { - width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin - RowLayout { - spacing: 20 - Layout.fillWidth: true + ColumnLayout { + visible: fileListViewItem.fileListModelEmpty + anchors.top: parent.top + anchors.topMargin: 60 + anchors.horizontalCenter: parent.horizontalCenter + spacing: 8 + + StatusRoundIcon { + Layout.alignment: Qt.AlignHCenter + asset.name: "info" + } + StatusBaseText { + id: infoText1 Layout.topMargin: 8 + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Qt.AlignHCenter + text: qsTr("Export the Discord channel’s chat history data using %1").arg("DiscordChatExporter") + onLinkActivated: Global.openLink(link) + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + } + StatusBaseText { + id: infoText2 + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Qt.AlignHCenter + text: qsTr("Refer to this documentation if you have any queries") + onLinkActivated: Global.openLink(link) + onHoveredLinkChanged: print(hoveredLink) + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + } + } + + Component { + id: floatingDivComp + Rectangle { + anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined + width: ListView.view ? ListView.view.width : 0 + height: 4 + color: Theme.palette.directColor8 + } + } + + StatusListView { + visible: !fileListViewItem.fileListModelEmpty + enabled: !root.communitiesStore.discordDataExtractionInProgress + anchors.fill: parent + leftMargin: 8 + rightMargin: 8 + model: fileListViewItem.fileListModel + header: !atYBeginning ? floatingDivComp : null + headerPositioning: ListView.OverlayHeader + footer: !atYEnd ? floatingDivComp : null + footerPositioning: ListView.OverlayHeader + delegate: ColumnLayout { + width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin + RowLayout { + spacing: 20 + Layout.fillWidth: true + Layout.topMargin: 8 + StatusBaseText { + Layout.fillWidth: true + text: model.filePath + font.pixelSize: 13 + elide: Text.ElideRight + wrapMode: Text.WordWrap + maximumLineCount: 2 + } + + StatusFlatRoundButton { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + type: StatusFlatRoundButton.Type.Secondary + icon.name: "close" + icon.color: Theme.palette.directColor1 + icon.width: 24 + icon.height: 24 + onClicked: root.communitiesStore.removeFileListItem(model.filePath) + } + } + StatusBaseText { Layout.fillWidth: true - text: model.filePath + text: "%1 %2".arg("⚠").arg(model.errorMessage) + visible: model.errorMessage font.pixelSize: 13 - elide: Text.ElideRight - wrapMode: Text.WordWrap - maximumLineCount: 2 + font.weight: Font.Medium + elide: Text.ElideMiddle + color: Theme.palette.dangerColor1 + verticalAlignment: Qt.AlignTop } - - StatusFlatRoundButton { - Layout.preferredWidth: 32 - Layout.preferredHeight: 32 - type: StatusFlatRoundButton.Type.Secondary - icon.name: "close" - icon.color: Theme.palette.directColor1 - icon.width: 24 - icon.height: 24 - onClicked: root.communitiesStore.removeFileListItem(model.filePath) - } - } - - StatusBaseText { - Layout.fillWidth: true - text: "%1 %2".arg("⚠").arg(model.errorMessage) - visible: model.errorMessage - font.pixelSize: 13 - font.weight: Font.Medium - elide: Text.ElideMiddle - color: Theme.palette.dangerColor1 - verticalAlignment: Qt.AlignTop } } } } - FileDialog { id: fileDialog title: qsTr("Choose files to import") @@ -368,11 +500,7 @@ StatusStackModal { } } }, - - ColumnLayout { - id: categoriesAndChannelsView - spacing: 24 - + Item { readonly property bool canGoNext: root.communitiesStore.discordChannelsModel.hasSelectedItems readonly property var nextAction: function () { d.requestImportDiscordChannel() @@ -382,94 +510,102 @@ StatusStackModal { root.replace(progressComponent) } - Component { - id: progressComponent - DiscordImportProgressContents { - width: root.availableWidth - store: root.communitiesStore - importingSingleChannel: true - onClose: root.close() - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - visible: !root.communitiesStore.discordChannelsModel.count - Loader { - anchors.centerIn: parent - active: parent.visible - sourceComponent: StatusLoadingIndicator { - width: 50 - height: 50 - } - } - } - ColumnLayout { - spacing: 12 - visible: root.communitiesStore.discordChannelsModel.count + id: categoriesAndChannelsView + spacing: 24 + anchors.fill: parent + anchors.leftMargin: 16 + anchors.rightMargin: 16 - StatusBaseText { - Layout.fillWidth: true - text: qsTr("Select the chat history you would like to import into #%1...").arg(StatusQUtils.Utils.filterXSS(nameInput.input.text)) - wrapMode: Text.WordWrap - } - - RowLayout { - spacing: 20 - Layout.fillWidth: true - StatusRadioButton { - text: qsTr("Import all history") - checked: true - } - StatusRadioButton { - id: startDateRadio - text: qsTr("Start date") - } - StatusDatePicker { - id: datePicker - Layout.fillWidth: true - selectedDate: new Date(root.communitiesStore.discordOldestMessageTimestamp * 1000) - enabled: startDateRadio.checked + Component { + id: progressComponent + DiscordImportProgressContents { + width: root.availableWidth + store: root.communitiesStore + importingSingleChannel: true + onClose: root.close() } } - Rectangle { + Item { Layout.fillWidth: true Layout.fillHeight: true - color: Theme.palette.baseColor4 + visible: !root.communitiesStore.discordChannelsModel.count + Loader { + anchors.centerIn: parent + active: parent.visible + sourceComponent: StatusLoadingIndicator { + width: 50 + height: 50 + } + } + } - StatusListView { - anchors.fill: parent - anchors.margins: 16 - model: root.communitiesStore.discordCategoriesModel - delegate: ColumnLayout { - width: ListView.view.width - spacing: 8 + ColumnLayout { + spacing: 12 + visible: root.communitiesStore.discordChannelsModel.count - StatusBaseText { - readonly property string categoryId: model.id - id: categoryCheckbox - text: model.name - } + StatusBaseText { + Layout.fillWidth: true + text: qsTr("Select the chat history you would like to import into #%1...").arg(StatusQUtils.Utils.filterXSS(nameInput.input.text)) + wrapMode: Text.WordWrap + } - ColumnLayout { + RowLayout { + spacing: 20 + Layout.fillWidth: true + StatusRadioButton { + text: qsTr("Import all history") + checked: true + } + StatusRadioButton { + id: startDateRadio + text: qsTr("Start date") + } + StatusDatePicker { + id: datePicker + Layout.fillWidth: true + selectedDate: new Date(root.communitiesStore.discordOldestMessageTimestamp * 1000) + enabled: startDateRadio.checked + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Theme.palette.baseColor4 + + StatusListView { + anchors.fill: parent + anchors.margins: 16 + model: root.communitiesStore.discordCategoriesModel + delegate: ColumnLayout { + width: ListView.view.width spacing: 8 - Layout.fillWidth: true - Layout.leftMargin: 24 - Repeater { + + StatusBaseText { + readonly property string categoryId: model.id + id: categoryCheckbox + text: model.name + } + + ColumnLayout { + spacing: 8 Layout.fillWidth: true - model: root.communitiesStore.discordChannelsModel - delegate: StatusRadioButton { - width: parent.width - text: model.name - checked: model.selected - visible: model.categoryId === categoryCheckbox.categoryId - onToggled: root.communitiesStore.toggleOneDiscordChannel(model.id) - Component.onCompleted: { - if (model.selected) { - root.communitiesStore.toggleOneDiscordChannel(model.id) + Layout.leftMargin: 24 + Repeater { + Layout.fillWidth: true + model: root.communitiesStore.discordChannelsModel + delegate: StatusRadioButton { + width: parent.width + text: model.name + checked: model.selected + visible: model.categoryId === categoryCheckbox.categoryId + onToggled: root.communitiesStore.toggleOneDiscordChannel(model.id) + Component.onCompleted: { + if (model.selected) { + root.communitiesStore.toggleOneDiscordChannel(model.id) + } } } } @@ -512,18 +648,17 @@ StatusStackModal { ColumnLayout { id: content - width: scrollView.availableWidth - spacing: 0 - + spacing: Style.current.padding StatusInput { id: nameInput Layout.fillWidth: true + Layout.leftMargin: Style.current.padding + Layout.rightMargin: Style.current.padding input.edit.objectName: "createOrEditCommunityChannelNameInput" label: qsTr("Channel name") charLimit: root.maxChannelNameLength placeholderText: qsTr("# Name the channel") - input.onTextChanged: { const cursorPosition = input.cursorPosition input.text = Utils.convertSpacesToDashes(input.text) @@ -532,7 +667,7 @@ StatusStackModal { input.letterIconName = text } } - input.asset.color: colorDialog.color.toString() + input.asset.color: colorPanel.color.toString() input.rightComponent: StatusRoundButton { objectName: "StatusChannelPopup_emojiButton" implicitWidth: 32 @@ -559,48 +694,48 @@ StatusStackModal { } Item { - Layout.preferredHeight: 16 Layout.fillWidth: true - } + Layout.preferredHeight: 82 + Layout.leftMargin: Style.current.padding + Layout.rightMargin: Style.current.padding + StatusBaseText { + width: parent.width + anchors.top: parent.top + anchors.topMargin: Style.current.halfPadding + text: qsTr("Channel colour") + } + StatusPickerButton { + id: colorSelectorButton - ColorPicker { - id: colorDialog - Layout.fillWidth: true - title: qsTr("Channel colour") - color: root.isEdit && root.channelColor ? root.channelColor : Theme.palette.primaryColor1 - onPick: root.replace(colorPanel) - - Component { - id: colorPanel - ColorPanel { - title: qsTr("Channel colour") - buttonText: qsTr("Select Colour") - Component.onCompleted: color = colorDialog.color - onAccepted: { - colorDialog.color = color - root.replaceItem = undefined + property string validationError: "" + width: parent.width + anchors.bottom: parent.bottom + bgColor: colorPanel.colorSelected ? colorPanel.color : Theme.palette.baseColor2 + contentColor: colorPanel.colorSelected ? Theme.palette.white : Theme.palette.baseColor1 + text: colorPanel.colorSelected ? colorPanel.color.toString().toUpperCase() : qsTr("Pick a colour") + onClicked: { d.currentPage = CreateChannelPopup.CurrentPage.ColorPicker; d.colorPickerOpened = true; } + onTextChanged: { + if (colorPanel.colorSelected) { + validationError = Utils.validateAndReturnError(text, communityColorValidator) } } } } - Item { - Layout.preferredHeight: 16 - Layout.fillWidth: true - } - StatusInput { id: descriptionTextArea Layout.fillWidth: true + Layout.topMargin: Style.current.halfPadding + Layout.leftMargin: Style.current.padding + Layout.rightMargin: Style.current.padding input.edit.objectName: "createOrEditCommunityChannelDescriptionInput" input.verticalAlignment: TextEdit.AlignTop label: qsTr("Description") charLimit: 140 - placeholderText: qsTr("Describe the channel") input.multiline: true - minimumHeight: 88 - maximumHeight: 88 + minimumHeight: 108 + maximumHeight: 108 validators: [ StatusMinLengthValidator { minLength: 1 @@ -612,6 +747,189 @@ StatusStackModal { } ] } + Separator { + Layout.fillWidth: true + visible: viewOnlyCanAddReactionCheckbox.visible + } + StatusCheckBox { + id: viewOnlyCanAddReactionCheckbox + Layout.fillWidth: true + Layout.preferredHeight: 48 + Layout.leftMargin: Style.current.padding + Layout.rightMargin: Style.current.padding + leftSide: false + text: qsTr("Hide channel from members who don't have permissions to view the channel") + visible: false //TODO: Enable connect to the backend when it's ready https://github.com/status-im/status-desktop/issues/13291 + checked: d.hideIfPermissionsNotMet + onToggled: { + d.hideIfPermissionsNotMet = checked; + } + } + Separator { + Layout.fillWidth: true + } + RowLayout { + Layout.fillWidth: true + Layout.preferredHeight: 56 + Layout.leftMargin: Style.current.padding + Layout.rightMargin: Style.current.padding + StatusBaseText { + text: qsTr("Permissions") + } + Item { Layout.fillWidth: true } + StatusButton { + text: qsTr("Add permission") + enabled: !!nameInput.text + property ListModel channelToAddPermission: ListModel { } + onClicked: { + channelToAddPermission.clear(); + channelToAddPermission.append({"key": root.chatId, "name": nameInput.text}); + const propertiess = { + channelsToEditModel: channelToAddPermission, + header: null, + topPadding: -root.subHeaderPadding - 8, + leftPadding: 0, + rightPadding: 16, + viewWidth: scrollView.availableWidth - 32 + }; + editPermissionView.pushEditView(propertiess); + d.currentPage = CreateChannelPopup.CurrentPage.ChannelPermissions; + } + } + } + PermissionsView { + Layout.fillWidth: true + Layout.alignment: Qt.AlignBottom + Layout.leftMargin: Style.current.padding + Layout.rightMargin: Style.current.padding + viewWidth: (scrollView.availableWidth - 32) + permissionsModel: d.channelEditModel.channelPermissionsModel + assetsModel: root.assetsModel + collectiblesModel: root.collectiblesModel + viewOnlyCanAddReaction: root.viewOnlyCanAddReaction + channelsModel: d.channelEditModel.liveChannelsModel + communityDetails: d.communityDetails + showChannelOptions: true + allowIntroPanel: false + onRemovePermissionRequested: { + console.assert(d.channelEditModel.removePermission(index)) + } + onDuplicatePermissionRequested: { + const item = StatusQUtils.ModelUtils.get(d.channelEditModel.channelPermissionsModel, index); + const properties = { + holdingsToEditModel: item.holdingsListModel, + channelsToEditModel: item.channelsListModel, + permissionTypeToEdit: item.permissionType, + isPrivateToEditValue: item.isPrivate, + header: null, + topPadding: -root.subHeaderPadding - 8, + leftPadding: 0, + rightPadding: 16, + viewWidth: scrollView.availableWidth - 32 + } + editPermissionView.pushEditView(properties); + editPermissionView.currentItem.resetChanges() + d.currentPage = CreateChannelPopup.CurrentPage.ChannelPermissions; + } + + onEditPermissionRequested: { + const item = d.channelEditModel.channelPermissionsModel.get(index); + const requireHoldings = (item.holdingsListModel.count ?? item.holdingsListModel.rowCount()) > 0; + const properties = { + permissionKeyToEdit: item.key, + holdingsToEditModel: item.holdingsListModel, + channelsToEditModel: item.channelsListModel, + permissionTypeToEdit: item.permissionType, + isPrivateToEditValue: item.isPrivate, + header: null, + topPadding: -root.subHeaderPadding - 8, + leftPadding: 0, + rightPadding: 16, + viewWidth: scrollView.availableWidth - 32 + } + editPermissionView.pushEditView(properties); + editPermissionView.currentItem.resetChanges() + + d.currentPage = CreateChannelPopup.CurrentPage.ChannelPermissions; + } + onUserRestrictionsToggled: { + d.viewOnlyCanAddReaction = checked; + } + } + } + readonly property var nextAction: function () { + if (!root.isDiscordImport) { + if (!d.isFormValid()) { + scrollView.scrollBackUp() + return + } + + d.saveAndClose() + } else { + d.currentPage = CreateChannelPopup.CurrentPage.DiscordImportUploadFile; + } + } + }, + ColorPanel { + id: colorPanel + readonly property string stackTitleText: qsTr("Channel Colour") + readonly property string nextButtonText: qsTr("Select Channel Colour") + padding: 0 + leftPadding: 16 + rightPadding: 16 + height: Math.min(parent.height, 624) + property bool colorSelected: root.isEdit && root.channelColor + color: root.isEdit && root.channelColor ? root.channelColor : Theme.palette.primaryColor1 + onAccepted: { + colorSelected = true; d.colorPickerOpened = false; d.currentPage = CreateChannelPopup.CurrentPage.ChannelDetails; + } + readonly property var nextAction: function () { + accepted(); + } + }, + PermissionsSettingsPanel { + id: editPermissionView + + leftPadding: 16 + rightPadding: 16 + initialPage.header: null + initialPage.topPadding: 0 + initialPage.leftPadding: 0 + viewWidth: scrollView.availableWidth - 32 + readonly property string nextButtonText: !!currentItem.permissionKeyToEdit ? + qsTr("Update permission") : qsTr("Create permission") + readonly property string stackTitleText: !!currentItem.permissionKeyToEdit ? + qsTr("Edit #%1 permission").arg(nameInput.text) : qsTr("New #%1 permission").arg(nameInput.text) + readonly property string deleteButtonText: !!currentItem.permissionKeyToEdit ? + qsTr("Revert changes") : "" + readonly property bool canGoNext: !!currentItem && currentItem.isDirty && currentItem.isFullyFilled ? currentItem.isDirty && currentItem.isFullyFilled : false + readonly property bool deleteButtonEnabled: editPermissionView.canGoNext + assetsModel: root.assetsModel + collectiblesModel: root.collectiblesModel + permissionsModel: d.channelEditModel.channelPermissionsModel + channelsModel: d.channelEditModel.liveChannelsModel + communityDetails: d.communityDetails + showChannelSelector: false + readonly property var nextDeleteAction: function () { + if (!!currentItem.permissionKeyToEdit) { + currentItem.resetChanges(); + } + } + readonly property var nextAction: function () { + if (!!currentItem.permissionKeyToEdit) { + currentItem.updatePermission(); + } else { + currentItem.createPermission(); + } + } + onCreatePermissionRequested: { + d.channelEditModel.appendPermission(holdings, channels, permissionType, isPrivate) + d.currentPage = CreateChannelPopup.CurrentPage.ChannelDetails; + } + + onUpdatePermissionRequested: { + d.channelEditModel.editPermission(key, permissionType, holdings, channels, isPrivate) + d.currentPage = CreateChannelPopup.CurrentPage.ChannelDetails; } } ] diff --git a/ui/app/AppLayouts/Communities/popups/PermissionsDropdown.qml b/ui/app/AppLayouts/Communities/popups/PermissionsDropdown.qml index 36e5f9c2cf..c621fb73d9 100644 --- a/ui/app/AppLayouts/Communities/popups/PermissionsDropdown.qml +++ b/ui/app/AppLayouts/Communities/popups/PermissionsDropdown.qml @@ -16,6 +16,7 @@ StatusDropdown { property int mode: PermissionsDropdown.Mode.Add property int initialPermissionType: PermissionTypes.Type.None + property bool allowCommunityOptions: true property bool enableAdminPermission: true @@ -97,13 +98,14 @@ StatusDropdown { CustomSeparator { Layout.fillWidth: true Layout.preferredHeight: d.sectionHeight - + visible: root.allowCommunityOptions text: qsTr("Community") } CustomPermissionListItem { permissionType: PermissionTypes.Type.Admin enabled: root.enableAdminPermission + visible: root.allowCommunityOptions Layout.fillWidth: true objectName: "becomeAdmin" @@ -112,6 +114,7 @@ StatusDropdown { CustomPermissionListItem { permissionType: PermissionTypes.Type.Member + visible: root.allowCommunityOptions Layout.fillWidth: true objectName: "becomeMember" } diff --git a/ui/app/AppLayouts/Communities/views/CommunityColumnView.qml b/ui/app/AppLayouts/Communities/views/CommunityColumnView.qml index 821ce36796..479383a4f4 100644 --- a/ui/app/AppLayouts/Communities/views/CommunityColumnView.qml +++ b/ui/app/AppLayouts/Communities/views/CommunityColumnView.qml @@ -42,6 +42,7 @@ Item { required property CurrenciesStore currencyStore property bool hasAddedContacts: false property var communityData + property alias createChannelPopup: createChannelPopup // Community transfer ownership related props: required property bool isPendingOwnershipRequest @@ -52,6 +53,11 @@ Item { communityData.memberRole === Constants.memberRole.admin || communityData.memberRole === Constants.memberRole.tokenMaster + readonly property var permissionsModel: { + root.store.prepareTokenModelForCommunity(communityData.id) + return root.store.permissionsModel + } + signal infoButtonClicked signal manageButtonClicked @@ -297,7 +303,6 @@ Item { chatListPopupMenu: ChatContextMenuView { id: chatContextMenuView - emojiPopup: root.emojiPopup showDebugOptions: root.store.isDebugEnabledfir // TODO pass the chatModel in its entirety instead of fetching the JSOn using just the id @@ -368,17 +373,18 @@ Item { onDisplayProfilePopup: { Global.openProfilePopup(publicKey) } - - onEditCommunityChannel: { - communitySectionModule.editCommunityChannel( - chatId, - newName, - newDescription, - newEmoji, - newColor, - newCategory, - channelPosition // TODO change this to the signal once it is modifiable - ) + onDisplayEditChannelPopup: { + Global.openPopup(createChannelPopup, { + isEdit: true, + channelName: chatName, + channelDescription: chatDescription, + channelEmoji: chatEmoji, + channelColor: chatColor, + categoryId: chatCategoryId, + chatId: chatContextMenuView.chatId, + channelPosition: channelPosition, + deleteChatConfirmationDialog: deleteChatConfirmationDialog + }); } } } @@ -607,11 +613,63 @@ Item { id: createChannelPopup CreateChannelPopup { communitiesStore: root.communitiesStore + assetsModel: root.store.assetsModel + collectiblesModel: root.store.collectiblesModel + permissionsModel: root.store.permissionsModel + channelsModel: root.store.chatCommunitySectionModule.model emojiPopup: root.emojiPopup + activeCommunity: root.communityData + + property int channelPosition: -1 + property var deleteChatConfirmationDialog + onCreateCommunityChannel: function (chName, chDescription, chEmoji, chColor, chCategoryId) { root.store.createCommunityChannel(chName, chDescription, chEmoji, chColor, chCategoryId) + chatId = root.store.currentChatContentModule().chatDetails.id + } + onEditCommunityChannel: { + root.store.editCommunityChannel(chatId, + chName, + chDescription, + chEmoji, + chColor, + chCategoryId, + channelPosition); + } + + onAddPermissions: function (permissions) { + for (var i = 0; i < permissions.length; i++) { + root.store.permissionsStore.createPermission(permissions[i].holdingsListModel, + permissions[i].permissionType, + permissions[i].isPrivate, + permissions[i].channelsListModel) + } + } + onRemovePermissions: function (permissions) { + for (var i = 0; i < permissions.length; i++) { + root.store.permissionsStore.removePermission(permissions[i].id) + } + } + onEditPermissions: function (permissions) { + for (var i = 0; i < permissions.length; i++) { + root.store.permissionsStore.editPermission(permissions[i].id, + permissions[i].holdingsListModel, + permissions[i].permissionType, + permissions[i].channelsListModel, + permissions[i].isPrivate) + } + } + onSetViewOnlyCanAddReaction: function (checked) { + root.store.permissionsStore.setViewOnlyCanAddReaction(chatId, checked) + } + onSetHideIfPermissionsNotMet: function (checked) { + root.store.permissionsStore.setHideIfPermissionsNotMet(chatId, checked) + } + onDeleteCommunityChannel: { + Global.openPopup(deleteChatConfirmationDialog); + close() } onClosed: { destroy() diff --git a/ui/app/AppLayouts/Communities/views/EditPermissionView.qml b/ui/app/AppLayouts/Communities/views/EditPermissionView.qml index c7c20c8b17..ccbc3c44e4 100644 --- a/ui/app/AppLayouts/Communities/views/EditPermissionView.qml +++ b/ui/app/AppLayouts/Communities/views/EditPermissionView.qml @@ -38,14 +38,14 @@ StatusScrollView { readonly property alias dirtyValues: d.dirtyValues - readonly property bool isFullyFilled: - (dirtyValues.selectedHoldingsModel.count > 0 || !whoHoldsSwitch.checked) && + readonly property bool isFullyFilled: (dirtyValues.selectedHoldingsModel.count > 0 || !whoHoldsSwitch.checked) && dirtyValues.permissionType !== PermissionTypes.Type.None && - (d.isCommunityPermission || dirtyValues.selectedChannelsModel.count > 0) + (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 // roles: type, key, name, amount, imageSource property var selectedHoldingsModel: ListModel {} @@ -430,6 +430,7 @@ StatusScrollView { PermissionsDropdown { id: permissionsDropdown + allowCommunityOptions: root.showChannelSelector initialPermissionType: d.dirtyValues.permissionType enableAdminPermission: root.communityDetails.owner @@ -454,7 +455,7 @@ StatusScrollView { } } - SequenceColumnLayout.Separator {} + SequenceColumnLayout.Separator { visible: root.showChannelSelector } StatusItemSelector { id: inSelector @@ -463,7 +464,7 @@ StatusScrollView { addButton.visible: editable itemsClickable: editable - + visible: root.showChannelSelector Layout.fillWidth: true icon: d.isCommunityPermission ? Style.svg("communities") : Style.svg("create-category") title: qsTr("In") @@ -566,7 +567,7 @@ StatusScrollView { color: Theme.palette.baseColor2 } - HidePermissionPanel { + StatusIconSwitch { Layout.topMargin: 12 Layout.fillWidth: true Layout.leftMargin: 16 @@ -574,6 +575,9 @@ StatusScrollView { enabled: d.dirtyValues.permissionType !== PermissionTypes.Type.Admin checked: d.dirtyValues.isPrivate + title: qsTr("Hide permission") + subTitle: qsTr("Make this permission hidden from members who don’t meet its requirements") + icon: "hide" onToggled: d.dirtyValues.isPrivate = checked } @@ -600,7 +604,7 @@ StatusScrollView { 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 @@ -613,7 +617,7 @@ StatusScrollView { Layout.fillWidth: true Layout.topMargin: Style.current.bigPadding - visible: !root.isEditState + visible: !root.isEditState && root.showChannelSelector text: qsTr("Create permission") enabled: root.isFullyFilled && !root.permissionDuplicated diff --git a/ui/app/AppLayouts/Communities/views/PermissionsView.qml b/ui/app/AppLayouts/Communities/views/PermissionsView.qml index 605170e07c..ad130d89b8 100644 --- a/ui/app/AppLayouts/Communities/views/PermissionsView.qml +++ b/ui/app/AppLayouts/Communities/views/PermissionsView.qml @@ -2,16 +2,27 @@ import QtQuick 2.14 import QtQuick.Layouts 1.14 import StatusQ.Core 0.1 +import StatusQ.Controls 0.1 import SortFilterProxyModel 0.2 +import shared.status 1.0 import shared.popups 1.0 import utils 1.0 import AppLayouts.Communities.controls 1.0 import AppLayouts.Communities.panels 1.0 -StatusScrollView { +ColumnLayout { id: root + width: root.viewWidth + property int topPadding: count ? 16 : 0 + spacing: 24 + + QtObject { + id: d + + property int permissionIndexToRemove + } required property var permissionsModel required property var assetsModel @@ -22,98 +33,110 @@ StatusScrollView { required property var communityDetails property int viewWidth: 560 // by design + property bool viewOnlyCanAddReaction + property bool showChannelOptions: false + property bool allowIntroPanel: true signal editPermissionRequested(int index) signal duplicatePermissionRequested(int index) signal removePermissionRequested(int index) + signal userRestrictionsToggled(bool checked) readonly property alias count: repeater.count + ListModel { + id: communityItemModel - padding: 0 - topPadding: count ? 16 : 0 - - QtObject { - id: d - - property int permissionIndexToRemove + Component.onCompleted: { + append({ + text: root.communityDetails.name, + imageSource: root.communityDetails.image, + color: root.communityDetails.color + }) + } } - ColumnLayout { - id: mainLayout - width: root.viewWidth - spacing: 24 + IntroPanel { + Layout.fillWidth: true - ListModel { - id: communityItemModel + visible: (root.count === 0 && root.allowIntroPanel) - Component.onCompleted: { - append({ - text: root.communityDetails.name, - imageSource: root.communityDetails.image, - color: root.communityDetails.color - }) - } - } + image: Style.png("community/permissions2_3") + title: qsTr("Permissions") + subtitle: qsTr("You can manage your community by creating and issuing membership and access permissions") + checkersModel: [ + qsTr("Give individual members access to private channels"), + qsTr("Monetise your community with subscriptions and fees"), + qsTr("Require holding a token or NFT to obtain exclusive membership rights") + ] + } - IntroPanel { + Repeater { + id: repeater + + model: root.permissionsModel + + delegate: PermissionItem { Layout.fillWidth: true - visible: root.count === 0 + holdingsListModel: HoldingsSelectionModel { + sourceModel: model.holdingsListModel + assetsModel: root.assetsModel + collectiblesModel: root.collectiblesModel + } - image: Style.png("community/permissions2_3") - title: qsTr("Permissions") - subtitle: qsTr("You can manage your community by creating and issuing membership and access permissions") - checkersModel: [ - qsTr("Give individual members access to private channels"), - qsTr("Monetise your community with subscriptions and fees"), - qsTr("Require holding a token or NFT to obtain exclusive membership rights") - ] - } + permissionType: model.permissionType + permissionState: model.permissionState // TODO: Backend! - Repeater { - id: repeater + ChannelsSelectionModel { + id: channelsSelectionModel - model: root.permissionsModel + selectedChannels: model.channelsListModel ?? null + allChannels: root.channelsModel + } - delegate: PermissionItem { - Layout.fillWidth: true + channelsListModel: channelsSelectionModel.count + ? channelsSelectionModel : communityItemModel + isPrivate: model.isPrivate - holdingsListModel: HoldingsSelectionModel { - sourceModel: model.holdingsListModel - assetsModel: root.assetsModel - collectiblesModel: root.collectiblesModel - } + showButtons: (model.permissionType !== PermissionTypes.Type.TokenMaster && + model.permissionType !== PermissionTypes.Type.Owner) && + (!!root.communityDetails && (root.communityDetails.owner || + ((root.communityDetails.admin || root.communityDetails.tokenMaster) && model.permissionType !== PermissionTypes.Type.Admin))) - permissionType: model.permissionType - permissionState: model.permissionState // TODO: Backend! + onEditClicked: root.editPermissionRequested(model.index) + onDuplicateClicked: root.duplicatePermissionRequested(model.index) - ChannelsSelectionModel { - id: channelsSelectionModel - - selectedChannels: model.channelsListModel ?? null - allChannels: root.channelsModel - } - - channelsListModel: channelsSelectionModel.count - ? channelsSelectionModel : communityItemModel - isPrivate: model.isPrivate - - showButtons: (model.permissionType !== PermissionTypes.Type.TokenMaster && - model.permissionType !== PermissionTypes.Type.Owner) && - (root.communityDetails.owner || - ((root.communityDetails.admin || root.communityDetails.tokenMaster) && model.permissionType !== PermissionTypes.Type.Admin)) - - onEditClicked: root.editPermissionRequested(model.index) - onDuplicateClicked: root.duplicatePermissionRequested(model.index) - - onRemoveClicked: { - d.permissionIndexToRemove = index - declineAllDialog.open() - } + onRemoveClicked: { + d.permissionIndexToRemove = index + declineAllDialog.open() } } } + StatusBaseText { + id: noPermissionsLabel + Layout.fillWidth: true + Layout.fillHeight: true + visible: (root.count === 0 && root.showChannelOptions) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: qsTr("No channel permissions") + color: Style.current.secondaryText + } + + StatusIconSwitch { + Layout.fillWidth: true + padding: 0 + + visible: false //TODO: enable this when we have the backend support https://github.com/status-im/status-desktop/issues/13292 + //visible: root.showChannelOptions + title: qsTr("Users with view only permissions can add reactions") + icon: "emojis" + checked: root.viewOnlyCanAddReaction + onToggled: { + root.userRestrictionsToggled(checked); + } + } ConfirmationDialog { id: declineAllDialog diff --git a/ui/imports/shared/views/chat/ChatContextMenuView.qml b/ui/imports/shared/views/chat/ChatContextMenuView.qml index 2e2d2a87f3..e7378e051c 100644 --- a/ui/imports/shared/views/chat/ChatContextMenuView.qml +++ b/ui/imports/shared/views/chat/ChatContextMenuView.qml @@ -26,10 +26,11 @@ StatusMenu { property bool chatMuted: false property int channelPosition: -1 property string chatCategoryId: "" - property var emojiPopup property bool showDebugOptions: false + property alias deleteChatConfirmationDialog: deleteChatConfirmationDialogComponent signal displayProfilePopup(string publicKey) + signal displayEditChannelPopup(string chatId) signal requestAllHistoricMessages(string chatId) signal unmuteChat(string chatId) signal muteChat(string chatId, int interval) @@ -140,38 +141,7 @@ StatusMenu { icon.name: "edit" enabled: root.isCommunityChat && root.amIChatAdmin onTriggered: { - Global.openPopup(editChannelPopup, { - isEdit: true, - channelName: root.chatName, - channelDescription: root.chatDescription, - channelEmoji: root.chatEmoji, - channelColor: root.chatColor, - categoryId: root.chatCategoryId - }); - } - } - - Component { - id: editChannelPopup - CreateChannelPopup { - anchors.centerIn: parent - isEdit: true - isDeleteable: root.isCommunityChat - emojiPopup: root.emojiPopup - onCreateCommunityChannel: { - root.createCommunityChannel(root.chatId, chName, chDescription, chEmoji, chColor); - } - onEditCommunityChannel: { - root.editCommunityChannel(root.chatId, chName, chDescription, chEmoji, chColor, - chCategoryId); - } - onDeleteCommunityChannel: { - Global.openPopup(deleteChatConfirmationDialogComponent) - close() - } - onClosed: { - destroy() - } + root.displayEditChannelPopup(root.chatId); } }