feat: Add logout functionality

Move the onboarding/login state machine to the top level in main.qml, so that logout events can trigger new states.

Add Loader to statemachine so that each component is lazy-loaded. Initial tests saved 50MB of memory on startup.

Currently, logging out, then logging back in to the same or different account results in a doubling-up of chats/messages/wallet accounts. These need to be reset, however I need help doing that and it would delayed and blown out this PR further. This reset has been done for Onboarding and Login, but needs to be done for chats, wallet, mailservers, etc.
This commit is contained in:
emizzle 2020-06-04 17:38:24 +10:00 committed by Iuri Matias
parent bd8d743385
commit 4ec593baed
18 changed files with 300 additions and 172 deletions

View File

@ -3,6 +3,7 @@ import ../../status/libstatus/types as status_types
import ../../signals/types
import ../../status/status
import view
import ../../status/accounts as status_accounts
type LoginController* = ref object of SignalSubscriber
status*: Status
@ -19,11 +20,19 @@ proc delete*(self: LoginController) =
delete self.view
delete self.variant
proc init*(self: LoginController, nodeAccounts: seq[NodeAccount]) =
proc init*(self: LoginController) =
let nodeAccounts = self.status.accounts.openAccounts()
self.status.accounts.nodeAccounts = nodeAccounts
for nodeAccount in nodeAccounts:
self.view.addAccountToList(nodeAccount)
proc reset*(self: LoginController) =
self.view.removeAccounts()
proc handleNodeStopped(self: LoginController, data: Signal) =
self.status.events.emit("nodeStopped", Args())
self.view.onLoggedOut()
proc handleNodeLogin(self: LoginController, data: Signal) =
let response = NodeSignal(data)
if self.view.currentAccount.account != nil:
@ -31,8 +40,13 @@ proc handleNodeLogin(self: LoginController, data: Signal) =
if ?.response.event.error == "":
self.status.events.emit("login", AccountArgs(account: self.view.currentAccount.account.toAccount))
proc handleNodeReady(self: LoginController, data: Signal) =
self.status.events.emit("nodeReady", Args())
method onSignal(self: LoginController, data: Signal) =
if data.signalType == SignalType.NodeLogin:
self.handleNodeLogin(data)
case data.signalType:
of SignalType.NodeLogin: handleNodeLogin(self, data)
of SignalType.NodeStopped: handleNodeStopped(self, data)
of SignalType.NodeReady: handleNodeReady(self, data)
else:
discard

View File

@ -43,6 +43,11 @@ QtObject:
self.accounts.add(account)
self.endInsertRows()
proc removeAccounts*(self: LoginView) =
self.beginRemoveRows(newQModelIndex(), self.accounts.len, self.accounts.len)
self.accounts = @[]
self.endRemoveRows()
method rowCount(self: LoginView, index: QModelIndex = nil): int =
return self.accounts.len
@ -86,3 +91,5 @@ QtObject:
proc setLastLoginResponse*(self: LoginView, loginResponse: StatusGoError) =
self.loginResponseChanged(loginResponse.error)
proc onLoggedOut*(self: LoginView) {.signal.}

View File

@ -29,6 +29,9 @@ proc init*(self: OnboardingController) =
for account in accounts:
self.view.addAccountToList(account)
proc reset*(self: OnboardingController) =
self.view.removeAccounts()
proc handleNodeLogin(self: OnboardingController, data: Signal) =
let response = NodeSignal(data)
if self.view.currentAccount.account != nil:
@ -37,7 +40,7 @@ proc handleNodeLogin(self: OnboardingController, data: Signal) =
self.status.events.emit("login", AccountArgs(account: self.view.currentAccount.account.toAccount))
method onSignal(self: OnboardingController, data: Signal) =
if data.signalType == SignalType.NodeLogin:
self.handleNodeLogin(data)
case data.signalType:
of SignalType.NodeLogin: handleNodeLogin(self, data)
else:
discard

View File

@ -41,6 +41,11 @@ QtObject:
self.accounts.add(account)
self.endInsertRows()
proc removeAccounts*(self: OnboardingView) =
self.beginResetModel()
self.accounts = @[]
self.endResetModel()
method rowCount(self: OnboardingView, index: QModelIndex = nil): int =
return self.accounts.len

