login process refactored
This commit is contained in:
parent
8974a8db5e
commit
4f3ca4eb78
|
@ -42,7 +42,7 @@ proc load*(self: AppController)
|
||||||
|
|
||||||
# Startup Module Delegate Interface
|
# Startup Module Delegate Interface
|
||||||
proc startupDidLoad*(self: AppController)
|
proc startupDidLoad*(self: AppController)
|
||||||
proc accountCreated*(self: AppController)
|
proc userLoggedIn*(self: AppController)
|
||||||
|
|
||||||
# Main Module Delegate Interface
|
# Main Module Delegate Interface
|
||||||
proc mainDidLoad*(self: AppController)
|
proc mainDidLoad*(self: AppController)
|
||||||
|
@ -114,7 +114,6 @@ proc mainDidLoad*(self: AppController) =
|
||||||
# self.appService.status.events.emit("loginCompleted", self.accountArgs)
|
# self.appService.status.events.emit("loginCompleted", self.accountArgs)
|
||||||
|
|
||||||
proc start*(self: AppController) =
|
proc start*(self: AppController) =
|
||||||
echo "AppStart"
|
|
||||||
self.accountsService.init()
|
self.accountsService.init()
|
||||||
|
|
||||||
self.startupModule.load()
|
self.startupModule.load()
|
||||||
|
@ -126,6 +125,5 @@ proc load*(self: AppController) =
|
||||||
|
|
||||||
self.mainModule.load()
|
self.mainModule.load()
|
||||||
|
|
||||||
proc accountCreated*(self: AppController) =
|
proc userLoggedIn*(self: AppController) =
|
||||||
echo "AppController account created"
|
|
||||||
self.load()
|
self.load()
|
|
@ -1,29 +1,51 @@
|
||||||
import Tables
|
import Tables, chronicles
|
||||||
|
|
||||||
import controller_interface
|
import controller_interface
|
||||||
import io_interface
|
import io_interface
|
||||||
|
|
||||||
|
import status/[signals]
|
||||||
|
import ../../../app_service/[main]
|
||||||
import ../../../app_service/service/accounts/service_interface as accounts_service
|
import ../../../app_service/service/accounts/service_interface as accounts_service
|
||||||
|
|
||||||
export controller_interface
|
export controller_interface
|
||||||
|
|
||||||
|
logScope:
|
||||||
|
topics = "startup-controller"
|
||||||
|
|
||||||
type
|
type
|
||||||
Controller* = ref object of controller_interface.AccessInterface
|
Controller* = ref object of controller_interface.AccessInterface
|
||||||
delegate: io_interface.AccessInterface
|
delegate: io_interface.AccessInterface
|
||||||
|
appService: AppService
|
||||||
accountsService: accounts_service.ServiceInterface
|
accountsService: accounts_service.ServiceInterface
|
||||||
|
|
||||||
proc newController*(delegate: io_interface.AccessInterface,
|
proc newController*(delegate: io_interface.AccessInterface,
|
||||||
|
appService: AppService,
|
||||||
accountsService: accounts_service.ServiceInterface):
|
accountsService: accounts_service.ServiceInterface):
|
||||||
Controller =
|
Controller =
|
||||||
result = Controller()
|
result = Controller()
|
||||||
result.delegate = delegate
|
result.delegate = delegate
|
||||||
|
result.appService = appService
|
||||||
result.accountsService = accountsService
|
result.accountsService = accountsService
|
||||||
|
|
||||||
method delete*(self: Controller) =
|
method delete*(self: Controller) =
|
||||||
discard
|
discard
|
||||||
|
|
||||||
method init*(self: Controller) =
|
method init*(self: Controller) =
|
||||||
discard
|
self.appService.status.events.on(SignalType.NodeLogin.event) do(e:Args):
|
||||||
|
let signal = NodeSignal(e)
|
||||||
|
if signal.event.error == "":
|
||||||
|
self.delegate.userLoggedIn()
|
||||||
|
else:
|
||||||
|
error "error: ", methodName="init", errDesription = "login error " & signal.event.error
|
||||||
|
|
||||||
|
self.appService.status.events.on(SignalType.NodeStopped.event) do(e:Args):
|
||||||
|
echo "-NEW-EVENT-- NodeStopped: ", repr(e)
|
||||||
|
#self.status.events.emit("nodeStopped", Args())
|
||||||
|
#self.view.onLoggedOut()
|
||||||
|
|
||||||
|
self.appService.status.events.on(SignalType.NodeReady.event) do(e:Args):
|
||||||
|
echo "-NEW-EVENT-- NodeReady: ", repr(e)
|
||||||
|
#self.status.events.emit("nodeReady", Args())
|
||||||
|
|
||||||
method shouldStartWithOnboardingScreen*(self: Controller): bool =
|
method shouldStartWithOnboardingScreen*(self: Controller): bool =
|
||||||
return self.accountsService.openedAccounts().len == 0
|
return self.accountsService.openedAccounts().len == 0
|
|
@ -16,4 +16,4 @@ include ./private_interfaces/module_login_delegate_interface
|
||||||
type
|
type
|
||||||
DelegateInterface* = concept c
|
DelegateInterface* = concept c
|
||||||
c.startupDidLoad()
|
c.startupDidLoad()
|
||||||
c.accountCreated()
|
c.userLoggedIn()
|
|
@ -29,21 +29,25 @@ method delete*(self: Controller) =
|
||||||
discard
|
discard
|
||||||
|
|
||||||
method init*(self: Controller) =
|
method init*(self: Controller) =
|
||||||
self.appService.status.events.on(SignalType.NodeStopped.event) do(e:Args):
|
|
||||||
echo "-NEW-LOGIN-- NodeStopped: ", repr(e)
|
|
||||||
#self.status.events.emit("nodeStopped", Args())
|
|
||||||
#self.view.onLoggedOut()
|
|
||||||
|
|
||||||
self.appService.status.events.on(SignalType.NodeReady.event) do(e:Args):
|
|
||||||
echo "-NEW-LOGIN-- NodeReady: ", repr(e)
|
|
||||||
#self.status.events.emit("nodeReady", Args())
|
|
||||||
|
|
||||||
self.appService.status.events.on(SignalType.NodeLogin.event) do(e:Args):
|
self.appService.status.events.on(SignalType.NodeLogin.event) do(e:Args):
|
||||||
echo "-NEW-LOGIN-- NodeLogin: ", repr(e)
|
let signal = NodeSignal(e)
|
||||||
#self.handleNodeLogin(NodeSignal(e))
|
if signal.event.error != "":
|
||||||
|
self.delegate.loginAccountError(signal.event.error)
|
||||||
|
|
||||||
method getOpenedAccounts*(self: Controller): seq[AccountDto] =
|
method getOpenedAccounts*(self: Controller): seq[AccountDto] =
|
||||||
return self.accountsService.openedAccounts()
|
return self.accountsService.openedAccounts()
|
||||||
|
|
||||||
method setSelectedAccountKeyUid*(self: Controller, keyUid: string) =
|
method setSelectedAccountKeyUid*(self: Controller, keyUid: string) =
|
||||||
self.selectedAccountKeyUid = keyUid
|
self.selectedAccountKeyUid = keyUid
|
||||||
|
|
||||||
|
method login*(self: Controller, password: string) =
|
||||||
|
let openedAccounts = self.getOpenedAccounts()
|
||||||
|
var selectedAccount: AccountDto
|
||||||
|
for acc in openedAccounts:
|
||||||
|
if(acc.keyUid == self.selectedAccountKeyUid):
|
||||||
|
selectedAccount = acc
|
||||||
|
break
|
||||||
|
|
||||||
|
let error = self.accountsService.login(selectedAccount, password)
|
||||||
|
if(error.len > 0):
|
||||||
|
self.delegate.loginAccountError(error)
|
|
@ -14,4 +14,7 @@ method getOpenedAccounts*(self: AccessInterface): seq[AccountDto] {.base.} =
|
||||||
raise newException(ValueError, "No implementation available")
|
raise newException(ValueError, "No implementation available")
|
||||||
|
|
||||||
method setSelectedAccountKeyUid*(self: AccessInterface, keyUid: string) {.base.} =
|
method setSelectedAccountKeyUid*(self: AccessInterface, keyUid: string) {.base.} =
|
||||||
|
raise newException(ValueError, "No implementation available")
|
||||||
|
|
||||||
|
method login*(self: AccessInterface, password: string) {.base.} =
|
||||||
raise newException(ValueError, "No implementation available")
|
raise newException(ValueError, "No implementation available")
|
|
@ -72,4 +72,10 @@ method viewDidLoad*(self: Module) =
|
||||||
|
|
||||||
method setSelectedAccount*(self: Module, item: Item) =
|
method setSelectedAccount*(self: Module, item: Item) =
|
||||||
self.controller.setSelectedAccountKeyUid(item.getKeyUid())
|
self.controller.setSelectedAccountKeyUid(item.getKeyUid())
|
||||||
self.view.setSelectedAccount(item)
|
self.view.setSelectedAccount(item)
|
||||||
|
|
||||||
|
method login*(self: Module, password: string) =
|
||||||
|
self.controller.login(password)
|
||||||
|
|
||||||
|
method loginAccountError*(self: Module, error: string) =
|
||||||
|
self.view.loginAccountError(error)
|
|
@ -0,0 +1,2 @@
|
||||||
|
method loginAccountError*(self: AccessInterface, error: string) {.base.} =
|
||||||
|
raise newException(ValueError, "No implementation available")
|
|
@ -4,4 +4,7 @@ method viewDidLoad*(self: AccessInterface) {.base.} =
|
||||||
raise newException(ValueError, "No implementation available")
|
raise newException(ValueError, "No implementation available")
|
||||||
|
|
||||||
method setSelectedAccount*(self: AccessInterface, item: Item) {.base.} =
|
method setSelectedAccount*(self: AccessInterface, item: Item) {.base.} =
|
||||||
|
raise newException(ValueError, "No implementation available")
|
||||||
|
|
||||||
|
method login*(self: AccessInterface, password: string) {.base.} =
|
||||||
raise newException(ValueError, "No implementation available")
|
raise newException(ValueError, "No implementation available")
|
|
@ -58,4 +58,12 @@ QtObject:
|
||||||
|
|
||||||
QtProperty[QVariant] accountsModel:
|
QtProperty[QVariant] accountsModel:
|
||||||
read = getModel
|
read = getModel
|
||||||
notify = modelChanged
|
notify = modelChanged
|
||||||
|
|
||||||
|
proc login*(self: View, password: string) {.slot.} =
|
||||||
|
self.delegate.login(password)
|
||||||
|
|
||||||
|
proc accountLoginError*(self: View, error: string) {.signal.}
|
||||||
|
|
||||||
|
proc loginAccountError*(self: View, error: string) =
|
||||||
|
self.accountLoginError(error)
|
|
@ -29,7 +29,7 @@ proc newModule*[T](delegate: T,
|
||||||
result.delegate = delegate
|
result.delegate = delegate
|
||||||
result.view = view.newView(result)
|
result.view = view.newView(result)
|
||||||
result.viewVariant = newQVariant(result.view)
|
result.viewVariant = newQVariant(result.view)
|
||||||
result.controller = controller.newController(result, accountsService)
|
result.controller = controller.newController(result, appService, accountsService)
|
||||||
|
|
||||||
# Submodules
|
# Submodules
|
||||||
result.onboardingModule = onboarding_module.newModule(result, appService, accountsService)
|
result.onboardingModule = onboarding_module.newModule(result, appService, accountsService)
|
||||||
|
@ -73,8 +73,8 @@ method onboardingDidLoad*[T](self: Module[T]) =
|
||||||
method loginDidLoad*[T](self: Module[T]) =
|
method loginDidLoad*[T](self: Module[T]) =
|
||||||
self.checkIfModuleDidLoad()
|
self.checkIfModuleDidLoad()
|
||||||
|
|
||||||
method accountCreated*[T](self: Module[T]) =
|
method userLoggedIn*[T](self: Module[T]) =
|
||||||
self.delegate.accountCreated()
|
self.delegate.userLoggedIn()
|
||||||
|
|
||||||
method moveToAppState*[T](self: Module[T]) =
|
method moveToAppState*[T](self: Module[T]) =
|
||||||
self.view.setAppState(AppState.MainAppState)
|
self.view.setAppState(AppState.MainAppState)
|
|
@ -35,12 +35,8 @@ method delete*(self: Controller) =
|
||||||
method init*(self: Controller) =
|
method init*(self: Controller) =
|
||||||
self.appService.status.events.on(SignalType.NodeLogin.event) do(e:Args):
|
self.appService.status.events.on(SignalType.NodeLogin.event) do(e:Args):
|
||||||
let signal = NodeSignal(e)
|
let signal = NodeSignal(e)
|
||||||
echo "-NEW-ONBOARDING-- OnNodeLoginEvent: ", repr(signal)
|
if signal.event.error != "":
|
||||||
if signal.event.error == "":
|
self.delegate.setupAccountError()
|
||||||
echo "-NEW-ONBOARDING-- OnNodeLoginEventA: ", repr(signal.event.error)
|
|
||||||
self.delegate.accountCreated()
|
|
||||||
else:
|
|
||||||
error "error: ", methodName="init", errDesription = "onboarding login error " & signal.event.error
|
|
||||||
|
|
||||||
method getGeneratedAccounts*(self: Controller): seq[GeneratedAccountDto] =
|
method getGeneratedAccounts*(self: Controller): seq[GeneratedAccountDto] =
|
||||||
return self.accountsService.generatedAccounts()
|
return self.accountsService.generatedAccounts()
|
||||||
|
|
|
@ -60,9 +60,6 @@ method setSelectedAccountByIndex*(self: Module, index: int) =
|
||||||
method storeSelectedAccountAndLogin*(self: Module, password: string) =
|
method storeSelectedAccountAndLogin*(self: Module, password: string) =
|
||||||
self.controller.storeSelectedAccountAndLogin(password)
|
self.controller.storeSelectedAccountAndLogin(password)
|
||||||
|
|
||||||
method accountCreated*(self: Module) =
|
|
||||||
self.delegate.accountCreated()
|
|
||||||
|
|
||||||
method setupAccountError*(self: Module) =
|
method setupAccountError*(self: Module) =
|
||||||
self.view.setupAccountError()
|
self.view.setupAccountError()
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
method accountCreated*(self: AccessInterface) {.base.} =
|
|
||||||
raise newException(ValueError, "No implementation available")
|
|
||||||
|
|
||||||
method setupAccountError*(self: AccessInterface) {.base.} =
|
method setupAccountError*(self: AccessInterface) {.base.} =
|
||||||
raise newException(ValueError, "No implementation available")
|
raise newException(ValueError, "No implementation available")
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,9 @@ QtObject:
|
||||||
proc accountImportError*(self: View) {.signal.}
|
proc accountImportError*(self: View) {.signal.}
|
||||||
|
|
||||||
proc importAccountError*(self: View) =
|
proc importAccountError*(self: View) =
|
||||||
self.accountImportError() # In QML we can connect to this signal and notify a user
|
# In QML we can connect to this signal and notify a user
|
||||||
|
# before refactoring we didn't have this signal
|
||||||
|
self.accountImportError()
|
||||||
|
|
||||||
proc importAccountSuccess*(self: View) =
|
proc importAccountSuccess*(self: View) =
|
||||||
self.importedAccountChanged()
|
self.importedAccountChanged()
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
method userLoggedIn*(self: AccessInterface) {.base.} =
|
||||||
|
raise newException(ValueError, "No implementation available")
|
|
@ -1,5 +1,2 @@
|
||||||
method onboardingDidLoad*(self: AccessInterface) {.base.} =
|
method onboardingDidLoad*(self: AccessInterface) {.base.} =
|
||||||
raise newException(ValueError, "No implementation available")
|
|
||||||
|
|
||||||
method accountCreated*(self: AccessInterface) {.base.} =
|
|
||||||
raise newException(ValueError, "No implementation available")
|
raise newException(ValueError, "No implementation available")
|
|
@ -44,5 +44,6 @@ proc toAccountDto*(jsonObj: JsonNode): AccountDto =
|
||||||
discard jsonObj.getProp("key-uid", result.keyUid)
|
discard jsonObj.getProp("key-uid", result.keyUid)
|
||||||
|
|
||||||
var imagesObj: JsonNode
|
var imagesObj: JsonNode
|
||||||
if(jsonObj.getProp("images", imagesObj)):
|
if(jsonObj.getProp("images", imagesObj) and imagesObj.kind == JArray):
|
||||||
result.images.add(toImage(imagesObj))
|
for imgObj in imagesObj:
|
||||||
|
result.images.add(toImage(imgObj))
|
|
@ -1,4 +1,5 @@
|
||||||
import Tables, json, sequtils, strutils, strformat, uuids, chronicles
|
import Tables, json, sequtils, strutils, strformat, uuids
|
||||||
|
import json_serialization, chronicles
|
||||||
|
|
||||||
import service_interface
|
import service_interface
|
||||||
import ./dto/accounts
|
import ./dto/accounts
|
||||||
|
@ -282,4 +283,31 @@ method importMnemonic*(self: Service, mnemonic: string): bool =
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error "error: ", methodName="importMnemonic", errName = e.name, errDesription = e.msg
|
error "error: ", methodName="importMnemonic", errName = e.name, errDesription = e.msg
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
method login*(self: Service, account: AccountDto, password: string): string =
|
||||||
|
try:
|
||||||
|
let hashedPassword = hashString(password)
|
||||||
|
var thumbnailImage: string
|
||||||
|
var largeImage: string
|
||||||
|
for img in account.images:
|
||||||
|
if(img.imgType == "thumbnail"):
|
||||||
|
thumbnailImage = img.uri
|
||||||
|
elif(img.imgType == "large"):
|
||||||
|
largeImage = img.uri
|
||||||
|
|
||||||
|
let response = status_go.login(account.name, account.keyUid, hashedPassword,
|
||||||
|
account.identicon, thumbnailImage, largeImage)
|
||||||
|
|
||||||
|
var error = "response doesn't contain \"error\""
|
||||||
|
if(response.result.contains("error")):
|
||||||
|
error = response.result["error"].getStr
|
||||||
|
if error == "":
|
||||||
|
debug "Account logged in"
|
||||||
|
self.loggedInAccount = account
|
||||||
|
|
||||||
|
return error
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error "error: ", methodName="setupAccount", errName = e.name, errDesription = e.msg
|
||||||
|
return e.msg
|
|
@ -44,4 +44,8 @@ method validateMnemonic*(self: ServiceInterface, mnemonic: string):
|
||||||
raise newException(ValueError, "No implementation available")
|
raise newException(ValueError, "No implementation available")
|
||||||
|
|
||||||
method importMnemonic*(self: ServiceInterface, mnemonic: string): bool {.base.} =
|
method importMnemonic*(self: ServiceInterface, mnemonic: string): bool {.base.} =
|
||||||
|
raise newException(ValueError, "No implementation available")
|
||||||
|
|
||||||
|
method login*(self: ServiceInterface, account: AccountDto, password: string):
|
||||||
|
string {.base.} =
|
||||||
raise newException(ValueError, "No implementation available")
|
raise newException(ValueError, "No implementation available")
|
|
@ -164,24 +164,14 @@ StatusAppThreePanelLayout {
|
||||||
sourceComponent: appSettings.communitiesEnabled && root.rootStore.chatsModelInst.communities.activeCommunity.active ? communtiyColumnComponent : contactsColumnComponent
|
sourceComponent: appSettings.communitiesEnabled && root.rootStore.chatsModelInst.communities.activeCommunity.active ? communtiyColumnComponent : contactsColumnComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
// centerPanel: ChatColumn {
|
centerPanel: ChatColumn {
|
||||||
// id: chatColumn
|
|
||||||
// chatGroupsListViewCount: contactColumnLoader.item.chatGroupsListViewCount
|
|
||||||
// }
|
|
||||||
|
|
||||||
centerPanel: Rectangle {
|
|
||||||
id: chatColumn
|
id: chatColumn
|
||||||
anchors.fill: parent
|
chatGroupsListViewCount: contactColumnLoader.item.chatGroupsListViewCount
|
||||||
color: "green"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showRightPanel: (appSettings.expandUsersList && (appSettings.showOnlineUsers || chatsModel.communities.activeCommunity.active)
|
showRightPanel: (appSettings.expandUsersList && (appSettings.showOnlineUsers || chatsModel.communities.activeCommunity.active)
|
||||||
&& (chatsModel.channelView.activeChannel.chatType !== Constants.chatTypeOneToOne))
|
&& (chatsModel.channelView.activeChannel.chatType !== Constants.chatTypeOneToOne))
|
||||||
//rightPanel: appSettings.communitiesEnabled && chatsModel.communities.activeCommunity.active ? communityUserListComponent : userListComponent
|
rightPanel: appSettings.communitiesEnabled && chatsModel.communities.activeCommunity.active ? communityUserListComponent : userListComponent
|
||||||
rightPanel: Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: "blue"
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: communityUserListComponent
|
id: communityUserListComponent
|
||||||
|
|
|
@ -160,7 +160,9 @@ ModalPopup {
|
||||||
if (storingPasswordModal)
|
if (storingPasswordModal)
|
||||||
{
|
{
|
||||||
accountSettings.storeToKeychain = Constants.storeToKeychainValueStore
|
accountSettings.storeToKeychain = Constants.storeToKeychainValueStore
|
||||||
loginModel.storePassword(profileModel.profile.username, repeatPasswordField.text)
|
// NEED TO HANDLE IT
|
||||||
|
// This part should be done via PrivacyAndSecurity submodule section of ProfileSection module
|
||||||
|
// loginModel.storePassword(profileModel.profile.username, repeatPasswordField.text)
|
||||||
popup.close()
|
popup.close()
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
@ -227,25 +227,23 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NEED TO HANDLE IT
|
Connections {
|
||||||
// Connections {
|
target: LoginStore.loginModul
|
||||||
// target: LoginStore.loginModelInst
|
onAccountLoginError: {
|
||||||
// ignoreUnknownSignals: true
|
if (error) {
|
||||||
// onLoginResponseChanged: {
|
// SQLITE_NOTADB: "file is not a database"
|
||||||
// if (error) {
|
if (error === "file is not a database") {
|
||||||
// // SQLITE_NOTADB: "file is not a database"
|
errMsg.text = errMsg.incorrectPasswordMsg
|
||||||
// if (error === "file is not a database") {
|
} else {
|
||||||
// errMsg.text = errMsg.incorrectPasswordMsg
|
//% "Login failed: %1"
|
||||||
// } else {
|
errMsg.text = qsTrId("login-failed---1").arg(error.toUpperCase())
|
||||||
// //% "Login failed: %1"
|
}
|
||||||
// errMsg.text = qsTrId("login-failed---1").arg(error.toUpperCase())
|
errMsg.visible = true
|
||||||
// }
|
loading = false
|
||||||
// errMsg.visible = true
|
txtPassword.textField.forceActiveFocus()
|
||||||
// loading = false
|
}
|
||||||
// txtPassword.textField.forceActiveFocus()
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
StatusQControls.StatusFlatButton {
|
StatusQControls.StatusFlatButton {
|
||||||
id: generateKeysLinkText
|
id: generateKeysLinkText
|
||||||
|
|
|
@ -320,7 +320,8 @@ StatusWindow {
|
||||||
|
|
||||||
onConfirmButtonClicked: {
|
onConfirmButtonClicked: {
|
||||||
accountSettings.storeToKeychain = Constants.storeToKeychainValueStore
|
accountSettings.storeToKeychain = Constants.storeToKeychainValueStore
|
||||||
loginModel.storePassword(username, password)
|
// This is need to be handled using KeyChain service via LoginModule
|
||||||
|
// loginModel.storePassword(username, password)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue