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:
Pascal Precht 2022-11-28 15:57:56 +01:00 committed by r4bbit
parent 16be01495a
commit b510b33730
14 changed files with 185 additions and 70 deletions

View File

@ -35,7 +35,12 @@ proc init*(self: Controller) =
self.events.on(SIGNAL_COMMUNITY_DATA_IMPORTED) do(e:Args): self.events.on(SIGNAL_COMMUNITY_DATA_IMPORTED) do(e:Args):
let args = CommunityArgs(e) 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): self.events.on(SIGNAL_CURATED_COMMUNITY_FOUND) do(e:Args):
let args = CuratedCommunityArgs(e) let args = CuratedCommunityArgs(e)
@ -48,7 +53,7 @@ proc init*(self: Controller) =
self.events.on(SIGNAL_COMMUNITY_IMPORTED) do(e:Args): self.events.on(SIGNAL_COMMUNITY_IMPORTED) do(e:Args):
let args = CommunityArgs(e) let args = CommunityArgs(e)
if(args.error.len > 0): if(args.error.len > 0):
self.delegate.onImportCommunityErrorOccured(args.error) self.delegate.onImportCommunityErrorOccured(args.community.id, args.error)
else: else:
self.delegate.communityImported(args.community) self.delegate.communityImported(args.community)

View File

@ -113,7 +113,7 @@ method curatedCommunityEdited*(self: AccessInterface, community: CuratedCommunit
method communityImported*(self: AccessInterface, community: CommunityDto) {.base.} = method communityImported*(self: AccessInterface, community: CommunityDto) {.base.} =
raise newException(ValueError, "No implementation available") 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") raise newException(ValueError, "No implementation available")
method viewDidLoad*(self: AccessInterface) {.base.} = method viewDidLoad*(self: AccessInterface) {.base.} =

View File

@ -301,14 +301,14 @@ method deleteCommunityChat*(self: Module, communityId: string, channelId: string
method communityImported*(self: Module, community: CommunityDto) = method communityImported*(self: Module, community: CommunityDto) =
self.view.addItem(self.getCommunityItem(community)) 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) = method importCommunity*(self: Module, communityKey: string) =
self.view.emitImportingCommunityStateChangedSignal(ImportCommunityState.ImportingInProgress.int, "") self.view.emitImportingCommunityStateChangedSignal(communityKey, ImportCommunityState.ImportingInProgress.int, "")
self.controller.importCommunity(communityKey) self.controller.importCommunity(communityKey)
method onImportCommunityErrorOccured*(self: Module, error: string) = method onImportCommunityErrorOccured*(self: Module, communityId: string, error: string) =
self.view.emitImportingCommunityStateChangedSignal(ImportCommunityState.ImportingError.int, error) self.view.emitImportingCommunityStateChangedSignal(communityId, ImportCommunityState.ImportingError.int, error)
method requestExtractDiscordChannelsAndCategories*(self: Module, filesToImport: seq[string]) = method requestExtractDiscordChannelsAndCategories*(self: Module, filesToImport: seq[string]) =
self.view.setDiscordDataExtractionInProgress(true) self.view.setDiscordDataExtractionInProgress(true)

View File

@ -471,9 +471,9 @@ QtObject:
proc importCommunity*(self: View, communityKey: string) {.slot.} = proc importCommunity*(self: View, communityKey: string) {.slot.} =
self.delegate.importCommunity(communityKey) self.delegate.importCommunity(communityKey)
proc importingCommunityStateChanged*(self:View, state: int, errorMsg: string) {.signal.} proc importingCommunityStateChanged*(self:View, communityId: string, state: int, errorMsg: string) {.signal.}
proc emitImportingCommunityStateChangedSignal*(self: View, state: int, errorMsg: string) = proc emitImportingCommunityStateChangedSignal*(self: View, communityId: string, state: int, errorMsg: string) =
self.importingCommunityStateChanged(state, errorMsg) self.importingCommunityStateChanged(communityId, state, 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)

View File

@ -235,6 +235,9 @@ method onMyRequestAdded*(self: AccessInterface) {.base.} =
method activateStatusDeepLink*(self: AccessInterface, statusDeepLink: string) {.base.} = method activateStatusDeepLink*(self: AccessInterface, statusDeepLink: string) {.base.} =
raise newException(ValueError, "No implementation available") 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 # This way (using concepts) is used only for the modules managed by AppController
type type
DelegateInterface* = concept c DelegateInterface* = concept c

View File

@ -575,6 +575,9 @@ method emitStoringPasswordSuccess*[T](self: Module[T]) =
method emitMailserverNotWorking*[T](self: Module[T]) = method emitMailserverNotWorking*[T](self: Module[T]) =
self.view.emitMailserverNotWorking() self.view.emitMailserverNotWorking()
method setCommunityIdToSpectate*[T](self: Module[T], communityId: string) =
self.statusUrlCommunityToSpectate = communityId
method getActiveSectionId*[T](self: Module[T]): string = method getActiveSectionId*[T](self: Module[T]): string =
return self.controller.getActiveSectionId() return self.controller.getActiveSectionId()

View File

@ -239,3 +239,6 @@ QtObject:
proc destroyKeycardSharedModuleFlow*(self: View) {.signal.} proc destroyKeycardSharedModuleFlow*(self: View) {.signal.}
proc emitDestroyKeycardSharedModuleFlow*(self: View) = proc emitDestroyKeycardSharedModuleFlow*(self: View) =
self.destroyKeycardSharedModuleFlow() self.destroyKeycardSharedModuleFlow()
proc setCommunityIdToSpectate*(self: View, communityId: string) {.slot.} =
self.delegate.setCommunityIdToSpectate(communityId)

View File

@ -8,4 +8,5 @@ type
const asyncRequestCommunityInfoTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = const asyncRequestCommunityInfoTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncRequestCommunityInfoTaskArg](argEncoded) let arg = decode[AsyncRequestCommunityInfoTaskArg](argEncoded)
let response = status_go.requestCommunityInfo(arg.communityId) let response = status_go.requestCommunityInfo(arg.communityId)
arg.finish(response) let tpl: tuple[communityId: string, response: RpcResponse[JsonNode]] = (arg.communityId, response)
arg.finish(tpl)