View File

@ -15,7 +15,7 @@ type ProfileController* = object
proc newController*(status: Status): ProfileController =
result = ProfileController()
result.status = status
result.view = newProfileView()
result.view = newProfileView(status)
result.variant = newQVariant(result.view)
proc delete*(self: ProfileController) =

View File

@ -3,12 +3,15 @@ import views/mailservers_list
import views/contact_list
import views/profile_info
import ../../status/profile
import ../../status/accounts as status_accounts
import ../../status/status
QtObject:
type ProfileView* = ref object of QObject
profile*: ProfileInfoView
mailserversList*: MailServersList
contactList*: ContactList
status*: Status
proc setup(self: ProfileView) =
self.QObject.setup
@ -16,12 +19,13 @@ QtObject:
proc delete*(self: ProfileView) =
self.QObject.delete
proc newProfileView*(): ProfileView =
proc newProfileView*(status: Status): ProfileView =
new(result, delete)
result = ProfileView()
result.profile = newProfileInfoView()
result.mailserversList = newMailServersList()
result.contactList = newContactList()
result.status = status
result.setup
proc addMailServerToList*(self: ProfileView, mailserver: MailServer) =
@ -50,3 +54,6 @@ QtObject:
QtProperty[QVariant] profile:
read = getProfile
proc logout*(self: ProfileView) {.slot.} =
self.status.profile.logout()

View File

@ -21,7 +21,7 @@ logScope:
proc mainProc() =
let status = statuslib.newStatusInstance()
let nodeAccounts = status.initNodeAccounts()
status.initNode()
let app = newQApplication()
let engine = newQQmlApplicationEngine()
@ -43,13 +43,12 @@ proc mainProc() =
engine.setRootContextProperty("chatsModel", chat.variant)
var node = node.newController(status)
node.init()
engine.setRootContextProperty("nodeModel", node.variant)
var profile = profile.newController(status)
engine.setRootContextProperty("profileModel", profile.variant)
status.events.once("login") do(a: Args):
status.events.on("login") do(a: Args):
var args = AccountArgs(a)
status.startMessenger()
chat.init()
@ -59,16 +58,37 @@ proc mainProc() =
var login = login.newController(status)
var onboarding = onboarding.newController(status)
# TODO: replace this with routing
let showLogin = nodeAccounts.len > 0
engine.setRootContextProperty("showLogin", newQVariant(showLogin))
login.init(nodeAccounts)
engine.setRootContextProperty("loginModel", login.variant)
onboarding.init()
engine.setRootContextProperty("onboardingModel", onboarding.variant)
# Initialize only controllers whose init functions
# do not need a running node
proc initControllers() =
node.init()
login.init()
onboarding.init()
initControllers()
# Handle node.stopped signal when user has logged out
status.events.on("nodeStopped") do(a: Args):
# TODO: remove this once accounts are not tracked in the AccountsModel
status.reset()
# 1. Reset controller data
login.reset()
onboarding.reset()
# TODO: implement all controller resets
# chat.reset()
# node.reset()
# wallet.reset()
# profile.reset()
# 2. Re-init controllers that don't require a running node
initControllers()
signalController.init()
signalController.addSubscriber(SignalType.Wallet, wallet)
signalController.addSubscriber(SignalType.Wallet, node)
@ -76,6 +96,9 @@ proc mainProc() =
signalController.addSubscriber(SignalType.DiscoverySummary, chat)
signalController.addSubscriber(SignalType.NodeLogin, login)
signalController.addSubscriber(SignalType.NodeLogin, onboarding)
signalController.addSubscriber(SignalType.NodeStopped, login)
signalController.addSubscriber(SignalType.NodeStarted, login)
signalController.addSubscriber(SignalType.NodeReady, login)
engine.setRootContextProperty("signals", signalController.variant)

View File

