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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 dont share the key with people you dont 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
}
}
}
}
}

View File

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