From a3239d9e2bd27e2e8d46e5e76d5c936a8b1c4610 Mon Sep 17 00:00:00 2001 From: Igor Sirotin Date: Fri, 27 Oct 2023 11:25:27 +0100 Subject: [PATCH] fix `ImportCommunityPopup` issues, remove private key importing (#12554) * feat(Storybook): added "Always on top" setting * fix(ImportCommunityPopup): show result, remove private key support --- .../modules/main/communities/controller.nim | 2 +- .../modules/main/communities/io_interface.nim | 3 + src/app/modules/main/communities/module.nim | 4 + src/app/modules/main/communities/view.nim | 4 + storybook/main.qml | 15 + storybook/pages/ImportCommunityPopupPage.qml | 328 ++++++++++++++++++ .../Communities/stores/CommunitiesStore.qml | 17 +- ui/app/mainui/Popups.qml | 2 +- .../shared/popups/ImportCommunityPopup.qml | 168 ++++----- vendor/status-go | 2 +- 10 files changed, 429 insertions(+), 116 deletions(-) create mode 100644 storybook/pages/ImportCommunityPopupPage.qml diff --git a/src/app/modules/main/communities/controller.nim b/src/app/modules/main/communities/controller.nim index 7dee840304..bd7ca52c76 100644 --- a/src/app/modules/main/communities/controller.nim +++ b/src/app/modules/main/communities/controller.nim @@ -85,7 +85,7 @@ proc init*(self: Controller) = self.events.on(SIGNAL_COMMUNITY_LOAD_DATA_FAILED) do(e: Args): let args = CommunityArgs(e) - self.delegate.onImportCommunityErrorOccured(args.communityId, args.error) + self.delegate.communityInfoRequestFailed(args.communityId, args.error) self.events.on(SIGNAL_COMMUNITY_INFO_ALREADY_REQUESTED) do(e: Args): self.delegate.communityInfoAlreadyRequested() diff --git a/src/app/modules/main/communities/io_interface.nim b/src/app/modules/main/communities/io_interface.nim index d337485eac..b3859bf0c6 100644 --- a/src/app/modules/main/communities/io_interface.nim +++ b/src/app/modules/main/communities/io_interface.nim @@ -108,6 +108,9 @@ method communityImported*(self: AccessInterface, community: CommunityDto) {.base method communityDataImported*(self: AccessInterface, community: CommunityDto) {.base.} = raise newException(ValueError, "No implementation available") +method communityInfoRequestFailed*(self: AccessInterface, communityId: string, errorMsg: string) {.base.} = + raise newException(ValueError, "No implementation available") + method onImportCommunityErrorOccured*(self: AccessInterface, communityId: string, error: string) {.base.} = raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/communities/module.nim b/src/app/modules/main/communities/module.nim index 1335b47293..75a9ccf8f9 100644 --- a/src/app/modules/main/communities/module.nim +++ b/src/app/modules/main/communities/module.nim @@ -346,6 +346,10 @@ method communityImported*(self: Module, community: CommunityDto) = method communityDataImported*(self: Module, community: CommunityDto) = self.view.addItem(self.getCommunityItem(community)) + self.view.emitCommunityInfoRequestCompleted(community.id, "") + +method communityInfoRequestFailed*(self: Module, communityId: string, errorMsg: string) = + self.view.emitCommunityInfoRequestCompleted(communityId, errorMsg) method importCommunity*(self: Module, communityId: string) = self.view.emitImportingCommunityStateChangedSignal(communityId, ImportCommunityState.ImportingInProgress.int, errorMsg = "") diff --git a/src/app/modules/main/communities/view.nim b/src/app/modules/main/communities/view.nim index 26af209bc4..92e092b2ef 100644 --- a/src/app/modules/main/communities/view.nim +++ b/src/app/modules/main/communities/view.nim @@ -546,6 +546,10 @@ QtObject: proc emitImportingCommunityStateChangedSignal*(self: View, communityId: string, state: int, errorMsg: string) = self.importingCommunityStateChanged(communityId, state, errorMsg) + proc communityInfoRequestCompleted*(self: View, communityId: string, errorMsg: string) {.signal.} + proc emitCommunityInfoRequestCompleted*(self: View, communityId: string, errorMsg: string) = + self.communityInfoRequestCompleted(communityId, errorMsg) + proc isMemberOfCommunity*(self: View, communityId: string, pubKey: string): bool {.slot.} = let sectionItem = self.model.getItemById(communityId) if (section_item.id == ""): diff --git a/storybook/main.qml b/storybook/main.qml index 3cfa837e99..2598d57df6 100644 --- a/storybook/main.qml +++ b/storybook/main.qml @@ -140,6 +140,20 @@ ApplicationWindow { onClicked: settingsPopup.open() } + CheckBox { + id: windowAlwaysOnTopCheckBox + + Layout.fillWidth: true + + text: "Always on top" + onCheckedChanged: { + if (checked) + root.flags |= Qt.WindowStaysOnTopHint + else + root.flags &= ~Qt.WindowStaysOnTopHint + } + } + CheckBox { id: darkModeCheckBox @@ -346,6 +360,7 @@ Tips: property alias darkMode: darkModeCheckBox.checked property alias hotReloading: hotReloaderControls.enabled property alias figmaToken: settingsLayout.figmaToken + property alias windowAlwaysOnTop: windowAlwaysOnTopCheckBox.checked } Shortcut { diff --git a/storybook/pages/ImportCommunityPopupPage.qml b/storybook/pages/ImportCommunityPopupPage.qml new file mode 100644 index 0000000000..070c170a76 --- /dev/null +++ b/storybook/pages/ImportCommunityPopupPage.qml @@ -0,0 +1,328 @@ +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 + +import utils 1.0 +import shared.popups 1.0 + +SplitView { + id: root + + Logs { id: logs } + + orientation: Qt.Vertical + + QtObject { + id: d + + readonly property alias dialog: loader.item + + property bool utilsReady: false + + readonly property string newCommunityLink: "https://status.app/c/Cw6AChsKBnlveW95bxIGeW95b3lvGAEiByM4OEIwRkYD#zQ3shwHXstcword5gUtJCWVi55ZxPsdtfTQitnWNEAR1p3Gzd" + readonly property string newCommunityPublicKey: "0x03f751777ab35759f98ad241150f4329b9cc13aa42052ef64d16e9d474b9677bea" + readonly property string newCommunityCompressedPublicKey: "zQ3shwHXstcword5gUtJCWVi55ZxPsdtfTQitnWNEAR1p3Gzd" + + readonly property var newCommunityDetails: QtObject { + readonly property string id: "0x039c47e9837a1a7dcd00a6516399d0eb521ab0a92d512ca20a44ac6278bfdbb5c5" + readonly property string name: "test-1" + readonly property int memberRole: 0 + readonly property bool isControlNode: false + readonly property string description: "test" + readonly property string introMessage: "123" + readonly property string outroMessage: "342" + readonly property string image: ModelsData.icons.superRare + readonly property string bannerImageData: ModelsData.banners.superRare + readonly property string icon: "" + readonly property string color: "#4360DF" + readonly property string tags: "null" + readonly property bool hasNotification: false + readonly property int notificationsCount: 0 + readonly property bool active: false + readonly property bool enabled: true + readonly property bool joined: false + readonly property bool spectated: false + readonly property bool canJoin: true + readonly property bool canManageUsers: false + readonly property bool canRequestAccess: false + readonly property bool isMember: false + readonly property bool amIBanned: false + readonly property int access: 1 + readonly property bool ensOnly: false + readonly property int nbMembers: 42 + readonly property bool encrypted: false + } + + readonly property string knownCommunityLink: "https://status.app/c/CwyAChcKBHRlc3QSBHRlc3QYAiIHIzQzNjBERgM=#zQ3shqAAKxRroS2BE4FgLjjombivfU7XgeNqVFj1eRZ4GHuAU" + readonly property string knownCommunityPublicKey: "0x03c892238f64e9b74cefbaaaf5d557fee401a20d6fb52da126de45755b2a2b8166" + readonly property string knownCommunityCompressedPublicKey: "zQ3shqAAKxRroS2BE4FgLjjombivfU7XgeNqVFj1eRZ4GHuAU" + + readonly property var knownCommunityDetails: QtObject { + readonly property string id: "0x03c892238f64e9b74cefbaaaf5d557fee401a20d6fb52da126de45755b2a2b8166" + readonly property string name: "test-2" + readonly property int memberRole: 0 + readonly property bool isControlNode: false + readonly property string description: "test" + readonly property string introMessage: "123" + readonly property string outroMessage: "342" + readonly property string image: ModelsData.icons.status + readonly property string bannerImageData: ModelsData.banners.status + readonly property string icon: "" + readonly property string color: "#4360DF" + readonly property string tags: "null" + readonly property bool hasNotification: false + readonly property int notificationsCount: 0 + readonly property bool active: false + readonly property bool enabled: true + readonly property bool joined: false + readonly property bool spectated: false + readonly property bool canJoin: true + readonly property bool canManageUsers: false + readonly property bool canRequestAccess: false + readonly property bool isMember: false + readonly property bool amIBanned: false + readonly property int access: 1 + readonly property bool ensOnly: false + readonly property int nbMembers: 15 + readonly property bool encrypted: false + } + + property bool currenKeyIsPublic: false + property string currentKey: "" + } + + QtObject { + id: communityStoreMock + + property bool newCommunityFetched: false + + signal communityInfoRequestCompleted(string communityId, string errorMsg) + + function getCommunityDetails(publicKey, importing, requestWhenNotFound) { + if (publicKey === d.knownCommunityPublicKey) { + return d.knownCommunityDetails + } + if (publicKey === d.newCommunityPublicKey && newCommunityFetched) + return d.newCommunityDetails + return null + } + + function requestCommunityInfo(communityId) { + // Dynamically create a timer to be able to simulate overlapping requests + let timer = Qt.createQmlObject("import QtQuick 2.0; Timer {}", root) + timer.interval = 1000 + timer.repeat = false + timer.triggered.connect(() => { + const communityFound = (communityId === d.knownCommunityPublicKey || communityId === d.newCommunityPublicKey) + const error = communityFound ? "" : "communtiy not found" + if (communityId === d.newCommunityPublicKey) { + newCommunityFetched = true + } + communityStoreMock.communityInfoRequestCompleted(communityId, error) + }) + timer.start() + } + } + QtObject { + id: utilsMock + + function getContactDetailsAsJson(arg1, arg2) { + return JSON.stringify({ + displayName: "Mock user", + displayIcon: Style.png("tokens/AST"), + publicKey: 123456789, + name: "", + ensVerified: false, + alias: "", + lastUpdated: 0, + lastUpdatedLocally: 0, + localNickname: "", + thumbnailImage: "", + largeImage: "", + isContact: false, + isAdded: false, + isBlocked: false, + requestReceived: false, + isSyncing: false, + removed: false, + trustStatus: Constants.trustStatus.unknown, + verificationStatus: Constants.verificationStatus.unverified, + incomingVerificationStatus: Constants.verificationStatus.unverified + }) + } + + function isCompressedPubKey(key) { + return d.dialog.text === d.knownCommunityCompressedPublicKey || + d.dialog.text === d.newCommunityCompressedPublicKey + } + + function changeCommunityKeyCompression(key) { + if (key === d.knownCommunityCompressedPublicKey) + return d.knownCommunityPublicKey + if (key === d.newCommunityCompressedPublicKey) + return d.newCommunityPublicKey + if (key === d.knownCommunityPublicKey) + return d.knownCommunityCompressedPublicKey + if (key === d.newCommunityPublicKey) + return d.newCommunityCompressedPublicKey + return "" + } + + function getCommunityDataFromSharedLink(link) { + return d.knownCommunityDetails + } + + function getCompressedPk(publicKey) { + return d.knownCommunityCompressedPublicKey + } + + signal importingCommunityStateChanged(string communityId, int state, string errorMsg) + + // sharedUrlsModuleInst + + function parseCommunitySharedUrl(link) { + if (link === d.knownCommunityLink) + return JSON.stringify({ communityId: d.knownCommunityPublicKey }) + if (link === d.newCommunityLink) + return JSON.stringify({ communityId: d.newCommunityPublicKey }) + return null + } + + Component.onCompleted: { + Utils.sharedUrlsModuleInst = this + Utils.globalUtilsInst = this + d.utilsReady = true + } + Component.onDestruction: { + d.utilsReady = false + Utils.sharedUrlsModuleInst = {} + Utils.globalUtilsInst = {} + } + } + + Item { + SplitView.fillWidth: true + SplitView.fillHeight: true + + PopupBackground { + id: popupBg + anchors.fill: parent + + Button { + anchors.centerIn: parent + text: "Reopen" + onClicked: loader.item.open() + } + } + + Loader { + id: loader + active: d.utilsReady + anchors.fill: parent + + sourceComponent: ImportCommunityPopup { + anchors.centerIn: parent + modal: false + closePolicy: Popup.NoAutoClose + destroyOnClose: false + + store: communityStoreMock + + Component.onCompleted: open() + + onJoinCommunityRequested: (communityId, communityDetails) => { + logs.logEvent("onJoinCommunity", ["communityId", "communityDetails"], communityId, communityDetails) + } + } + } + } + + LogsAndControlsPanel { + SplitView.minimumHeight: 100 + SplitView.preferredHeight: 200 + + logsView.logText: logs.logText + + GridLayout { + columns: 4 + + Button { + text: "Reset communities storage" + Layout.fillWidth: false + Layout.columnSpan: 4 + Layout.alignment: Qt.AlignRight + onClicked: { + communityStoreMock.newCommunityFetched = false + const prevText = d.dialog.text + d.dialog.text = "" + d.dialog.text = prevText + } + } + + Label { + text: "Known community" + } + + Button { + checked: d.dialog && d.dialog.text === d.knownCommunityLink + text: "Link" + onClicked: { + d.dialog.text = d.knownCommunityLink + } + } + + Button { + checked: d.dialog && d.dialog.text === d.knownCommunityPublicKey + text: "Public key" + onClicked: { + d.dialog.text = d.knownCommunityPublicKey + } + } + + Button { + checked: d.dialog && d.dialog.text === d.knownCommunityCompressedPublicKey + text: "Compressed public key" + onClicked: { + d.dialog.text = d.knownCommunityCompressedPublicKey + } + } + + Label { + text: "Never fetched community" + } + + Button { + checked: d.dialog && d.dialog.text === d.newCommunityLink + text: "Link" + onClicked: { + d.dialog.text = d.newCommunityLink + } + } + + Button { + checked: d.dialog && d.dialog.text === d.newCommunityPublicKey + text: "Public key" + onClicked: { + d.dialog.text = d.newCommunityPublicKey + } + } + + Button { + checked: d.dialog && d.dialog.text === d.newCommunityCompressedPublicKey + text: "Compressed public key" + onClicked: { + d.dialog.text = d.newCommunityCompressedPublicKey + } + } + + } + } +} + +// category: Popups diff --git a/ui/app/AppLayouts/Communities/stores/CommunitiesStore.qml b/ui/app/AppLayouts/Communities/stores/CommunitiesStore.qml index bab313d862..a57062bdb2 100644 --- a/ui/app/AppLayouts/Communities/stores/CommunitiesStore.qml +++ b/ui/app/AppLayouts/Communities/stores/CommunitiesStore.qml @@ -62,6 +62,8 @@ QtObject { signal communityInfoAlreadyRequested() + signal communityInfoRequestCompleted(string communityId, string errorMsg) + function createCommunity(args = { name: "", description: "", @@ -120,19 +122,14 @@ QtObject { root.communitiesModuleInst.prepareTokenModelForCommunity(publicKey); } - function getCommunityDetails(communityId, importing = false) { + function getCommunityDetails(communityId) { const publicKey = Utils.isCompressedPubKey(communityId) ? Utils.changeCommunityKeyCompression(communityId) : communityId try { const communityJson = root.communitiesList.getSectionByIdJson(publicKey) - - if (!communityJson) { - root.requestCommunityInfo(publicKey, importing) - return null - } - - return JSON.parse(communityJson); + if (!!communityJson) + return JSON.parse(communityJson) } catch (e) { console.error("Error parsing community", e) } @@ -251,5 +248,9 @@ QtObject { function onCommunityInfoAlreadyRequested() { root.communityInfoAlreadyRequested() } + + function onCommunityInfoRequestCompleted(communityId, erorrMsg) { + root.communityInfoRequestCompleted(communityId, erorrMsg) + } } } diff --git a/ui/app/mainui/Popups.qml b/ui/app/mainui/Popups.qml index 9a6a3d7a58..791d40e865 100644 --- a/ui/app/mainui/Popups.qml +++ b/ui/app/mainui/Popups.qml @@ -504,7 +504,7 @@ QtObject { id: importCommunitiesPopupComponent ImportCommunityPopup { store: root.communitiesStore - onJoinCommunity: { + onJoinCommunityRequested: { close() openCommunityIntroPopup(communityId, communityDetails.name, diff --git a/ui/imports/shared/popups/ImportCommunityPopup.qml b/ui/imports/shared/popups/ImportCommunityPopup.qml index 82ff43c085..ec316e6e58 100644 --- a/ui/imports/shared/popups/ImportCommunityPopup.qml +++ b/ui/imports/shared/popups/ImportCommunityPopup.qml @@ -15,111 +15,97 @@ import StatusQ.Controls 0.1 StatusDialog { id: root + property var store + property alias text: keyInput.text + + signal joinCommunityRequested(string communityId, var communityDetails) + width: 640 title: qsTr("Import Community") - signal joinCommunity(string communityId, var communityDetails) - QtObject { id: d property string importErrorMessage - property bool loading - property bool communityFound: (d.communityDetails !== null && !!d.communityDetails.name) - property var communityDetails: { - if (!isInputValid) { - loading = false - return null - } - loading = true - const key = isPublicKey ? Utils.getCompressedPk(publicKey) : - root.store.getCommunityPublicKeyFromPrivateKey(inputKey, true /*importing*/); + readonly property bool communityFound: !!d.communityDetails && !!d.communityDetails.name + property var communityDetails: null - const details = root.store.getCommunityDetails(key) - if (!!details) // the above can return `null` in which case we continue loading - loading = false - return details - } + property var requestedCommunityDetails: null readonly property string inputErrorMessage: isInputValid ? "" : qsTr("Invalid key") readonly property string errorMessage: importErrorMessage || inputErrorMessage readonly property string inputKey: keyInput.text.trim() - readonly property bool isPrivateKey: (Utils.isPrivateKey(inputKey)) - readonly property bool isPublicKey: (publicKey !== "") - readonly property string privateKey: inputKey readonly property string publicKey: { - if (!Utils.isStatusDeepLink(inputKey)) { - const key = Utils.dropCommunityLinkPrefix(inputKey) - if (!Utils.isCommunityPublicKey(key)) - return "" - if (!Utils.isCompressedPubKey(key)) - return key - return Utils.changeCommunityKeyCompression(key) - } else { - return Utils.getCommunityDataFromSharedLink(inputKey).communityId; + if (Utils.isStatusDeepLink(inputKey)) { + const linkData = Utils.getCommunityDataFromSharedLink(inputKey) + return !linkData ? "" : linkData.communityId + } + if (!Utils.isCommunityPublicKey(inputKey)) + return "" + if (!Utils.isCompressedPubKey(inputKey)) + return inputKey + return Utils.changeCommunityKeyCompression(inputKey) + } + readonly property bool isInputValid: publicKey !== "" + + property bool communityInfoRequested: false + + function updateCommunityDetails(requestIfNotFound) { + if (!isInputValid) { + d.communityInfoRequested = false + d.communityDetails = null + return + } + + const details = root.store.getCommunityDetails(publicKey) + + if (!!details) { + d.communityInfoRequested = false + d.communityDetails = details + return + } + + if (requestIfNotFound) { + root.store.requestCommunityInfo(publicKey, false) + d.communityInfoRequested = true + d.communityDetails = null } } - readonly property bool isInputValid: isPrivateKey || isPublicKey - } - Timer { - interval: 20000 // 20s - running: d.loading - onTriggered: { - d.loading = false - d.importErrorMessage = qsTr("Timeout reached while getting community info") + onPublicKeyChanged: { + // call later to make sure all proeprties used by `updateCommunityDetails` are udpated + Qt.callLater(() => { d.updateCommunityDetails(true) }) } } Connections { target: root.store - function onImportingCommunityStateChanged(communityId, state, errorMsg) { - switch (state) - { - case Constants.communityImported: - const community = root.store.getCommunityDetailsAsJson(communityId) - d.loading = false - d.communityFound = true - d.communityDetails = community - d.importErrorMessage = "" - break - case Constants.communityImportingInProgress: - d.loading = true - break - case Constants.communityImportingError: - d.loading = false - d.communityFound = false - d.communityDetails = null - d.importErrorMessage = errorMsg - break - default: - const msg = qsTr("Error state '%1' while importing community: %2").arg(state).arg(communityId) - console.error(msg) - d.loading = false - d.communityFound = false - d.communityDetails = null - d.importErrorMessage = msg + function onCommunityInfoRequestCompleted(communityId, errorMsg) { + if (!d.communityInfoRequested) + return + + d.communityInfoRequested = false + + if (errorMsg !== "") { + d.importErrorMessage = qsTr("Couldn't find community") return } + + d.updateCommunityDetails(false) + d.importErrorMessage = "" } } footer: StatusDialogFooter { rightButtons: ObjectModel { StatusButton { - enabled: d.communityFound && ((d.isPublicKey) || (d.isPrivateKey && agreeToKeepOnline.checked)) - loading: d.loading - text: d.isPrivateKey && d.communityFound ? qsTr("Make this device the control node for %1").arg(d.communityDetails.name) - : qsTr("Import") + enabled: d.isInputValid && d.communityFound + loading: d.isInputValid && !d.communityFound && d.communityInfoRequested + text: qsTr("Import") onClicked: { - if (d.isPrivateKey) { - root.store.importCommunity(d.privateKey); - root.close(); - } else if (d.isPublicKey) { - root.joinCommunity(d.publicKey, d.communityDetails); - } + root.joinCommunityRequested(d.publicKey, d.communityDetails) } } } @@ -139,7 +125,7 @@ StatusDialog { StatusBaseText { id: infoText1 Layout.fillWidth: true - text: qsTr("Enter the public key of the community you wish to access, or enter the private key of a community you own. Remember to always keep any private key safe and never share a private key with anyone else.") + text: qsTr("Enter the public key of the community you wish to access") wrapMode: Text.WordWrap font.pixelSize: Style.current.additionalTextSize color: Theme.palette.baseColor1 @@ -178,43 +164,15 @@ StatusDialog { font.pixelSize: Style.current.additionalTextSize visible: !!d.inputKey text: { - if (d.errorMessage !== "") { + if (d.errorMessage !== "") return d.errorMessage - } - if (d.isPrivateKey) { - return qsTr("Private key detected") - } - if (d.isPublicKey) { + if (d.isInputValid) return qsTr("Public key detected") - } + return "" } color: d.errorMessage === "" ? Theme.palette.successColor1 : Theme.palette.dangerColor1 } } - ColumnLayout { - Layout.fillWidth: true - Layout.fillHeight: true - visible: (d.communityFound && d.isPrivateKey) - Layout.topMargin: 12 - spacing: Style.current.padding - StatusWarningBox { - Layout.fillWidth: true - icon: "caution" - text: qsTr("Another device might currently have the control node for this Community. Running multiple control nodes will cause unforeseen issues. Make sure you delete the private key in that other device in the community management tab.") - bgColor: borderColor - } - StatusDialogDivider { Layout.fillWidth: true; Layout.topMargin: Style.current.padding } - StatusBaseText { - Layout.topMargin: Style.current.halfPadding - visible: (d.communityFound && d.isPrivateKey) - text: qsTr("I acknowledge that...") - } - StatusCheckBox { - id: agreeToKeepOnline - Layout.fillWidth: true - text: qsTr("I must keep this device online and running Status for the Community to function") - } - } } } } diff --git a/vendor/status-go b/vendor/status-go index 27b770c41b..74396b461d 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit 27b770c41bd2896ebc30b01985eeea1fbe642fba +Subproject commit 74396b461df0c18593262a67c9fd79d706287f7a