@ -1,14 +1,17 @@
import libstatus/accounts as status_accounts
import libstatus/types
import options
import eventemitter
type
AccountModel* = ref object
generatedAddresses*: seq[GeneratedAccount]
nodeAccounts*: seq[NodeAccount]
events: EventEmitter
proc newAccountModel*(): AccountModel =
proc newAccountModel*(events: EventEmitter): AccountModel =
result = AccountModel()
result.events = events
proc generateAddresses*(self: AccountModel): seq[GeneratedAccount] =
var accounts = status_accounts.generateAddresses()
@ -16,7 +19,10 @@ proc generateAddresses*(self: AccountModel): seq[GeneratedAccount] =
account.name = status_accounts.generateAlias(account.derived.whisper.publicKey)
account.photoPath = status_accounts.generateIdenticon(account.derived.whisper.publicKey)
self.generatedAddresses.add(account)
self.generatedAddresses
result = self.generatedAddresses
proc openAccounts*(self: AccountModel): seq[NodeAccount] =
result = status_accounts.openAccounts()
proc login*(self: AccountModel, selectedAccountIndex: int, password: string): NodeAccount =
let currentNodeAccount = self.nodeAccounts[selectedAccountIndex]
@ -35,3 +41,7 @@ proc importMnemonic*(self: AccountModel, mnemonic: string): GeneratedAccount =
importedAccount.name = status_accounts.generateAlias(importedAccount.derived.whisper.publicKey)
importedAccount.photoPath = status_accounts.generateIdenticon(importedAccount.derived.whisper.publicKey)
result = importedAccount
proc reset*(self: AccountModel) =
self.nodeAccounts = @[]
self.generatedAddresses = @[]

View File

@ -36,17 +36,15 @@ proc ensureDir(dirname: string) =
# removeDir(dirname)
createDir(dirname)
proc initNodeAccounts*(): seq[NodeAccount] =
const datadir = "./data/"
const keystoredir = "./data/keystore/"
const nobackupdir = "./noBackup/"
proc initNode*() =
ensureDir(DATADIR)
ensureDir(KEYSTOREDIR)
ensureDir(NOBACKUPDIR)
ensureDir(datadir)
ensureDir(keystoredir)
ensureDir(nobackupdir)
discard $libstatus.initKeystore(KEYSTOREDIR)
discard $libstatus.initKeystore(keystoredir);
let strNodeAccounts = $libstatus.openAccounts(datadir);
proc openAccounts*(): seq[NodeAccount] =
let strNodeAccounts = $libstatus.openAccounts(DATADIR)
result = Json.decode(strNodeAccounts, seq[NodeAccount])
proc saveAccountAndLogin*(
@ -215,6 +213,8 @@ proc deriveAccounts*(accountId: string): MultiAccounts =
"accountID": accountId,
"paths": [PATH_WALLET_ROOT, PATH_EIP_1581, PATH_WHISPER, PATH_DEFAULT_WALLET]
}
# libstatus.multiAccountImportMnemonic never results in an error given ANY input
let deriveResult = $libstatus.multiAccountDeriveAddresses($deriveJson)
result = Json.decode(deriveResult, MultiAccounts)
proc logout*(): StatusGoError =
result = Json.decode($libstatus.logout(), StatusGoError)

View File

@ -175,3 +175,7 @@ let NODE_CONFIG* = %* {
"Enabled": true
}
}
const DATADIR* = "./data/"
const KEYSTOREDIR* = "./data/keystore/"
const NOBACKUPDIR* = "./noBackup/"

View File

@ -33,3 +33,5 @@ proc generateAlias*(p0: GoString): cstring {.importc: "GenerateAlias".}
proc identicon*(p0: GoString): cstring {.importc: "Identicon".}
proc login*(acctData: cstring, password: cstring): cstring {.importc: "Login".}
proc logout*(): cstring {.importc: "Logout".}

View File

@ -9,6 +9,7 @@ type SignalType* {.pure.} = enum
Wallet = "wallet"
NodeReady = "node.ready"
NodeStarted = "node.started"
NodeStopped = "node.stopped"
NodeLogin = "node.login"
EnvelopeSent = "envelope.sent"
EnvelopeExpired = "envelope.expired"

View File

@ -2,6 +2,7 @@ import json
import eventemitter
import libstatus/types
import libstatus/core as libstatus_core
import libstatus/accounts as status_accounts
type
MailServer* = ref object
@ -45,3 +46,13 @@ proc getContactByID*(id: string): Profile =
let val = parseJSON($response)
result = toProfileModel(val)
type
ProfileModel* = ref object
proc newProfileModel*(): ProfileModel =
result = ProfileModel()
proc logout*(self: ProfileModel) =
discard status_accounts.logout()

