feat(communities): allow community import via public key
Status allows for importing communities via their private keys. There's a requested feature that users should be able to import a community via its public key as well. This will behave differently as private keys won't give users ownership of the communities. When importing via a (compressed) public key, Status will try to fetch information about the community from the network. If it finds such information, it'll load it into the app and create a communitiy view from which users can then request access. If it can't find a community or community information in the network, the user will get a dedicated error message. This commit also refactors the `ImportCommunityPopup` such that it uses `StatusDialog` and updates the copy accordingly since importing via public key is now possible as well. Closes #8339
This commit is contained in:
parent
16be01495a
commit
b510b33730
|
@ -35,7 +35,12 @@ proc init*(self: Controller) =
|
|||
|
||||
self.events.on(SIGNAL_COMMUNITY_DATA_IMPORTED) do(e:Args):
|
||||
let args = CommunityArgs(e)
|
||||
self.delegate.communityAdded(args.community)
|
||||
self.delegate.communityImported(args.community)
|
||||
|
||||
self.events.on(SIGNAL_COMMUNITY_LOAD_DATA_FAILED) do(e: Args):
|
||||
let args = CommunityArgs(e)
|
||||
self.delegate.onImportCommunityErrorOccured(args.community.id, args.error)
|
||||
|
||||
|
||||
self.events.on(SIGNAL_CURATED_COMMUNITY_FOUND) do(e:Args):
|
||||
let args = CuratedCommunityArgs(e)
|
||||
|
@ -48,7 +53,7 @@ proc init*(self: Controller) =
|
|||
self.events.on(SIGNAL_COMMUNITY_IMPORTED) do(e:Args):
|
||||
let args = CommunityArgs(e)
|
||||
if(args.error.len > 0):
|
||||
self.delegate.onImportCommunityErrorOccured(args.error)
|
||||
self.delegate.onImportCommunityErrorOccured(args.community.id, args.error)
|
||||
else:
|
||||
self.delegate.communityImported(args.community)
|
||||
|
||||
|
|
|
@ -113,7 +113,7 @@ method curatedCommunityEdited*(self: AccessInterface, community: CuratedCommunit
|
|||
method communityImported*(self: AccessInterface, community: CommunityDto) {.base.} =
|
||||
raise newException(ValueError, "No implementation available")
|
||||
|
||||
method onImportCommunityErrorOccured*(self: AccessInterface, error: string) {.base.} =
|
||||
method onImportCommunityErrorOccured*(self: AccessInterface, communityId: string, error: string) {.base.} =
|
||||
raise newException(ValueError, "No implementation available")
|
||||
|
||||
method viewDidLoad*(self: AccessInterface) {.base.} =
|
||||
|
|
|
@ -301,14 +301,14 @@ method deleteCommunityChat*(self: Module, communityId: string, channelId: string
|
|||
|
||||
method communityImported*(self: Module, community: CommunityDto) =
|
||||
self.view.addItem(self.getCommunityItem(community))
|
||||
self.view.emitImportingCommunityStateChangedSignal(ImportCommunityState.Imported.int, "")
|
||||
self.view.emitImportingCommunityStateChangedSignal(community.id, ImportCommunityState.Imported.int, "")
|
||||
|
||||
method importCommunity*(self: Module, communityKey: string) =
|
||||
self.view.emitImportingCommunityStateChangedSignal(ImportCommunityState.ImportingInProgress.int, "")
|
||||
self.view.emitImportingCommunityStateChangedSignal(communityKey, ImportCommunityState.ImportingInProgress.int, "")
|
||||
self.controller.importCommunity(communityKey)
|
||||
|
||||
method onImportCommunityErrorOccured*(self: Module, error: string) =
|
||||
self.view.emitImportingCommunityStateChangedSignal(ImportCommunityState.ImportingError.int, error)
|
||||
method onImportCommunityErrorOccured*(self: Module, communityId: string, error: string) =
|
||||
self.view.emitImportingCommunityStateChangedSignal(communityId, ImportCommunityState.ImportingError.int, error)
|
||||
|
||||
method requestExtractDiscordChannelsAndCategories*(self: Module, filesToImport: seq[string]) =
|
||||
self.view.setDiscordDataExtractionInProgress(true)
|
||||
|
|
|
@ -471,9 +471,9 @@ QtObject:
|
|||
proc importCommunity*(self: View, communityKey: string) {.slot.} =
|
||||
self.delegate.importCommunity(communityKey)
|
||||
|
||||
proc importingCommunityStateChanged*(self:View, state: int, errorMsg: string) {.signal.}
|
||||
proc emitImportingCommunityStateChangedSignal*(self: View, state: int, errorMsg: string) =
|
||||
self.importingCommunityStateChanged(state, errorMsg)
|
||||
proc importingCommunityStateChanged*(self:View, communityId: string, state: int, errorMsg: string) {.signal.}
|
||||
proc emitImportingCommunityStateChangedSignal*(self: View, communityId: string, state: int, errorMsg: string) =
|
||||
self.importingCommunityStateChanged(communityId, state, errorMsg)
|
||||
|
||||
proc isMemberOfCommunity*(self: View, communityId: string, pubKey: string): bool {.slot.} =
|
||||
let sectionItem = self.model.getItemById(communityId)
|
||||
|
|
|
@ -235,6 +235,9 @@ method onMyRequestAdded*(self: AccessInterface) {.base.} =
|
|||
method activateStatusDeepLink*(self: AccessInterface, statusDeepLink: string) {.base.} =
|
||||
raise newException(ValueError, "No implementation available")
|
||||
|
||||
method setCommunityIdToSpectate*(self: AccessInterface, commnityId: string) {.base.} =
|
||||
raise newException(ValueError, "No implementation available")
|
||||
|
||||
# This way (using concepts) is used only for the modules managed by AppController
|
||||
type
|
||||
DelegateInterface* = concept c
|
||||
|
|
|
@ -575,6 +575,9 @@ method emitStoringPasswordSuccess*[T](self: Module[T]) =
|
|||
method emitMailserverNotWorking*[T](self: Module[T]) =
|
||||
self.view.emitMailserverNotWorking()
|
||||
|
||||
method setCommunityIdToSpectate*[T](self: Module[T], communityId: string) =
|
||||
self.statusUrlCommunityToSpectate = communityId
|
||||
|
||||
method getActiveSectionId*[T](self: Module[T]): string =
|
||||
return self.controller.getActiveSectionId()
|
||||
|
||||
|
|
|
@ -238,4 +238,7 @@ QtObject:
|
|||
|
||||
proc destroyKeycardSharedModuleFlow*(self: View) {.signal.}
|
||||
proc emitDestroyKeycardSharedModuleFlow*(self: View) =
|
||||
self.destroyKeycardSharedModuleFlow()
|
||||
self.destroyKeycardSharedModuleFlow()
|
||||
|
||||
proc setCommunityIdToSpectate*(self: View, communityId: string) {.slot.} =
|
||||
self.delegate.setCommunityIdToSpectate(communityId)
|
||||
|
|
|
@ -8,4 +8,5 @@ type
|
|||
const asyncRequestCommunityInfoTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
|
||||
let arg = decode[AsyncRequestCommunityInfoTaskArg](argEncoded)
|
||||
let response = status_go.requestCommunityInfo(arg.communityId)
|
||||
arg.finish(response)
|
||||
let tpl: tuple[communityId: string, response: RpcResponse[JsonNode]] = (arg.communityId, response)
|
||||
arg.finish(tpl)
|
||||
|
|
|
@ -100,6 +100,7 @@ const SIGNAL_COMMUNITY_CREATED* = "communityCreated"
|
|||
const SIGNAL_COMMUNITY_ADDED* = "communityAdded"
|
||||
const SIGNAL_COMMUNITY_IMPORTED* = "communityImported"
|
||||
const SIGNAL_COMMUNITY_DATA_IMPORTED* = "communityDataImported" # This one is when just loading the data with requestCommunityInfo
|
||||
const SIGNAL_COMMUNITY_LOAD_DATA_FAILED* = "communityLoadDataFailed"
|
||||
const SIGNAL_COMMUNITY_EDITED* = "communityEdited"
|
||||
const SIGNAL_COMMUNITIES_UPDATE* = "communityUpdated"
|
||||
const SIGNAL_COMMUNITY_CHANNEL_CREATED* = "communityChannelCreated"
|
||||
|
@ -1165,16 +1166,21 @@ QtObject:
|
|||
error "Error reordering category channel", msg = e.msg, communityId, categoryId, position
|
||||
|
||||
|
||||
proc asyncCommunityInfoLoaded*(self: Service, rpcResponse: string) {.slot.} =
|
||||
let rpcResponseObj = rpcResponse.parseJson
|
||||
if (rpcResponseObj{"error"}.kind != JNull):
|
||||
let error = Json.decode($rpcResponseObj["error"], RpcError)
|
||||
proc asyncCommunityInfoLoaded*(self: Service, communityIdAndRpcResponse: string) {.slot.} =
|
||||
let rpcResponseObj = communityIdAndRpcResponse.parseJson
|
||||
if (rpcResponseObj{"response"}{"error"}.kind != JNull):
|
||||
let error = Json.decode($rpcResponseObj["response"]["error"], RpcError)
|
||||
error "Error requesting community info", msg = error.message
|
||||
return
|
||||
|
||||
let community = rpcResponseObj{"result"}.toCommunityDto()
|
||||
self.allCommunities[community.id] = community
|
||||
self.events.emit(SIGNAL_COMMUNITY_DATA_IMPORTED, CommunityArgs(community: community))
|
||||
var community = rpcResponseObj{"response"}{"result"}.toCommunityDto()
|
||||
if community.id != "":
|
||||
self.allCommunities[community.id] = community
|
||||
self.events.emit(SIGNAL_COMMUNITY_DATA_IMPORTED, CommunityArgs(community: community))
|
||||
else:
|
||||
community.id = rpcResponseObj{"response"}{"communityId"}.getStr()
|
||||
self.events.emit(SIGNAL_COMMUNITY_LOAD_DATA_FAILED, CommunityArgs(community: community, error: "Couldn't find community info"))
|
||||
|
||||
|
||||
proc requestCommunityInfo*(self: Service, communityId: string) =
|
||||
try:
|
||||
|
|
|
@ -47,6 +47,8 @@ QtObject {
|
|||
|
||||
property var advancedModule: profileSectionModule.advancedModule
|
||||
|
||||
signal importingCommunityStateChanged(string communityId, int state, string errorMsg)
|
||||
|
||||
function setActiveCommunity(communityId) {
|
||||
mainModule.setActiveSectionById(communityId);
|
||||
}
|
||||
|
@ -570,4 +572,11 @@ QtObject {
|
|||
function hex2Eth(value) {
|
||||
return globalUtils.hex2Eth(value)
|
||||
}
|
||||
|
||||
readonly property Connections communitiesModuleConnections: Connections {
|
||||
target: communitiesModuleInst
|
||||
function onImportingCommunityStateChanged(communityId, state, errorMsg) {
|
||||
root.importingCommunityStateChanged(communityId, state, errorMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -319,8 +319,8 @@ Item {
|
|||
}
|
||||
|
||||
Connections {
|
||||
target: root.store.communitiesModuleInst
|
||||
function onImportingCommunityStateChanged(state, errorMsg) {
|
||||
target: root.store
|
||||
function onImportingCommunityStateChanged(communityId, state, errorMsg) {
|
||||
let title = ""
|
||||
let loading = false
|
||||
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import QtQuick 2.13
|
||||
import QtQml.Models 2.2
|
||||
|
||||
import utils 1.0
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property var communitiesModuleInst: communitiesModule
|
||||
property var mainModuleInst: mainModule
|
||||
|
||||
readonly property var curatedCommunitiesModel: root.communitiesModuleInst.curatedCommunities
|
||||
|
||||
|
@ -51,6 +54,8 @@ QtObject {
|
|||
|
||||
property string communityTags: communitiesModuleInst.tags
|
||||
|
||||
signal importingCommunityStateChanged(string communityId, int state, string errorMsg)
|
||||
|
||||
function createCommunity(args = {
|
||||
name: "",
|
||||
description: "",
|
||||
|
@ -85,6 +90,16 @@ QtObject {
|
|||
root.communitiesModuleInst.importCommunity(communityKey);
|
||||
}
|
||||
|
||||
function requestCommunityInfo(communityKey) {
|
||||
let publicKey = communityKey
|
||||
if (Utils.isCompressedPubKey(communityKey)) {
|
||||
publicKey = Utils.changeCommunityKeyCompression(communityKey)
|
||||
}
|
||||
|
||||
root.mainModuleInst.setCommunityIdToSpectate(publicKey)
|
||||
root.communitiesModuleInst.requestCommunityInfo(publicKey);
|
||||
}
|
||||
|
||||
function setActiveCommunity(communityId) {
|
||||
mainModule.setActiveSectionById(communityId);
|
||||
}
|
||||
|
@ -155,4 +170,12 @@ QtObject {
|
|||
args.image.src, args.image.AX, args.image.AY, args.image.BX, args.image.BY,
|
||||
args.options.historyArchiveSupportEnabled, args.options.pinMessagesAllowedForMembers, from, args.options.encrypted);
|
||||
}
|
||||
|
||||
|
||||
readonly property Connections connections: Connections {
|
||||
target: communitiesModuleInst
|
||||
function onImportingCommunityStateChanged(communityId, state, errorMsg) {
|
||||
root.importingCommunityStateChanged(communityId, state, errorMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,82 +1,135 @@
|
|||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.14
|
||||
import QtGraphicalEffects 1.13
|
||||
import QtQuick.Dialogs 1.3
|
||||
import QtQml.Models 2.14
|
||||
|
||||
import utils 1.0
|
||||
import shared.controls 1.0
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Popups 0.1
|
||||
import StatusQ.Controls 0.1 as StatusQControls
|
||||
import StatusQ.Popups.Dialog 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
|
||||
StatusModal {
|
||||
StatusDialog {
|
||||
id: root
|
||||
width: 640
|
||||
height: 400
|
||||
|
||||
property var store
|
||||
width: 640
|
||||
title: qsTr("Import Community")
|
||||
|
||||
function validate(communityKey) {
|
||||
return Utils.isPrivateKey(communityKey) && Utils.startsWith0x(communityKey)
|
||||
QtObject {
|
||||
id: d
|
||||
property string importErrorMessage
|
||||
readonly property string inputErrorMessage: isInputValid ? "" : qsTr("Invalid key")
|
||||
readonly property string errorMessage: importErrorMessage || inputErrorMessage
|
||||
readonly property bool isPrivateKey: Utils.isPrivateKey(keyInput.text)
|
||||
readonly property bool isPublicKey: Utils.isChatKey(keyInput.text)
|
||||
readonly property bool isInputValid: isPrivateKey || isPublicKey
|
||||
}
|
||||
|
||||
header.title: qsTr("Import Community")
|
||||
|
||||
onClosed: {
|
||||
root.destroy();
|
||||
footer: StatusDialogFooter {
|
||||
rightButtons: ObjectModel {
|
||||
StatusFlatButton {
|
||||
text: qsTr("Cancel")
|
||||
onClicked: root.reject()
|
||||
}
|
||||
StatusButton {
|
||||
id: importButton
|
||||
enabled: d.isInputValid
|
||||
text: d.isPrivateKey ? qsTr("Make this an Owner Node") : qsTr("Import")
|
||||
onClicked: {
|
||||
let communityKey = keyInput.text.trim();
|
||||
if (d.isPrivateKey) {
|
||||
if (!communityKey.startsWith("0x")) {
|
||||
communityKey = "0x" + communityKey;
|
||||
}
|
||||
root.store.importCommunity(communityKey);
|
||||
root.close();
|
||||
}
|
||||
if (d.isPublicKey) {
|
||||
importButton.loading = true
|
||||
root.store.requestCommunityInfo(communityKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
width: root.width - 32
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
height: childrenRect.height
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Style.current.padding
|
||||
|
||||
StatusBaseText {
|
||||
id: infoText1
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Style.current.padding
|
||||
text: qsTr("Entering a community key will grant you the ownership of that community. Please be responsible with it and don’t share the key with people you don’t trust.")
|
||||
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.")
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
font.pixelSize: 13
|
||||
color: Theme.palette.baseColor1
|
||||
}
|
||||
|
||||
StyledTextArea {
|
||||
id: keyInput
|
||||
label: qsTr("Community private key")
|
||||
placeholderText: "0x0..."
|
||||
customHeight: 110
|
||||
anchors.top: infoText1.bottom
|
||||
anchors.topMargin: Style.current.bigPadding
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
StatusBaseText {
|
||||
id: inputLabel
|
||||
text: qsTr("Community key")
|
||||
color: Theme.palette.directColor1
|
||||
font.pixelSize: 15
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
importButton.enabled = root.validate(keyInput.text)
|
||||
StatusTextArea {
|
||||
id: keyInput
|
||||
placeholderText: "0x0..."
|
||||
height: 110
|
||||
Layout.fillWidth: true
|
||||
onTextChanged: d.importErrorMessage = ""
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
id: detectionLabel
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.pixelSize: 13
|
||||
visible: keyInput.text.trim() !== ""
|
||||
text: {
|
||||
if (d.errorMessage !== "") {
|
||||
return d.errorMessage
|
||||
}
|
||||
if (d.isPrivateKey) {
|
||||
return qsTr("Private key detected")
|
||||
}
|
||||
if (d.isPublicKey) {
|
||||
return qsTr("Public key detected")
|
||||
}
|
||||
}
|
||||
color: d.errorMessage === "" ? Theme.palette.successColor1 : Theme.palette.dangerColor1
|
||||
}
|
||||
}
|
||||
|
||||
rightButtons: [
|
||||
StatusQControls.StatusButton {
|
||||
id: importButton
|
||||
enabled: false
|
||||
text: qsTr("Import")
|
||||
onClicked: {
|
||||
let communityKey = keyInput.text.trim();
|
||||
if (!communityKey.startsWith("0x")) {
|
||||
communityKey = "0x" + communityKey;
|
||||
}
|
||||
|
||||
root.store.importCommunity(communityKey);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
]
|
||||
Connections {
|
||||
target: root.store
|
||||
function onImportingCommunityStateChanged(communityId, state, errorMsg) {
|
||||
let communityKey = keyInput.text.trim();
|
||||
if (d.isPublicKey) {
|
||||
let currentCommunityKey = Utils.isCompressedPubKey(communityKey) ?
|
||||
Utils.changeCommunityKeyCompression(communityKey) :
|
||||
communityKey
|
||||
|
||||
if (communityId == currentCommunityKey) {
|
||||
importButton.loading = false
|
||||
if (state === Constants.communityImported && root.opened) {
|
||||
root.close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (state === Constants.communityImportingError) {
|
||||
d.importErrorMessage = errorMsg
|
||||
importButton.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,10 @@ QtObject {
|
|||
return (startsWith0x(value) && isHex(value) && value.length === 132) || globalUtilsInst.isCompressedPubKey(value)
|
||||
}
|
||||
|
||||
function isCompressedPubKey(pubKey) {
|
||||
return globalUtilsInst.isCompressedPubKey(pubKey)
|
||||
}
|
||||
|
||||
function isValidETHNamePrefix(value) {
|
||||
return !(value.trim() === "" || value.endsWith(".") || value.indexOf("..") > -1)
|
||||
}
|
||||
|
@ -620,6 +624,11 @@ QtObject {
|
|||
return communityKey
|
||||
}
|
||||
|
||||
|
||||
function changeCommunityKeyCompression(communityKey) {
|
||||
return globalUtilsInst.changeCommunityKeyCompression(communityKey)
|
||||
}
|
||||
|
||||
function getCompressedPk(publicKey) {
|
||||
if (publicKey === "") {
|
||||
return ""
|
||||
|
|
Loading…
Reference in New Issue