View File

@ -100,6 +100,7 @@ const SIGNAL_COMMUNITY_CREATED* = "communityCreated"
const SIGNAL_COMMUNITY_ADDED* = "communityAdded" const SIGNAL_COMMUNITY_ADDED* = "communityAdded"
const SIGNAL_COMMUNITY_IMPORTED* = "communityImported" const SIGNAL_COMMUNITY_IMPORTED* = "communityImported"
const SIGNAL_COMMUNITY_DATA_IMPORTED* = "communityDataImported" # This one is when just loading the data with requestCommunityInfo 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_COMMUNITY_EDITED* = "communityEdited"
const SIGNAL_COMMUNITIES_UPDATE* = "communityUpdated" const SIGNAL_COMMUNITIES_UPDATE* = "communityUpdated"
const SIGNAL_COMMUNITY_CHANNEL_CREATED* = "communityChannelCreated" const SIGNAL_COMMUNITY_CHANNEL_CREATED* = "communityChannelCreated"
@ -1165,16 +1166,21 @@ QtObject:
error "Error reordering category channel", msg = e.msg, communityId, categoryId, position error "Error reordering category channel", msg = e.msg, communityId, categoryId, position
proc asyncCommunityInfoLoaded*(self: Service, rpcResponse: string) {.slot.} = proc asyncCommunityInfoLoaded*(self: Service, communityIdAndRpcResponse: string) {.slot.} =
let rpcResponseObj = rpcResponse.parseJson let rpcResponseObj = communityIdAndRpcResponse.parseJson
if (rpcResponseObj{"error"}.kind != JNull): if (rpcResponseObj{"response"}{"error"}.kind != JNull):
let error = Json.decode($rpcResponseObj["error"], RpcError) let error = Json.decode($rpcResponseObj["response"]["error"], RpcError)
error "Error requesting community info", msg = error.message error "Error requesting community info", msg = error.message
return return
let community = rpcResponseObj{"result"}.toCommunityDto() var community = rpcResponseObj{"response"}{"result"}.toCommunityDto()
if community.id != "":
self.allCommunities[community.id] = community self.allCommunities[community.id] = community
self.events.emit(SIGNAL_COMMUNITY_DATA_IMPORTED, CommunityArgs(community: 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) = proc requestCommunityInfo*(self: Service, communityId: string) =
try: try:

View File

@ -47,6 +47,8 @@ QtObject {
property var advancedModule: profileSectionModule.advancedModule property var advancedModule: profileSectionModule.advancedModule
signal importingCommunityStateChanged(string communityId, int state, string errorMsg)
function setActiveCommunity(communityId) { function setActiveCommunity(communityId) {
mainModule.setActiveSectionById(communityId); mainModule.setActiveSectionById(communityId);
} }
@ -570,4 +572,11 @@ QtObject {
function hex2Eth(value) { function hex2Eth(value) {
return globalUtils.hex2Eth(value) return globalUtils.hex2Eth(value)
} }
readonly property Connections communitiesModuleConnections: Connections {
target: communitiesModuleInst
function onImportingCommunityStateChanged(communityId, state, errorMsg) {
root.importingCommunityStateChanged(communityId, state, errorMsg)
}
}
} }

View File

@ -319,8 +319,8 @@ Item {
} }
Connections { Connections {
target: root.store.communitiesModuleInst target: root.store
function onImportingCommunityStateChanged(state, errorMsg) { function onImportingCommunityStateChanged(communityId, state, errorMsg) {
let title = "" let title = ""
let loading = false let loading = false

View File

@ -1,10 +1,13 @@
import QtQuick 2.13 import QtQuick 2.13
import QtQml.Models 2.2 import QtQml.Models 2.2
import utils 1.0
QtObject { QtObject {
id: root id: root
property var communitiesModuleInst: communitiesModule property var communitiesModuleInst: communitiesModule
property var mainModuleInst: mainModule
readonly property var curatedCommunitiesModel: root.communitiesModuleInst.curatedCommunities readonly property var curatedCommunitiesModel: root.communitiesModuleInst.curatedCommunities
@ -51,6 +54,8 @@ QtObject {
property string communityTags: communitiesModuleInst.tags property string communityTags: communitiesModuleInst.tags
signal importingCommunityStateChanged(string communityId, int state, string errorMsg)
function createCommunity(args = { function createCommunity(args = {
name: "", name: "",
description: "", description: "",
@ -85,6 +90,16 @@ QtObject {
root.communitiesModuleInst.importCommunity(communityKey); 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) { function setActiveCommunity(communityId) {
mainModule.setActiveSectionById(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.image.src, args.image.AX, args.image.AY, args.image.BX, args.image.BY,
args.options.historyArchiveSupportEnabled, args.options.pinMessagesAllowedForMembers, from, args.options.encrypted); 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)
}
}
} }

View File

@ -1,82 +1,135 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Layouts 1.14
import QtGraphicalEffects 1.13 import QtGraphicalEffects 1.13
import QtQuick.Dialogs 1.3 import QtQuick.Dialogs 1.3
import QtQml.Models 2.14
import utils 1.0 import utils 1.0
import shared.controls 1.0 import shared.controls 1.0
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Popups 0.1 import StatusQ.Popups.Dialog 0.1
import StatusQ.Controls 0.1 as StatusQControls import StatusQ.Controls 0.1
StatusModal { StatusDialog {
id: root id: root
width: 640
height: 400
property var store property var store
width: 640
title: qsTr("Import Community")
function validate(communityKey) { QtObject {
return Utils.isPrivateKey(communityKey) && Utils.startsWith0x(communityKey) 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") footer: StatusDialogFooter {
rightButtons: ObjectModel {
onClosed: { StatusFlatButton {
root.destroy(); 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 { ColumnLayout {
width: root.width - 32 anchors.fill: parent
anchors.left: parent.left spacing: Style.current.padding
anchors.right: parent.right
anchors.leftMargin: 16
anchors.rightMargin: 16
height: childrenRect.height
StatusBaseText { StatusBaseText {
id: infoText1 id: infoText1
anchors.top: parent.top Layout.fillWidth: true
anchors.topMargin: Style.current.padding 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("Entering a community key will grant you the ownership of that community. Please be responsible with it and dont share the key with people you dont trust.")
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: parent.width
font.pixelSize: 13 font.pixelSize: 13
color: Theme.palette.baseColor1 color: Theme.palette.baseColor1
} }
StyledTextArea { StatusBaseText {
id: inputLabel
text: qsTr("Community key")
color: Theme.palette.directColor1
font.pixelSize: 15
}
StatusTextArea {
id: keyInput id: keyInput
label: qsTr("Community private key")
placeholderText: "0x0..." placeholderText: "0x0..."
customHeight: 110 height: 110
anchors.top: infoText1.bottom Layout.fillWidth: true
anchors.topMargin: Style.current.bigPadding onTextChanged: d.importErrorMessage = ""
anchors.left: parent.left
anchors.right: parent.right
onTextChanged: {
importButton.enabled = root.validate(keyInput.text)
} }
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 { Connections {
id: importButton target: root.store
enabled: false function onImportingCommunityStateChanged(communityId, state, errorMsg) {
text: qsTr("Import")
onClicked: {
let communityKey = keyInput.text.trim(); let communityKey = keyInput.text.trim();
if (!communityKey.startsWith("0x")) { if (d.isPublicKey) {
communityKey = "0x" + communityKey; let currentCommunityKey = Utils.isCompressedPubKey(communityKey) ?
Utils.changeCommunityKeyCompression(communityKey) :
communityKey
if (communityId == currentCommunityKey) {
importButton.loading = false
if (state === Constants.communityImported && root.opened) {
root.close()
return
}
} }
root.store.importCommunity(communityKey); if (state === Constants.communityImportingError) {
root.close(); d.importErrorMessage = errorMsg
importButton.loading = false
}
}
} }
} }
]
} }

View File

@ -26,6 +26,10 @@ QtObject {
return (startsWith0x(value) && isHex(value) && value.length === 132) || globalUtilsInst.isCompressedPubKey(value) return (startsWith0x(value) && isHex(value) && value.length === 132) || globalUtilsInst.isCompressedPubKey(value)
} }
function isCompressedPubKey(pubKey) {
return globalUtilsInst.isCompressedPubKey(pubKey)
}
function isValidETHNamePrefix(value) { function isValidETHNamePrefix(value) {
return !(value.trim() === "" || value.endsWith(".") || value.indexOf("..") > -1) return !(value.trim() === "" || value.endsWith(".") || value.indexOf("..") > -1)
} }
@ -620,6 +624,11 @@ QtObject {
return communityKey return communityKey
} }
function changeCommunityKeyCompression(communityKey) {
return globalUtilsInst.changeCommunityKeyCompression(communityKey)
}
function getCompressedPk(publicKey) { function getCompressedPk(publicKey) {
if (publicKey === "") { if (publicKey === "") {
return "" return ""