diff --git a/src/app/modules/main/chat_section/view.nim b/src/app/modules/main/chat_section/view.nim index 40a77c717..5e0be57d7 100644 --- a/src/app/modules/main/chat_section/view.nim +++ b/src/app/modules/main/chat_section/view.nim @@ -249,7 +249,7 @@ QtObject: let addressesArray = map(parseJson(addressesToShare).getElems(), proc(x:JsonNode):string = x.getStr()) self.delegate.requestToJoinCommunityWithAuthentication(ensName, addressesArray, airdropAddress) except Exception as e: - echo "Error requesting to join community with authetication and shared addresses: ", e.msg + echo "Error requesting to join community with authentication and shared addresses: ", e.msg proc joinGroupChatFromInvitation*(self: View, groupName: string, chatId: string, adminPK: string) {.slot.} = self.delegate.joinGroupChatFromInvitation(groupName, chatId, adminPK) @@ -421,4 +421,4 @@ QtObject: read = getAllTokenRequirementsMet notify = allTokenRequirementsMetChanged - proc userAuthenticationCanceled*(self: View) {.signal.} \ No newline at end of file + proc userAuthenticationCanceled*(self: View) {.signal.} diff --git a/src/app/modules/shared_models/token_criteria_model.nim b/src/app/modules/shared_models/token_criteria_model.nim index 1c4c55e51..a2d68aeda 100644 --- a/src/app/modules/shared_models/token_criteria_model.nim +++ b/src/app/modules/shared_models/token_criteria_model.nim @@ -57,7 +57,6 @@ QtObject: let enumRole = role.ModelRole case enumRole: of ModelRole.Key: - if item.getType() == ord(TokenType.ENS): result = newQVariant(item.getEnsPattern()) else: @@ -69,7 +68,7 @@ QtObject: of ModelRole.ShortName: result = newQVariant(item.getSymbol()) of ModelRole.Name: - result = newQVariant(item.getSymbol()) + result = newQVariant(item.getName()) of ModelRole.Amount: result = newQVariant(item.getAmount()) of ModelRole.CriteriaMet: diff --git a/src/app/modules/shared_models/token_model.nim b/src/app/modules/shared_models/token_model.nim index 7b4f83ccf..cd0ba32cf 100644 --- a/src/app/modules/shared_models/token_model.nim +++ b/src/app/modules/shared_models/token_model.nim @@ -162,7 +162,7 @@ QtObject: of "description": result = $item.getDescription() of "assetWebsiteUrl": result = $item.getAssetWebsiteUrl() of "builtOn": result = $item.getBuiltOn() - of "Address": result = $item.getAddress() + of "address": result = $item.getAddress() of "marketCap": result = $item.getMarketCap() of "highDay": result = $item.getHighDay() of "lowDay": result = $item.getLowDay() diff --git a/src/app/modules/shared_models/token_permission_item.nim b/src/app/modules/shared_models/token_permission_item.nim index 97d922393..50df67f68 100644 --- a/src/app/modules/shared_models/token_permission_item.nim +++ b/src/app/modules/shared_models/token_permission_item.nim @@ -89,7 +89,7 @@ proc buildTokenPermissionItem*(tokenPermission: CommunityTokenPermissionDto): To tokenCriteriaItems, tokenPermissionChatListItems, tokenPermission.isPrivate, - false # allTokenCriteriaMet will be update by a call to checkPermissinosToJoin + false # allTokenCriteriaMet will be updated by a call to checkPermissionsToJoin ) return tokenPermissionItem diff --git a/storybook/PagesModel.qml b/storybook/PagesModel.qml index b2f5d30ed..deed0229f 100644 --- a/storybook/PagesModel.qml +++ b/storybook/PagesModel.qml @@ -253,6 +253,10 @@ ListModel { title: "RemotelyDestructPopup" section: "Popups" } + ListElement { + title: "SharedAddressesPopup" + section: "Popups" + } ListElement { title: "AlertPopup" section: "Popups" diff --git a/storybook/figma.json b/storybook/figma.json index 446b21d3d..dc4d5f0dd 100644 --- a/storybook/figma.json +++ b/storybook/figma.json @@ -242,6 +242,13 @@ ], "EditOwnerTokenView": [ "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?type=design&node-id=34794-590207&mode=design&t=ZnwK9yenS5oSgwws-0" - + ], + "CommunityIntroDialog": [ + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=31461%3A563897&mode=dev" + ], + "SharedAddressesPopup": [ + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=31461%3A564367&mode=dev", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=31461%3A563905&mode=dev", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=31461%3A579875&mode=dev" ] } diff --git a/storybook/pages/CommunityIntroDialogPage.qml b/storybook/pages/CommunityIntroDialogPage.qml index deee0c343..cb6326171 100644 --- a/storybook/pages/CommunityIntroDialogPage.qml +++ b/storybook/pages/CommunityIntroDialogPage.qml @@ -44,10 +44,16 @@ SplitView { 5. 🚗 consectetur adipiscing elit Nemo enim 😋 ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.".arg(dialog.name) + loginType: ctrlLoginType.currentIndex - onJoined: logs.logEvent("CommunityIntroDialog::onJoined()") + walletAccountsModel: WalletAccountsModel {} + permissionsModel: dialog.accessType === Constants.communityChatOnRequestAccess ? PermissionsModel.complexPermissionsModel + : null + assetsModel: AssetsModel {} + collectiblesModel: CollectiblesModel {} + + onJoined: logs.logEvent("CommunityIntroDialog::onJoined", ["airdropAddress", "sharedAddresses"], arguments) onCancelMembershipRequest: logs.logEvent("CommunityIntroDialog::onCancelMembershipRequest()") - } } @@ -146,6 +152,20 @@ Nemo enim 😋 ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, } } + ColumnLayout { + visible: dialog.accessType == Constants.communityChatOnRequestAccess && !dialog.isInvitationPending + Label { + Layout.fillWidth: true + text: "Login type" + } + + ComboBox { + id: ctrlLoginType + Layout.fillWidth: true + model: ["Password","Biometrics","Keycard"] + } + } + Item { Layout.fillHeight: true } diff --git a/storybook/pages/CommunityPermissionsSettingsPanelEditor.qml b/storybook/pages/CommunityPermissionsSettingsPanelEditor.qml index 108a613d7..3eda1f04a 100644 --- a/storybook/pages/CommunityPermissionsSettingsPanelEditor.qml +++ b/storybook/pages/CommunityPermissionsSettingsPanelEditor.qml @@ -4,6 +4,8 @@ import QtQuick.Layouts 1.14 import StatusQ.Core.Utils 0.1 +import AppLayouts.Communities.controls 1.0 + Flickable { id: root diff --git a/storybook/pages/JoinPermissionsOverlayPanelPage.qml b/storybook/pages/JoinPermissionsOverlayPanelPage.qml index fc01cdc0c..eeda2bcc7 100644 --- a/storybook/pages/JoinPermissionsOverlayPanelPage.qml +++ b/storybook/pages/JoinPermissionsOverlayPanelPage.qml @@ -18,7 +18,7 @@ SplitView { id: d property string name: "Uniswap" - property string channelName: "#vip" + property string channelName: "vip" property bool joinCommunity: true // Otherwise, enter channel property bool requirementsMet: true property bool isInvitationPending: false @@ -40,8 +40,6 @@ SplitView { orientation: Qt.Vertical SplitView.fillWidth: true - - Item { SplitView.fillWidth: true SplitView.fillHeight: true diff --git a/storybook/pages/PermissionsViewPage.qml b/storybook/pages/PermissionsViewPage.qml index 7467a54fe..1e6ec58df 100644 --- a/storybook/pages/PermissionsViewPage.qml +++ b/storybook/pages/PermissionsViewPage.qml @@ -59,6 +59,7 @@ SplitView { readonly property string image: ModelsData.icons.socks readonly property string color: "red" readonly property bool owner: isOwnerCheckBox.checked + readonly property bool admin: isAdminCheckBox.checked } function log(method, index) { @@ -94,6 +95,12 @@ SplitView { text: "Is owner" } + CheckBox { + id: isAdminCheckBox + + text: "Is admin" + } + CheckBox { id: emptyModelCheckBox diff --git a/storybook/pages/SharedAddressesPopupPage.qml b/storybook/pages/SharedAddressesPopupPage.qml new file mode 100644 index 000000000..9900418d9 --- /dev/null +++ b/storybook/pages/SharedAddressesPopupPage.qml @@ -0,0 +1,155 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQml 2.15 + +import Storybook 1.0 +import Models 1.0 + +import AppLayouts.Communities.popups 1.0 + +SplitView { + id: root + Logs { id: logs } + + ListModel { + id: emptyModel + } + + Component { + id: dlgComponent + SharedAddressesPopup { + //anchors.centerIn: parent + isEditMode: ctrlEditMode.checked + communityName: "Decentraland" + communityIcon: ModelsData.assets.uni + loginType: ctrlLoginType.currentIndex + walletAccountsModel: WalletAccountsModel {} + permissionsModel: { + if (ctrlPermissions.checked && ctrlTokenGatedChannels.checked) + return PermissionsModel.complexPermissionsModel + if (ctrlPermissions.checked) + return PermissionsModel.permissionsModel + if (ctrlTokenGatedChannels.checked) + return PermissionsModel.channelsOnlyPermissionsModel + + return emptyModel + } + + assetsModel: AssetsModel {} + collectiblesModel: CollectiblesModel {} + visible: true + + onShareSelectedAddressesClicked: logs.logEvent("::shareSelectedAddressesClicked", ["airdropAddress", "sharedAddresses"], arguments) + onClosed: destroy() + } + } + + property var dialog + + function createAndOpenDialog() { + dialog = dlgComponent.createObject(root) + dialog.open() + } + + Component.onCompleted: createAndOpenDialog() + + SplitView { + orientation: Qt.Vertical + SplitView.fillWidth: true + + Pane { + id: pane + + SplitView.fillWidth: true + SplitView.fillHeight: true + + PopupBackground { + anchors.fill: parent + } + + Button { + anchors.centerIn: parent + text: "Reopen" + + onClicked: createAndOpenDialog() + } + } + + LogsAndControlsPanel { + id: logsAndControlsPanel + + SplitView.minimumHeight: 100 + SplitView.preferredHeight: 150 + + logsView.logText: logs.logText + } + } + + Pane { + SplitView.minimumWidth: 300 + SplitView.preferredWidth: 300 + + ColumnLayout { + anchors.fill: parent + + Switch { + id: ctrlPermissions + text: "With permissions" + checked: true + } + + Switch { + id: ctrlTokenGatedChannels + text: "With token gated channels" + checked: true + } + + Switch { + id: ctrlEditMode + text: "Edit mode" + } + + ColumnLayout { + visible: ctrlEditMode.checked + Label { + Layout.fillWidth: true + text: "Login type" + } + + ComboBox { + id: ctrlLoginType + Layout.fillWidth: true + model: ["Password","Biometrics","Keycard"] + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: "lightgray" + } + + Text { + text: "Info" + font.bold: true + } + + Text { + Layout.fillWidth: true + text: "Shared addresses: %1".arg(!!dialog ? dialog.selectedSharedAddresses.join(";") : "") + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + } + + Text { + Layout.fillWidth: true + text: "Airdrop address: %1".arg(!!dialog ? dialog.selectedAirdropAddress : "") + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + } + + Item { + Layout.fillHeight: true + } + } + } +} diff --git a/storybook/pages/SignMultiTokenTransactionsPopupPage.qml b/storybook/pages/SignMultiTokenTransactionsPopupPage.qml index f3a144071..1f91e4881 100644 --- a/storybook/pages/SignMultiTokenTransactionsPopupPage.qml +++ b/storybook/pages/SignMultiTokenTransactionsPopupPage.qml @@ -65,8 +65,8 @@ SplitView { errorText: errorTextField.text totalFeeText: "0.01 ETH ($265.43)" - onSignTransactionClicked: logs.logEvent("SignTokenTransactionsPopup::onSignTransactionClicked") - onCancelClicked: logs.logEvent("SignTokenTransactionsPopup::onCancelClicked") + onSignTransactionClicked: logs.logEvent("SignMultiTokenTransactionsPopup::onSignTransactionClicked") + onCancelClicked: logs.logEvent("SignMultiTokenTransactionsPopup::onCancelClicked") } } diff --git a/storybook/pages/SignTokenTransactionsPopupPage.qml b/storybook/pages/SignTokenTransactionsPopupPage.qml index f449e8cd2..5a52cd43f 100644 --- a/storybook/pages/SignTokenTransactionsPopupPage.qml +++ b/storybook/pages/SignTokenTransactionsPopupPage.qml @@ -100,7 +100,7 @@ SplitView { Label { Layout.fillWidth: true - text: "Network name" + text: "Network fee" } TextField { diff --git a/storybook/pages/TokenPermissionsPopupPage.qml b/storybook/pages/TokenPermissionsPopupPage.qml index 452660c30..e6f2422e9 100644 --- a/storybook/pages/TokenPermissionsPopupPage.qml +++ b/storybook/pages/TokenPermissionsPopupPage.qml @@ -50,7 +50,7 @@ SplitView { id: editor isOnlyChannelPanelEditor: true - channelName: "#vip" + channelName: "vip" joinCommunity: false } } diff --git a/storybook/src/Models/AssetsModel.qml b/storybook/src/Models/AssetsModel.qml index e3c1dacd0..5ee28ddde 100644 --- a/storybook/src/Models/AssetsModel.qml +++ b/storybook/src/Models/AssetsModel.qml @@ -63,7 +63,7 @@ ListModel { iconSource: ModelsData.assets.snt, name: "snt", shortName: "snt", - symbol: "snt", + symbol: "SNT", category: TokenCategories.Category.General, communityId: "" } diff --git a/storybook/src/Models/CollectiblesModel.qml b/storybook/src/Models/CollectiblesModel.qml index e178a6d40..4885fa51c 100644 --- a/storybook/src/Models/CollectiblesModel.qml +++ b/storybook/src/Models/CollectiblesModel.qml @@ -8,7 +8,7 @@ ListModel { key: "Anniversary", iconSource: ModelsData.collectibles.anniversary, name: "Anniversary", - symbol: "Anniversary", + symbol: "ANN", category: TokenCategories.Category.Community, imageUrl: ModelsData.collectibles.anniversary, id: 1767698, @@ -18,7 +18,7 @@ ListModel { key: "Anniversary2", iconSource: ModelsData.collectibles.anniversary, name: "Anniversary2", - symbol: "Anniversary2", + symbol: "ANN2", category: TokenCategories.Category.Community, imageUrl: ModelsData.collectibles.anniversary, id: 1767699, @@ -28,7 +28,7 @@ ListModel { key: "CryptoKitties", iconSource: ModelsData.collectibles.cryptoKitties, name: "CryptoKitties", - symbol: "CryptoKitties", + symbol: "CK", category: TokenCategories.Category.Own, subItems: [ { @@ -82,7 +82,7 @@ ListModel { key: "SuperRare", iconSource: ModelsData.collectibles.superRare, name: "SuperRare", - symbol: "SuperRare", + symbol: "SR", category: TokenCategories.Category.Own, imageUrl: ModelsData.collectibles.superRare, id: 1767701, @@ -92,7 +92,7 @@ ListModel { key: "Custom", iconSource: ModelsData.collectibles.custom, name: "Custom Collectible", - symbol: "Custom", + symbol: "CUS", category: TokenCategories.Category.General, imageUrl: ModelsData.collectibles.custom, id: 1767764, diff --git a/storybook/src/Models/PermissionsModel.qml b/storybook/src/Models/PermissionsModel.qml index 172938ead..86094deab 100644 --- a/storybook/src/Models/PermissionsModel.qml +++ b/storybook/src/Models/PermissionsModel.qml @@ -14,13 +14,15 @@ QtObject { holdingsListModel: root.createHoldingsModel1(), channelsListModel: root.createChannelsModel1(), permissionType: PermissionTypes.Type.Admin, - isPrivate: true + isPrivate: true, + tokenCriteriaMet: false }, { holdingsListModel: root.createHoldingsModel2(), channelsListModel: root.createChannelsModel2(), permissionType: PermissionTypes.Type.Member, - isPrivate: false + isPrivate: false, + tokenCriteriaMet: true } ] @@ -29,7 +31,7 @@ QtObject { holdingsListModel: root.createHoldingsModel4(), channelsListModel: root.createChannelsModel1(), permissionType: PermissionTypes.Type.Admin, - isPrivate: true, + isPrivate: true } ] @@ -138,6 +140,124 @@ QtObject { } ] + readonly property var complexPermissionsModelData: [ + { + id: "admin1", + holdingsListModel: root.createHoldingsModel2b(), + channelsListModel: root.createChannelsModel2(), + permissionType: PermissionTypes.Type.Admin, + isPrivate: false, + tokenCriteriaMet: true + }, + { + id: "admin2", + holdingsListModel: root.createHoldingsModel3(), + channelsListModel: root.createChannelsModel2(), + permissionType: PermissionTypes.Type.Admin, + isPrivate: false, + tokenCriteriaMet: false + }, + { + id: "member1", + holdingsListModel: root.createHoldingsModel2(), + channelsListModel: root.createChannelsModel2(), + permissionType: PermissionTypes.Type.Member, + isPrivate: false, + tokenCriteriaMet: true + }, + { + id: "member2", + holdingsListModel: root.createHoldingsModel3(), + channelsListModel: root.createChannelsModel2(), + permissionType: PermissionTypes.Type.Member, + isPrivate: false, + tokenCriteriaMet: false + } + ] + + readonly property var channelsOnlyPermissionsModelData: [ + { + 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.createHoldingsModel2(), + channelsListModel: root.createChannelsModel3(), + permissionType: PermissionTypes.Type.Read, + isPrivate: false, + tokenCriteriaMet: true + }, + { + id: "read2b", + holdingsListModel: root.createHoldingsModel5(), + 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 @@ -215,6 +335,29 @@ QtObject { } } + readonly property var complexPermissionsModel: ListModel { + readonly property ModelChangeGuard guard: ModelChangeGuard { + model: root.complexPermissionsModel + } + + Component.onCompleted: { + append(complexPermissionsModelData) + append(channelsOnlyPermissionsModelData) + guard.enabled = true + } + } + + readonly property var channelsOnlyPermissionsModel: ListModel { + readonly property ModelChangeGuard guard: ModelChangeGuard { + model: root.channelsOnlyPermissionsModel + } + + Component.onCompleted: { + append(channelsOnlyPermissionsModelData) + guard.enabled = true + } + } + function createHoldingsModel1() { return [ { @@ -230,7 +373,7 @@ QtObject { return [ { type: HoldingTypes.Type.Ens, - key: "Ens", + key: "*.eth", amount: 1, available: true } @@ -249,7 +392,24 @@ QtObject { type: HoldingTypes.Type.Asset, key: "Dai", amount: 11, - available: false + available: true + } + ] + } + + function createHoldingsModel2b() { + return [ + { + type: HoldingTypes.Type.Collectible, + key: "Anniversary2", + amount: 1, + available: true + }, + { + type: HoldingTypes.Type.Asset, + key: "snt", + amount: 666, + available: true } ] } @@ -293,7 +453,7 @@ QtObject { }, { type: HoldingTypes.Type.Ens, - key: "ENS", + key: "foo.bar.eth", amount: 1, available: false }, @@ -317,7 +477,7 @@ QtObject { { type: HoldingTypes.Type.Asset, key: "zrx", - amount: 1, + amount: 10, available: false }, { @@ -355,4 +515,8 @@ QtObject { function createChannelsModel2() { return [] } + + function createChannelsModel3() { + return [{ key: "_vip" } ] + } } diff --git a/storybook/src/Models/WalletAccountsModel.qml b/storybook/src/Models/WalletAccountsModel.qml index a5598f386..2449d93c4 100644 --- a/storybook/src/Models/WalletAccountsModel.qml +++ b/storybook/src/Models/WalletAccountsModel.qml @@ -1,9 +1,122 @@ import QtQuick 2.15 +import utils 1.0 + ListModel { - ListElement { name: "Test account"; emoji: "😋"; colorId: "primary"; address: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240"; walletType: "" } - ListElement { name: "Another account - generated"; emoji: "🚗"; colorId: "army"; address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8888"; walletType: "generated" } - ListElement { name: "Another account - seed"; emoji: "🎨"; colorId: "army"; address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8888"; walletType: "seed" } - ListElement { name: "Another account - watch"; emoji: "🔗"; colorId: "army"; address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8888"; walletType: "watch" } - ListElement { name: "Another account - key"; emoji: "💼"; colorId: "army"; address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8888"; walletType: "key" } + readonly property var data: [ + { + name: "helloworld", + emoji: "😋", + colorId: "primary", + address: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", + walletType: "", + position: 0, + assets: [ + { + symbol: "socks", + enabledNetworkBalance: { + displayDecimals: 2, + stripTrailingZeroes: true, + amount: 15.0, + symbol: "SOX" + } + }, + { + symbol: "snt", + enabledNetworkBalance: { + displayDecimals: 2, + stripTrailingZeroes: true, + amount: 670.2345, + symbol: "SNT" + } + }, + { + symbol: "zrx", + enabledNetworkBalance: { + displayDecimals: 4, + stripTrailingZeroes: true, + amount: 7.456000, + symbol: "ZRX" + } + } + ] + }, + { + name: "Hot wallet (generated)", + emoji: "🚗", + colorId: "army", + address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", + walletType: Constants.generatedWalletType, + position: 3, + assets: [ + { + symbol: "deadbeef", + enabledNetworkBalance: { + displayDecimals: 1, + stripTrailingZeroes: true, + amount: 1, + symbol: "DBF" + } + } + ] + }, + { + name: "Family (seed)", + emoji: "🎨", colorId: "magenta", + address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8882", + walletType: Constants.seedWalletType, + position: 1, + assets: [ + { + symbol: "Aave", + enabledNetworkBalance: { + displayDecimals: 6, + stripTrailingZeroes: true, + amount: 42, + symbol: "AAVE" + } + }, + { + symbol: "dai", + enabledNetworkBalance: { + displayDecimals: 2, + stripTrailingZeroes: true, + amount: 120.123, + symbol: "DAI" + } + } + ] + }, + { + name: "Tag Heuer (watch)", + emoji: "⌚", + colorId: "copper", + address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8883", + walletType: Constants.watchWalletType, + position: 2, + assets: [ + ] + }, + { + name: "Fab (key)", + emoji: "⌚", + colorId: Constants.walletAccountColors.camel, + address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8884", + walletType: Constants.keyWalletType, + position: 4, + assets: [ + { + symbol: "socks", + enabledNetworkBalance: { + displayDecimals: 2, + stripTrailingZeroes: false, + amount: 3.5, + symbol: "SOX" + } + } + ] + } + ] + + Component.onCompleted: append(data) } diff --git a/ui/StatusQ/CMakeLists.txt b/ui/StatusQ/CMakeLists.txt index 7229fbc02..d91d3b247 100644 --- a/ui/StatusQ/CMakeLists.txt +++ b/ui/StatusQ/CMakeLists.txt @@ -91,11 +91,13 @@ add_library(StatusQ SHARED src/plugin.cpp include/StatusQ/QClipboardProxy.h include/StatusQ/modelutilsinternal.h + include/StatusQ/permissionutilsinternal.h include/StatusQ/rxvalidator.h include/StatusQ/statussyntaxhighlighter.h include/StatusQ/statuswindow.h src/QClipboardProxy.cpp src/modelutilsinternal.cpp + src/permissionutilsinternal.cpp src/rxvalidator.cpp src/statussyntaxhighlighter.cpp src/statuswindow.cpp diff --git a/ui/StatusQ/include/StatusQ/permissionutilsinternal.h b/ui/StatusQ/include/StatusQ/permissionutilsinternal.h new file mode 100644 index 000000000..bafccb91b --- /dev/null +++ b/ui/StatusQ/include/StatusQ/permissionutilsinternal.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +class QAbstractItemModel; + +class PermissionUtilsInternal : public QObject +{ + Q_OBJECT + +public: + explicit PermissionUtilsInternal(QObject* parent = nullptr); + + //!< 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 channel keys recursively under channelsListModel->key; filtering out @permissionTypes ([PermissionTypes.Type.FOO]) + Q_INVOKABLE QStringList getUniquePermissionChannels(QAbstractItemModel *model, const QList &permissionTypes = {}) const; +}; diff --git a/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml b/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml index aa4418717..b8e1ac5a2 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusListItem.qml @@ -359,8 +359,8 @@ Rectangle { id: tagsScrollView visible: tagsRepeater.count > 0 anchors.top: statusListItemTertiaryTitle.bottom - anchors.topMargin: visible ? 8 : 0 - width: Math.min(statusListItemTagsSlotInline.width, statusListItemTagsSlotInline.availableWidth) + anchors.topMargin: visible ? 2 : 0 + width: Math.min(statusListItemTagsSlotInline.width, statusListItemTagsSlotInline.availableWidth, parent.width) height: visible ? contentHeight : 0 padding: 0 @@ -378,7 +378,7 @@ Rectangle { RowLayout { anchors.top: tagsScrollView.bottom - anchors.topMargin: visible ? 8 : 0 + anchors.topMargin: visible ? 4 : 0 width: parent.width visible: !!root.beneathTagsIcon || !!root.beneathTagsTitle spacing: 4 diff --git a/ui/StatusQ/src/StatusQ/Components/private/qmldir b/ui/StatusQ/src/StatusQ/Components/private/qmldir index bb7434c1c..c5403e813 100644 --- a/ui/StatusQ/src/StatusQ/Components/private/qmldir +++ b/ui/StatusQ/src/StatusQ/Components/private/qmldir @@ -2,4 +2,3 @@ module StatusQ.Components.private StatusImageMessage 0.1 statusMessage/StatusImageMessage.qml StatusMessageImageAlbum 0.1 statusMessage/StatusMessageImageAlbum.qml -StatusBaseDateInput 0.1 dateInput/StatusBaseDateInput.qml diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusCheckBox.qml b/ui/StatusQ/src/StatusQ/Controls/StatusCheckBox.qml index 1cd9dc88c..e9782570e 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusCheckBox.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusCheckBox.qml @@ -79,6 +79,7 @@ CheckBox { : root.indicator.width) : 0 rightPadding: !root.leftSide? (!!root.text ? root.indicator.width + root.spacing : root.indicator.width) : 0 + visible: !!text } HoverHandler { diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusRadioButton.qml b/ui/StatusQ/src/StatusQ/Controls/StatusRadioButton.qml index 4f9cf81db..adc99180f 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusRadioButton.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusRadioButton.qml @@ -21,6 +21,7 @@ RadioButton { Large } + opacity: enabled ? 1.0 : 0.3 font.family: Theme.palette.baseFont.name indicator: Rectangle { @@ -47,5 +48,6 @@ RadioButton { verticalAlignment: Text.AlignVCenter leftPadding: root.indicator && !root.mirrored ? root.indicator.width + root.spacing : 0 rightPadding: root.indicator && root.mirrored ? root.indicator.width + root.spacing : 0 + visible: !!text } } diff --git a/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml b/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml index 0762f0303..3bb900431 100644 --- a/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml +++ b/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml @@ -65,6 +65,7 @@ QtObject { return num.toString().split('.')[1].length } + function stripTrailingZeroes(numStr, locale) { let regEx = locale.decimalPoint == "." ? /(\.[0-9]*[1-9])0+$|\.0*$/ : /(\,[0-9]*[1-9])0+$|\,0*$/ return numStr.replace(regEx, '$1') @@ -101,8 +102,6 @@ QtObject { } function currencyAmountToLocaleString(currencyAmount, options = null, locale = null) { - locale = locale || Qt.locale() - if (!currencyAmount) { return qsTr("N/A") } @@ -114,6 +113,8 @@ QtObject { if (typeof currencyAmount.amount === "undefined") return qsTr("N/A") + locale = locale || Qt.locale() + // Parse options var optNoSymbol = false var optRawAmount = false @@ -141,7 +142,7 @@ QtObject { if (currencyAmount.amount > 0 && currencyAmount.amount < minAmount && !optRawAmount) { // Handle amounts smaller than resolution - amountStr = "<%1".arg(numberToLocaleString(minAmount, displayDecimals, locale)) + amountStr = "<%1".arg(numberToLocaleString(minAmount, optDisplayDecimals, locale)) } else { var amount var displayDecimals @@ -158,11 +159,10 @@ QtObject { // For normal numbers, we show the whole integral part and as many decimal places not // not to exceed the maximum amount = currencyAmount.amount - // For numbers over 1M , dont show decimal places + // For numbers over 1M , dont show decimal places if(numIntegerDigits > maxDigitsToShowDecimal) { displayDecimals = 0 - } - else { + } else { displayDecimals = Math.min(optDisplayDecimals, Math.max(0, maxDigits - numIntegerDigits)) } } diff --git a/ui/StatusQ/src/StatusQ/Core/StatusIcon.qml b/ui/StatusQ/src/StatusQ/Core/StatusIcon.qml index ef1430b15..e5808d55d 100644 --- a/ui/StatusQ/src/StatusQ/Core/StatusIcon.qml +++ b/ui/StatusQ/src/StatusQ/Core/StatusIcon.qml @@ -16,9 +16,8 @@ Image { if(icon.startsWith("data:image/") || icon.startsWith("https://") || icon.startsWith("qrc:/") || icon.startsWith("file:/")) { //raw image data source = icon - objectName = "custom-icon" - } - else if (icon !== "") { + objectName = "custom-icon" + } else if (icon !== "") { source = "../../assets/img/icons/" + icon+ ".svg"; objectName = icon + "-icon" } diff --git a/ui/StatusQ/src/StatusQ/Core/Utils/ModelUtils.qml b/ui/StatusQ/src/StatusQ/Core/Utils/ModelUtils.qml index 5de87c2a5..0e852331e 100644 --- a/ui/StatusQ/src/StatusQ/Core/Utils/ModelUtils.qml +++ b/ui/StatusQ/src/StatusQ/Core/Utils/ModelUtils.qml @@ -45,6 +45,10 @@ QtObject { return array } + function modelToFlatArray(model, role) { + return modelToArray(model, [role]).map(entry => entry[role]) + } + function indexOf(model, role, key) { const count = model.rowCount() diff --git a/ui/StatusQ/src/StatusQ/Core/Utils/OperatorsUtils.qml b/ui/StatusQ/src/StatusQ/Core/Utils/OperatorsUtils.qml index 8439b8f02..b042369e5 100644 --- a/ui/StatusQ/src/StatusQ/Core/Utils/OperatorsUtils.qml +++ b/ui/StatusQ/src/StatusQ/Core/Utils/OperatorsUtils.qml @@ -32,7 +32,6 @@ QtObject { case OperatorsUtils.Operators.None: default: return "" - } } } diff --git a/ui/StatusQ/src/StatusQ/Popups/Dialog/StatusDialog.qml b/ui/StatusQ/src/StatusQ/Popups/Dialog/StatusDialog.qml index 46ebd91b5..ead110783 100644 --- a/ui/StatusQ/src/StatusQ/Popups/Dialog/StatusDialog.qml +++ b/ui/StatusQ/src/StatusQ/Popups/Dialog/StatusDialog.qml @@ -1,7 +1,8 @@ -import QtQuick 2.14 -import QtQuick.Controls 2.14 -import QtQuick.Layouts 1.14 -import QtQml.Models 2.14 +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQml.Models 2.15 +import QtQml 2.15 import StatusQ.Core 0.1 import StatusQ.Controls 0.1 @@ -28,6 +29,14 @@ Dialog { margins: 64 modal: true + // workaround for https://bugreports.qt.io/browse/QTBUG-87804 + Binding on margins { + id: workaroundBinding + + when: false + restoreMode: Binding.RestoreBindingOrValue + } + standardButtons: Dialog.Cancel | Dialog.Ok Overlay.modal: Rectangle { diff --git a/ui/StatusQ/src/StatusQ/Popups/StatusStackModal.qml b/ui/StatusQ/src/StatusQ/Popups/StatusStackModal.qml index bf6b4e530..54e86da56 100644 --- a/ui/StatusQ/src/StatusQ/Popups/StatusStackModal.qml +++ b/ui/StatusQ/src/StatusQ/Popups/StatusStackModal.qml @@ -23,7 +23,7 @@ StatusModal { visible: replaceItem || stackLayout.currentIndex > 0 onClicked: { if (replaceItem) { - replaceItem = null; + replaceItem = undefined; // unload the replaceItem } else { let prevAction = stackLayout.currentItem.prevAction stackLayout.currentIndex--; diff --git a/ui/StatusQ/src/modelutilsinternal.cpp b/ui/StatusQ/src/modelutilsinternal.cpp index 99d776619..2e3beddde 100644 --- a/ui/StatusQ/src/modelutilsinternal.cpp +++ b/ui/StatusQ/src/modelutilsinternal.cpp @@ -1,6 +1,7 @@ #include "StatusQ/modelutilsinternal.h" #include +#include ModelUtilsInternal::ModelUtilsInternal(QObject* parent) : QObject(parent) @@ -16,7 +17,6 @@ QStringList ModelUtilsInternal::roleNames(QAbstractItemModel *model) const return {roles.cbegin(), roles.cend()}; } - int ModelUtilsInternal::roleByName(QAbstractItemModel* model, const QString &roleName) const { diff --git a/ui/StatusQ/src/permissionutilsinternal.cpp b/ui/StatusQ/src/permissionutilsinternal.cpp new file mode 100644 index 000000000..39bce33d6 --- /dev/null +++ b/ui/StatusQ/src/permissionutilsinternal.cpp @@ -0,0 +1,109 @@ +#include "StatusQ/permissionutilsinternal.h" + +#include +#include + +#include + +namespace { +int roleByName(QAbstractItemModel* model, const QString &roleName) +{ + if (!model) + return -1; + + return model->roleNames().key(roleName.toUtf8(), -1); +} +} + +PermissionUtilsInternal::PermissionUtilsInternal(QObject* parent) + : QObject(parent) +{ +} + +QStringList PermissionUtilsInternal::getUniquePermissionTokenKeys(QAbstractItemModel* model) const +{ + if (!model) + return {}; + + const auto role = roleByName(model, QStringLiteral("holdingsListModel")); + if (role == -1) { + qWarning() << Q_FUNC_INFO << "Requested roleName 'holdingsListModel' not found!"; + return {}; + } + + std::set result; // unique, sorted by default + + const auto permissionsCount = model->rowCount(); + for (int i = 0; i < permissionsCount; i++) { + const auto isPrivate = model->data(model->index(i, 0), roleByName(model, QStringLiteral("isPrivate"))).toBool(); + if (isPrivate) + continue; + + const auto holdings = model->data(model->index(i, 0), role); + if (holdings.isValid() && !holdings.isNull()) { + const auto holdingItems = holdings.value(); + if (!holdingItems) { + qWarning() << Q_FUNC_INFO << "Unable to cast 'holdingsListModel' to QAbstractItemModel *!"; + continue; + } + const auto holdingItemsCount = holdingItems->rowCount(); + for (int j = 0; j < holdingItemsCount; j++) { + const auto keyRole = roleByName(holdingItems, QStringLiteral("key")); + if (keyRole == -1) { + qWarning() << Q_FUNC_INFO << "Requested roleName 'key' not found!"; + continue; + } + result.insert(holdingItems->data(holdingItems->index(j, 0), keyRole).toString().toUpper()); + } + } + } + + return {result.cbegin(), result.cend()}; +} + +// TODO return a QVariantMap (https://github.com/status-im/status-desktop/issues/11481) with key->channelName +QStringList PermissionUtilsInternal::getUniquePermissionChannels(QAbstractItemModel* model, const QList &permissionTypes) const +{ + if (!model) + return {}; + + const auto role = roleByName(model, QStringLiteral("channelsListModel")); + if (role == -1) { + qWarning() << Q_FUNC_INFO << "Requested roleName 'channelsListModel' not found!"; + return {}; + } + + const auto permissionTypeRole = roleByName(model, QStringLiteral("permissionType")); + + std::set result; // unique, sorted by default + + const auto permissionsCount = model->rowCount(); + for (int i = 0; i < permissionsCount; i++) { + if (!permissionTypes.isEmpty()) { + const auto permissionType = model->data(model->index(i, 0), permissionTypeRole).toInt(); + if (!permissionTypes.contains(permissionType)) + continue; + } + + const auto channels = model->data(model->index(i, 0), role); + if (channels.isValid() && !channels.isNull()) { + const auto channelItems = channels.value(); + if (!channelItems) { + qWarning() << Q_FUNC_INFO << "Unable to cast 'channelsListModel' to QAbstractItemModel *!"; + continue; + } + + const auto channelItemsCount = channelItems->rowCount(); + for (int j = 0; j < channelItemsCount; j++) { + const auto keyRole = roleByName(channelItems, QStringLiteral("key")); + if (keyRole == -1) { + qWarning() << Q_FUNC_INFO << "Requested roleName 'key' not found!"; + continue; + } + result.insert(channelItems->data(channelItems->index(j, 0), keyRole).toString()); + } + } + } + + return {result.cbegin(), result.cend()}; +} diff --git a/ui/StatusQ/src/plugin.cpp b/ui/StatusQ/src/plugin.cpp index 4e8c0e83f..220464029 100644 --- a/ui/StatusQ/src/plugin.cpp +++ b/ui/StatusQ/src/plugin.cpp @@ -5,6 +5,7 @@ #include "StatusQ/QClipboardProxy.h" #include "StatusQ/modelutilsinternal.h" +#include "StatusQ/permissionutilsinternal.h" #include "StatusQ/rxvalidator.h" #include "StatusQ/statussyntaxhighlighter.h" #include "StatusQ/statuswindow.h" @@ -26,6 +27,10 @@ public: qmlRegisterSingletonType( "StatusQ.Internal", 0, 1, "ModelUtils", &ModelUtilsInternal::qmlInstance); + qmlRegisterSingletonType("StatusQ.Internal", 0, 1, "PermissionUtils", [](QQmlEngine *, QJSEngine *) { + return new PermissionUtilsInternal; + }); + QZXing::registerQMLTypes(); qqsfpm::registerTypes(); } diff --git a/ui/app/AppLayouts/Chat/ChatLayout.qml b/ui/app/AppLayouts/Chat/ChatLayout.qml index 082265ad8..e740fbbe4 100644 --- a/ui/app/AppLayouts/Chat/ChatLayout.qml +++ b/ui/app/AppLayouts/Chat/ChatLayout.qml @@ -11,6 +11,7 @@ import "stores" import AppLayouts.Communities.popups 1.0 import AppLayouts.Chat.stores 1.0 +import AppLayouts.Wallet.stores 1.0 as WalletStore StackLayout { id: root @@ -176,8 +177,14 @@ StackLayout { property string communityId + loginType: root.rootStore.loginType + walletAccountsModel: WalletStore.RootStore.receiveAccounts + permissionsModel: root.permissionsStore.permissionsModel + assetsModel: root.rootStore.assetsModel + collectiblesModel: root.rootStore.collectiblesModel + onJoined: { - root.rootStore.requestToJoinCommunityWithAuthentication(root.rootStore.userProfileInst.name) + root.rootStore.requestToJoinCommunityWithAuthentication(root.rootStore.userProfileInst.name, sharedAddresses, airdropAddress) } onCancelMembershipRequest: { diff --git a/ui/app/AppLayouts/Chat/stores/RootStore.qml b/ui/app/AppLayouts/Chat/stores/RootStore.qml index b279b5a14..35fc3de75 100644 --- a/ui/app/AppLayouts/Chat/stores/RootStore.qml +++ b/ui/app/AppLayouts/Chat/stores/RootStore.qml @@ -378,8 +378,8 @@ QtObject { return communitiesModuleInst.spectateCommunity(id, ensName) } - function requestToJoinCommunityWithAuthentication(ensName) { - chatCommunitySectionModule.requestToJoinCommunityWithAuthentication(ensName) + function requestToJoinCommunityWithAuthentication(ensName, addressesToShare = [], airdropAddress = "") { + chatCommunitySectionModule.requestToJoinCommunityWithAuthenticationWithSharedAddresses(ensName, JSON.stringify(addressesToShare), airdropAddress) } function userCanJoin(id) { @@ -475,7 +475,7 @@ QtObject { const userCanJoin = userCanJoin(result.communityId) // TODO find what to do when you can't join if (userCanJoin) { - requestToJoinCommunityWithAuthentication(userProfileInst.preferredName) + requestToJoinCommunityWithAuthentication(userProfileInst.preferredName) // FIXME what addresses to share? } } return result @@ -608,9 +608,9 @@ QtObject { if(userProfileInst.usingBiometricLogin) return Constants.LoginType.Biometrics - else if(userProfileInst.isKeycardUser) + if(userProfileInst.isKeycardUser) return Constants.LoginType.Keycard - else return Constants.LoginType.Password + return Constants.LoginType.Password } readonly property Connections communitiesModuleConnections: Connections { diff --git a/ui/app/AppLayouts/Chat/views/ChatView.qml b/ui/app/AppLayouts/Chat/views/ChatView.qml index 400bad887..6fafa53ce 100644 --- a/ui/app/AppLayouts/Chat/views/ChatView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatView.qml @@ -26,7 +26,7 @@ StatusSectionLayout { id: root property var contactsStore - property bool hasAddedContacts: root.contactsStore.myContactsModel.count > 0 + property bool hasAddedContacts: contactsStore.myContactsModel.count > 0 property RootStore rootStore property var createChatPropertiesStore @@ -36,7 +36,7 @@ StatusSectionLayout { property var stickersPopup property bool stickersLoaded: false - readonly property var chatContentModule: root.rootStore.currentChatContentModule() || null + readonly property var chatContentModule: rootStore.currentChatContentModule() || null readonly property bool viewOnlyPermissionsSatisfied: chatContentModule.viewOnlyPermissionsSatisfied readonly property bool viewAndPostPermissionsSatisfied: chatContentModule.viewAndPostPermissionsSatisfied property bool hasViewOnlyPermissions: false diff --git a/ui/app/AppLayouts/Communities/controls/PermissionTypes.qml b/ui/app/AppLayouts/Communities/controls/PermissionTypes.qml index d7a051c91..056553481 100644 --- a/ui/app/AppLayouts/Communities/controls/PermissionTypes.qml +++ b/ui/app/AppLayouts/Communities/controls/PermissionTypes.qml @@ -6,7 +6,7 @@ import StatusQ.Core.Theme 0.1 QtObject { enum Type { - None, Admin, Member, Read, ViewAndPost + None, Admin, Member, Read, ViewAndPost, Moderator } function getName(type) { @@ -15,12 +15,12 @@ QtObject { return qsTr("Become admin") case PermissionTypes.Type.Member: return qsTr("Become member") - case PermissionTypes.Type.Moderator: - return qsTr("Moderate") - case PermissionTypes.Type.ViewAndPost: - return qsTr("View and post") case PermissionTypes.Type.Read: return qsTr("View only") + case PermissionTypes.Type.ViewAndPost: + return qsTr("View and post") + case PermissionTypes.Type.Moderator: + return qsTr("Moderate") } return "" @@ -32,12 +32,12 @@ QtObject { return "admin" case PermissionTypes.Type.Member: return "in-contacts" - case PermissionTypes.Type.Moderator: - return "arbitrator" case PermissionTypes.Type.ViewAndPost: return "edit" case PermissionTypes.Type.Read: return "show" + case PermissionTypes.Type.Moderator: + return "arbitrator" } return "" diff --git a/ui/app/AppLayouts/Communities/helpers/PermissionsHelpers.qml b/ui/app/AppLayouts/Communities/helpers/PermissionsHelpers.qml index e6b11d437..591553b72 100644 --- a/ui/app/AppLayouts/Communities/helpers/PermissionsHelpers.qml +++ b/ui/app/AppLayouts/Communities/helpers/PermissionsHelpers.qml @@ -4,6 +4,7 @@ import QtQml 2.14 import StatusQ.Core 0.1 import StatusQ.Core.Utils 0.1 +import StatusQ.Internal 0.1 as Internal import AppLayouts.Communities.controls 1.0 @@ -63,6 +64,15 @@ QtObject { return "" } + function getUniquePermissionTokenKeys(model) { + return Internal.PermissionUtils.getUniquePermissionTokenKeys(model) + } + + function getUniquePermissionChannels(model, permissionsTypesArray = []) { + // TODO return a QVariantMap (https://github.com/status-im/status-desktop/issues/11481) + return Internal.PermissionUtils.getUniquePermissionChannels(model, permissionsTypesArray) + } + function setHoldingsTextFormat(type, name, amount) { switch (type) { case HoldingTypes.Type.Asset: diff --git a/ui/app/AppLayouts/Communities/panels/HoldingsListPanel.qml b/ui/app/AppLayouts/Communities/panels/HoldingsListPanel.qml index 72336b91e..170766184 100644 --- a/ui/app/AppLayouts/Communities/panels/HoldingsListPanel.qml +++ b/ui/app/AppLayouts/Communities/panels/HoldingsListPanel.qml @@ -27,10 +27,6 @@ Control { // By design values: readonly property int defaultHoldingsSpacing: 8 - - function holdingsTextFormat(name, amount) { - return PermissionsHelpers.setHoldingsTextFormat(HoldingTypes.Type.Asset, name, amount) - } } contentItem: ColumnLayout { @@ -83,7 +79,7 @@ Control { asset.color: asset.isImage ? "transparent" : titleText.color closeButtonVisible: false titleText.color: model.available ? Theme.palette.primaryColor1 : Theme.palette.dangerColor1 - bgColor: model.available ? Theme.palette.primaryColor2 :Theme.palette.dangerColor2 + bgColor: model.available ? Theme.palette.primaryColor2 : Theme.palette.dangerColor2 titleText.font.pixelSize: 15 } } diff --git a/ui/app/AppLayouts/Communities/panels/JoinPermissionsOverlayPanel.qml b/ui/app/AppLayouts/Communities/panels/JoinPermissionsOverlayPanel.qml index 38679e9b3..a3032a14b 100644 --- a/ui/app/AppLayouts/Communities/panels/JoinPermissionsOverlayPanel.qml +++ b/ui/app/AppLayouts/Communities/panels/JoinPermissionsOverlayPanel.qml @@ -92,15 +92,23 @@ Control { ] } + readonly property var moderatePermissionsModel: SortFilterProxyModel { + sourceModel: root.moderateHoldingsModel + filters: [ + ExpressionFilter { + expression: d.filterPermissions(model) + } + ] + } } padding: 35 // default by design spacing: 32 // default by design + contentItem: ColumnLayout { id: column spacing: root.spacing - component CustomHoldingsListPanel: HoldingsListPanel { Layout.fillWidth: true @@ -123,25 +131,23 @@ Control { CustomHoldingsListPanel { visible: !root.joinCommunity && d.viewOnlyPermissionsModel.count > 0 introText: root.requiresRequest ? - qsTr("To view the #%1 channel you need to join %2 and prove that you hold").arg(root.channelName).arg(root.communityName) : - qsTr("To view the #%1 channel you need to hold").arg(root.channelName) + qsTr("To view the #%1 channel you need to join %2 and prove that you hold").arg(root.channelName).arg(root.communityName) : + qsTr("To view the #%1 channel you need to hold").arg(root.channelName) model: d.viewOnlyPermissionsModel } CustomHoldingsListPanel { visible: !root.joinCommunity && d.viewAndPostPermissionsModel.count > 0 introText: root.requiresRequest ? - qsTr("To view and post in the #%1 channel you need to join %2 and prove that you hold").arg(root.channelName).arg(root.communityName) : - qsTr("To view and post in the #%1 channel you need to hold").arg(root.channelName) + qsTr("To view and post in the #%1 channel you need to join %2 and prove that you hold").arg(root.channelName).arg(root.communityName) : + qsTr("To view and post in the #%1 channel you need to hold").arg(root.channelName) model: d.viewAndPostPermissionsModel } - HoldingsListPanel { - Layout.fillWidth: true - spacing: root.spacing - visible: !root.joinCommunity && !!d.moderateHoldings - introText: qsTr("To moderate in the %1 channel you need to hold").arg(root.channelName) - model: d.moderateHoldingsModel + CustomHoldingsListPanel { + visible: !root.joinCommunity && d.moderatePermissionsModel.count > 0 + introText: qsTr("To moderate in the #%1 channel you need to hold").arg(root.channelName) + model: d.moderatePermissionsModel } StatusButton { @@ -150,7 +156,7 @@ Control { text: root.isInvitationPending ? d.getInvitationPendingText() : d.getRevealAddressText() icon.name: root.isInvitationPending ? "" : Constants.authenticationIconByType[root.loginType] font.pixelSize: 13 - enabled: root.requirementsMet || d.communityPermissionsModel.count == 0 + enabled: root.requirementsMet || d.communityPermissionsModel.count === 0 onClicked: root.isInvitationPending ? root.invitationPendingClicked() : root.revealAddressClicked() } @@ -163,4 +169,3 @@ Control { } } } - diff --git a/ui/app/AppLayouts/Communities/panels/SharedAddressesAccountSelector.qml b/ui/app/AppLayouts/Communities/panels/SharedAddressesAccountSelector.qml new file mode 100644 index 000000000..b142e942d --- /dev/null +++ b/ui/app/AppLayouts/Communities/panels/SharedAddressesAccountSelector.qml @@ -0,0 +1,141 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 + +import SortFilterProxyModel 0.2 + +import utils 1.0 + +StatusListView { + id: root + + property bool hasPermissions + property var uniquePermissionTokenKeys + + // read/write properties + property string selectedAirdropAddress: selectedSharedAddresses.length ? selectedSharedAddresses[0] : "" + property var selectedSharedAddresses: count ? ModelUtils.modelToFlatArray(model, "address") : [] + + leftMargin: d.absLeftMargin + topMargin: Style.current.padding + rightMargin: Style.current.padding + bottomMargin: Style.current.padding + + QtObject { + id: d + + // UI + readonly property int absLeftMargin: 12 + + readonly property ButtonGroup airdropGroup: ButtonGroup { + exclusive: true + } + + readonly property ButtonGroup addressesGroup: ButtonGroup { + exclusive: false + } + + function selectFirstAvailableAirdropAddress() { + root.selectedAirdropAddress = ModelUtils.modelToFlatArray(root.model, "address").find(address => selectedSharedAddresses.includes(address)) + } + } + + spacing: Style.current.halfPadding + delegate: StatusListItem { + width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin + statusListItemTitle.font.weight: Font.Medium + title: model.name + tertiaryTitle: !walletAccountAssetsModel.count && root.hasPermissions ? qsTr("No relevant tokens") : "" + + tagsModel: SortFilterProxyModel { + id: walletAccountAssetsModel + sourceModel: model.assets + + function filterPredicate(modelData) { + return root.uniquePermissionTokenKeys.includes(modelData.symbol.toUpperCase()) + } + + filters: ExpressionFilter { + expression: walletAccountAssetsModel.filterPredicate(model) + } + sorters: ExpressionSorter { + expression: { + return modelLeft.enabledNetworkBalance.amount > modelRight.enabledNetworkBalance.amount // descending, biggest first + } + } + } + statusListItemInlineTagsSlot.spacing: Style.current.padding + tagsDelegate: Row { + spacing: 4 + StatusRoundedImage { + anchors.verticalCenter: parent.verticalCenter + width: 16 + height: 16 + image.source: Constants.tokenIcon(model.symbol.toUpperCase()) + } + StatusBaseText { + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: Theme.tertiaryTextFontSize + text: LocaleUtils.currencyAmountToLocaleString(enabledNetworkBalance) + } + } + + asset.color: !!model.colorId ? Utils.getColorForId(model.colorId): "" + asset.emoji: model.emoji + asset.name: !model.emoji ? "filled-account": "" + asset.letterSize: 14 + asset.isLetterIdenticon: !!model.emoji + asset.isImage: asset.isLetterIdenticon + + components: [ + StatusFlatButton { + ButtonGroup.group: d.airdropGroup + anchors.verticalCenter: parent.verticalCenter + icon.name: "airdrop" + icon.color: hovered ? Theme.palette.primaryColor3 : + checked ? Theme.palette.primaryColor1 : disabledTextColor + checkable: true + checked: model.address === root.selectedAirdropAddress + enabled: shareAddressCheckbox.checked && root.selectedSharedAddresses.length > 1 // last cannot be unchecked + visible: shareAddressCheckbox.checked + opacity: enabled ? 1.0 : 0.3 + onCheckedChanged: if (checked) root.selectedAirdropAddress = model.address + + StatusToolTip { + text: qsTr("Use this address for any Community airdrops") + visible: parent.hovered + delay: 500 + } + }, + StatusCheckBox { + id: shareAddressCheckbox + ButtonGroup.group: d.addressesGroup + anchors.verticalCenter: parent.verticalCenter + checkable: true + checked: root.selectedSharedAddresses.includes(model.address) + enabled: !(root.selectedSharedAddresses.length === 1 && checked) // last cannot be unchecked + onToggled: { + // handle selected addresses + const index = root.selectedSharedAddresses.indexOf(model.address) + const selectedSharedAddressesCopy = Object.assign([], root.selectedSharedAddresses) // deep copy + if (index === -1) { + selectedSharedAddressesCopy.push(model.address) + } else { + selectedSharedAddressesCopy.splice(index, 1) + } + root.selectedSharedAddresses = selectedSharedAddressesCopy + + // switch to next available airdrop address when unchecking + if (!checked && model.address === root.selectedAirdropAddress) { + d.selectFirstAvailableAirdropAddress() + } + } + } + ] + } +} diff --git a/ui/app/AppLayouts/Communities/panels/SharedAddressesPanel.qml b/ui/app/AppLayouts/Communities/panels/SharedAddressesPanel.qml new file mode 100644 index 000000000..ad8b27c23 --- /dev/null +++ b/ui/app/AppLayouts/Communities/panels/SharedAddressesPanel.qml @@ -0,0 +1,160 @@ +import QtQuick 2.15 +import QtQml.Models 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtGraphicalEffects 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 + +import SortFilterProxyModel 0.2 + +import AppLayouts.Profile.controls 1.0 +import AppLayouts.Communities.controls 1.0 +import AppLayouts.Communities.views 1.0 +import AppLayouts.Communities.helpers 1.0 + +import utils 1.0 + +Control { + id: root + + property bool isEditMode + + required property string communityName + required property string communityIcon + property int loginType: Constants.LoginType.Password + + required property var walletAccountsModel // name, address, emoji, colorId, assets + required property var permissionsModel // id, key, permissionType, holdingsListModel, channelsListModel, isPrivate, tokenCriteriaMet + required property var assetsModel + required property var collectiblesModel + + readonly property string title: isEditMode ? qsTr("Edit which addresses you share with %1").arg(communityName) + : qsTr("Select addresses to share with %1").arg(communityName) + + readonly property var buttons: ObjectModel { + StatusFlatButton { + visible: root.isEditMode + borderColor: Theme.palette.baseColor2 + text: qsTr("Cancel") + onClicked: root.close() + } + StatusButton { + enabled: root.selectedSharedAddresses.length && root.selectedAirdropAddress + visible: root.isEditMode + icon.name: Constants.authenticationIconByType[root.loginType] + text: qsTr("Save changes") + onClicked: { + // TODO connect to backend + 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) + root.close() + } + } + // NB no more buttons after this, see property `rightButtons` below + } + + 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 + + signal close() + signal shareSelectedAddressesClicked(string airdropAddress, var sharedAddresses) + + spacing: Style.current.padding + + QtObject { + id: d + + // internal logic + readonly property bool hasPermissions: root.permissionsModel && root.permissionsModel.count + } + + padding: 0 + + contentItem: ColumnLayout { + spacing: 0 + // addresses + SharedAddressesAccountSelector { + id: accountSelector + hasPermissions: d.hasPermissions + uniquePermissionTokenKeys: PermissionsHelpers.getUniquePermissionTokenKeys(root.permissionsModel) + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + topMargin + bottomMargin + Layout.maximumHeight: hasPermissions ? permissionsView.implicitHeight > root.availableHeight / 2 ? root.availableHeight / 2 : root.availableHeight : -1 + Layout.fillHeight: !hasPermissions + model: SortFilterProxyModel { + 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" + } + ] + } + } + + // divider with top rounded corners + drop shadow + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.current.padding * 2 + color: Theme.palette.baseColor2 + radius: Style.current.padding + border.width: 1 + border.color: Theme.palette.baseColor3 + visible: d.hasPermissions + + layer.enabled: true + layer.effect: DropShadow { + horizontalOffset: 0 + verticalOffset: -9 + radius: 14 + samples: 29 + color: Qt.rgba(0, 0, 0, 0.04) + } + } + + // permissions + SharedAddressesPermissionsPanel { + id: permissionsView + permissionsModel: root.permissionsModel + assetsModel: root.assetsModel + collectiblesModel: root.collectiblesModel + communityName: root.communityName + communityIcon: root.communityIcon + + Layout.fillHeight: true + Layout.fillWidth: true + Layout.topMargin: -Style.current.padding // compensate for the half-rounded divider above + visible: d.hasPermissions + } + } +} diff --git a/ui/app/AppLayouts/Communities/panels/SharedAddressesPermissionsPanel.qml b/ui/app/AppLayouts/Communities/panels/SharedAddressesPermissionsPanel.qml new file mode 100644 index 000000000..4639edbff --- /dev/null +++ b/ui/app/AppLayouts/Communities/panels/SharedAddressesPermissionsPanel.qml @@ -0,0 +1,437 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Components 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 + +import SortFilterProxyModel 0.2 + +import AppLayouts.Communities.controls 1.0 +import AppLayouts.Communities.views 1.0 +import AppLayouts.Communities.helpers 1.0 + +import utils 1.0 + +Rectangle { + id: root + + property var permissionsModel + property var assetsModel + property var collectiblesModel + property string communityName + property string communityIcon + + implicitHeight: permissionsScrollView.contentHeight - permissionsScrollView.anchors.topMargin + color: Theme.palette.baseColor2 + + QtObject { + id: d + + // UI + readonly property int absLeftMargin: 12 + readonly property color tableBorderColor: Theme.palette.directColor7 + + // internal logic + readonly property var uniquePermissionChannels: + root.permissionsModel && root.permissionsModel.count ? + PermissionsHelpers.getUniquePermissionChannels(root.permissionsModel, [PermissionTypes.Type.Read, PermissionTypes.Type.ViewAndPost]) + : [] + + // models + readonly property var adminPermissionsModel: SortFilterProxyModel { + id: adminPermissionsModel + sourceModel: root.permissionsModel + function filterPredicate(modelData) { + return (modelData.permissionType === Constants.permissionType.admin) && + (modelData.tokenCriteriaMet && !modelData.isPrivate) // admin privs are hidden if criteria not met + } + filters: ExpressionFilter { + expression: adminPermissionsModel.filterPredicate(model) + } + } + readonly property var joinPermissionsModel: SortFilterProxyModel { + id: joinPermissionsModel + sourceModel: root.permissionsModel + function filterPredicate(modelData) { + return (modelData.permissionType === Constants.permissionType.member) && + (modelData.tokenCriteriaMet || !modelData.isPrivate) + } + filters: ExpressionFilter { + expression: joinPermissionsModel.filterPredicate(model) + } + } + } + + StatusScrollView { + id: permissionsScrollView + anchors.fill: parent + anchors.topMargin: -Style.current.padding + contentWidth: availableWidth + + ColumnLayout { + width: parent.width + spacing: Style.current.halfPadding + + // header + RowLayout { + Layout.fillWidth: true + Layout.bottomMargin: 4 + spacing: Style.current.padding + StatusRoundedImage { + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + Layout.leftMargin: d.absLeftMargin + image.source: root.communityIcon + } + StatusBaseText { + font.weight: Font.Medium + text: qsTr("Permissions") + } + } + + // permission types + PermissionPanel { + permissionType: PermissionTypes.Type.Member + permissionsModel: d.joinPermissionsModel + } + PermissionPanel { + permissionType: PermissionTypes.Type.Admin + permissionsModel: d.adminPermissionsModel + } + + Repeater { // channel repeater + model: d.uniquePermissionChannels // TODO get channelName in addition (https://github.com/status-im/status-desktop/issues/11481) + delegate: ChannelPermissionPanel {} + } + } + } + + component PanelBg: Rectangle { + color: Theme.palette.statusListItem.backgroundColor + border.width: 1 + border.color: Theme.palette.baseColor2 + radius: Style.current.radius + } + + component PanelIcon: StatusRoundIcon { + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + Layout.alignment: Qt.AlignTop + asset.name: { + switch (permissionType) { + case PermissionTypes.Type.Admin: + return "admin" + case PermissionTypes.Type.Member: + return "communities" + default: + return "channel" + } + } + radius: height/2 + } + + component PanelHeading: StatusBaseText { + Layout.fillWidth: true + elide: Text.ElideRight + font.weight: Font.Medium + text: { + switch (permissionType) { + case PermissionTypes.Type.Admin: + return qsTr("Become an admin") + case PermissionTypes.Type.Member: + return qsTr("Join %1").arg(root.communityName) + default: + return modelData // TODO display channel name https://github.com/status-im/status-desktop/issues/11481 + } + } + } + + component SinglePermissionFlow: Flow { + width: parent.width + spacing: Style.current.halfPadding + Repeater { + model: HoldingsSelectionModel { + sourceModel: model.holdingsListModel + assetsModel: root.assetsModel + collectiblesModel: root.collectiblesModel + } + delegate: Row { + spacing: 4 + StatusRoundedImage { + anchors.verticalCenter: parent.verticalCenter + width: 16 + height: 16 + image.source: model.imageSource + } + StatusBaseText { + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: Theme.tertiaryTextFontSize + text: model.text + color: model.available ? Theme.palette.successColor1 : Theme.palette.directColor1 + } + } + } + } + + component PermissionPanel: Control { + id: permissionPanel + property int permissionType: PermissionTypes.Type.None + property var permissionsModel + + readonly property bool tokenCriteriaMet: overallPermissionRow.tokenCriteriaMet + + visible: permissionsModel.count + Layout.fillWidth: true + padding: d.absLeftMargin + background: PanelBg {} + contentItem: RowLayout { + spacing: Style.current.padding + PanelIcon {} + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + PanelHeading {} + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: grid.implicitHeight + grid.anchors.margins*2 + border.width: 1 + border.color: d.tableBorderColor + radius: Style.current.radius + color: "transparent" + + GridLayout { + id: grid + anchors.fill: parent + anchors.margins: Style.current.halfPadding + rowSpacing: Style.current.halfPadding + columnSpacing: Style.current.halfPadding + columns: 2 + + Repeater { + id: permissionsRepeater + + property int revision + + model: permissionPanel.permissionsModel + delegate: Column { + Layout.column: 0 + Layout.row: index + Layout.fillWidth: true + spacing: Style.current.halfPadding + + readonly property bool tokenCriteriaMet: model.tokenCriteriaMet ?? false + onTokenCriteriaMetChanged: permissionsRepeater.revision++ + + SinglePermissionFlow {} + + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + grid.anchors.margins*2 + height: 1 + color: d.tableBorderColor + visible: index < permissionsRepeater.count - 1 + } + } + } + RowLayout { + id: overallPermissionRow + Layout.column: 1 + Layout.rowSpan: permissionsRepeater.count || 1 + Layout.preferredWidth: 110 + Layout.fillHeight: true + + readonly property bool tokenCriteriaMet: { + permissionsRepeater.revision // NB no let/const here b/c of https://bugreports.qt.io/browse/QTBUG-91917 + for (var i = 0; i < permissionsRepeater.count; i++) { + var permissionItem = permissionsRepeater.itemAt(i); + if (permissionItem.tokenCriteriaMet) + return true + } + return false + } + + Rectangle { + Layout.preferredWidth: 1 + Layout.fillHeight: true + Layout.topMargin: -Style.current.halfPadding + Layout.bottomMargin: -Style.current.halfPadding + color: d.tableBorderColor + } + Row { + Layout.alignment: Qt.AlignCenter + StatusIcon { + anchors.verticalCenter: parent.verticalCenter + width: 16 + height: 16 + icon: overallPermissionRow.tokenCriteriaMet ? "tiny/checkmark" : "tiny/secure" + color: overallPermissionRow.tokenCriteriaMet ? Theme.palette.successColor1 : Theme.palette.baseColor1 + } + StatusBaseText { + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: Theme.tertiaryTextFontSize + text: { + switch (permissionPanel.permissionType) { + case PermissionTypes.Type.Admin: + return qsTr("Admin") + case PermissionTypes.Type.Member: + return qsTr("Join") + case PermissionTypes.Type.Read: + return qsTr("View only") + case PermissionTypes.Type.ViewAndPost: + return qsTr("View & post") + default: + return "???" + } + } + + color: overallPermissionRow.tokenCriteriaMet ? Theme.palette.directColor1 : Theme.palette.baseColor1 + } + } + } + } + } + } + } + } + + component ChannelPermissionPanel: Control { + id: channelPermsPanel + Layout.fillWidth: true + spacing: 10 + padding: d.absLeftMargin + background: PanelBg {} + + readonly property string channelKey: modelData + + contentItem: RowLayout { + spacing: Style.current.padding + PanelIcon {} + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: Style.current.smallPadding + PanelHeading {} + Repeater { // permissions repeater + model: [PermissionTypes.Type.Read, PermissionTypes.Type.ViewAndPost] + + delegate: Rectangle { + id: channelPermsSubPanel + + readonly property int permissionType: modelData + + Layout.fillWidth: true + Layout.preferredHeight: grid2.implicitHeight + grid2.anchors.margins*2 + border.width: 1 + border.color: d.tableBorderColor + radius: Style.current.radius + color: "transparent" + + GridLayout { + id: grid2 + anchors.fill: parent + anchors.margins: Style.current.halfPadding + rowSpacing: Style.current.halfPadding + columnSpacing: Style.current.halfPadding + columns: 2 + + Repeater { + id: permissionsRepeater2 + + property int revision + + model: SortFilterProxyModel { + id: channelPermissionsModel + sourceModel: root.permissionsModel + function filterPredicate(modelData) { + return modelData.permissionType === channelPermsSubPanel.permissionType && + !modelData.isPrivate && + ModelUtils.contains(modelData.channelsListModel, "key", channelPermsPanel.channelKey) // filter and group by channel "key" + } + filters: ExpressionFilter { + expression: channelPermissionsModel.filterPredicate(model) + } + } + delegate: Column { + Layout.column: 0 + Layout.row: index + Layout.fillWidth: true + spacing: Style.current.halfPadding + + readonly property bool tokenCriteriaMet: model.tokenCriteriaMet ?? false + onTokenCriteriaMetChanged: permissionsRepeater2.revision++ + + SinglePermissionFlow {} + + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + grid2.anchors.margins*2 + height: 1 + color: d.tableBorderColor + visible: index < permissionsRepeater2.count - 1 + } + } + } + + RowLayout { + id: overallPermissionRow2 + Layout.column: 1 + Layout.rowSpan: channelPermissionsModel.count || 1 + Layout.preferredWidth: 110 + Layout.fillHeight: true + + readonly property bool tokenCriteriaMet: { + permissionsRepeater2.revision + for (let i = 0; i < permissionsRepeater2.count; i++) { + const permissionItem = permissionsRepeater2.itemAt(i); + if (permissionItem.tokenCriteriaMet) + return true + } + return false + } + + Rectangle { + Layout.preferredWidth: 1 + Layout.fillHeight: true + Layout.topMargin: -Style.current.halfPadding + Layout.bottomMargin: -Style.current.halfPadding + color: d.tableBorderColor + } + Row { + Layout.alignment: Qt.AlignCenter + StatusIcon { + anchors.verticalCenter: parent.verticalCenter + width: 16 + height: 16 + icon: overallPermissionRow2.tokenCriteriaMet ? "tiny/checkmark" : "tiny/secure" + color: overallPermissionRow2.tokenCriteriaMet ? Theme.palette.successColor1 : Theme.palette.baseColor1 + } + StatusBaseText { + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: Theme.tertiaryTextFontSize + text: { + switch (channelPermsSubPanel.permissionType) { + case PermissionTypes.Type.Read: + return qsTr("View only") + case PermissionTypes.Type.ViewAndPost: + return qsTr("View & post") + case PermissionTypes.Type.Moderator: + return qsTr("Moderate") + default: + return "???" + } + } + + color: overallPermissionRow2.tokenCriteriaMet ? Theme.palette.directColor1 : Theme.palette.baseColor1 + } + } + } + } + } + } + } + } + } +} diff --git a/ui/app/AppLayouts/Communities/panels/qmldir b/ui/app/AppLayouts/Communities/panels/qmldir index 1c82571ef..780197167 100644 --- a/ui/app/AppLayouts/Communities/panels/qmldir +++ b/ui/app/AppLayouts/Communities/panels/qmldir @@ -23,6 +23,7 @@ PrivilegedTokenArtworkPanel 1.0 PrivilegedTokenArtworkPanel.qml ProfilePopupInviteFriendsPanel 1.0 ProfilePopupInviteFriendsPanel.qml ProfilePopupInviteMessagePanel 1.0 ProfilePopupInviteMessagePanel.qml ProfilePopupOverviewPanel 1.0 ProfilePopupOverviewPanel.qml +SharedAddressesPanel 1.0 SharedAddressesPanel.qml SortableTokenHoldersList 1.0 SortableTokenHoldersList.qml SortableTokenHoldersPanel 1.0 SortableTokenHoldersPanel.qml TagsPanel 1.0 TagsPanel.qml diff --git a/ui/app/AppLayouts/Communities/popups/SharedAddressesPopup.qml b/ui/app/AppLayouts/Communities/popups/SharedAddressesPopup.qml new file mode 100644 index 000000000..617cc632d --- /dev/null +++ b/ui/app/AppLayouts/Communities/popups/SharedAddressesPopup.qml @@ -0,0 +1,50 @@ +import QtQuick 2.15 + +import StatusQ.Popups.Dialog 0.1 + +import AppLayouts.Communities.panels 1.0 + +import utils 1.0 + +StatusDialog { + id: root + + property bool isEditMode + + required property string communityName + required property string communityIcon + property int loginType: Constants.LoginType.Password + + required property var walletAccountsModel // name, address, emoji, colorId, assets + required property var permissionsModel // id, key, permissionType, holdingsListModel, channelsListModel, isPrivate, tokenCriteriaMet + required property var assetsModel + required property var collectiblesModel + + readonly property string selectedAirdropAddress: panel.selectedAirdropAddress + readonly property var selectedSharedAddresses: panel.selectedSharedAddresses + + signal shareSelectedAddressesClicked(string airdropAddress, var sharedAddresses) + + title: panel.title + implicitWidth: 640 // by design + padding: 0 + + contentItem: SharedAddressesPanel { + id: panel + isEditMode: root.isEditMode + communityName: root.communityName + communityIcon: root.communityIcon + loginType: root.loginType + walletAccountsModel: root.walletAccountsModel + permissionsModel: root.permissionsModel + assetsModel: root.assetsModel + collectiblesModel: root.collectiblesModel + onShareSelectedAddressesClicked: root.shareSelectedAddressesClicked(airdropAddress, sharedAddresses) + onClose: root.close() + } + + footer: StatusDialogFooter { + spacing: Style.current.padding + rightButtons: panel.buttons + } +} diff --git a/ui/app/AppLayouts/Communities/popups/qmldir b/ui/app/AppLayouts/Communities/popups/qmldir index 1f6f9d02f..c67bbd2a4 100644 --- a/ui/app/AppLayouts/Communities/popups/qmldir +++ b/ui/app/AppLayouts/Communities/popups/qmldir @@ -16,3 +16,4 @@ RemotelyDestructPopup 1.0 RemotelyDestructPopup.qml SignMultiTokenTransactionsPopup 1.0 SignMultiTokenTransactionsPopup.qml SignTokenTransactionsPopup 1.0 SignTokenTransactionsPopup.qml TransferOwnershipPopup 1.0 TransferOwnershipPopup.qml +SharedAddressesPopup 1.0 SharedAddressesPopup.qml diff --git a/ui/app/AppLayouts/Communities/views/CommunityColumnView.qml b/ui/app/AppLayouts/Communities/views/CommunityColumnView.qml index d1a730577..114cdf287 100644 --- a/ui/app/AppLayouts/Communities/views/CommunityColumnView.qml +++ b/ui/app/AppLayouts/Communities/views/CommunityColumnView.qml @@ -20,6 +20,7 @@ import shared.views.chat 1.0 import AppLayouts.Communities.popups 1.0 import AppLayouts.Communities.panels 1.0 +import AppLayouts.Wallet.stores 1.0 as WalletStore // FIXME: Rework me to use ColumnLayout instead of anchors!! Item { @@ -126,10 +127,15 @@ Item { introMessage: communityData.introMessage imageSrc: communityData.image accessType: communityData.access + loginType: root.store.loginType + walletAccountsModel: WalletStore.RootStore.receiveAccounts + permissionsModel: root.store.permissionsStore.permissionsModel + assetsModel: root.store.assetsModel + collectiblesModel: root.store.collectiblesModel onJoined: { joinCommunityButton.loading = true - root.store.requestToJoinCommunityWithAuthentication(root.store.userProfileInst.name) + root.store.requestToJoinCommunityWithAuthentication(root.store.userProfileInst.name, sharedAddresses, airdropAddress) } onCancelMembershipRequest: { root.store.cancelPendingRequest(communityData.id) diff --git a/ui/app/AppLayouts/Profile/views/CommunitiesView.qml b/ui/app/AppLayouts/Profile/views/CommunitiesView.qml index 0e1810ace..bd2797ce5 100644 --- a/ui/app/AppLayouts/Profile/views/CommunitiesView.qml +++ b/ui/app/AppLayouts/Profile/views/CommunitiesView.qml @@ -18,6 +18,8 @@ import SortFilterProxyModel 0.2 import "../panels" import AppLayouts.Communities.popups 1.0 import AppLayouts.Communities.panels 1.0 +import AppLayouts.Wallet.stores 1.0 as WalletStore +import AppLayouts.Chat.stores 1.0 as ChatStore SettingsContentBase { id: root @@ -208,22 +210,23 @@ SettingsContentBase { property string communityId - readonly property var chatCommunitySectionModule: { - root.rootStore.mainModuleInst.prepareCommunitySectionModuleForCommunityId(communityIntroDialog.communityId) - return root.rootStore.mainModuleInst.getCommunitySectionModule() + readonly property var chatStore: ChatStore.RootStore { + chatCommunitySectionModule: { + root.rootStore.mainModuleInst.prepareCommunitySectionModuleForCommunityId(communityIntroDialog.communityId) + return root.rootStore.mainModuleInst.getCommunitySectionModule() + } } - onJoined: { - chatCommunitySectionModule.requestToJoinCommunityWithAuthentication(root.rootStore.userProfileInst.name) - } + loginType: chatStore.loginType + walletAccountsModel: WalletStore.RootStore.receiveAccounts + permissionsModel: chatStore.permissionsStore.permissionsModel + assetsModel: chatStore.assetsModel + collectiblesModel: chatStore.collectiblesModel - onCancelMembershipRequest: { - root.rootStore.cancelPendingRequest(communityIntroDialog.communityId) - } + onJoined: chatStore.requestToJoinCommunityWithAuthentication(root.rootStore.userProfileInst.name, JSON.stringify(sharedAddresses), airdropAddress) + onCancelMembershipRequest: root.rootStore.cancelPendingRequest(communityIntroDialog.communityId) - onClosed: { - destroy() - } + onClosed: destroy() } } } diff --git a/ui/imports/shared/popups/CommunityIntroDialog.qml b/ui/imports/shared/popups/CommunityIntroDialog.qml index cb622dd82..62c6cdc17 100644 --- a/ui/imports/shared/popups/CommunityIntroDialog.qml +++ b/ui/imports/shared/popups/CommunityIntroDialog.qml @@ -1,7 +1,6 @@ -import QtQuick 2.14 -import QtQuick.Controls 2.14 -import QtQuick.Layouts 1.1 -import QtQml.Models 2.14 +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 import utils 1.0 @@ -9,9 +8,14 @@ import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Controls 0.1 import StatusQ.Components 0.1 -import StatusQ.Popups.Dialog 0.1 +import StatusQ.Popups 0.1 +import StatusQ.Core.Utils 0.1 -StatusDialog { +import AppLayouts.Communities.panels 1.0 + +import SortFilterProxyModel 0.2 + +StatusStackModal { id: root property string name @@ -19,74 +23,131 @@ StatusDialog { property int accessType property url imageSrc property bool isInvitationPending: false + property int loginType: Constants.LoginType.Password - signal joined - signal cancelMembershipRequest + required property var walletAccountsModel // name, address, emoji, colorId + required property var permissionsModel // id, key, permissionType, holdingsListModel, channelsListModel, isPrivate, tokenCriteriaMet + required property var assetsModel + required property var collectiblesModel + signal joined(string airdropAddress, var sharedAddresses) + signal cancelMembershipRequest() + + width: 640 // by design padding: 0 - title: qsTr("Welcome to %1").arg(name) + stackTitle: root.accessType === Constants.communityChatOnRequestAccess ? qsTr("Request to join %1").arg(name) : qsTr("Welcome to %1").arg(name) - footer: StatusDialogFooter { - rightButtons: ObjectModel { - StatusButton { - text: root.isInvitationPending ? qsTr("Cancel Membership Request") - : (root.accessType === Constants.communityChatOnRequestAccess - ? qsTr("Request to join %1").arg(root.name) - : qsTr("Join %1").arg(root.name) ) - type: root.isInvitationPending ? StatusBaseButton.Type.Danger - : StatusBaseButton.Type.Normal - enabled: checkBox.checked || root.isInvitationPending - onClicked: { - if (root.isInvitationPending) { - root.cancelMembershipRequest() - } else { - root.joined() + rightButtons: [d.shareButton, finishButton] + + finishButton: StatusButton { + text: root.isInvitationPending ? qsTr("Cancel Membership Request") + : (root.accessType === Constants.communityChatOnRequestAccess + ? qsTr("Share your addresses to join") + : qsTr("Join %1").arg(root.name) ) + type: root.isInvitationPending ? StatusBaseButton.Type.Danger + : StatusBaseButton.Type.Normal + icon.name: root.accessType === Constants.communityChatOnRequestAccess && !root.isInvitationPending ? Constants.authenticationIconByType[root.loginType] : "" + onClicked: { + if (root.isInvitationPending) { + root.cancelMembershipRequest() + } else { + root.joined(d.selectedAirdropAddress, d.selectedSharedAddresses) + } + + root.close() + } + } + + QtObject { + id: d + + readonly property var tempAddressesModel: SortFilterProxyModel { + sourceModel: root.walletAccountsModel + filters: [ + ValueFilter { + roleName: "walletType" + value: Constants.watchWalletType + inverted: true + } + ] + sorters: [ + ExpressionSorter { + function isGenerated(modelData) { + return modelData.walletType === Constants.generatedWalletType } - root.close() + expression: { + return isGenerated(modelLeft) + } + }, + RoleSorter { + roleName: "position" + }, + RoleSorter { + roleName: "name" + } + ] + } + + // all non-watched addresses by default, unless selected otherwise below in SharedAddressesPanel + property var selectedSharedAddresses: tempAddressesModel.count ? ModelUtils.modelToFlatArray(tempAddressesModel, "address") : [] + property string selectedAirdropAddress: selectedSharedAddresses.length ? selectedSharedAddresses[0] : "" + + readonly property var shareButton: StatusFlatButton { + height: finishButton.height + visible: !root.isInvitationPending && !root.replaceItem + borderColor: Theme.palette.baseColor2 + text: qsTr("Select addresses to share") + onClicked: root.replace(sharedAddressesPanelComponent) + } + } + + Component { + id: sharedAddressesPanelComponent + SharedAddressesPanel { + communityName: root.name + communityIcon: root.imageSrc + loginType: root.loginType + walletAccountsModel: root.walletAccountsModel + permissionsModel: root.permissionsModel + assetsModel: root.assetsModel + collectiblesModel: root.collectiblesModel + onShareSelectedAddressesClicked: { + d.selectedAirdropAddress = airdropAddress + d.selectedSharedAddresses = sharedAddresses + root.replaceItem = undefined // go back, unload us + } + } + } + + stackItems: [ + StatusScrollView { + id: scrollView + contentWidth: availableWidth + + ColumnLayout { + spacing: 24 + width: scrollView.availableWidth + + StatusRoundedImage { + id: roundImage + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight: 64 + Layout.preferredWidth: Layout.preferredHeight + visible: image.status == Image.Loading || image.status == Image.Ready + image.source: root.imageSrc + } + + StatusBaseText { + id: introText + + Layout.fillWidth: true + text: root.introMessage || qsTr("Community %1 has no intro message...").arg(root.name) + color: Theme.palette.directColor1 + wrapMode: Text.WordWrap } } } - } - - StatusScrollView { - id: scrollView - anchors.fill: parent - implicitWidth: 640 // by design - contentWidth: availableWidth - - ColumnLayout { - id: columnContent - - spacing: 24 - width: scrollView.availableWidth - - StatusRoundedImage { - id: roundImage - - Layout.alignment: Qt.AlignCenter - Layout.preferredHeight: 64 - Layout.preferredWidth: Layout.preferredHeight - visible: image.status == Image.Loading || image.status == Image.Ready - image.source: root.imageSrc - } - - StatusBaseText { - id: introText - - Layout.fillWidth: true - text: root.introMessage !== "" ? root.introMessage : qsTr("Community %1 has no intro message...").arg(root.name) - color: Theme.palette.directColor1 - wrapMode: Text.WordWrap - } - - StatusCheckBox { - id: checkBox - - Layout.alignment: Qt.AlignCenter - visible: !root.isInvitationPending - text: qsTr("I agree with the above") - } - } - } + ] } diff --git a/ui/imports/shared/stores/PermissionsStore.qml b/ui/imports/shared/stores/PermissionsStore.qml index f598c81cb..59d9fc6f6 100644 --- a/ui/imports/shared/stores/PermissionsStore.qml +++ b/ui/imports/shared/stores/PermissionsStore.qml @@ -54,7 +54,6 @@ QtObject { return (modelData.permissionType == Constants.permissionType.viewAndPost) && root.permissionsModel.belongsToChat(modelData.id, root.activeChannelId) && (modelData.tokenCriteriaMet || !modelData.isPrivate) - } filters: [ ExpressionFilter { diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index e986768b1..884218601 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -987,7 +987,8 @@ QtObject { enum TokenType { Unknown = 0, ERC20 = 1, // Asset - ERC721 = 2 // Collectible + ERC721 = 2, // Collectible + ENS = 3 } // Mirrors src/backend/activity.nim ActivityStatus diff --git a/ui/imports/utils/Utils.qml b/ui/imports/utils/Utils.qml index 8709348d2..448d6dc88 100644 --- a/ui/imports/utils/Utils.qml +++ b/ui/imports/utils/Utils.qml @@ -24,7 +24,7 @@ QtObject { } function startsWith0x(value) { - return value.startsWith('0x') + return !!value && value.startsWith('0x') } function isChatKey(value) { @@ -248,8 +248,8 @@ QtObject { * Returns text in the format "✓ 12 words" for seed phrases input boxes */ function seedPhraseWordCountText(text) { - let wordCount = countWords(text); - return getTick(wordCount) + wordCount.toString() + " " + qsTr("words") + const wordCount = countWords(text); + return getTick(wordCount) + qsTr("%n word(s)", "", wordCount) } function uuid() { @@ -288,7 +288,7 @@ QtObject { } else if (!/^\d+$/.test(firstPINField.pinInput)) { return [false, qsTr("The PIN must contain only digits")]; } else if (firstPINField.pinInput.length != Constants.keycard.general.keycardPinLength) { - return [false, qsTr("The PIN must be exactly %1 digits").arg(Constants.keycard.general.keycardPinLength)]; + return [false, qsTr("The PIN must be exactly %n digit(s)", "", Constants.keycard.general.keycardPinLength)]; } return [true, ""]; @@ -296,7 +296,7 @@ QtObject { if (repeatPINField.pinInput === "") { return [false, qsTr("You need to repeat your PIN")]; } else if (repeatPINField.pinInput !== firstPINField.pinInput) { - return [false, qsTr("PIN don't match")]; + return [false, qsTr("PINs don't match")]; } return [true, ""]; @@ -373,7 +373,7 @@ QtObject { } if(validation & Utils.Validate.TextLength && str.length > limit) { - errMsg = qsTr("The %1 cannot exceed %2 characters").arg(fieldName, limit) + errMsg = qsTr("The %1 cannot exceed %n character(s)", "", limit).arg(fieldName) } if(validation & Utils.Validate.TextHexColor && !isHexColor(str)) { @@ -392,7 +392,7 @@ QtObject { if (errors.minLength) { return errors.minLength.min === 1 ? qsTr("You need to enter a %1").arg(fieldName) : - qsTr("Value has to be at least %1 characters long").arg(errors.minLength.min) + qsTr("Value has to be at least %n character(s) long", "", errors.minLength.min) } } return "" @@ -657,7 +657,7 @@ QtObject { // special handling because on an index attached to the constant if (key.startsWith(Constants.appTranslatableConstants.keycardAccountNameOfUnknownWalletAccount)) { let num = key.substring(Constants.appTranslatableConstants.keycardAccountNameOfUnknownWalletAccount.length) - return "%1%2".arg(qsTr("acc")).arg(num) //short name of an unknown (removed) wallet account + return "%1%2".arg(qsTr("acc", "short for account")).arg(num) //short name of an unknown (removed) wallet account } return key diff --git a/vendor/SortFilterProxyModel b/vendor/SortFilterProxyModel index 6a471f1be..70b76297f 160000 --- a/vendor/SortFilterProxyModel +++ b/vendor/SortFilterProxyModel @@ -1 +1 @@ -Subproject commit 6a471f1bef1288407751400d93b12b7fa911474d +Subproject commit 70b76297fd074b4adda5e659d260c8cc18e51ef9