feat(Import Control Node): Adding ImportControlNode flow

1. Create a new popup as per Design: ImportControlNodePopup
2. Add the popup in storybook
3. Integrate ImportControlNodePopup in the app
This commit is contained in:
Alex Jbanca 2023-07-21 11:44:10 +03:00 committed by Alex Jbanca
parent 83303a011e
commit 4aaae242b5
12 changed files with 478 additions and 5 deletions

View File

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

View File

@ -253,5 +253,8 @@
],
"ExportControlNodePopup": [
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba⎜Desktop?type=design&node-id=31171-627949&mode=design&t=WxK2N6sL8idHBKMZ-0"
],
"ImportControlNodePopup": [
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba⎜Desktop?type=design&node-id=31171-628434&mode=design&t=IFFCNUpRS3oQbzAR-0"
]
}

View File

@ -0,0 +1,193 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml 2.15
import AppLayouts.Communities.popups 1.0
import utils 1.0
import Storybook 1.0
SplitView {
id: root
orientation: Qt.Vertical
Logs { id: logs }
SplitView {
SplitView.fillWidth: true
SplitView.fillHeight: true
Pane {
id: mainPane
SplitView.fillWidth: true
SplitView.fillHeight: true
PopupBackground {
anchors.fill: parent
}
Button {
anchors.centerIn: parent
text: "Reopen"
onClicked: popupComponent.createObject(mainPane)
}
Component.onCompleted: popupComponent.createObject(mainPane)
}
Pane {
SplitView.preferredWidth: 300
contentItem: ColumnLayout {
Label {
text: "Matching private key"
}
TextEdit {
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: "red"
border.width: 1
}
id: matchingPrivateKey
Layout.fillWidth: true
wrapMode: TextEdit.Wrap
readOnly: true
text: "0x0454f2231543ba02583e4c55e513a75092a4f2c86c04d0796b195e964656d6cd"
}
Button {
text: "Copy"
onClicked: {
matchingPrivateKey.selectAll()
matchingPrivateKey.copy()
matchingPrivateKey.deselect()
}
}
Label {
text: "Mismatching private key"
}
TextEdit {
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: "red"
border.width: 1
}
id: mismatchingPrivateKey
Layout.fillWidth: true
wrapMode: TextEdit.Wrap
readOnly: true
text: "0x0454f2231543ba02583e4c55e513a75092a4f2c86c04d0796b195e964656d6ce"
}
Button {
text: "Copy"
onClicked: {
mismatchingPrivateKey.selectAll()
mismatchingPrivateKey.copy()
mismatchingPrivateKey.deselect()
}
}
Label {
text: "Load in progress private key"
}
TextEdit {
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: "red"
border.width: 1
}
id: loadInProgressPrivateKey
Layout.fillWidth: true
wrapMode: TextEdit.Wrap
readOnly: true
text: "0x0454f2231543ba02583e4c55e513a75092a4f2c86c04d0796b195e964656d6ca"
}
Button {
text: "Copy"
onClicked: {
loadInProgressPrivateKey.selectAll()
loadInProgressPrivateKey.copy()
loadInProgressPrivateKey.deselect()
}
}
Item {
Layout.fillHeight: true
}
}
}
}
QtObject {
id: d
readonly property var community: QtObject {
property string id: "1"
property string name: "Socks"
property var members: { "count": 5 }
property string image: Style.png("tokens/UNI")
property string color: "orchid"
}
readonly property var otherCommunity: QtObject {
property string id: "2"
property string name: "Socks"
property var members: { "count": 5 }
property string image: Style.png("tokens/UNI")
property string color: "orchid"
}
readonly property Timer timer: Timer {
//id: _timer
interval: 1000
repeat: false
function callWithDelay(cb) {
d.timer.triggered.connect(cb);
d.timer.triggered.connect(function release () {
d.timer.triggered.disconnect(cb);
d.timer.triggered.disconnect(release);
});
d.timer.start();
}
}
}
Component {
id: popupComponent
ImportControlNodePopup {
id: popup
anchors.centerIn: parent
modal: false
visible: true
onRequestCommunityInfo: {
logs.logEvent("ImportControlNodePopup::onRequestCommunityInfo", ["private key"], [privateKey])
if(privateKey === matchingPrivateKey.text)
d.timer.callWithDelay(() => popup.setCommunityInfo(d.community))
else if (privateKey === mismatchingPrivateKey.text)
d.timer.callWithDelay(() => popup.setCommunityInfo(d.otherCommunity))
}
community: d.community
}
}
LogsAndControlsPanel {
id: logsAndControlsPanel
SplitView.minimumHeight: 100
SplitView.preferredHeight: 160
logsView.logText: logs.logText
}
}

View File

@ -77,6 +77,8 @@ QtObject {
signal importingCommunityStateChanged(string communityId, int state, string errorMsg)
signal communityAdded(string communityId)
signal communityInfoAlreadyRequested()
signal communityAccessRequested(string communityId)
@ -631,6 +633,10 @@ QtObject {
function onCommunityAccessRequested(communityId) {
root.communityAccessRequested(communityId)
}
function onCommunityAdded(communityId) {
root.communityAdded(communityId)
}
}
readonly property Connections mainModuleInstConnections: Connections {

View File

@ -222,7 +222,6 @@ Item {
target: root.store
function onImportingCommunityStateChanged(communityId, state, errorMsg) {
const community = root.store.getCommunityDetailsAsJson(communityId)
let title = ""
let subTitle = ""

View File

@ -52,6 +52,7 @@ StackLayout {
signal inviteNewPeopleClicked
signal airdropTokensClicked
signal exportControlNodeClicked
signal importControlNodeClicked
clip: true
@ -137,6 +138,7 @@ StackLayout {
communityName: root.name
isControlNode: root.isControlNode
onExportControlNodeClicked: root.exportControlNodeClicked()
onImportControlNodeClicked: root.importControlNodeClicked()
//TODO update once the domain changes
onLearnMoreClicked: Global.openLink(Constants.statusHelpLinkPrefix + "status-communities/about-the-control-node-in-status-communities")
}

View File

@ -0,0 +1,207 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml 2.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
required property var community
signal importControlNode(string privateKey)
signal requestCommunityInfo(string privateKey)
function setCommunityInfo(communityInfo) {
d.requestedCommunityInfo = communityInfo
d.privateKeyCheckInProgress = false
}
onRequestCommunityInfo: d.privateKeyCheckInProgress = true
width: 640
height: Math.max(552, implicitHeight)
title: qsTr("Make this device the control node for %1").arg(root.community.name)
closePolicy: Popup.NoAutoClose
component Paragraph: StatusBaseText {
Layout.fillWidth: true
Layout.preferredHeight: 40
font.pixelSize: Style.current.primaryTextFontSize
lineHeightMode: Text.FixedHeight
lineHeight: 22
wrapMode: Text.Wrap
verticalAlignment: Text.AlignVCenter
}
component PasteButton: StatusButton {
id: pasteButton
borderColor: textColor
text: qsTr("Paste")
size: StatusButton.Size.Tiny
}
component ChatDetails: Control {
verticalPadding: 6
horizontalPadding: 4
contentItem: RowLayout {
StatusChatInfoButton {
id: communityInfoButton
Layout.alignment: Qt.AlignVCenter
title: community.name
subTitle: qsTr("%n member(s)", "", community.members.count || 0)
asset.name: community.image
asset.color: community.color
asset.isImage: true
type: StatusChatInfoButton.Type.OneToOneChat
hoverEnabled: false
visible: false
}
Item { Layout.fillWidth: true }
StatusBaseText {
id: detectionLabel
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
font.pixelSize: Style.current.additionalTextSize
visible: !!text
}
}
states: [
State {
name: "matchingPrivateKey"
when: d.isPrivateKeyMatching
PropertyChanges { target: detectionLabel; text: qsTr("Private key is valid") }
PropertyChanges { target: detectionLabel; color: Theme.palette.successColor1 }
PropertyChanges { target: communityInfoButton; visible: true }
},
State {
name: "mismatchingPrivateKey"
when: !d.isPrivateKeyMatching && d.isPrivateKey && !d.privateKeyCheckInProgress
PropertyChanges { target: detectionLabel; text: qsTr("This is not the correct private key for %1").arg(root.community.name) }
PropertyChanges { target: detectionLabel; color: Theme.palette.dangerColor1 }
},
State {
name: "checking"
when: d.privateKeyCheckInProgress
PropertyChanges { target: detectionLabel; text: qsTr("Checking private key...") }
PropertyChanges { target: detectionLabel; color: Theme.palette.baseColor1 }
},
State {
name: "invalidPrivateKey"
when: !d.isPrivateKey && d.isPrivateKeyInserted
PropertyChanges { target: detectionLabel; text: qsTr("This is not a private key") }
PropertyChanges { target: detectionLabel; color: Theme.palette.dangerColor1 }
}
]
}
QtObject {
id: d
readonly property bool isPrivateKey: Utils.isPrivateKey(privateKeyTextArea.text)
readonly property bool isPrivateKeyMatching: d.requestedCommunityInfo ? d.requestedCommunityInfo.id === community.id : false
readonly property bool isPrivateKeyInserted: privateKeyTextArea.text.length > 0
property bool privateKeyCheckInProgress: false
property var requestedCommunityInfo: undefined
onIsPrivateKeyChanged: {
if(!isPrivateKey) {
requestedCommunityInfo = undefined
privateKeyCheckInProgress = false
return
}
privateKeyCheckInProgress = true
requestedCommunityInfo = undefined
requestCommunityInfo(privateKeyTextArea.text)
}
}
ColumnLayout {
id: mainLayout
anchors.fill: parent
spacing: 0
Paragraph {
Layout.preferredHeight: 22
Layout.bottomMargin: Style.current.halfPadding
text: qsTr("To move the %1 control node to this device: ").arg(root.community.name)
}
Paragraph {
text: qsTr("1. Stop using any other devices as the control node for this Community")
}
Paragraph {
text: qsTr("2. Paste the Communitys private key below:")
}
StatusBaseInput {
id: privateKeyTextArea
Layout.fillWidth: true
Layout.preferredHeight: 86
rightPadding: Style.current.padding
multiline: true
valid: d.isPrivateKey || !d.isPrivateKeyInserted
placeholderText: qsTr("e.g. %1").arg("0x0454f2231543ba02583e4c55e513a75092a4f2c86c04d0796b195e964656d6cd94b8237c64ef668eb0fe268387adc3fe699bce97190a631563c82b718c19cf1fb8")
rightComponent: PasteButton {
onClicked: {
privateKeyTextArea.edit.clear()
privateKeyTextArea.edit.paste()
}
}
}
ChatDetails {
Layout.topMargin: Style.current.halfPadding
Layout.fillWidth: true
Layout.minimumHeight: 46
}
Item {
Layout.fillHeight: true
Layout.minimumHeight: Style.current.xlPadding
}
ColumnLayout {
id: agreementLayout
Layout.fillWidth: true
Layout.fillHeight: true
spacing: mainLayout.spacing
visible: d.isPrivateKeyMatching
StatusDialogDivider {
Layout.fillWidth: true
}
Paragraph {
Layout.topMargin: Style.current.padding
text: qsTr("I acknowledge that...")
}
StatusCheckBox {
id: agreementCheckBox
Layout.fillWidth: true
font.pixelSize: Style.current.primaryTextFontSize
text: qsTr("I must keep this device online and running Status for the Community to function")
}
}
}
footer: StatusDialogFooter {
rightButtons: ObjectModel {
StatusButton {
text: qsTr("Make this device the control node for %1").arg(root.community.name)
enabled: d.isPrivateKeyMatching && agreementCheckBox.checked
onClicked: {
root.importControlNode(privateKeyTextArea.text)
root.close()
}
}
}
}
}

View File

@ -8,6 +8,7 @@ CreateCommunityPopup 1.0 CreateCommunityPopup.qml
DiscordImportProgressDialog 1.0 DiscordImportProgressDialog.qml
ExportControlNodePopup 1.0 ExportControlNodePopup.qml
HoldingsDropdown 1.0 HoldingsDropdown.qml
ImportControlNodePopup 1.0 ImportControlNodePopup.qml
InDropdown 1.0 InDropdown.qml
InviteFriendsToCommunityPopup 1.0 InviteFriendsToCommunityPopup.qml
MembersDropdown 1.0 MembersDropdown.qml

View File

@ -37,8 +37,7 @@ 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 bool isControlNode: community.isControlNode
readonly property string filteredSelectedTags: {
let tagsArray = []
@ -223,6 +222,13 @@ StatusSectionLayout {
})
})
}
onImportControlNodeClicked: {
if(root.isControlNode)
return
Global.openImportControlNodePopup(root.community, d.importControlNodePopupOpened)
}
}
MembersSettingsPanel {
@ -535,6 +541,44 @@ StatusSectionLayout {
}
}
}
function requestCommunityInfoWithCallback(privateKey, callback) {
if(!callback) return
//success
root.rootStore.communityAdded.connect(function communityAddedHandler(communityId) {
root.rootStore.communityAdded.disconnect(communityAddedHandler)
let community = null
try {
const communityJson = root.rootStore.getSectionByIdJson(communityId)
community = JSON.parse(communityJson)
} catch (e) {
console.warn("Error parsing community json: ", communityJson, " error: ", e.message)
}
callback(community)
})
//error
root.rootStore.importingCommunityStateChanged.connect(function communityImportingStateChangedHandler(communityId, status) {
root.rootStore.importingCommunityStateChanged.disconnect(communityImportingStateChangedHandler)
if(status === Constants.communityImportingError) {
callback(null)
}
})
root.rootStore.requestCommunityInfo(privateKey, false)
}
function importControlNodePopupOpened(popup) {
popup.requestCommunityInfo.connect((privateKey) => {
requestCommunityInfoWithCallback(privateKey, popup.setCommunityInfo)
})
popup.importControlNode.connect((privateKey) => {
root.rootStore.importCommunity(privateKey)
})
}
}
StatusQUtils.ModelChangeTracker {

View File

@ -57,6 +57,7 @@ QtObject {
Global.leaveCommunityRequested.connect(openLeaveCommunityPopup)
Global.openTestnetPopup.connect(openTestnetPopup)
Global.openExportControlNodePopup.connect(openExportControlNodePopup)
Global.openImportControlNodePopup.connect(openImportControlNodePopup)
}
property var currentPopup
@ -256,6 +257,10 @@ QtObject {
}, cb)
}
function openImportControlNodePopup(community, cb) {
openPopup(importControlNodePopup, {community: community}, cb)
}
readonly property list<Component> _components: [
Component {
id: removeContactConfirmationDialog
@ -603,6 +608,14 @@ QtObject {
ExportControlNodePopup {
onClosed: destroy()
}
},
Component {
id: importControlNodePopup
ImportControlNodePopup {
onClosed: destroy()
}
}
]
}

View File

@ -45,7 +45,8 @@ 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 openExportControlNodePopup(string communityName, string privateKey, var cb)
signal openImportControlNodePopup(var community, var cb)
signal contactRenamed(string publicKey)
signal openLink(string link)