View File

@ -9,6 +9,8 @@ import accounts as accounts
import wallet as wallet
import node as node
import mailservers as mailservers
import profile
import ../signals/types as signal_types
type Status* = ref object
events*: EventEmitter
@ -17,19 +19,33 @@ type Status* = ref object
accounts*: AccountModel
wallet*: WalletModel
node*: NodeModel
profile*: ProfileModel
proc newStatusInstance*(): Status =
result = Status()
result.events = createEventEmitter()
result.chat = chat.newChatModel(result.events)
result.accounts = accounts.newAccountModel()
result.accounts = accounts.newAccountModel(result.events)
result.wallet = wallet.newWalletModel(result.events)
result.wallet.initEvents()
result.node = node.newNodeModel()
result.mailservers = mailservers.newMailserverModel(result.events)
result.profile = profile.newProfileModel()
proc initNodeAccounts*(self: Status): seq[NodeAccount] =
libstatus_accounts.initNodeAccounts()
proc initNode*(self: Status) =
libstatus_accounts.initNode()
proc startMessenger*(self: Status) =
libstatus_core.startMessenger()
proc reset*(self: Status) =
# TODO: remove this once accounts are not tracked in the AccountsModel
self.accounts.reset()
# NOT NEEDED self.chat.reset()
# NOT NEEDED self.wallet.reset()
# NOT NEEDED self.node.reset()
# NOT NEEDED self.mailservers.reset()
# NOT NEEDED self.profile.reset()
# TODO: add all resets here

View File

