feat: Export control node (except backend for primary action)

Adding the export control node popup. The popup is behind an authentication wall.
1. Create ExportControlNodePopup qml
2. Add the popup in storybook
3. Create authentication flow with qml callback to open the popup after authentication
4. Add the popup open action in Global.qml
This commit is contained in:
Alex Jbanca 2023-07-19 08:58:21 +03:00 committed by Alex Jbanca
parent 64422afed7
commit 27c159d464
16 changed files with 305 additions and 14 deletions

View File

@ -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()

View File

@ -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")

View File

@ -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)

View File

@ -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.}

View File

@ -273,6 +273,10 @@ ListModel {
title: "UserAgreementPopup"
section: "Popups"
}
ListElement {
title: "ExportControlNodePopup"
section: "Popups"
}
ListElement {
title: "StatusButton"
section: "Controls"

View File

@ -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"
]
}

View File

@ -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
}
}

View File

@ -40,6 +40,7 @@ ColumnLayout {
Layout.fillWidth: true
placeholderText: "search"
selectByMouse: true
Keys.onEscapePressed: {
clear()

View File

@ -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

View File

@ -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 }
}
]
}

View File

@ -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")
}
}

View File

@ -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 Communitys 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()
}
}
}
}
}

View File

@ -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

View File

@ -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")
})
})
})
}
}

View File

@ -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<Component> _components: [
Component {
id: removeContactConfirmationDialog
@ -588,6 +596,13 @@ QtObject {
}
onCancelClicked: close()
}
},
Component {
id: exportControlNodePopup
ExportControlNodePopup {
onClosed: destroy()
}
}
]
}

View File

@ -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)