diff --git a/src/app/modules/main/chat_section/controller.nim b/src/app/modules/main/chat_section/controller.nim index 413a072520..0538e1ea43 100644 --- a/src/app/modules/main/chat_section/controller.nim +++ b/src/app/modules/main/chat_section/controller.nim @@ -49,6 +49,7 @@ type tmpRequestToJoinEnsName: string tmpAddressesToShare: seq[string] tmpAirdropAddress: string + tmpAuthenticationWithCallbackInProgress: bool proc newController*(delegate: io_interface.AccessInterface, sectionId: string, isCommunity: bool, events: EventEmitter, settingsService: settings_service.Service, nodeConfigurationService: node_configuration_service.Service, @@ -82,7 +83,8 @@ proc newController*(delegate: io_interface.AccessInterface, sectionId: string, i result.tmpRequestToJoinEnsName = "" result.tmpAirdropAddress = "" result.tmpAddressesToShare = @[] - + result.tmpAuthenticationWithCallbackInProgress = true + proc delete*(self: Controller) = self.events.disconnect() @@ -232,6 +234,10 @@ proc init*(self: Controller) = return if self.tmpAuthenticationForJoinInProgress or self.tmpAuthenticationForEditSharedAddresses: self.delegate.onUserAuthenticated(args.pin, args.password, args.keyUid) + if self.tmpAuthenticationWithCallbackInProgress: + let authenticated = not (args.password == "" and args.pin == "") + self.delegate.callbackFromAuthentication(authenticated) + self.tmpAuthenticationWithCallbackInProgress = false if (self.isCommunitySection): self.events.on(SIGNAL_COMMUNITY_CHANNEL_CREATED) do(e:Args): @@ -730,3 +736,7 @@ proc getContractAddressesForToken*(self: Controller, symbol: string): Table[int, proc getCommunityTokenList*(self: Controller): seq[CommunityTokenDto] = return self.communityTokensService.getCommunityTokens(self.getMySectionId()) + +proc authenticateWithCallback*(self: Controller) = + self.tmpAuthenticationWithCallbackInProgress = true + self.authenticate() \ No newline at end of file diff --git a/src/app/modules/main/chat_section/io_interface.nim b/src/app/modules/main/chat_section/io_interface.nim index 9e791259ec..475971fa15 100644 --- a/src/app/modules/main/chat_section/io_interface.nim +++ b/src/app/modules/main/chat_section/io_interface.nim @@ -400,3 +400,9 @@ method onCommunityCheckChannelPermissionsResponse*(self: AccessInterface, chatId method onCommunityCheckAllChannelsPermissionsResponse*(self: AccessInterface, checkAllChannelsPermissionsResponse: CheckAllChannelsPermissionsResponseDto) {.base.} = raise newException(ValueError, "No implementation available") + +method authenticateWithCallback*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + +method callbackFromAuthentication*(self: AccessInterface, authenticated: bool) {.base.} = + raise newException(ValueError, "No implementation available") \ No newline at end of file diff --git a/src/app/modules/main/chat_section/module.nim b/src/app/modules/main/chat_section/module.nim index d0e98761dd..dabc070a2e 100644 --- a/src/app/modules/main/chat_section/module.nim +++ b/src/app/modules/main/chat_section/module.nim @@ -1336,3 +1336,8 @@ method editSharedAddressesWithAuthentication*(self: Module, addressesToShare: se method onDeactivateChatLoader*(self: Module, chatId: string) = self.view.chatsModel().disableChatLoader(chatId) +method authenticateWithCallback*(self: Module) = + self.controller.authenticateWithCallback() + +method callbackFromAuthentication*(self: Module, authenticated: bool) = + self.view.callbackFromAuthentication(authenticated) \ No newline at end of file diff --git a/src/app/modules/main/chat_section/view.nim b/src/app/modules/main/chat_section/view.nim index 235c3eb61b..c55aaf1993 100644 --- a/src/app/modules/main/chat_section/view.nim +++ b/src/app/modules/main/chat_section/view.nim @@ -429,3 +429,8 @@ QtObject: notify = allTokenRequirementsMetChanged proc userAuthenticationCanceled*(self: View) {.signal.} + + proc authenticateWithCallback*(self: View) {.slot.} = + self.delegate.authenticateWithCallback() + + proc callbackFromAuthentication*(self: View, authenticated: bool) {.signal.} diff --git a/storybook/PagesModel.qml b/storybook/PagesModel.qml index deed0229f9..f62076c4fc 100644 --- a/storybook/PagesModel.qml +++ b/storybook/PagesModel.qml @@ -273,6 +273,10 @@ ListModel { title: "UserAgreementPopup" section: "Popups" } + ListElement { + title: "ExportControlNodePopup" + section: "Popups" + } ListElement { title: "StatusButton" section: "Controls" diff --git a/storybook/figma.json b/storybook/figma.json index dc4d5f0dd1..7983997e43 100644 --- a/storybook/figma.json +++ b/storybook/figma.json @@ -250,5 +250,8 @@ "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" + ], + "ExportControlNodePopup": [ + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba⎜Desktop?type=design&node-id=31171-627949&mode=design&t=WxK2N6sL8idHBKMZ-0" ] } diff --git a/storybook/pages/ExportControlNodePopupPage.qml b/storybook/pages/ExportControlNodePopupPage.qml new file mode 100644 index 0000000000..8c6e8778ea --- /dev/null +++ b/storybook/pages/ExportControlNodePopupPage.qml @@ -0,0 +1,53 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import AppLayouts.Communities.popups 1.0 + +import Storybook 1.0 + +SplitView { + id: root + orientation: Qt.Vertical + + Logs { id: logs } + + Item { + + SplitView.fillWidth: true + SplitView.fillHeight: true + + PopupBackground { + anchors.fill: parent + } + + Button { + anchors.centerIn: parent + text: "Reopen" + + onClicked: popupComponent.createObject(parent) + } + Component.onCompleted: popupComponent.createObject(parent) + } + + Component { + id: popupComponent + ExportControlNodePopup { + id: popup + anchors.centerIn: parent + modal: false + visible: true + communityName: "Socks" + privateKey: "0x0454f2231543ba02583e4c55e513a75092a4f2c86c04d0796b195e964656d6cd94b8237c64ef668eb0fe268387adc3fe699bce97190a631563c82b718c19cf1fb8" + onDeletePrivateKey: logs.logEvent("ExportControlNodePopup::onDeletePrivateKey") + } + } + + LogsAndControlsPanel { + id: logsAndControlsPanel + + SplitView.minimumHeight: 100 + SplitView.preferredHeight: 160 + + logsView.logText: logs.logText + } +} diff --git a/storybook/src/Storybook/FilteredPagesList.qml b/storybook/src/Storybook/FilteredPagesList.qml index 1cdccdac4b..1288d6f8e4 100644 --- a/storybook/src/Storybook/FilteredPagesList.qml +++ b/storybook/src/Storybook/FilteredPagesList.qml @@ -40,6 +40,7 @@ ColumnLayout { Layout.fillWidth: true placeholderText: "search" + selectByMouse: true Keys.onEscapePressed: { clear() diff --git a/ui/app/AppLayouts/Chat/stores/RootStore.qml b/ui/app/AppLayouts/Chat/stores/RootStore.qml index 35fc3de753..6871b144e6 100644 --- a/ui/app/AppLayouts/Chat/stores/RootStore.qml +++ b/ui/app/AppLayouts/Chat/stores/RootStore.qml @@ -613,6 +613,11 @@ QtObject { return Constants.LoginType.Password } + function authenticateWithCallback(callback) { + _d.authenticationCallbacks.push(callback) + chatCommunitySectionModule.authenticateWithCallback() + } + readonly property Connections communitiesModuleConnections: Connections { target: communitiesModuleInst function onImportingCommunityStateChanged(communityId, state, errorMsg) { @@ -640,6 +645,19 @@ QtObject { } readonly property QtObject _d: QtObject { + property var authenticationCallbacks: [] + + readonly property Connections chatCommunitySectionModuleConnections: Connections { + target: chatCommunitySectionModule + function onCallbackFromAuthentication(authenticated: bool) { + _d.authenticationCallbacks.forEach((callback) => { + if(!!callback) + callback(authenticated) + }) + _d.authenticationCallbacks = [] + } + } + readonly property var sectionDetailsInstantiator: Instantiator { model: SortFilterProxyModel { sourceModel: mainModuleInst.sectionsModel diff --git a/ui/app/AppLayouts/Communities/panels/OverviewSettingsFooter.qml b/ui/app/AppLayouts/Communities/panels/OverviewSettingsFooter.qml index f1ed11aeb9..afc962ead8 100644 --- a/ui/app/AppLayouts/Communities/panels/OverviewSettingsFooter.qml +++ b/ui/app/AppLayouts/Communities/panels/OverviewSettingsFooter.qml @@ -16,8 +16,9 @@ Control { property int loginType: Constants.LoginType.Password property string communityName: "" - signal primaryButtonClicked - signal secondaryButtonClicked + signal exportControlNodeClicked + signal importControlNodeClicked + signal learnMoreClicked QtObject { id: d @@ -33,6 +34,7 @@ Control { property string secondaryButtonIcon property string indicatorBgColor property string indicatorColor + property var primaryButtonAction: root.exportControlNodeClicked } contentItem: GridLayout { @@ -91,14 +93,14 @@ Control { size: StatusBaseButton.Size.Small text: d.secondaryButtonText icon.name: d.secondaryButtonIcon - onClicked: root.secondaryButtonClicked() + onClicked: root.learnMoreClicked() } StatusButton { size: StatusBaseButton.Size.Small text: d.primaryButtonText icon.name: d.primaryButtonIcon - onClicked: root.primaryButtonClicked() + onClicked: d.primaryButtonAction() } } } @@ -116,6 +118,7 @@ Control { PropertyChanges { target: d; primaryButtonIcon: Constants.authenticationIconByType[root.loginType] } PropertyChanges { target: d; secondaryButtonText: qsTr("Learn more") } PropertyChanges { target: d; secondaryButtonIcon: "external-link" } + PropertyChanges { target: d; primaryButtonAction: root.exportControlNodeClicked } }, State { name: "isNotControlNode" @@ -128,6 +131,7 @@ Control { PropertyChanges { target: d; primaryButtonIcon: "" } PropertyChanges { target: d; secondaryButtonText: qsTr("Learn more") } PropertyChanges { target: d; secondaryButtonIcon: "external-link" } + PropertyChanges { target: d; primaryButtonAction: root.importControlNodeClicked } } ] } diff --git a/ui/app/AppLayouts/Communities/panels/OverviewSettingsPanel.qml b/ui/app/AppLayouts/Communities/panels/OverviewSettingsPanel.qml index 4633d955bb..af217e6eb6 100644 --- a/ui/app/AppLayouts/Communities/panels/OverviewSettingsPanel.qml +++ b/ui/app/AppLayouts/Communities/panels/OverviewSettingsPanel.qml @@ -35,6 +35,7 @@ StackLayout { property bool editable: false property bool owned: false + property bool isControlNode: false property int loginType: Constants.LoginType.Password function navigateBack() { @@ -48,7 +49,7 @@ StackLayout { signal inviteNewPeopleClicked signal airdropTokensClicked - signal backUpClicked + signal exportControlNodeClicked clip: true @@ -133,11 +134,10 @@ StackLayout { topPadding: 0 loginType: root.loginType communityName: root.name - //TODO connect to backend - isControlNode: root.owned - onPrimaryButtonClicked: isControlNode = !isControlNode + isControlNode: root.isControlNode + onExportControlNodeClicked: root.exportControlNodeClicked() //TODO update once the domain changes - onSecondaryButtonClicked: Global.openLink(Constants.statusHelpLinkPrefix + "en/status-communities/about-the-control-node-in-status-communities") + onLearnMoreClicked: Global.openLink(Constants.statusHelpLinkPrefix + "en/status-communities/about-the-control-node-in-status-communities") } } diff --git a/ui/app/AppLayouts/Communities/popups/ExportControlNodePopup.qml b/ui/app/AppLayouts/Communities/popups/ExportControlNodePopup.qml new file mode 100644 index 0000000000..a987c206de --- /dev/null +++ b/ui/app/AppLayouts/Communities/popups/ExportControlNodePopup.qml @@ -0,0 +1,150 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQml.Models 2.14 + +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Popups.Dialog 0.1 +import StatusQ.Core.Theme 0.1 + +import utils 1.0 + +StatusDialog { + id: root + + property string communityName: "" + property string privateKey: "" + + signal deletePrivateKey + + width: 640 + title: qsTr("Move %1 community control node").arg(root.communityName) + + closePolicy: Popup.NoAutoClose + + component Paragraph: StatusBaseText { + Layout.fillWidth: true + Layout.minimumHeight: 40 + font.pixelSize: Style.current.primaryTextFontSize + lineHeightMode: Text.FixedHeight + lineHeight: 22 + wrapMode: Text.Wrap + verticalAlignment: Text.AlignVCenter + } + + component CopyButton: StatusButton { + id: copyButton + borderColor: textColor + disabledTextColor: textColor + disabledColor: normalColor + text: qsTr("Copy") + size: StatusButton.Size.Tiny + states: [ + State { + name: "success" + PropertyChanges { + target: copyButton + text: "" + icon.name: "checkmark" + normalColor: Theme.palette.successColor2 + textColor: Theme.palette.successColor1 + enabled: false + } + } + ] + + onClicked: { + width = width // break the biding to prevent the button from shrinking + copyButton.state = "success" + Backpressure.debounce(root, 2000, function () { + copyButton.state = "" + })() + } + } + + StatusScrollView { + id: scroll + anchors.fill: parent + contentWidth: availableWidth + + ColumnLayout { + id: layout + width: scroll.availableWidth + spacing: 20 + Paragraph { + text: qsTr("For a Status Community to function, it needs to have a single control node running. This installation of Status Desktop is currently the %1 community control node. To move the %1 control node to another device: ").arg(root.communityName) + } + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + Paragraph { + text: qsTr("1. Copy your Community’s private key:") + } + StatusBaseInput { + id: privateKeyTextArea + Layout.fillWidth: true + multiline: true + edit.readOnly: true + text: root.privateKey + rightComponent: CopyButton { + onClicked: { + privateKeyTextArea.edit.selectAll() + privateKeyTextArea.edit.copy() + privateKeyTextArea.edit.deselect() + } + } + } + Paragraph { + text: qsTr("2. Stop using this computer as a control node") + } + Paragraph { + text: qsTr("3. Import this Community via private key on another installation of Status desktop") + } + } + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + StatusDialogDivider { Layout.fillWidth: true } + Item { Layout.fillHeight: true } + Paragraph { + text: qsTr("I acknowledge that...") + } + + StatusCheckBox { + id: agreeToStopControl + Layout.fillWidth: true + font.pixelSize: Style.current.primaryTextFontSize + text: qsTr("%1 will stop working without a control node").arg(root.communityName) + } + StatusCheckBox { + id: agreeToSavePrivateKey + Layout.fillWidth: true + Layout.minimumHeight: 40 + font.pixelSize: Style.current.primaryTextFontSize + text: qsTr("I have saved the %1 private key").arg(root.communityName) + } + StatusCheckBox { + id: agreeToDeletePrivateKey + Layout.fillWidth: true + Layout.minimumHeight: 40 + font.pixelSize: Style.current.primaryTextFontSize + text: qsTr("If I lose the private key, %1 will be unrecoverable").arg(root.communityName) + } + } + } + } + footer: StatusDialogFooter { + rightButtons: ObjectModel { + StatusButton { + text: qsTr("Delete private key and stop control node") + enabled: agreeToStopControl.checked && agreeToSavePrivateKey.checked && agreeToDeletePrivateKey.checked + type: StatusBaseButton.Type.Danger + onClicked: { + root.deletePrivateKey() + root.close() + } + } + } + } +} diff --git a/ui/app/AppLayouts/Communities/popups/qmldir b/ui/app/AppLayouts/Communities/popups/qmldir index c67bbd2a49..0293121cc2 100644 --- a/ui/app/AppLayouts/Communities/popups/qmldir +++ b/ui/app/AppLayouts/Communities/popups/qmldir @@ -6,6 +6,7 @@ CreateCategoryPopup 1.0 CreateCategoryPopup.qml CreateChannelPopup 1.0 CreateChannelPopup.qml CreateCommunityPopup 1.0 CreateCommunityPopup.qml DiscordImportProgressDialog 1.0 DiscordImportProgressDialog.qml +ExportControlNodePopup 1.0 ExportControlNodePopup.qml HoldingsDropdown 1.0 HoldingsDropdown.qml InDropdown 1.0 InDropdown.qml InviteFriendsToCommunityPopup 1.0 InviteFriendsToCommunityPopup.qml diff --git a/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml b/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml index 79fd5858fb..01dd008b5d 100644 --- a/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml +++ b/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml @@ -36,6 +36,8 @@ StatusSectionLayout { readonly property bool isOwner: community.memberRole === Constants.memberRole.owner readonly property bool isAdmin: isOwner || community.memberRole === Constants.memberRole.admin + //TODO: get proper value from backend + readonly property bool isControlNode: isOwner readonly property string filteredSelectedTags: { let tagsArray = [] @@ -170,6 +172,7 @@ StatusSectionLayout { editable: true owned: root.community.memberRole === Constants.memberRole.owner loginType: root.rootStore.loginType + isControlNode: root.isControlNode onEdited: { const error = root.chatCommunitySectionModule.editCommunity( @@ -199,10 +202,22 @@ StatusSectionLayout { } onAirdropTokensClicked: root.goTo(Constants.CommunitySettingsSections.Airdrops) - onBackUpClicked: { - Global.openPopup(transferOwnershipPopup, { - privateKey: root.chatCommunitySectionModule.exportCommunity(root.community.id), - }) + onExportControlNodeClicked: { + if(!root.isControlNode) + return + + root.rootStore.authenticateWithCallback((authenticated) => { + if(!authenticated) + return + + Global.openExportControlNodePopup(root.community.name, root.chatCommunitySectionModule.exportCommunity(root.community.id), (popup) => { + //TODO: connect to backend + // Delete private key and remove control node status + popup.onDeletePrivateKey.connect(() => { + console.log("Delete private key") + }) + }) + }) } } diff --git a/ui/app/mainui/Popups.qml b/ui/app/mainui/Popups.qml index 6fd2012d07..dc39b813b9 100644 --- a/ui/app/mainui/Popups.qml +++ b/ui/app/mainui/Popups.qml @@ -56,6 +56,7 @@ QtObject { Global.openDownloadImageDialog.connect(openDownloadImageDialog) Global.leaveCommunityRequested.connect(openLeaveCommunityPopup) Global.openTestnetPopup.connect(openTestnetPopup) + Global.openExportControlNodePopup.connect(openExportControlNodePopup) } property var currentPopup @@ -248,6 +249,13 @@ QtObject { openPopup(testnetModal) } + function openExportControlNodePopup(communityName, privateKey, cb) { + openPopup(exportControlNodePopup, { + communityName: communityName, + privateKey: privateKey + }, cb) + } + readonly property list _components: [ Component { id: removeContactConfirmationDialog @@ -588,6 +596,13 @@ QtObject { } onCancelClicked: close() } + }, + + Component { + id: exportControlNodePopup + ExportControlNodePopup { + onClosed: destroy() + } } ] } diff --git a/ui/imports/utils/Global.qml b/ui/imports/utils/Global.qml index d9248ead85..c39cd0ebf2 100644 --- a/ui/imports/utils/Global.qml +++ b/ui/imports/utils/Global.qml @@ -45,6 +45,7 @@ QtObject { signal openOutgoingIDRequestPopup(string publicKey, var cb) signal openDeleteMessagePopup(string messageId, var messageStore) signal openDownloadImageDialog(string imageSource) + signal openExportControlNodePopup(string communityName, string privateKey, var ctaHandler) signal contactRenamed(string publicKey) signal openLink(string link)