@ -2,6 +2,7 @@ import QtQuick 2.3
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.3
import "../../../../imports"
import "../../../../shared"
Item {
id: signoutContainer
@ -11,7 +12,7 @@ Item {
Layout.fillWidth: true
Text {
id: element10
id: txtTitle
text: qsTr("Sign out controls")
anchors.left: parent.left
anchors.leftMargin: 24
@ -20,4 +21,15 @@ Item {
font.weight: Font.Bold
font.pixelSize: 20
}
StyledButton {
id: btnLogout
anchors.top: txtTitle.bottom
anchors.topMargin: Theme.padding
label: qsTr("Logout")
onClicked: {
profileModel.logout();
}
}
}

View File

@ -2,6 +2,7 @@ import QtQuick 2.3
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import Qt.labs.platform 1.1
import QtQml.StateMachine 1.14 as DSM
import "./onboarding"
import "./app"
@ -13,6 +14,8 @@ ApplicationWindow {
visible: true
font.family: "Inter"
signal navigateTo(string path)
SystemTrayIcon {
visible: true
icon.source: "shared/img/status-logo.png"
@ -30,18 +33,145 @@ ApplicationWindow {
}
}
OnboardingMain {
id: onboarding
visible: !app.visible
DSM.StateMachine {
id: stateMachine
initialState: onboardingState
running: true
DSM.State {
id: onboardingState
initialState: loginModel.rowCount() ? stateLogin : stateIntro
DSM.State {
id: stateIntro
onEntered: loader.sourceComponent = intro
DSM.SignalTransition {
targetState: keysMainState
signal: applicationWindow.navigateTo
guard: path === "KeysMain"
}
}
DSM.State {
id: keysMainState
onEntered: loader.sourceComponent = keysMain
DSM.SignalTransition {
targetState: existingKeyState
signal: applicationWindow.navigateTo
guard: path === "ExistingKey"
}
DSM.SignalTransition {
targetState: genKeyState
signal: applicationWindow.navigateTo
guard: path === "GenKey"
}
}
DSM.State {
id: existingKeyState
onEntered: loader.sourceComponent = existingKey
DSM.SignalTransition {
targetState: appState
signal: onboardingModel.loginResponseChanged
guard: !error
}
}
DSM.State {
id: genKeyState
onEntered: loader.sourceComponent = genKey
DSM.SignalTransition {
targetState: appState
signal: onboardingModel.loginResponseChanged
guard: !error
}
DSM.SignalTransition {
targetState: existingKeyState
signal: applicationWindow.navigateTo
guard: path === "ExistingKey"
}
}
DSM.State {
id: stateLogin
onEntered: loader.sourceComponent = login
DSM.SignalTransition {
targetState: appState
signal: loginModel.loginResponseChanged
guard: !error
}
DSM.SignalTransition {
targetState: genKeyState
signal: applicationWindow.navigateTo
guard: path === "GenKey"
}
}
DSM.FinalState {
id: onboardingDoneState
}
}
DSM.State {
id: appState
onEntered: loader.sourceComponent = app
DSM.SignalTransition {
targetState: stateLogin
signal: loginModel.onLoggedOut
}
}
}
Loader {
id: loader
anchors.fill: parent
}
AppMain {
Component {
id: app
// TODO: Set this to a logic result determining when we need to show the onboarding screens
// Set to true to hide the onboarding screens manually
// Set to false to show the onboarding screens manually
visible: false // logic.accountResult !== ""
anchors.fill: parent
AppMain {}
}
Component {
id: intro
Intro {
btnGetStarted.onClicked: applicationWindow.navigateTo("KeysMain")
}
}
Component {
id: keysMain
KeysMain {
btnGenKey.onClicked: applicationWindow.navigateTo("GenKey")
btnExistingKey.onClicked: applicationWindow.navigateTo("ExistingKey")
}
}
Component {
id: existingKey
ExistingKey {}
}
Component {
id: genKey
GenKey {
btnExistingKey.onClicked: applicationWindow.navigateTo("ExistingKey")
}
}
Component {
id: login
Login {
btnGenKey.onClicked: applicationWindow.navigateTo("GenKey")
}
}
}

View File

@ -192,6 +192,19 @@ SwipeView {
standardButtons: StandardButton.Ok
}
MessageDialog {
id: passwordsDontMatchError
title: "Error"
text: "Passwords don't match"
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok
onAccepted: {
txtConfirmPassword.clear();
swipeView.currentIndex = 1;
txtPassword.focus = true;
}
}
Connections {
target: onboardingModel
ignoreUnknownSignals: true

View File

@ -1,130 +0,0 @@
import QtQuick 2.3
import QtQml.StateMachine 1.14 as DSM
import QtQuick.Controls 2.3
Page {
id: onboardingMain
property string state
anchors.fill: parent
DSM.StateMachine {
id: stateMachine
initialState: showLogin ? stateLogin : stateIntro
running: onboardingMain.visible
DSM.State {
id: stateIntro
onEntered: intro.visible = true
onExited: intro.visible = false
DSM.SignalTransition {
targetState: keysMainState
signal: intro.btnGetStarted.clicked
}
}
DSM.State {
id: keysMainState
onEntered: keysMain.visible = true
onExited: keysMain.visible = false
DSM.SignalTransition {
targetState: existingKeyState
signal: keysMain.btnExistingKey.clicked
}
DSM.SignalTransition {
targetState: genKeyState
signal: keysMain.btnGenKey.clicked
}
}
DSM.State {
id: existingKeyState
onEntered: existingKey.visible = true
onExited: existingKey.visible = false
DSM.SignalTransition {
targetState: appState
signal: onboardingModel.loginResponseChanged
guard: !error
}
}
DSM.State {
id: genKeyState
onEntered: genKey.visible = true
onExited: genKey.visible = false
DSM.SignalTransition {
targetState: appState
signal: onboardingModel.loginResponseChanged
guard: !error
}
DSM.SignalTransition {
targetState: existingKeyState
signal: genKey.btnExistingKey.clicked
}
}
DSM.State {
id: stateLogin
onEntered: login.visible = true
onExited: login.visible = false
DSM.SignalTransition {
targetState: appState
signal: loginModel.loginResponseChanged
guard: !error
}
DSM.SignalTransition {
targetState: genKeyState
signal: login.btnGenKey.clicked
}
}
DSM.FinalState {
id: appState
onEntered: app.visible = true
onExited: app.visible = false
}
}
Intro {
id: intro
anchors.fill: parent
visible: false
}
KeysMain {
id: keysMain
anchors.fill: parent
visible: false
}
ExistingKey {
id: existingKey
anchors.fill: parent
visible: false
}
GenKey {
id: genKey
anchors.fill: parent
visible: false
}
Login {
id: login
anchors.fill: parent
visible: false
}
}
/*##^##
Designer {
D{i:0;autoSize:true;height:770;width:1232}
}
##^##*/