fix `ImportCommunityPopup` issues, remove private key importing (#12554)

* feat(Storybook): added "Always on top" setting
* fix(ImportCommunityPopup): show result, remove private key support
This commit is contained in:
Igor Sirotin 2023-10-27 11:25:27 +01:00 committed by GitHub
parent dd2b58a7f5
commit a3239d9e2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 429 additions and 116 deletions

View File

@ -85,7 +85,7 @@ proc init*(self: Controller) =
self.events.on(SIGNAL_COMMUNITY_LOAD_DATA_FAILED) do(e: Args): self.events.on(SIGNAL_COMMUNITY_LOAD_DATA_FAILED) do(e: Args):
let args = CommunityArgs(e) 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.events.on(SIGNAL_COMMUNITY_INFO_ALREADY_REQUESTED) do(e: Args):
self.delegate.communityInfoAlreadyRequested() self.delegate.communityInfoAlreadyRequested()

View File

@ -108,6 +108,9 @@ method communityImported*(self: AccessInterface, community: CommunityDto) {.base
method communityDataImported*(self: AccessInterface, community: CommunityDto) {.base.} = method communityDataImported*(self: AccessInterface, community: CommunityDto) {.base.} =
raise newException(ValueError, "No implementation available") 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.} = method onImportCommunityErrorOccured*(self: AccessInterface, communityId: string, error: string) {.base.} =
raise newException(ValueError, "No implementation available") raise newException(ValueError, "No implementation available")

View File

@ -346,6 +346,10 @@ method communityImported*(self: Module, community: CommunityDto) =
method communityDataImported*(self: Module, community: CommunityDto) = method communityDataImported*(self: Module, community: CommunityDto) =
self.view.addItem(self.getCommunityItem(community)) 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) = method importCommunity*(self: Module, communityId: string) =
self.view.emitImportingCommunityStateChangedSignal(communityId, ImportCommunityState.ImportingInProgress.int, errorMsg = "") self.view.emitImportingCommunityStateChangedSignal(communityId, ImportCommunityState.ImportingInProgress.int, errorMsg = "")

View File

@ -546,6 +546,10 @@ QtObject:
proc emitImportingCommunityStateChangedSignal*(self: View, communityId: string, state: int, errorMsg: string) = proc emitImportingCommunityStateChangedSignal*(self: View, communityId: string, state: int, errorMsg: string) =
self.importingCommunityStateChanged(communityId, state, errorMsg) 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.} = proc isMemberOfCommunity*(self: View, communityId: string, pubKey: string): bool {.slot.} =
let sectionItem = self.model.getItemById(communityId) let sectionItem = self.model.getItemById(communityId)
if (section_item.id == ""): if (section_item.id == ""):

View File

@ -140,6 +140,20 @@ ApplicationWindow {
onClicked: settingsPopup.open() 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 { CheckBox {
id: darkModeCheckBox id: darkModeCheckBox
@ -346,6 +360,7 @@ Tips:
property alias darkMode: darkModeCheckBox.checked property alias darkMode: darkModeCheckBox.checked
property alias hotReloading: hotReloaderControls.enabled property alias hotReloading: hotReloaderControls.enabled
property alias figmaToken: settingsLayout.figmaToken property alias figmaToken: settingsLayout.figmaToken
property alias windowAlwaysOnTop: windowAlwaysOnTopCheckBox.checked
} }
Shortcut { Shortcut {

View File

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

View File

@ -62,6 +62,8 @@ QtObject {
signal communityInfoAlreadyRequested() signal communityInfoAlreadyRequested()
signal communityInfoRequestCompleted(string communityId, string errorMsg)
function createCommunity(args = { function createCommunity(args = {
name: "", name: "",
description: "", description: "",
@ -120,19 +122,14 @@ QtObject {
root.communitiesModuleInst.prepareTokenModelForCommunity(publicKey); root.communitiesModuleInst.prepareTokenModelForCommunity(publicKey);
} }
function getCommunityDetails(communityId, importing = false) { function getCommunityDetails(communityId) {
const publicKey = Utils.isCompressedPubKey(communityId) const publicKey = Utils.isCompressedPubKey(communityId)
? Utils.changeCommunityKeyCompression(communityId) ? Utils.changeCommunityKeyCompression(communityId)
: communityId : communityId
try { try {
const communityJson = root.communitiesList.getSectionByIdJson(publicKey) const communityJson = root.communitiesList.getSectionByIdJson(publicKey)
if (!!communityJson)
if (!communityJson) { return JSON.parse(communityJson)
root.requestCommunityInfo(publicKey, importing)
return null
}
return JSON.parse(communityJson);
} catch (e) { } catch (e) {
console.error("Error parsing community", e) console.error("Error parsing community", e)
} }
@ -251,5 +248,9 @@ QtObject {
function onCommunityInfoAlreadyRequested() { function onCommunityInfoAlreadyRequested() {
root.communityInfoAlreadyRequested() root.communityInfoAlreadyRequested()
} }
function onCommunityInfoRequestCompleted(communityId, erorrMsg) {
root.communityInfoRequestCompleted(communityId, erorrMsg)
}
} }
} }

View File

@ -504,7 +504,7 @@ QtObject {
id: importCommunitiesPopupComponent id: importCommunitiesPopupComponent
ImportCommunityPopup { ImportCommunityPopup {
store: root.communitiesStore store: root.communitiesStore
onJoinCommunity: { onJoinCommunityRequested: {
close() close()
openCommunityIntroPopup(communityId, openCommunityIntroPopup(communityId,
communityDetails.name, communityDetails.name,

View File

@ -15,111 +15,97 @@ import StatusQ.Controls 0.1
StatusDialog { StatusDialog {
id: root id: root
property var store property var store
property alias text: keyInput.text
signal joinCommunityRequested(string communityId, var communityDetails)
width: 640 width: 640
title: qsTr("Import Community") title: qsTr("Import Community")
signal joinCommunity(string communityId, var communityDetails)
QtObject { QtObject {
id: d id: d
property string importErrorMessage property string importErrorMessage
property bool loading readonly property bool communityFound: !!d.communityDetails && !!d.communityDetails.name
property bool communityFound: (d.communityDetails !== null && !!d.communityDetails.name) property var communityDetails: null
property var communityDetails: {
if (!isInputValid) {
loading = false
return null
}
loading = true
const key = isPublicKey ? Utils.getCompressedPk(publicKey) :
root.store.getCommunityPublicKeyFromPrivateKey(inputKey, true /*importing*/);
const details = root.store.getCommunityDetails(key) property var requestedCommunityDetails: null
if (!!details) // the above can return `null` in which case we continue loading
loading = false
return details
}
readonly property string inputErrorMessage: isInputValid ? "" : qsTr("Invalid key") readonly property string inputErrorMessage: isInputValid ? "" : qsTr("Invalid key")
readonly property string errorMessage: importErrorMessage || inputErrorMessage readonly property string errorMessage: importErrorMessage || inputErrorMessage
readonly property string inputKey: keyInput.text.trim() 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: { readonly property string publicKey: {
if (!Utils.isStatusDeepLink(inputKey)) { if (Utils.isStatusDeepLink(inputKey)) {
const key = Utils.dropCommunityLinkPrefix(inputKey) const linkData = Utils.getCommunityDataFromSharedLink(inputKey)
if (!Utils.isCommunityPublicKey(key)) return !linkData ? "" : linkData.communityId
return "" }
if (!Utils.isCompressedPubKey(key)) if (!Utils.isCommunityPublicKey(inputKey))
return key return ""
return Utils.changeCommunityKeyCompression(key) if (!Utils.isCompressedPubKey(inputKey))
} else { return inputKey
return Utils.getCommunityDataFromSharedLink(inputKey).communityId; 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 { onPublicKeyChanged: {
interval: 20000 // 20s // call later to make sure all proeprties used by `updateCommunityDetails` are udpated
running: d.loading Qt.callLater(() => { d.updateCommunityDetails(true) })
onTriggered: {
d.loading = false
d.importErrorMessage = qsTr("Timeout reached while getting community info")
} }
} }
Connections { Connections {
target: root.store target: root.store
function onImportingCommunityStateChanged(communityId, state, errorMsg) { function onCommunityInfoRequestCompleted(communityId, errorMsg) {
switch (state) if (!d.communityInfoRequested)
{ return
case Constants.communityImported:
const community = root.store.getCommunityDetailsAsJson(communityId) d.communityInfoRequested = false
d.loading = false
d.communityFound = true if (errorMsg !== "") {
d.communityDetails = community d.importErrorMessage = qsTr("Couldn't find 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
return return
} }
d.updateCommunityDetails(false)
d.importErrorMessage = ""
} }
} }
footer: StatusDialogFooter { footer: StatusDialogFooter {
rightButtons: ObjectModel { rightButtons: ObjectModel {
StatusButton { StatusButton {
enabled: d.communityFound && ((d.isPublicKey) || (d.isPrivateKey && agreeToKeepOnline.checked)) enabled: d.isInputValid && d.communityFound
loading: d.loading loading: d.isInputValid && !d.communityFound && d.communityInfoRequested
text: d.isPrivateKey && d.communityFound ? qsTr("Make this device the control node for %1").arg(d.communityDetails.name) text: qsTr("Import")
: qsTr("Import")
onClicked: { onClicked: {
if (d.isPrivateKey) { root.joinCommunityRequested(d.publicKey, d.communityDetails)
root.store.importCommunity(d.privateKey);
root.close();
} else if (d.isPublicKey) {
root.joinCommunity(d.publicKey, d.communityDetails);
}
} }
} }
} }
@ -139,7 +125,7 @@ StatusDialog {
StatusBaseText { StatusBaseText {
id: infoText1 id: infoText1
Layout.fillWidth: true 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 wrapMode: Text.WordWrap
font.pixelSize: Style.current.additionalTextSize font.pixelSize: Style.current.additionalTextSize
color: Theme.palette.baseColor1 color: Theme.palette.baseColor1
@ -178,43 +164,15 @@ StatusDialog {
font.pixelSize: Style.current.additionalTextSize font.pixelSize: Style.current.additionalTextSize
visible: !!d.inputKey visible: !!d.inputKey
text: { text: {
if (d.errorMessage !== "") { if (d.errorMessage !== "")
return d.errorMessage return d.errorMessage
} if (d.isInputValid)
if (d.isPrivateKey) {
return qsTr("Private key detected")
}
if (d.isPublicKey) {
return qsTr("Public key detected") return qsTr("Public key detected")
} return ""
} }
color: d.errorMessage === "" ? Theme.palette.successColor1 : Theme.palette.dangerColor1 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")
}
}
} }
} }
} }

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit 27b770c41bd2896ebc30b01985eeea1fbe642fba Subproject commit 74396b461df0c18593262a67c9fd79d706287f7a